"""Utils for Comelit.""" from collections.abc import Awaitable, Callable, Coroutine from functools import wraps from typing import TYPE_CHECKING, Any, Concatenate, Literal from aiocomelit.api import ComelitSerialBridgeObject from aiocomelit.exceptions import ( CannotAuthenticate, CannotConnect, CannotRetrieveData, DeviceStorageFailureError, ) 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 from homeassistant.helpers import ( aiohttp_client, device_registry as dr, entity_registry as er, ) from .const import DOMAIN, LOGGER, ObjectClassType from .coordinator import ComelitBaseCoordinator from .entity import ComelitBridgeBaseEntity async def async_client_session(hass: HomeAssistant) -> ClientSession: """Return a new aiohttp session.""" return aiohttp_client.async_create_clientsession( hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True) ) def load_api_data( device: ComelitSerialBridgeObject, domain: Literal["climate", "humidifier"], ) -> 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) # CLIMATE has a 2 item tuple: # - first for Clima # - second for Humidifier return device.val[0] if domain == CLIMATE_DOMAIN else device.val[1] async def cleanup_stale_entity( hass: HomeAssistant, config_entry: ConfigEntry, entry_unique_id: str, device: ComelitSerialBridgeObject, ) -> None: """Cleanup stale entity.""" entity_reg: er.EntityRegistry = er.async_get(hass) identifiers: list[str] = [] for entry in er.async_entries_for_config_entry(entity_reg, config_entry.entry_id): if entry.unique_id == entry_unique_id: entry_name = entry.name or entry.original_name LOGGER.info("Removing entity: %s [%s]", entry.entity_id, entry_name) entity_reg.async_remove(entry.entity_id) identifiers.append(f"{config_entry.entry_id}-{device.type}-{device.index}") if len(identifiers) > 0: _async_remove_stale_devices(hass, identifiers, config_entry) def _async_remove_stale_devices( hass: HomeAssistant, identifiers: list[str], config_entry: ConfigEntry ) -> None: """Remove stale devices.""" device_registry = dr.async_get(hass) for identifier in identifiers: device = device_registry.async_get_device_by_identifier( (DOMAIN, identifier), config_entry.entry_id ) if device: LOGGER.info("Removing device %s", device.name) device_registry.async_remove_device(device.id) def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P]( func: Callable[Concatenate[_T, _P], Awaitable[None]], ) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: """Catch Bridge API call exceptions.""" @wraps(func) async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: """Wrap all command methods.""" try: await func(self, *args, **kwargs) except CannotConnect as err: self.coordinator.last_update_success = False raise HomeAssistantError( translation_domain=DOMAIN, translation_key="cannot_connect", translation_placeholders={"error": repr(err)}, ) from err except CannotRetrieveData as err: self.coordinator.last_update_success = False raise HomeAssistantError( translation_domain=DOMAIN, translation_key="cannot_retrieve_data", translation_placeholders={"error": repr(err)}, ) from err except DeviceStorageFailureError as err: self.coordinator.last_update_success = False raise HomeAssistantError( translation_domain=DOMAIN, translation_key="device_storage_failure", ) from err except CannotAuthenticate: self.coordinator.last_update_success = False self.coordinator.config_entry.async_start_reauth(self.hass) return cmd_wrapper def new_device_listener( coordinator: ComelitBaseCoordinator, new_devices_callback: Callable[ [ list[ObjectClassType], str, ], None, ], data_type: str, ) -> Callable[[], None]: """Subscribe to coordinator updates to check for new devices.""" known_devices: dict[str, list[int]] = {} def _check_devices() -> None: """Check for new devices and call callback with any new monitors.""" if TYPE_CHECKING: assert coordinator.data new_devices: list[ObjectClassType] = [] for _id in coordinator.data[data_type]: if _id not in (id_list := known_devices.get(data_type, [])): known_devices.update({data_type: [*id_list, _id]}) new_devices.append(coordinator.data[data_type][_id]) if new_devices: new_devices_callback(new_devices, data_type) # Check for devices immediately _check_devices() return coordinator.async_add_listener(_check_devices)