Compare commits

..

1 Commits

Author SHA1 Message Date
Erik
edaeea88ad Set up calendar frontend resources when first platform is set up 2026-03-30 16:27:14 +02:00
429 changed files with 3380 additions and 11301 deletions

View File

@@ -579,7 +579,6 @@ homeassistant.components.trmnl.*
homeassistant.components.tts.*
homeassistant.components.twentemilieu.*
homeassistant.components.unifi.*
homeassistant.components.unifi_access.*
homeassistant.components.unifiprotect.*
homeassistant.components.upcloud.*
homeassistant.components.update.*

8
CODEOWNERS generated
View File

@@ -741,8 +741,8 @@ build.json @home-assistant/supervisor
/tests/components/honeywell/ @rdfurman @mkmer
/homeassistant/components/hr_energy_qube/ @MattieGit
/tests/components/hr_energy_qube/ @MattieGit
/homeassistant/components/html5/ @alexyao2015 @tr4nt0r
/tests/components/html5/ @alexyao2015 @tr4nt0r
/homeassistant/components/html5/ @alexyao2015
/tests/components/html5/ @alexyao2015
/homeassistant/components/http/ @home-assistant/core
/tests/components/http/ @home-assistant/core
/homeassistant/components/huawei_lte/ @scop @fphammerle
@@ -1228,8 +1228,8 @@ build.json @home-assistant/supervisor
/tests/components/onewire/ @garbled1 @epenet
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
/tests/components/onkyo/ @arturpragacz @eclair4151
/homeassistant/components/onvif/ @jterrace
/tests/components/onvif/ @jterrace
/homeassistant/components/onvif/ @hunterjm @jterrace
/tests/components/onvif/ @hunterjm @jterrace
/homeassistant/components/open_meteo/ @frenck
/tests/components/open_meteo/ @frenck
/homeassistant/components/open_router/ @joostlek

View File

@@ -468,7 +468,6 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
translation.async_setup(hass)
recovery = hass.config.recovery_mode
device_registry.async_setup(hass)
try:
await asyncio.gather(
create_eager_task(get_internal_store_manager(hass).async_initialize()),

View File

@@ -13,9 +13,6 @@ from homeassistant.helpers import (
config_entry_oauth2_flow,
device_registry as dr,
)
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from . import api
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
@@ -28,17 +25,11 @@ async def async_setup_entry(
hass: HomeAssistant, entry: AladdinConnectConfigEntry
) -> bool:
"""Set up Aladdin Connect Genie from a config entry."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)

View File

@@ -37,9 +37,6 @@
"close_door_failed": {
"message": "Failed to close the garage door"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"open_door_failed": {
"message": "Failed to open the garage door"
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==13.3.1"]
"requirements": ["aioamazondevices==13.3.0"]
}

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from dataclasses import dataclass
from python_homeassistant_analytics import (
Environment,
HomeassistantAnalyticsClient,
HomeassistantAnalyticsConnectionError,
)
@@ -39,7 +38,7 @@ async def async_setup_entry(
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
try:
integrations = await client.get_integrations(Environment.NEXT)
integrations = await client.get_integrations()
except HomeassistantAnalyticsConnectionError as ex:
raise ConfigEntryNotReady("Could not fetch integration list") from ex

View File

@@ -45,21 +45,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
try:
await client.models.list(timeout=10.0)
except anthropic.AuthenticationError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="api_authentication_error",
translation_placeholders={"message": err.message},
) from err
raise ConfigEntryAuthFailed(err) from err
except anthropic.AnthropicError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={
"message": err.message
if isinstance(err, anthropic.APIError)
else str(err)
},
) from err
raise ConfigEntryNotReady(err) from err
entry.runtime_data = client

View File

@@ -12,7 +12,6 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.json import json_loads
from .const import DOMAIN
from .entity import AnthropicBaseLLMEntity
if TYPE_CHECKING:
@@ -61,7 +60,7 @@ class AnthropicTaskEntity(
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="response_not_found"
"Last content in chat log is not an AssistantContent"
)
text = chat_log.content[-1].content or ""
@@ -79,9 +78,7 @@ class AnthropicTaskEntity(
err,
text,
)
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="json_parse_error"
) from err
raise HomeAssistantError("Error with Claude structured response") from err
return ai_task.GenDataTaskResult(
conversation_id=chat_log.conversation_id,

View File

@@ -401,11 +401,7 @@ def _convert_content(
messages[-1]["content"] = messages[-1]["content"][0]["text"]
else:
# Note: We don't pass SystemContent here as it's passed to the API as the prompt
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unexpected_chat_log_content",
translation_placeholders={"type": type(content).__name__},
)
raise HomeAssistantError("Unexpected content type in chat log")
return messages, container_id
@@ -447,9 +443,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
Each message could contain multiple blocks of the same type.
"""
if stream is None or not hasattr(stream, "__aiter__"):
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="unexpected_stream_object"
)
raise HomeAssistantError("Expected a stream of messages")
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
current_tool_args: str
@@ -611,9 +605,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
chat_log.async_trace(_create_token_stats(input_usage, usage))
content_details.container = response.delta.container
if response.delta.stop_reason == "refusal":
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_refusal"
)
raise HomeAssistantError("Potential policy violation detected")
elif isinstance(response, RawMessageStopEvent):
if content_details:
content_details.delete_empty()
@@ -672,9 +664,7 @@ class AnthropicBaseLLMEntity(Entity):
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="system_message_not_found"
)
raise HomeAssistantError("First message must be a system message")
# System prompt with caching enabled
system_prompt: list[TextBlockParam] = [
@@ -764,7 +754,7 @@ class AnthropicBaseLLMEntity(Entity):
last_message = messages[-1]
if last_message["role"] != "user":
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="user_message_not_found"
"Last message must be a user message to add attachments"
)
if isinstance(last_message["content"], str):
last_message["content"] = [
@@ -869,19 +859,11 @@ class AnthropicBaseLLMEntity(Entity):
except anthropic.AuthenticationError as err:
self.entry.async_start_reauth(self.hass)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_authentication_error",
translation_placeholders={"message": err.message},
"Authentication error with Anthropic API, reauthentication required"
) from err
except anthropic.AnthropicError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={
"message": err.message
if isinstance(err, anthropic.APIError)
else str(err)
},
f"Sorry, I had a problem talking to Anthropic: {err}"
) from err
if not chat_log.unresponded_tool_results:
@@ -901,23 +883,15 @@ async def async_prepare_files_for_prompt(
for file_path, mime_type in files:
if not file_path.exists():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="wrong_file_path",
translation_placeholders={"file_path": file_path.as_posix()},
)
raise HomeAssistantError(f"`{file_path}` does not exist")
if mime_type is None:
mime_type = guess_file_type(file_path)[0]
if not mime_type or not mime_type.startswith(("image/", "application/pdf")):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="wrong_file_type",
translation_placeholders={
"file_path": file_path.as_posix(),
"mime_type": mime_type or "unknown",
},
"Only images and PDF are supported by the Anthropic API,"
f"`{file_path}` is not an image file or PDF"
)
if mime_type == "image/jpg":
mime_type = "image/jpeg"

View File

@@ -59,7 +59,10 @@ rules:
status: exempt
comment: |
No data updates.
docs-examples: done
docs-examples:
status: todo
comment: |
To give examples of how people use the integration
docs-known-limitations: done
docs-supported-devices:
status: todo
@@ -85,7 +88,7 @@ rules:
comment: |
No entities disabled by default.
entity-translations: todo
exception-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: done
repair-issues: done

View File

@@ -161,9 +161,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
is None
or (subentry := entry.subentries.get(self._current_subentry_id)) is None
):
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="subentry_not_found"
)
raise HomeAssistantError("Subentry not found")
updated_data = {
**subentry.data,
@@ -192,6 +190,4 @@ async def async_create_fix_flow(
"""Create flow."""
if issue_id == "model_deprecated":
return ModelDeprecatedRepairFlow()
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="unknown_issue_id"
)
raise HomeAssistantError("Unknown issue ID")

View File

@@ -149,47 +149,6 @@
}
}
},
"exceptions": {
"api_authentication_error": {
"message": "Authentication error with Anthropic API: {message}. Reauthentication required."
},
"api_error": {
"message": "Anthropic API error: {message}."
},
"api_refusal": {
"message": "Potential policy violation detected."
},
"json_parse_error": {
"message": "Error with Claude structured response."
},
"response_not_found": {
"message": "Last content in chat log is not an AssistantContent."
},
"subentry_not_found": {
"message": "Subentry not found."
},
"system_message_not_found": {
"message": "First message must be a system message."
},
"unexpected_chat_log_content": {
"message": "Unexpected content type in chat log: {type}."
},
"unexpected_stream_object": {
"message": "Expected a stream of messages."
},
"unknown_issue_id": {
"message": "Unknown issue ID."
},
"user_message_not_found": {
"message": "Last message must be a user message to add attachments."
},
"wrong_file_path": {
"message": "`{file_path}` does not exist."
},
"wrong_file_type": {
"message": "Only images and PDF are supported by the Anthropic API, `{file_path}` ({mime_type}) is not an image file or PDF."
}
},
"issues": {
"model_deprecated": {
"fix_flow": {

View File

@@ -142,13 +142,6 @@ class WellKnownOAuthInfoView(HomeAssistantView):
"authorization_endpoint": f"{url_prefix}/auth/authorize",
"token_endpoint": f"{url_prefix}/auth/token",
"revocation_endpoint": f"{url_prefix}/auth/revoke",
# Home Assistant already accepts URL-based client_ids via
# IndieAuth without prior registration, which is compatible with
# draft-ietf-oauth-client-id-metadata-document. This flag
# advertises that support to encourage clients to use it. The
# metadata document is not actually fetched as IndieAuth doesn't
# require it.
"client_id_metadata_document_supported": True,
"response_types_supported": ["code"],
"service_documentation": (
"https://developers.home-assistant.io/docs/auth_api"

View File

@@ -122,9 +122,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
"alarm_control_panel",
"assist_satellite",
"battery",
"calendar",
"climate",
"counter",
"cover",
"device_tracker",
"door",
@@ -144,14 +142,11 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
"person",
"power",
"schedule",
"select",
"siren",
"switch",
"temperature",
"text",
"timer",
"vacuum",
"valve",
"water_heater",
"window",
}
@@ -194,7 +189,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"todo",
"update",
"vacuum",
"valve",
"water_heater",
"window",
}

View File

@@ -19,8 +19,6 @@
unit_of_measurement: "%"
- domain: sensor
device_class: battery
- domain: number
device_class: battery
.battery_threshold_number: &battery_threshold_number
min: 0

View File

@@ -13,8 +13,6 @@
.battery_threshold_entity: &battery_threshold_entity
- domain: input_number
unit_of_measurement: "%"
- domain: number
device_class: battery
- domain: sensor
device_class: battery

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/bluesound",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["pyblu==2.0.6"],
"requirements": ["pyblu==2.0.5"],
"zeroconf": [
{
"type": "_musc._tcp.local."

View File

@@ -36,6 +36,9 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_point_in_time
from homeassistant.helpers.integration_platform import (
async_process_integration_platforms,
)
from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
@@ -312,14 +315,32 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
hass.http.register_view(CalendarListView(component))
hass.http.register_view(CalendarEventView(component))
frontend_loaded = False
frontend.async_register_built_in_panel(hass, "calendar", "calendar", "mdi:calendar")
@callback
def async_platform_loaded(
hass: HomeAssistant, integration_domain: str, platform: Any
) -> None:
"""Register frontend resources for calendar."""
nonlocal frontend_loaded
websocket_api.async_register_command(hass, handle_calendar_event_create)
websocket_api.async_register_command(hass, handle_calendar_event_delete)
websocket_api.async_register_command(hass, handle_calendar_event_update)
if frontend_loaded:
return
frontend_loaded = True
hass.http.register_view(CalendarListView(component))
hass.http.register_view(CalendarEventView(component))
frontend.async_register_built_in_panel(
hass, "calendar", "calendar", "mdi:calendar"
)
websocket_api.async_register_command(hass, handle_calendar_event_create)
websocket_api.async_register_command(hass, handle_calendar_event_delete)
websocket_api.async_register_command(hass, handle_calendar_event_update)
await async_process_integration_platforms(hass, DOMAIN, async_platform_loaded)
component.async_register_entity_service(
CREATE_EVENT_SERVICE,
@@ -578,13 +599,13 @@ class CalendarEntity(Entity):
return STATE_OFF
@callback
def _async_write_ha_state(self) -> None:
def async_write_ha_state(self) -> None:
"""Write the state to the state machine.
This sets up listeners to handle state transitions for start or end of
the current or upcoming event.
"""
super()._async_write_ha_state()
super().async_write_ha_state()
if self._alarm_unsubs is None:
self._alarm_unsubs = []
_LOGGER.debug(

View File

@@ -1,16 +0,0 @@
"""Provides conditions for calendars."""
from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from .const import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_event_active": make_entity_state_condition(DOMAIN, STATE_ON),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the calendar conditions."""
return CONDITIONS

View File

@@ -1,14 +0,0 @@
is_event_active:
target:
entity:
- domain: calendar
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any

View File

@@ -1,9 +1,4 @@
{
"conditions": {
"is_event_active": {
"condition": "mdi:calendar-check"
}
},
"entity_component": {
"_": {
"default": "mdi:calendar",

View File

@@ -1,20 +1,4 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted calendars.",
"condition_behavior_name": "Behavior"
},
"conditions": {
"is_event_active": {
"description": "Tests if one or more calendars have an active event.",
"fields": {
"behavior": {
"description": "[%key:component::calendar::common::condition_behavior_description%]",
"name": "[%key:component::calendar::common::condition_behavior_name%]"
}
},
"name": "Calendar event is active"
}
},
"entity_component": {
"_": {
"name": "[%key:component::calendar::title%]",
@@ -62,12 +46,6 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_offset_type": {
"options": {
"after": "After",

View File

@@ -760,12 +760,12 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return CameraCapabilities(frontend_stream_types)
@callback
def _async_write_ha_state(self) -> None:
def async_write_ha_state(self) -> None:
"""Write the state to the state machine.
Schedules async_refresh_providers if support of streams have changed.
"""
super()._async_write_ha_state()
super().async_write_ha_state()
if self.__supports_stream != (
supports_stream := self.supported_features & CameraEntityFeature.STREAM
):

View File

@@ -11,12 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.LIGHT,
Platform.SELECT,
]
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:

View File

@@ -12,7 +12,5 @@ SORTED_BRIGHTNESS_LEVELS = sorted(BRIGHTNESS_LEVELS)
DEFAULT_DIMMING_TIME_MINUTES: int = DIMMING_TIME_MINUTES[0]
DIMMING_TIME_OPTIONS: tuple[str, ...] = tuple(str(m) for m in DIMMING_TIME_MINUTES)
# Interval between periodic state polls to catch externally-triggered changes.
STATE_POLL_INTERVAL = timedelta(seconds=30)

View File

@@ -19,7 +19,7 @@ from homeassistant.components.bluetooth.active_update_coordinator import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from .const import SORTED_BRIGHTNESS_LEVELS, STATE_POLL_INTERVAL
from .const import STATE_POLL_INTERVAL
_LOGGER = logging.getLogger(__name__)
@@ -51,15 +51,6 @@ class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
)
self.title = title
# The device API couples brightness and dimming time into a
# single command (set_brightness_and_dimming_time), so both
# values must be tracked here for cross-entity use.
self.last_brightness_pct: int = (
device.state.brightness_level
if device.state.brightness_level is not None
else SORTED_BRIGHTNESS_LEVELS[0]
)
@callback
def _needs_poll(
self,

View File

@@ -1,31 +0,0 @@
"""Diagnostics support for the Casper Glow integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components import bluetooth
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from .coordinator import CasperGlowConfigEntry
SERVICE_INFO_TO_REDACT = frozenset({"address", "name", "source", "device"})
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: CasperGlowConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
service_info = bluetooth.async_last_service_info(
hass, coordinator.device.address, connectable=True
)
return {
"service_info": async_redact_data(
service_info.as_dict() if service_info else None,
SERVICE_INFO_TO_REDACT,
),
}

View File

@@ -12,11 +12,6 @@
"resume": {
"default": "mdi:play"
}
},
"select": {
"dimming_time": {
"default": "mdi:timer-outline"
}
}
}
}

View File

@@ -71,7 +71,6 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
self._attr_color_mode = ColorMode.BRIGHTNESS
if state.brightness_level is not None:
self._attr_brightness = _device_pct_to_ha_brightness(state.brightness_level)
self.coordinator.last_brightness_pct = state.brightness_level
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
@@ -98,7 +97,6 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
)
)
self._attr_brightness = _device_pct_to_ha_brightness(brightness_pct)
self.coordinator.last_brightness_pct = brightness_pct
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""

View File

@@ -15,5 +15,5 @@
"iot_class": "local_polling",
"loggers": ["pycasperglow"],
"quality_scale": "silver",
"requirements": ["pycasperglow==1.2.0"]
"requirements": ["pycasperglow==1.1.0"]
}

View File

@@ -39,7 +39,7 @@ rules:
# Gold
devices: done
diagnostics: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: No network discovery.
@@ -52,10 +52,8 @@ rules:
docs-troubleshooting: done
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class:
status: exempt
comment: No applicable device classes for binary_sensor, button, light, or select entities.
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: done
exception-translations: done

View File

@@ -1,92 +0,0 @@
"""Casper Glow integration select platform for dimming time."""
from __future__ import annotations
from pycasperglow import GlowState
from homeassistant.components.select import SelectEntity
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .const import DIMMING_TIME_OPTIONS
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
from .entity import CasperGlowEntity
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
entry: CasperGlowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the select platform for Casper Glow."""
async_add_entities([CasperGlowDimmingTimeSelect(entry.runtime_data)])
class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity):
"""Select entity for Casper Glow dimming time."""
_attr_translation_key = "dimming_time"
_attr_entity_category = EntityCategory.CONFIG
_attr_options = list(DIMMING_TIME_OPTIONS)
_attr_unit_of_measurement = UnitOfTime.MINUTES
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
"""Initialize the dimming time select entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_dimming_time"
@property
def current_option(self) -> str | None:
"""Return the currently selected dimming time from the coordinator."""
if self.coordinator.last_dimming_time_minutes is None:
return None
return str(self.coordinator.last_dimming_time_minutes)
async def async_added_to_hass(self) -> None:
"""Restore last known dimming time and register state update callback."""
await super().async_added_to_hass()
if self.coordinator.last_dimming_time_minutes is None and (
last_state := await self.async_get_last_state()
):
if last_state.state in DIMMING_TIME_OPTIONS:
self.coordinator.last_dimming_time_minutes = int(last_state.state)
self.async_on_remove(
self._device.register_callback(self._async_handle_state_update)
)
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
"""Handle a state update from the device."""
if state.brightness_level is not None:
self.coordinator.last_brightness_pct = state.brightness_level
if (
state.configured_dimming_time_minutes is not None
and self.coordinator.last_dimming_time_minutes is None
):
self.coordinator.last_dimming_time_minutes = (
state.configured_dimming_time_minutes
)
# Dimming time is not part of the device state
# that is provided via BLE update. Therefore
# we need to trigger a state update for the select entity
# to update the current state.
self.async_write_ha_state()
async def async_select_option(self, option: str) -> None:
"""Set the dimming time."""
await self._async_command(
self._device.set_brightness_and_dimming_time(
self.coordinator.last_brightness_pct, int(option)
)
)
self.coordinator.last_dimming_time_minutes = int(option)
# Dimming time is not part of the device state
# that is provided via BLE update. Therefore
# we need to trigger a state update for the select entity
# to update the current state.
self.async_write_ha_state()

View File

@@ -39,11 +39,6 @@
"resume": {
"name": "Resume dimming"
}
},
"select": {
"dimming_time": {
"name": "Dimming time"
}
}
},
"exceptions": {

View File

@@ -1,18 +1,10 @@
"""Provides conditions for climates."""
from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
Condition,
ConditionConfig,
EntityConditionBase,
EntityNumericalConditionWithUnitBase,
make_entity_numerical_condition,
make_entity_state_condition,
@@ -21,36 +13,6 @@ from homeassistant.util.unit_conversion import TemperatureConverter
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
CONF_HVAC_MODE = "hvac_mode"
_HVAC_MODE_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
{
vol.Required(CONF_OPTIONS): {
vol.Required(CONF_HVAC_MODE): vol.All(
cv.ensure_list, vol.Length(min=1), [vol.Coerce(HVACMode)]
),
},
}
)
class ClimateHVACModeCondition(EntityConditionBase):
"""Condition for climate HVAC mode."""
_domain_specs = {DOMAIN: DomainSpec()}
_schema = _HVAC_MODE_CONDITION_SCHEMA
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize the HVAC mode condition."""
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.options is not None
self._hvac_modes: set[str] = set(config.options[CONF_HVAC_MODE])
def is_valid_state(self, entity_state: State) -> bool:
"""Check if the state matches any of the expected HVAC modes."""
return entity_state.state in self._hvac_modes
class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
"""Mixin for climate target temperature conditions with unit conversion."""
@@ -66,7 +28,6 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
CONDITIONS: dict[str, type[Condition]] = {
"is_hvac_mode": ClimateHVACModeCondition,
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
"is_on": make_entity_state_condition(
DOMAIN,

View File

@@ -45,21 +45,6 @@ is_cooling: *condition_common
is_drying: *condition_common
is_heating: *condition_common
is_hvac_mode:
target: *condition_climate_target
fields:
behavior: *condition_behavior
hvac_mode:
context:
filter_target: target
required: true
selector:
state:
hide_states:
- unavailable
- unknown
multiple: true
target_humidity:
target: *condition_climate_target
fields:

View File

@@ -9,9 +9,6 @@
"is_heating": {
"condition": "mdi:fire"
},
"is_hvac_mode": {
"condition": "mdi:thermostat"
},
"is_off": {
"condition": "mdi:power-off"
},

View File

@@ -41,20 +41,6 @@
},
"name": "Climate-control device is heating"
},
"is_hvac_mode": {
"description": "Tests if one or more climate-control devices are set to a specific HVAC mode.",
"fields": {
"behavior": {
"description": "[%key:component::climate::common::condition_behavior_description%]",
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"hvac_mode": {
"description": "The HVAC modes to test for.",
"name": "Modes"
}
},
"name": "Climate-control device HVAC mode"
},
"is_off": {
"description": "Tests if one or more climate-control devices are off.",
"fields": {

View File

@@ -9,6 +9,7 @@ from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import CLIMATE
from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
@@ -91,7 +92,7 @@ async def async_setup_entry(
entities: list[ClimateEntity] = []
for device in coordinator.data[CLIMATE].values():
values = load_api_data(device, "climate")
values = load_api_data(device, CLIMATE_DOMAIN)
if values[0] == 0 and values[4] == 0:
# No climate data, device is only a humidifier/dehumidifier
@@ -139,7 +140,7 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
def _update_attributes(self) -> None:
"""Update class attributes."""
device = self.coordinator.data[CLIMATE][self._device.index]
values = load_api_data(device, "climate")
values = load_api_data(device, CLIMATE_DOMAIN)
_active = values[1]
_mode = values[2] # Values from API: "O", "L", "U"

View File

@@ -9,6 +9,7 @@ from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import CLIMATE
from homeassistant.components.humidifier import (
DOMAIN as HUMIDIFIER_DOMAIN,
MODE_AUTO,
MODE_NORMAL,
HumidifierAction,
@@ -67,7 +68,7 @@ async def async_setup_entry(
entities: list[ComelitHumidifierEntity] = []
for device in coordinator.data[CLIMATE].values():
values = load_api_data(device, "humidifier")
values = load_api_data(device, HUMIDIFIER_DOMAIN)
if values[0] == 0 and values[4] == 0:
# No humidity data, device is only a climate
@@ -141,7 +142,7 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity):
def _update_attributes(self) -> None:
"""Update class attributes."""
device = self.coordinator.data[CLIMATE][self._device.index]
values = load_api_data(device, "humidifier")
values = load_api_data(device, HUMIDIFIER_DOMAIN)
_active = values[1]
_mode = values[2] # Values from API: "O", "L", "U"

View File

@@ -113,6 +113,9 @@
"humidity_while_off": {
"message": "Cannot change humidity while off"
},
"invalid_clima_data": {
"message": "Invalid 'clima' data"
},
"update_failed": {
"message": "Failed to update data: {error}"
}

View File

@@ -2,12 +2,13 @@
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import TYPE_CHECKING, Any, Concatenate, Literal
from typing import TYPE_CHECKING, Any, Concatenate
from aiocomelit.api import ComelitSerialBridgeObject
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiohttp import ClientSession, CookieJar
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -29,19 +30,17 @@ async def async_client_session(hass: HomeAssistant) -> ClientSession:
)
def load_api_data(
device: ComelitSerialBridgeObject,
domain: Literal["climate", "humidifier"],
) -> list[Any]:
def load_api_data(device: ComelitSerialBridgeObject, domain: str) -> list[Any]:
"""Load data from the API."""
# This function is called when the data is loaded from the API.
# For climate and humidifier device.val is always a list.
if TYPE_CHECKING:
assert isinstance(device.val, list)
# This function is called when the data is loaded from the API
if not isinstance(device.val, list):
raise HomeAssistantError(
translation_domain=domain, translation_key="invalid_clima_data"
)
# CLIMATE has a 2 item tuple:
# - first for Clima
# - second for Humidifier
return device.val[0] if domain == "climate" else device.val[1]
return device.val[0] if domain == CLIMATE_DOMAIN else device.val[1]
async def cleanup_stale_entity(

View File

@@ -1,15 +0,0 @@
"""Provides conditions for counters."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_numerical_condition
DOMAIN = "counter"
CONDITIONS: dict[str, type[Condition]] = {
"is_value": make_entity_numerical_condition(DOMAIN),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the conditions for counters."""
return CONDITIONS

View File

@@ -1,25 +0,0 @@
is_value:
target:
entity:
- domain: counter
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
threshold:
required: true
selector:
numeric_threshold:
entity:
- domain: counter
- domain: input_number
- domain: number
mode: is
number:
mode: box

View File

@@ -1,9 +1,4 @@
{
"conditions": {
"is_value": {
"condition": "mdi:counter"
}
},
"services": {
"decrement": {
"service": "mdi:numeric-negative-1"

View File

@@ -3,22 +3,6 @@
"trigger_behavior_description": "The behavior of the targeted counters to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_value": {
"description": "Tests the value of one or more counters.",
"fields": {
"behavior": {
"description": "How the state should match on the targeted counters.",
"name": "Behavior"
},
"threshold": {
"description": "What to test for and threshold values.",
"name": "Threshold"
}
},
"name": "Counter value"
}
},
"entity_component": {
"_": {
"name": "[%key:component::counter::title%]",
@@ -46,12 +30,6 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -44,18 +44,18 @@ class DemoRemote(RemoteEntity):
return {"last_command_sent": self._last_command_sent}
return None
async def async_turn_on(self, **kwargs: Any) -> None:
def turn_on(self, **kwargs: Any) -> None:
"""Turn the remote on."""
self._attr_is_on = True
self.async_write_ha_state()
self.schedule_update_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
def turn_off(self, **kwargs: Any) -> None:
"""Turn the remote off."""
self._attr_is_on = False
self.async_write_ha_state()
self.schedule_update_ha_state()
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a command to a device."""
for com in command:
self._last_command_sent = com
self.async_write_ha_state()
self.schedule_update_ha_state()

View File

@@ -61,12 +61,12 @@ class DemoSwitch(SwitchEntity):
name=device_name,
)
async def async_turn_on(self, **kwargs: Any) -> None:
def turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self._attr_is_on = True
self.async_write_ha_state()
self.schedule_update_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
self._attr_is_on = False
self.async_write_ha_state()
self.schedule_update_ha_state()

View File

@@ -51,6 +51,7 @@ def _entity_entry_filter(a: attr.Attribute, _: Any) -> bool:
return a.name not in (
"_cache",
"compat_aliases",
"compat_name",
"original_name_unprefixed",
)

View File

@@ -353,10 +353,10 @@ class DlnaDmrEntity(MediaPlayerEntity):
# Device was de/re-connected, state might have changed
self.async_write_ha_state()
def _async_write_ha_state(self) -> None:
def async_write_ha_state(self) -> None:
"""Write the state."""
self._attr_supported_features = self._supported_features()
super()._async_write_ha_state()
super().async_write_ha_state()
async def _device_connect(self, location: str) -> None:
"""Connect to the device now that it's available."""

View File

@@ -45,13 +45,6 @@ SUPPORT_FLAGS_HEATER = (
)
def _operation_mode_to_ha(mode: WaterHeaterOperationMode | None) -> str:
"""Translate an EcoNet operation mode to a Home Assistant state."""
if mode in (None, WaterHeaterOperationMode.VACATION):
return STATE_OFF
return ECONET_STATE_TO_HA[mode]
async def async_setup_entry(
hass: HomeAssistant,
entry: EconetConfigEntry,
@@ -87,22 +80,26 @@ class EcoNetWaterHeater(EcoNetEntity[WaterHeater], WaterHeaterEntity):
@property
def current_operation(self) -> str:
"""Return current operation."""
return _operation_mode_to_ha(self.water_heater.mode)
econet_mode = self.water_heater.mode
_current_op = STATE_OFF
if econet_mode is not None:
_current_op = ECONET_STATE_TO_HA[econet_mode]
return _current_op
@property
def operation_list(self) -> list[str]:
"""List of available operation modes."""
return list(
dict.fromkeys(
ECONET_STATE_TO_HA[mode]
for mode in self.water_heater.modes
if mode
not in (
WaterHeaterOperationMode.UNKNOWN,
WaterHeaterOperationMode.VACATION,
)
)
)
econet_modes = self.water_heater.modes
operation_modes = set()
for mode in econet_modes:
if (
mode is not WaterHeaterOperationMode.UNKNOWN
and mode is not WaterHeaterOperationMode.VACATION
):
ha_mode = ECONET_STATE_TO_HA[mode]
operation_modes.add(ha_mode)
return list(operation_modes)
@property
def supported_features(self) -> WaterHeaterEntityFeature:

View File

@@ -10,7 +10,6 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_IP_ADDRESS, CONF_PORT
from homeassistant.helpers.httpx_client import get_async_client
from .const import DOMAIN, UPNP_AVAILABLE
@@ -41,7 +40,6 @@ class FingConfigFlow(ConfigFlow, domain=DOMAIN):
ip=user_input[CONF_IP_ADDRESS],
port=int(user_input[CONF_PORT]),
key=user_input[CONF_API_KEY],
client=get_async_client(self.hass),
)
try:

View File

@@ -11,7 +11,6 @@ import httpx
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_IP_ADDRESS, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPNP_AVAILABLE
@@ -39,7 +38,6 @@ class FingDataUpdateCoordinator(DataUpdateCoordinator[FingDataObject]):
ip=config_entry.data[CONF_IP_ADDRESS],
port=int(config_entry.data[CONF_PORT]),
key=config_entry.data[CONF_API_KEY],
client=get_async_client(hass),
)
self._upnp_available = config_entry.data[UPNP_AVAILABLE]
update_interval = timedelta(seconds=30)

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["fing_agent_api==1.1.0"]
"requirements": ["fing_agent_api==1.0.3"]
}

View File

@@ -68,5 +68,5 @@ rules:
# Platinum
async-dependency: todo
inject-websession: done
inject-websession: todo
strict-typing: todo

View File

@@ -4,12 +4,9 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from . import api
from .const import DOMAIN, FitbitScope
from .const import FitbitScope
from .coordinator import FitbitConfigEntry, FitbitData, FitbitDeviceCoordinator
from .exceptions import FitbitApiException, FitbitAuthException
from .model import config_from_entry_data
@@ -19,17 +16,11 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: FitbitConfigEntry) -> bool:
"""Set up fitbit from a config entry."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
fitbit_api = api.OAuthFitbitApi(
hass, session, unit_system=entry.data.get("unit_system")

View File

@@ -121,10 +121,5 @@
"name": "Water"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -34,17 +34,23 @@ rules:
# Gold
devices: done
diagnostics: done
discovery-update-info: done
discovery-update-info: todo
discovery: done
docs-data-update: done
docs-examples: done
docs-known-limitations:
status: exempt
comment: no known limitations, yet
docs-supported-devices: done
docs-supported-functions: done
docs-supported-devices:
status: todo
comment: add the known supported devices
docs-supported-functions:
status: todo
comment: need to be overhauled
docs-troubleshooting: done
docs-use-cases: done
docs-use-cases:
status: todo
comment: need to be overhauled
dynamic-devices: done
entity-category: done
entity-device-class: done

View File

@@ -97,7 +97,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
super().__init__(coordinator, ain)
@callback
def _async_write_ha_state(self) -> None:
def async_write_ha_state(self) -> None:
"""Write the state to the HASS state machine."""
if self.data.holiday_active:
self._attr_supported_features = ClimateEntityFeature.PRESET_MODE
@@ -109,7 +109,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
self._attr_supported_features = SUPPORTED_FEATURES
self._attr_hvac_modes = HVAC_MODES
self._attr_preset_modes = PRESET_MODES
return super()._async_write_ha_state()
return super().async_write_ha_state()
@property
def current_temperature(self) -> float:

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260325.2"]
"requirements": ["home-assistant-frontend==20260325.0"]
}

View File

@@ -15,5 +15,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
"requirements": ["gardena-bluetooth==2.3.0"]
"requirements": ["gardena-bluetooth==2.1.0"]
}

View File

@@ -7,7 +7,7 @@ from homelink.mqtt_provider import MQTTProvider
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from . import oauth2
@@ -29,17 +29,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) ->
hass, DOMAIN, auth_implementation
)
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
authenticated_session = oauth2.AsyncConfigEntryAuth(

View File

@@ -49,10 +49,5 @@
}
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -2,14 +2,11 @@
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
from .const import DOMAIN
from .coordinator import GeocachingConfigEntry, GeocachingDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
@@ -17,13 +14,7 @@ PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: GeocachingConfigEntry) -> bool:
"""Set up Geocaching from a config entry."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
implementation = await async_get_config_entry_implementation(hass, entry)
oauth_session = OAuth2Session(hass, entry, implementation)
coordinator = GeocachingDataUpdateCoordinator(

View File

@@ -65,10 +65,5 @@
"unit_of_measurement": "souvenirs"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["goodwe"],
"requirements": ["goodwe==0.4.10"]
"requirements": ["goodwe==0.4.8"]
}

View File

@@ -24,9 +24,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from homeassistant.helpers.entity import generate_entity_id
from .api import ApiAuthImpl, get_feature_access
@@ -91,17 +88,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bo
_LOGGER.error("Configuration error in %s: %s", YAML_DEVICES, str(err))
return False
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
# Force a token refresh to fix a bug where tokens were persisted with
# expires_in (relative time delta) and expires_at (absolute time) swapped.

View File

@@ -57,11 +57,6 @@
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"options": {
"step": {
"init": {

View File

@@ -2,22 +2,16 @@
from __future__ import annotations
from aiohttp import ClientError
import aiohttp
from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials
from homeassistant.components import conversation
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery, intent
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
@@ -53,21 +47,17 @@ async def async_setup_entry(
hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry
) -> bool:
"""Set up Google Assistant SDK from a config entry."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="reauth_required"
) from err
except (OAuth2TokenRequestError, ClientError) as err:
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="reauth_required"
) from err
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
raise ConfigEntryNotReady from err
mem_storage = InMemoryStorage(hass)

View File

@@ -8,6 +8,7 @@ import logging
from typing import Any
import uuid
import aiohttp
from aiohttp import web
from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials
@@ -25,11 +26,7 @@ from homeassistant.components.media_player import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
HomeAssistantError,
OAuth2TokenRequestReauthError,
ServiceValidationError,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.event import async_call_later
@@ -82,8 +79,9 @@ async def async_send_text_commands(
session = entry.runtime_data.session
try:
await session.async_ensure_token_valid()
except OAuth2TokenRequestReauthError:
entry.async_start_reauth(hass)
except aiohttp.ClientResponseError as err:
if 400 <= err.status < 500:
entry.async_start_reauth(hass)
raise
credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]

View File

@@ -48,9 +48,6 @@
"grpc_error": {
"message": "Failed to communicate with Google Assistant"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"reauth_required": {
"message": "Credentials are invalid, re-authentication required"
}

View File

@@ -5,10 +5,8 @@ from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
@@ -36,13 +34,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -> bool:
"""Set up Google Mail from a config entry."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
auth = AsyncConfigEntryAuth(hass, session)
await auth.check_and_refresh_token()

View File

@@ -51,9 +51,6 @@
"exceptions": {
"missing_from_for_alias": {
"message": "Missing 'from' email when setting an alias to show. You have to provide a 'from' email"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"services": {

View File

@@ -33,18 +33,11 @@ async def async_setup_entry(
hass: HomeAssistant, entry: GooglePhotosConfigEntry
) -> bool:
"""Set up Google Photos from a config entry."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
web_session = async_get_clientsession(hass)
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
auth = api.AsyncConfigEntryAuth(web_session, oauth_session)

View File

@@ -68,9 +68,6 @@
"no_access_to_path": {
"message": "Cannot read {filename}, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"upload_error": {
"message": "Failed to upload content: {message}"
}

View File

@@ -15,7 +15,6 @@ from homeassistant.exceptions import (
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
@@ -41,13 +40,7 @@ async def async_setup_entry(
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
) -> bool:
"""Set up Google Sheets from a config entry."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()

View File

@@ -42,11 +42,6 @@
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"services": {
"append_sheet": {
"description": "Appends data to a worksheet in Google Sheets.",

View File

@@ -25,17 +25,11 @@ PLATFORMS: list[Platform] = [Platform.TODO]
async def async_setup_entry(hass: HomeAssistant, entry: GoogleTasksConfigEntry) -> bool:
"""Set up Google Tasks from a config entry."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
auth = api.AsyncConfigEntryAuth(hass, session)
try:

View File

@@ -42,10 +42,5 @@
"title": "[%key:common::config_flow::title::reauth%]"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["habiticalib"],
"quality_scale": "platinum",
"requirements": ["habiticalib==0.4.7"]
"requirements": ["habiticalib==0.4.6"]
}

View File

@@ -27,36 +27,15 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import instance_id
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
TextSelector,
)
from homeassistant.helpers.selector import TextSelector
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import (
CONF_PRODUCT_NAME,
CONF_PRODUCT_TYPE,
CONF_SERIAL,
CONF_USAGE,
DOMAIN,
ENERGY_MONITORING_DEVICES,
LOGGER,
)
USAGE_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=["consumption", "generation"],
translation_key="usage",
mode=SelectSelectorMode.LIST,
)
)
from .const import CONF_PRODUCT_NAME, CONF_PRODUCT_TYPE, CONF_SERIAL, DOMAIN, LOGGER
class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HomeWizard devices."""
"""Handle a config flow for P1 meter."""
VERSION = 1
@@ -64,8 +43,6 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
product_name: str | None = None
product_type: str | None = None
serial: str | None = None
token: str | None = None
usage: str | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -87,12 +64,6 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
f"{device_info.product_type}_{device_info.serial}"
)
self._abort_if_unique_id_configured(updates=user_input)
if device_info.product_type in ENERGY_MONITORING_DEVICES:
self.ip_address = user_input[CONF_IP_ADDRESS]
self.product_name = device_info.product_name
self.product_type = device_info.product_type
self.serial = device_info.serial
return await self.async_step_usage()
return self.async_create_entry(
title=f"{device_info.product_name}",
data=user_input,
@@ -111,45 +82,6 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_usage(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step where we ask how the energy monitor is used."""
assert self.ip_address
assert self.product_name
assert self.product_type
assert self.serial
data: dict[str, Any] = {CONF_IP_ADDRESS: self.ip_address}
if self.token:
data[CONF_TOKEN] = self.token
if user_input is not None:
return self.async_create_entry(
title=f"{self.product_name}",
data=data | user_input,
)
return self.async_show_form(
step_id="usage",
data_schema=vol.Schema(
{
vol.Required(
CONF_USAGE,
default=user_input.get(CONF_USAGE)
if user_input is not None
else "consumption",
): USAGE_SELECTOR,
}
),
description_placeholders={
CONF_PRODUCT_NAME: self.product_name,
CONF_PRODUCT_TYPE: self.product_type,
CONF_SERIAL: self.serial,
CONF_IP_ADDRESS: self.ip_address,
},
)
async def async_step_authorize(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -169,7 +101,8 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
# Now we got a token, we can ask for some more info
device_info = await HomeWizardEnergyV2(self.ip_address, token=token).device()
async with HomeWizardEnergyV2(self.ip_address, token=token) as api:
device_info = await api.device()
data = {
CONF_IP_ADDRESS: self.ip_address,
@@ -180,14 +113,6 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
f"{device_info.product_type}_{device_info.serial}"
)
self._abort_if_unique_id_configured(updates=data)
self.product_name = device_info.product_name
self.product_type = device_info.product_type
self.serial = device_info.serial
if device_info.product_type in ENERGY_MONITORING_DEVICES:
self.token = token
return await self.async_step_usage()
return self.async_create_entry(
title=f"{device_info.product_name}",
data=data,
@@ -214,8 +139,6 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: discovery_info.host}
)
if self.product_type in ENERGY_MONITORING_DEVICES:
return await self.async_step_usage()
return await self.async_step_discovery_confirm()

View File

@@ -5,8 +5,6 @@ from __future__ import annotations
from datetime import timedelta
import logging
from homewizard_energy.const import Model
from homeassistant.const import Platform
DOMAIN = "homewizard"
@@ -24,14 +22,5 @@ LOGGER = logging.getLogger(__package__)
CONF_PRODUCT_NAME = "product_name"
CONF_PRODUCT_TYPE = "product_type"
CONF_SERIAL = "serial"
CONF_USAGE = "usage"
UPDATE_INTERVAL = timedelta(seconds=5)
ENERGY_MONITORING_DEVICES = (
Model.ENERGY_SOCKET,
Model.ENERGY_METER_1_PHASE,
Model.ENERGY_METER_3_PHASE,
Model.ENERGY_METER_EASTRON_SDM230,
Model.ENERGY_METER_EASTRON_SDM630,
)

View File

@@ -39,7 +39,7 @@ from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from homeassistant.util.variance import ignore_variance
from .const import CONF_USAGE, DOMAIN, ENERGY_MONITORING_DEVICES
from .const import DOMAIN
from .coordinator import HomeWizardConfigEntry, HWEnergyDeviceUpdateCoordinator
from .entity import HomeWizardEntity
@@ -267,6 +267,15 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
enabled_fn=lambda data: data.measurement.energy_export_t4_kwh != 0,
value_fn=lambda data: data.measurement.energy_export_t4_kwh or None,
),
HomeWizardSensorEntityDescription(
key="active_power_w",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
has_fn=lambda data: data.measurement.power_w is not None,
value_fn=lambda data: data.measurement.power_w,
),
HomeWizardSensorEntityDescription(
key="active_power_l1_w",
translation_key="active_power_phase_w",
@@ -692,30 +701,22 @@ async def async_setup_entry(
entry: HomeWizardConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Cleanup deleted entrity registry item."""
"""Initialize sensors."""
# Initialize default sensors
entities: list = [
HomeWizardSensorEntity(entry.runtime_data, description)
for description in SENSORS
if description.has_fn(entry.runtime_data.data)
]
active_power_sensor_description = HomeWizardSensorEntityDescription(
key="active_power_w",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
entity_registry_enabled_default=(
entry.runtime_data.data.device.product_type != Model.BATTERY
and entry.data.get(CONF_USAGE, "consumption") == "consumption"
),
has_fn=lambda x: True,
value_fn=lambda data: data.measurement.power_w,
)
# Add optional production power sensor for supported energy monitoring devices
# or plug-in battery
if entry.runtime_data.data.device.product_type in (
*ENERGY_MONITORING_DEVICES,
Model.ENERGY_SOCKET,
Model.ENERGY_METER_1_PHASE,
Model.ENERGY_METER_3_PHASE,
Model.ENERGY_METER_EASTRON_SDM230,
Model.ENERGY_METER_EASTRON_SDM630,
Model.BATTERY,
):
active_prodution_power_sensor_description = HomeWizardSensorEntityDescription(
@@ -735,26 +736,16 @@ async def async_setup_entry(
is not None
and total_export > 0
)
or entry.data.get(CONF_USAGE, "consumption") == "generation"
),
has_fn=lambda x: True,
value_fn=lambda data: (
power_w * -1 if (power_w := data.measurement.power_w) else power_w
),
)
entities.extend(
(
HomeWizardSensorEntity(
entry.runtime_data, active_power_sensor_description
),
HomeWizardSensorEntity(
entry.runtime_data, active_prodution_power_sensor_description
),
)
)
elif (data := entry.runtime_data.data) and data.measurement.power_w is not None:
entities.append(
HomeWizardSensorEntity(entry.runtime_data, active_power_sensor_description)
HomeWizardSensorEntity(
entry.runtime_data, active_prodution_power_sensor_description
)
)
# Initialize external devices

View File

@@ -41,16 +41,6 @@
},
"description": "Update configuration for {title}."
},
"usage": {
"data": {
"usage": "Usage"
},
"data_description": {
"usage": "This will enable either a power consumption or power production sensor the first time this device is set up."
},
"description": "What are you going to monitor with your {product_name} ({product_type} {serial} at {ip_address})?",
"title": "Usage"
},
"user": {
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]"
@@ -209,13 +199,5 @@
},
"title": "Update the authentication method for {title}"
}
},
"selector": {
"usage": {
"options": {
"consumption": "Monitoring consumed energy",
"generation": "Monitoring generated energy"
}
}
}
}

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["python_qube_heatpump"],
"quality_scale": "bronze",
"requirements": ["python-qube-heatpump==1.8.0"]
"requirements": ["python-qube-heatpump==1.7.0"]
}

View File

@@ -9,7 +9,7 @@ from .const import DOMAIN
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [Platform.EVENT, Platform.NOTIFY]
PLATFORMS = [Platform.NOTIFY]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -7,9 +7,3 @@ SERVICE_DISMISS = "dismiss"
ATTR_VAPID_PUB_KEY = "vapid_pub_key"
ATTR_VAPID_PRV_KEY = "vapid_prv_key"
ATTR_VAPID_EMAIL = "vapid_email"
REGISTRATIONS_FILE = "html5_push_registrations.conf"
ATTR_ACTION = "action"
ATTR_DATA = "data"
ATTR_TAG = "tag"

View File

@@ -1,73 +0,0 @@
"""Base entities for HTML5 integration."""
from __future__ import annotations
from typing import NotRequired, TypedDict
from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class Keys(TypedDict):
"""Types for keys."""
p256dh: str
auth: str
class Subscription(TypedDict):
"""Types for subscription."""
endpoint: str
expirationTime: int | None
keys: Keys
class Registration(TypedDict):
"""Types for registration."""
subscription: Subscription
browser: str
name: NotRequired[str]
class HTML5Entity(Entity):
"""Base entity for HTML5 integration."""
_attr_has_entity_name = True
_attr_name = None
_key: str
def __init__(
self,
config_entry: ConfigEntry,
target: str,
registrations: dict[str, Registration],
session: ClientSession,
json_path: str,
) -> None:
"""Initialize the entity."""
self.config_entry = config_entry
self.target = target
self.registrations = registrations
self.registration = registrations[target]
self.session = session
self.json_path = json_path
self._attr_unique_id = f"{config_entry.entry_id}_{target}_{self._key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
name=target,
model=self.registration["browser"].capitalize(),
identifiers={(DOMAIN, f"{config_entry.entry_id}_{target}")},
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self.target in self.registrations

View File

@@ -1,67 +0,0 @@
"""Event platform for HTML5 integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.event import EventEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ATTR_ACTION, ATTR_DATA, ATTR_TAG, DOMAIN, REGISTRATIONS_FILE
from .entity import HTML5Entity
from .notify import _load_config
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the event entity platform."""
json_path = hass.config.path(REGISTRATIONS_FILE)
registrations = await hass.async_add_executor_job(_load_config, json_path)
session = async_get_clientsession(hass)
async_add_entities(
HTML5EventEntity(config_entry, target, registrations, session, json_path)
for target in registrations
)
class HTML5EventEntity(HTML5Entity, EventEntity):
"""Representation of an event entity."""
_key = "event"
_attr_event_types = ["clicked", "received", "closed"]
_attr_translation_key = "event"
@callback
def _async_handle_event(
self, target: str, event_type: str, event_data: dict[str, Any]
) -> None:
"""Handle the event."""
if target == self.target:
self._trigger_event(
event_type,
{
**event_data.get(ATTR_DATA, {}),
ATTR_ACTION: event_data.get(ATTR_ACTION),
ATTR_TAG: event_data.get(ATTR_TAG),
},
)
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register event callback."""
self.async_on_remove(
async_dispatcher_connect(self.hass, DOMAIN, self._async_handle_event)
)

View File

@@ -1,11 +1,4 @@
{
"entity": {
"event": {
"event": {
"default": "mdi:gesture-tap-button"
}
}
},
"services": {
"dismiss": {
"service": "mdi:bell-off"

View File

@@ -1,7 +1,7 @@
{
"domain": "html5",
"name": "HTML5 Push Notifications",
"codeowners": ["@alexyao2015", "@tr4nt0r"],
"codeowners": ["@alexyao2015"],
"config_flow": true,
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/html5",

View File

@@ -8,7 +8,7 @@ from http import HTTPStatus
import json
import logging
import time
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any, NotRequired, TypedDict, cast
from urllib.parse import urlparse
import uuid
@@ -38,7 +38,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.json import save_json
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -46,19 +46,17 @@ from homeassistant.util import ensure_unique_string
from homeassistant.util.json import load_json_object
from .const import (
ATTR_ACTION,
ATTR_TAG,
ATTR_VAPID_EMAIL,
ATTR_VAPID_PRV_KEY,
ATTR_VAPID_PUB_KEY,
DOMAIN,
REGISTRATIONS_FILE,
SERVICE_DISMISS,
)
from .entity import HTML5Entity, Registration
_LOGGER = logging.getLogger(__name__)
REGISTRATIONS_FILE = "html5_push_registrations.conf"
ATTR_SUBSCRIPTION = "subscription"
ATTR_BROWSER = "browser"
@@ -69,6 +67,8 @@ ATTR_AUTH = "auth"
ATTR_P256DH = "p256dh"
ATTR_EXPIRATIONTIME = "expirationTime"
ATTR_TAG = "tag"
ATTR_ACTION = "action"
ATTR_ACTIONS = "actions"
ATTR_TYPE = "type"
ATTR_URL = "url"
@@ -156,6 +156,29 @@ HTML5_SHOWNOTIFICATION_PARAMETERS = (
)
class Keys(TypedDict):
"""Types for keys."""
p256dh: str
auth: str
class Subscription(TypedDict):
"""Types for subscription."""
endpoint: str
expirationTime: int | None
keys: Keys
class Registration(TypedDict):
"""Types for registration."""
subscription: Subscription
browser: str
name: NotRequired[str]
async def async_get_service(
hass: HomeAssistant,
config: ConfigType,
@@ -396,15 +419,7 @@ class HTML5PushCallbackView(HomeAssistantView):
)
event_name = f"{NOTIFY_CALLBACK_EVENT}.{event_payload[ATTR_TYPE]}"
hass = request.app[KEY_HASS]
hass.bus.fire(event_name, event_payload)
async_dispatcher_send(
hass,
DOMAIN,
event_payload[ATTR_TARGET],
event_payload[ATTR_TYPE],
event_payload,
)
request.app[KEY_HASS].bus.fire(event_name, event_payload)
return self.json({"status": "ok", "event": event_payload[ATTR_TYPE]})
@@ -598,11 +613,37 @@ async def async_setup_entry(
)
class HTML5NotifyEntity(HTML5Entity, NotifyEntity):
class HTML5NotifyEntity(NotifyEntity):
"""Representation of a notification entity."""
_attr_has_entity_name = True
_attr_name = None
_attr_supported_features = NotifyEntityFeature.TITLE
_key = "device"
def __init__(
self,
config_entry: ConfigEntry,
target: str,
registrations: dict[str, Registration],
session: ClientSession,
json_path: str,
) -> None:
"""Initialize the entity."""
self.config_entry = config_entry
self.target = target
self.registrations = registrations
self.registration = registrations[target]
self.session = session
self.json_path = json_path
self._attr_unique_id = f"{config_entry.entry_id}_{target}_device"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
name=target,
model=self.registration["browser"].capitalize(),
identifiers={(DOMAIN, f"{config_entry.entry_id}_{target}")},
)
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message to a device."""
@@ -673,3 +714,8 @@ class HTML5NotifyEntity(HTML5Entity, NotifyEntity):
translation_key="connection_error",
translation_placeholders={"target": self.target},
) from e
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self.target in self.registrations

View File

@@ -20,23 +20,6 @@
}
}
},
"entity": {
"event": {
"event": {
"state_attributes": {
"action": { "name": "Action" },
"event_type": {
"state": {
"clicked": "Clicked",
"closed": "Closed",
"received": "Received"
}
},
"tag": { "name": "Tag" }
}
}
}
},
"exceptions": {
"channel_expired": {
"message": "Notification channel for {target} has expired"
@@ -48,6 +31,12 @@
"message": "Sending notification to {target} failed due to a request error"
}
},
"issues": {
"deprecated_yaml_import_issue": {
"description": "Configuring HTML5 push notification using YAML has been deprecated. An automatic import of your existing configuration was attempted, but it failed.\n\nPlease remove the HTML5 push notification YAML configuration from your configuration.yaml file and reconfigure HTML5 push notification again manually.",
"title": "HTML5 YAML configuration import failed"
}
},
"services": {
"dismiss": {
"description": "Dismisses an HTML5 notification.",

View File

@@ -1,73 +1,15 @@
"""Provides conditions for humidifiers."""
from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.const import ATTR_MODE, CONF_OPTIONS, PERCENTAGE, STATE_OFF, STATE_ON
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
Condition,
ConditionConfig,
EntityStateConditionBase,
make_entity_numerical_condition,
make_entity_state_condition,
)
from homeassistant.helpers.entity import get_supported_features
from .const import (
ATTR_ACTION,
ATTR_HUMIDITY,
DOMAIN,
HumidifierAction,
HumidifierEntityFeature,
)
CONF_MODE = "mode"
IS_MODE_CONDITION_SCHEMA = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL.extend(
{
vol.Required(CONF_OPTIONS): {
vol.Required(CONF_MODE): vol.All(cv.ensure_list, vol.Length(min=1), [str]),
},
}
)
def _supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
"""Test if an entity supports the specified features."""
try:
return bool(get_supported_features(hass, entity_id) & features)
except HomeAssistantError:
return False
class IsModeCondition(EntityStateConditionBase):
"""Condition for humidifier mode."""
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_MODE)}
_schema = IS_MODE_CONDITION_SCHEMA
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
"""Initialize the mode condition."""
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.options is not None
self._states = set(config.options[CONF_MODE])
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if _supports_feature(self._hass, entity_id, HumidifierEntityFeature.MODES)
}
from .const import ATTR_ACTION, ATTR_HUMIDITY, DOMAIN, HumidifierAction
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
@@ -78,7 +20,6 @@ CONDITIONS: dict[str, type[Condition]] = {
"is_humidifying": make_entity_state_condition(
{DOMAIN: DomainSpec(value_source=ATTR_ACTION)}, HumidifierAction.HUMIDIFYING
),
"is_mode": IsModeCondition,
"is_target_humidity": make_entity_numerical_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit=PERCENTAGE,

View File

@@ -32,19 +32,6 @@ is_on: *condition_common
is_drying: *condition_common
is_humidifying: *condition_common
is_mode:
target: *condition_humidifier_target
fields:
behavior: *condition_behavior
mode:
context:
filter_target: target
required: true
selector:
state:
attribute: available_modes
multiple: true
is_target_humidity:
target: *condition_humidifier_target
fields:

View File

@@ -6,9 +6,6 @@
"is_humidifying": {
"condition": "mdi:arrow-up-bold"
},
"is_mode": {
"condition": "mdi:air-humidifier"
},
"is_off": {
"condition": "mdi:air-humidifier-off"
},

View File

@@ -28,20 +28,6 @@
},
"name": "Humidifier is humidifying"
},
"is_mode": {
"description": "Tests if one or more humidifiers are set to a specific mode.",
"fields": {
"behavior": {
"description": "[%key:component::humidifier::common::condition_behavior_description%]",
"name": "[%key:component::humidifier::common::condition_behavior_name%]"
},
"mode": {
"description": "The operation modes to check for.",
"name": "Mode"
}
},
"name": "Humidifier is in mode"
},
"is_off": {
"description": "Tests if one or more humidifiers are off.",
"fields": {

View File

@@ -10,11 +10,8 @@ from homeassistant.components.humidifier import (
ATTR_CURRENT_HUMIDITY as HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
DOMAIN as HUMIDIFIER_DOMAIN,
)
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberDeviceClass
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.components.weather import (
ATTR_WEATHER_HUMIDITY,
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec
@@ -28,9 +25,7 @@ HUMIDITY_DOMAIN_SPECS = {
value_source=HUMIDIFIER_ATTR_CURRENT_HUMIDITY,
),
SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.HUMIDITY),
WEATHER_DOMAIN: DomainSpec(
value_source=ATTR_WEATHER_HUMIDITY,
),
NUMBER_DOMAIN: DomainSpec(device_class=NumberDeviceClass.HUMIDITY),
}
CONDITIONS: dict[str, type[Condition]] = {

View File

@@ -17,9 +17,10 @@ is_value:
entity:
- domain: sensor
device_class: humidity
- domain: number
device_class: humidity
- domain: climate
- domain: humidifier
- domain: weather
fields:
behavior:
required: true

View File

@@ -11,9 +11,6 @@ from homeassistant.helpers import (
config_entry_oauth2_flow,
config_validation as cv,
)
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
@@ -45,17 +42,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: AutomowerConfigEntry) -> bool:
"""Set up this integration using UI."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
api_api = api.AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass),

Some files were not shown because too many files have changed in this diff Show More