"""Base Entities for Homee integration.""" from pyHomee.const import AttributeState, AttributeType, NodeProfile, NodeState from pyHomee.model import HomeeAttribute, HomeeNode from websockets.exceptions import ConnectionClosed from homeassistant.exceptions import HomeAssistantError 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 class HomeeEntity(Entity): """Represents a Homee entity consisting of a single HomeeAttribute.""" _attr_has_entity_name = True _attr_should_poll = False def __init__(self, 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=(DOMAIN, entry.runtime_data.settings.uid), ) if attribute.name: self._attr_name = attribute.name self._host_connected = entry.runtime_data.connected 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 def available(self) -> bool: """Return the availability of the underlying node.""" return (self._attribute.state == AttributeState.NORMAL) 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 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: 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, 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=(DOMAIN, entry.runtime_data.settings.uid), ) self._host_connected = entry.runtime_data.connected 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 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 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: self.schedule_update_ha_state() async def _on_connection_changed(self, connected: bool) -> None: self._host_connected = connected self.schedule_update_ha_state()