"""Home Connect entity base class.""" from abc import abstractmethod from collections.abc import Callable, Coroutine import contextlib from datetime import datetime import logging from typing import Any, Concatenate, cast, override from aiohomeconnect.model import EventKey, OptionKey from aiohomeconnect.model.error import ( ActiveProgramNotSetError, HomeConnectError, TooManyRequestsError, ) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import API_DEFAULT_RETRY_AFTER, DOMAIN from .coordinator import HomeConnectApplianceCoordinator from .utils import get_dict_from_home_connect_error _LOGGER = logging.getLogger(__name__) class HomeConnectEntity(CoordinatorEntity[HomeConnectApplianceCoordinator]): """Generic Home Connect entity (base class).""" _attr_has_entity_name = True def __init__( self, appliance_coordinator: HomeConnectApplianceCoordinator, desc: EntityDescription, context_override: Any | None = None, ) -> None: """Initialize the entity.""" appliance_ha_id = appliance_coordinator.data.info.ha_id context = EventKey(desc.key) if context_override is not None: context = context_override super().__init__(appliance_coordinator, context) self.appliance = appliance_coordinator.data self.entity_description = desc self._attr_unique_id = f"{appliance_ha_id}-{desc.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, appliance_ha_id)}, ) self.update_native_value() @abstractmethod def update_native_value(self) -> None: """Set the value of the entity.""" @callback @override def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self.update_native_value() self.async_write_ha_state() _LOGGER.debug("Updated %s", self) @property def bsh_key(self) -> str: """Return the BSH key.""" return self.entity_description.key @property @override def available(self) -> bool: """Return True if entity is available. Do not use self.last_update_success for available state as event updates should take precedence over the coordinator refresh. """ return self.appliance.info.connected and self._attr_available async def async_set_option_with_key( self, option_key: OptionKey, value: Any ) -> None: """Set an option for the entity.""" try: # We try to set the active program option first, # if it fails we try to set the selected program option with contextlib.suppress(ActiveProgramNotSetError): await self.coordinator.client.set_active_program_option( self.appliance.info.ha_id, option_key=option_key, value=value ) return await self.coordinator.client.set_selected_program_option( self.appliance.info.ha_id, option_key=option_key, value=value ) except HomeConnectError as err: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="set_option", translation_placeholders=get_dict_from_home_connect_error(err), ) from err class HomeConnectOptionEntity(HomeConnectEntity): """Class for entities that represents program options.""" @property @override def available(self) -> bool: """Return True if entity is available.""" return super().available and self.bsh_key in self.appliance.options @property def option_value(self) -> str | int | float | bool | None: """Return the state of the entity.""" if event := self.appliance.events.get(EventKey(self.bsh_key)): return event.value return None async def async_set_option(self, value: Any) -> None: """Set an option for the entity.""" await super().async_set_option_with_key(self.bsh_key, value) @property @override def bsh_key(self) -> OptionKey: """Return the BSH key.""" return cast(OptionKey, self.entity_description.key) def constraint_fetcher[_EntityT: HomeConnectEntity, **_P]( func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]], ) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: """Catch Home Connect too many requests error and retry later. If it needs to be called later, it will call async_write_ha_state function """ async def handler_to_return( self: _EntityT, *args: _P.args, **kwargs: _P.kwargs ) -> None: async def handler(_datetime: datetime | None = None) -> None: try: await func(self, *args, **kwargs) except TooManyRequestsError as err: if (retry_after := err.retry_after) is None: retry_after = API_DEFAULT_RETRY_AFTER async_call_later(self.hass, retry_after, handler) except HomeConnectError as err: _LOGGER.error( "Error fetching constraints for %s: %s", self.entity_id, err ) else: if _datetime is not None: self.async_write_ha_state() await handler() return handler_to_return