"""Base Entities for Homee integration.""" import logging from typing import override from pyHomee.const import AttributeType, NodeProfile, NodeState from pyHomee.model import HomeeAttribute, HomeeNode from websockets.exceptions import ConnectionClosed 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 . import HomeeConfigEntry from .const import DOMAIN from .helpers import get_name_for_enum _LOGGER = logging.getLogger(__name__) FIRST_UNAVAILABLE_ATTRIBUTE_STATE = 5 class HomeeEntity(Entity): """Represents a Homee entity consisting of a single HomeeAttribute.""" _attr_has_entity_name = True _attr_should_poll = False def __init__( self, hass: HomeAssistant, attribute: HomeeAttribute, entry: HomeeConfigEntry ) -> None: """Initialize the wrapper using a HomeeAttribute and target entity.""" self._attribute = attribute self._attr_unique_id = ( f"{entry.runtime_data.settings.uid}-{attribute.node_id}-{attribute.id}" ) self._entry = entry node = entry.runtime_data.get_node_by_id(attribute.node_id) # Homee hub itself has node-id -1 assert node is not None if node.id == -1: self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.runtime_data.settings.uid)}, ) else: self._attr_device_info = DeviceInfo( identifiers={ ( DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}", ) }, name=node.name, model=get_name_for_enum(NodeProfile, node.profile), via_device_id=dr.async_get_device_id_by_identifier( hass, (DOMAIN, entry.runtime_data.settings.uid), config_entry_id=entry.entry_id, ), ) if attribute.name: self._attr_name = attribute.name self._host_connected = entry.runtime_data.connected @override async def async_added_to_hass(self) -> None: """Add the homee attribute entity to home assistant.""" self.async_on_remove( self._attribute.add_on_changed_listener(self._on_node_updated) ) self.async_on_remove( self._entry.runtime_data.add_connection_listener( self._on_connection_changed ) ) @property @override def available(self) -> bool: """Return the availability of the underlying node.""" return ( self._attribute.state < FIRST_UNAVAILABLE_ATTRIBUTE_STATE ) and self._host_connected async def async_set_homee_value(self, value: float) -> None: """Set an attribute value on the homee node.""" homee = self._entry.runtime_data _LOGGER.debug("Setting attribute %s to %s", self._attribute.id, value) try: await homee.set_value(self._attribute.node_id, self._attribute.id, value) except ConnectionClosed as exception: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="connection_closed", ) from exception async def async_update(self) -> None: """Update entity from homee.""" homee = self._entry.runtime_data await homee.update_attribute(self._attribute.node_id, self._attribute.id) def _on_node_updated(self, attribute: HomeeAttribute) -> None: _LOGGER.debug("Update for attribute %s received", attribute.id) self.schedule_update_ha_state() async def _on_connection_changed(self, connected: bool) -> None: self._host_connected = connected self.schedule_update_ha_state() class HomeeNodeEntity(Entity): """Representation of an Entity that uses more than one HomeeAttribute.""" _attr_has_entity_name = True _attr_should_poll = False def __init__( self, hass: HomeAssistant, node: HomeeNode, entry: HomeeConfigEntry ) -> None: """Initialize the wrapper using a HomeeNode and target entity.""" self._node = node self._attr_unique_id = f"{entry.unique_id}-{node.id}" self._entry = entry ## Homee hub itself has node-id -1 if node.id == -1: self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.runtime_data.settings.uid)}, ) else: self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{entry.unique_id}-{node.id}")}, name=node.name, model=get_name_for_enum(NodeProfile, node.profile), sw_version=self._get_software_version(), via_device_id=dr.async_get_device_id_by_identifier( hass, (DOMAIN, entry.runtime_data.settings.uid), config_entry_id=entry.entry_id, ), ) self._host_connected = entry.runtime_data.connected @override async def async_added_to_hass(self) -> None: """Add the homee binary sensor device to home assistant.""" self.async_on_remove(self._node.add_on_changed_listener(self._on_node_updated)) self.async_on_remove( self._entry.runtime_data.add_connection_listener( self._on_connection_changed ) ) @property @override def available(self) -> bool: """Return the availability of the underlying node.""" return self._node.state == NodeState.AVAILABLE and self._host_connected async def async_update(self) -> None: """Fetch new state data for this node.""" # Base class requests the whole node, if only a single attribute is needed # the platform will overwrite this method. homee = self._entry.runtime_data await homee.update_node(self._node.id) def _get_software_version(self) -> str | None: """Return the software version of the node.""" if ( attribute := self._node.get_attribute_by_type( AttributeType.FIRMWARE_REVISION ) ) is not None: return str(attribute.get_value()) if ( attribute := self._node.get_attribute_by_type( AttributeType.SOFTWARE_REVISION ) ) is not None: return str(attribute.get_value()) return None async def async_set_homee_value( self, attribute: HomeeAttribute, value: float ) -> None: """Set an attribute value on the homee node.""" homee = self._entry.runtime_data _LOGGER.debug("Setting attribute %s to %s", attribute.id, value) try: await homee.set_value(attribute.node_id, attribute.id, value) except ConnectionClosed as exception: raise HomeAssistantError( translation_domain=DOMAIN, translation_key="connection_closed", ) from exception def _on_node_updated(self, node: HomeeNode) -> None: _LOGGER.debug("Update for node %s with id %s received", node.name, node.id) self.schedule_update_ha_state() async def _on_connection_changed(self, connected: bool) -> None: self._host_connected = connected self.schedule_update_ha_state()