"""Support for VELUX KLF 200 devices.""" from collections.abc import Awaitable, Callable, Coroutine from functools import wraps import logging from typing import Any, override from pyvlx import Node, PyVLXException from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from .const import DOMAIN _LOGGER = logging.getLogger(__name__) def velux_unique_id(node: Node, config_entry_id: str) -> str: """Build unique ID base for a Velux node.""" return node.serial_number or f"{config_entry_id}_{node.node_id}" def velux_device_info( hass: HomeAssistant, node: Node, config_entry_id: str ) -> DeviceInfo: """Build DeviceInfo for a Velux node.""" unique_id = velux_unique_id(node, config_entry_id) return DeviceInfo( identifiers={(DOMAIN, unique_id)}, name=node.name or f"#{node.node_id}", serial_number=node.serial_number, via_device_id=dr.async_get_device_id_by_identifier( hass, (DOMAIN, f"gateway_{config_entry_id}"), config_entry_id=config_entry_id, ), ) def wrap_pyvlx_call_exceptions[**P]( func: Callable[P, Coroutine[Any, Any, None]], ) -> Callable[P, Coroutine[Any, Any, None]]: """Decorate pyvlx calls to handle exceptions. Catches OSError and PyVLXException and wraps them into HomeAssistantError with translation support. """ @wraps(func) async def wrapper(*args: P.args, **kwargs: P.kwargs) -> None: """Wrap async function to catch exceptions thrown in pyvlx calls.""" try: await func(*args, **kwargs) except (OSError, PyVLXException) as err: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="device_communication_error", translation_placeholders={"error": str(err)}, ) from err return wrapper class VeluxEntity(Entity): """Abstraction for all Velux entities.""" _attr_should_poll = False _attr_has_entity_name = True update_callback: Callable[[Node], Awaitable[None]] | None = None _attr_available = True _unavailable_logged = False def __init__(self, hass: HomeAssistant, node: Node, config_entry_id: str) -> None: """Initialize the Velux device.""" self.node = node self._attr_unique_id = velux_unique_id(node, config_entry_id) self._attr_device_info = velux_device_info(hass, node, config_entry_id) async def after_update_callback(self, _: Node) -> None: """Call after device was updated.""" self._attr_available = self.node.pyvlx.get_connected() if not self._attr_available: if not self._unavailable_logged: _LOGGER.info("Entity %s is unavailable", self.entity_id) self._unavailable_logged = True elif self._unavailable_logged: _LOGGER.info("Entity %s is back online", self.entity_id) self._unavailable_logged = False self.async_write_ha_state() @override async def async_added_to_hass(self) -> None: """Register callback and store reference for cleanup.""" self.update_callback = self.after_update_callback self.node.register_device_updated_cb(self.update_callback) @override async def async_will_remove_from_hass(self) -> None: """Clean up registered callbacks.""" if self.update_callback: self.node.unregister_device_updated_cb(self.update_callback) self.update_callback = None