diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 6a29c83f29e..953ebcdae0d 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -2,14 +2,21 @@ from __future__ import annotations from pyisy import ISY -from pyisy.constants import PROTO_INSTEON +from pyisy.constants import ( + ATTR_ACTION, + NC_NODE_ENABLED, + PROTO_INSTEON, + TAG_ADDRESS, + TAG_ENABLED, +) +from pyisy.helpers import EventListener, NodeProperty from pyisy.networking import NetworkCommand from pyisy.nodes import Node from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -98,6 +105,34 @@ class ISYNodeButtonEntity(ButtonEntity): self._attr_entity_category = entity_category self._attr_unique_id = unique_id self._attr_device_info = device_info + self._node_enabled = getattr(node, TAG_ENABLED, True) + self._availability_handler: EventListener | None = None + + @property + def available(self) -> bool: + """Return entity availability.""" + return self._node_enabled + + async def async_added_to_hass(self) -> None: + """Subscribe to the node change events.""" + # No status for NetworkResources or ISY Query buttons + if not hasattr(self._node, "status_events") or not hasattr(self._node, "isy"): + return + self._availability_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + key=self.unique_id, + ) + + @callback + def async_on_update(self, event: NodeProperty, key: str) -> None: + """Handle the update event from the ISY Node.""" + # Watch for node availability/enabled changes only + self._node_enabled = getattr(self._node, TAG_ENABLED, True) + self.async_write_ha_state() class ISYNodeQueryButtonEntity(ISYNodeButtonEntity): diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index a880025ad9f..e85a22241c9 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -1,17 +1,22 @@ """Representation of ISYEntity Types.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from pyisy.constants import ( + ATTR_ACTION, + ATTR_CONTROL, COMMAND_FRIENDLY_NAME, EMPTY_TIME, EVENT_PROPS_IGNORED, + NC_NODE_ENABLED, PROTO_INSTEON, PROTO_ZWAVE, + TAG_ADDRESS, + TAG_ENABLED, ) from pyisy.helpers import EventListener, NodeProperty -from pyisy.nodes import Group, Node +from pyisy.nodes import Group, Node, NodeChangedEvent from pyisy.programs import Program from pyisy.variables import Variable @@ -35,7 +40,7 @@ class ISYEntity(Entity): node: Node | Group | Variable | Program, device_info: DeviceInfo | None = None, ) -> None: - """Initialize the insteon device.""" + """Initialize the ISY/IoX entity.""" self._node = node self._attr_name = node.name if device_info is None: @@ -82,6 +87,22 @@ class ISYEntity(Entity): class ISYNodeEntity(ISYEntity): """Representation of a ISY Nodebase (Node/Group) entity.""" + def __init__( + self, + node: Node | Group | Variable | Program, + device_info: DeviceInfo | None = None, + ) -> None: + """Initialize the ISY/IoX node entity.""" + super().__init__(node, device_info=device_info) + if node.address == node.primary_node: + self._attr_has_entity_name = True + self._attr_name = None + + @property + def available(self) -> bool: + """Return entity availability.""" + return getattr(self._node, TAG_ENABLED, True) + @property def extra_state_attributes(self) -> dict: """Get the state attributes for the device. @@ -215,16 +236,31 @@ class ISYAuxControlEntity(Entity): self._attr_has_entity_name = node.address == node.primary_node self._attr_unique_id = unique_id self._attr_device_info = device_info - self._change_handler: EventListener | None = None + self._change_handler: EventListener = None + self._availability_handler: EventListener = None async def async_added_to_hass(self) -> None: """Subscribe to the node control change events.""" - self._change_handler = self._node.control_events.subscribe(self.async_on_update) + self._change_handler = self._node.control_events.subscribe( + self.async_on_update, + event_filter={ATTR_CONTROL: self._control}, + key=self.unique_id, + ) + self._availability_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + key=self.unique_id, + ) @callback - def async_on_update(self, event: NodeProperty) -> None: + def async_on_update(self, event: NodeProperty | NodeChangedEvent, key: str) -> None: """Handle a control event from the ISY Node.""" - # Only watch for our control changing or the node being enabled/disabled - if event.control != self._control: - return self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return entity availability.""" + return cast(bool, self._node.enabled) diff --git a/homeassistant/components/isy994/select.py b/homeassistant/components/isy994/select.py index decf95d6489..807e623768a 100644 --- a/homeassistant/components/isy994/select.py +++ b/homeassistant/components/isy994/select.py @@ -41,7 +41,6 @@ async def async_setup_entry( ) -> None: """Set up ISY/IoX select entities from config entry.""" isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id] - isy = isy_data.root device_info = isy_data.devices entities: list[ISYAuxControlIndexSelectEntity | ISYRampRateSelectEntity] = [] @@ -68,7 +67,7 @@ async def async_setup_entry( entity_detail = { "node": node, "control": control, - "unique_id": f"{isy.uuid}_{node.address}_{control}", + "unique_id": f"{isy_data.uid_base(node)}_{control}", "description": description, "device_info": device_info.get(node.primary_node), } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 35c699f2b71..b44321d9b59 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -4,8 +4,11 @@ from __future__ import annotations from typing import Any, cast from pyisy.constants import ( + ATTR_ACTION, + ATTR_CONTROL, COMMAND_FRIENDLY_NAME, ISY_VALUE_UNKNOWN, + NC_NODE_ENABLED, PROP_BATTERY_LEVEL, PROP_COMMS_ERROR, PROP_ENERGY_MODE, @@ -15,9 +18,10 @@ from pyisy.constants import ( PROP_RAMP_RATE, PROP_STATUS, PROP_TEMPERATURE, + TAG_ADDRESS, ) -from pyisy.helpers import NodeProperty -from pyisy.nodes import Node +from pyisy.helpers import EventListener, NodeProperty +from pyisy.nodes import Node, NodeChangedEvent from pyisy.variables import Variable from homeassistant.components.sensor import ( @@ -242,6 +246,8 @@ class ISYAuxSensorEntity(ISYSensorEntity): self._attr_device_class = ISY_CONTROL_TO_DEVICE_CLASS.get(control) self._attr_state_class = ISY_CONTROL_TO_STATE_CLASS.get(control) self._attr_unique_id = unique_id + self._change_handler: EventListener = None + self._availability_handler: EventListener = None name = COMMAND_FRIENDLY_NAME.get(self._control, self._control) self._attr_name = f"{node.name} {name.replace('_', ' ').title()}" @@ -266,15 +272,27 @@ class ISYAuxSensorEntity(ISYSensorEntity): this control is changed on the device and prevent duplicate firing of `isy994_control` events. """ - self._change_handler = self._node.control_events.subscribe(self.async_on_update) + self._change_handler = self._node.control_events.subscribe( + self.async_on_update, event_filter={ATTR_CONTROL: self._control} + ) + self._availability_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + ) @callback - def async_on_update(self, event: NodeProperty) -> None: + def async_on_update(self, event: NodeProperty | NodeChangedEvent) -> None: """Handle a control event from the ISY Node.""" - if event.control != self._control: - return self.async_write_ha_state() + @property + def available(self) -> bool: + """Return entity availability.""" + return cast(bool, self._node.enabled) + class ISYSensorVariableEntity(ISYEntity, SensorEntity): """Representation of an ISY variable as a sensor device.""" diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 4e3b42cb8f0..d8688487c37 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -9,27 +9,27 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import _LOGGER, DOMAIN from .entity import ISYNodeEntity, ISYProgramEntity +from .models import IsyData async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the ISY switch platform.""" - isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] - devices: dict[str, DeviceInfo] = isy_data.devices + device_info = isy_data.devices for node in isy_data.nodes[Platform.SWITCH]: primary = node.primary_node if node.protocol == PROTO_GROUP and len(node.controllers) == 1: # If Group has only 1 Controller, link to that device instead of the hub primary = node.isy.nodes.get_by_id(node.controllers[0]).primary_node - entities.append(ISYSwitchEntity(node, devices.get(primary))) + entities.append(ISYSwitchEntity(node, device_info.get(primary))) for name, status, actions in isy_data.programs[Platform.SWITCH]: entities.append(ISYSwitchProgramEntity(name, status, actions))