From 8d40d9df8511e90e73a3a105a4fd1b32b63197c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 11:49:52 -0500 Subject: [PATCH] Create ISY auxiliary sensors as sensor entities instead of attributes (#71254) --- homeassistant/components/isy994/__init__.py | 3 +- homeassistant/components/isy994/const.py | 2 + homeassistant/components/isy994/entity.py | 19 +++-- homeassistant/components/isy994/helpers.py | 5 ++ homeassistant/components/isy994/sensor.py | 93 +++++++++++++++++++-- 5 files changed, 104 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index faa2f7cfb5d..e94b8215746 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -41,6 +41,7 @@ from .const import ( MANUFACTURER, PLATFORMS, PROGRAM_PLATFORMS, + SENSOR_AUX, ) from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables from .services import async_setup_services, async_unload_services @@ -120,7 +121,7 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id] = {} hass_isy_data = hass.data[DOMAIN][entry.entry_id] - hass_isy_data[ISY994_NODES] = {} + hass_isy_data[ISY994_NODES] = {SENSOR_AUX: []} for platform in PLATFORMS: hass_isy_data[ISY994_NODES][platform] = [] diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index bc463655f27..ddabe1b9680 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -191,6 +191,8 @@ UOM_INDEX = "25" UOM_ON_OFF = "2" UOM_PERCENTAGE = "51" +SENSOR_AUX = "sensor_aux" + # Do not use the Home Assistant consts for the states here - we're matching exact API # responses, not using them for Home Assistant states # Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index e5db8de5872..54ee9a2ded5 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -8,6 +8,7 @@ from pyisy.constants import ( EMPTY_TIME, EVENT_PROPS_IGNORED, PROTO_GROUP, + PROTO_INSTEON, PROTO_ZWAVE, ) from pyisy.helpers import EventListener, NodeProperty @@ -35,6 +36,7 @@ class ISYEntity(Entity): """Representation of an ISY994 device.""" _name: str | None = None + _attr_should_poll = False def __init__(self, node: Node) -> None: """Initialize the insteon device.""" @@ -86,7 +88,7 @@ class ISYEntity(Entity): node = self._node url = _async_isy_to_configuration_url(isy) - basename = self.name + basename = self._name or str(self._node.name) if hasattr(self._node, "parent_node") and self._node.parent_node is not None: # This is not the parent node, get the parent node. @@ -151,11 +153,6 @@ class ISYEntity(Entity): """Get the name of the device.""" return self._name or str(self._node.name) - @property - def should_poll(self) -> bool: - """No polling required since we're using the subscription.""" - return False - class ISYNodeEntity(ISYEntity): """Representation of a ISY Nodebase (Node/Group) entity.""" @@ -169,9 +166,13 @@ class ISYNodeEntity(ISYEntity): the combined result are returned as the device state attributes. """ attr = {} - if hasattr(self._node, "aux_properties"): - # Cast as list due to RuntimeError if a new property is added while running. - for name, value in list(self._node.aux_properties.items()): + node = self._node + # Insteon aux_properties are now their own sensors + if ( + hasattr(self._node, "aux_properties") + and getattr(node, "protocol", None) != PROTO_INSTEON + ): + for name, value in self._node.aux_properties.items(): attr_name = COMMAND_FRIENDLY_NAME.get(name, name) attr[attr_name] = str(value.formatted).lower() diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 6d0a1d303bb..7efbb3bade7 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -44,6 +44,7 @@ from .const import ( NODE_FILTERS, PLATFORMS, PROGRAM_PLATFORMS, + SENSOR_AUX, SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, SUBNODE_EZIO2X4_SENSORS, @@ -295,6 +296,10 @@ def _categorize_nodes( hass_isy_data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node) continue + if getattr(node, "protocol", None) == PROTO_INSTEON: + for control in node.aux_properties: + hass_isy_data[ISY994_NODES][SENSOR_AUX].append((node, control)) + if sensor_identifier in path or sensor_identifier in node.name: # User has specified to treat this as a sensor. First we need to # determine if it should be a binary_sensor. diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index d9751fd707b..2dee990c249 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -3,9 +3,15 @@ from __future__ import annotations from typing import Any, cast -from pyisy.constants import ISY_VALUE_UNKNOWN +from pyisy.constants import COMMAND_FRIENDLY_NAME, ISY_VALUE_UNKNOWN +from pyisy.helpers import NodeProperty +from pyisy.nodes import Node -from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity +from homeassistant.components.sensor import ( + DOMAIN as SENSOR, + SensorDeviceClass, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant @@ -16,6 +22,7 @@ from .const import ( DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_VARIABLES, + SENSOR_AUX, UOM_DOUBLE_TEMP, UOM_FRIENDLY_NAME, UOM_INDEX, @@ -25,6 +32,20 @@ from .const import ( from .entity import ISYEntity, ISYNodeEntity from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids +# Disable general purpose and redundant sensors by default +AUX_DISABLED_BY_DEFAULT = ["ERR", "GV", "CLIEMD", "CLIHCS", "DO", "OL", "RR", "ST"] + +ISY_CONTROL_TO_DEVICE_CLASS = { + "BARPRES": SensorDeviceClass.PRESSURE, + "BATLVL": SensorDeviceClass.BATTERY, + "CLIHUM": SensorDeviceClass.HUMIDITY, + "CLITEMP": SensorDeviceClass.TEMPERATURE, + "CO2LVL": SensorDeviceClass.CO2, + "CV": SensorDeviceClass.VOLTAGE, + "LUMIN": SensorDeviceClass.ILLUMINANCE, + "PF": SensorDeviceClass.POWER_FACTOR, +} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -37,6 +58,13 @@ async def async_setup_entry( _LOGGER.debug("Loading %s", node.name) entities.append(ISYSensorEntity(node)) + for node, control in hass_isy_data[ISY994_NODES][SENSOR_AUX]: + _LOGGER.debug("Loading %s %s", node.name, node.aux_properties[control]) + enabled_default = not any( + control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT + ) + entities.append(ISYAuxSensorEntity(node, control, enabled_default)) + for vname, vobj in hass_isy_data[ISY994_VARIABLES]: entities.append(ISYSensorVariableEntity(vname, vobj)) @@ -47,10 +75,20 @@ async def async_setup_entry( class ISYSensorEntity(ISYNodeEntity, SensorEntity): """Representation of an ISY994 sensor device.""" + @property + def target(self) -> Node | NodeProperty: + """Return target for the sensor.""" + return self._node + + @property + def target_value(self) -> Any: + """Return the target value.""" + return self._node.status + @property def raw_unit_of_measurement(self) -> dict | str | None: """Get the raw unit of measurement for the ISY994 sensor device.""" - uom = self._node.uom + uom = self.target.uom # Backwards compatibility for ISYv4 Firmware: if isinstance(uom, list): @@ -69,7 +107,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): @property def native_value(self) -> float | int | str | None: """Get the state of the ISY994 sensor device.""" - if (value := self._node.status) == ISY_VALUE_UNKNOWN: + if (value := self.target_value) == ISY_VALUE_UNKNOWN: return None # Get the translated ISY Unit of Measurement @@ -80,14 +118,14 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return uom.get(value, value) if uom in (UOM_INDEX, UOM_ON_OFF): - return cast(str, self._node.formatted) + return cast(str, self.target.formatted) # Check if this is an index type and get formatted value - if uom == UOM_INDEX and hasattr(self._node, "formatted"): - return cast(str, self._node.formatted) + if uom == UOM_INDEX and hasattr(self.target, "formatted"): + return cast(str, self.target.formatted) # Handle ISY precision and rounding - value = convert_isy_value_to_hass(value, uom, self._node.prec) + value = convert_isy_value_to_hass(value, uom, self.target.prec) # Convert temperatures to Home Assistant's unit if uom in (TEMP_CELSIUS, TEMP_FAHRENHEIT): @@ -111,6 +149,45 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return raw_units +class ISYAuxSensorEntity(ISYSensorEntity): + """Representation of an ISY994 aux sensor device.""" + + def __init__(self, node: Node, control: str, enabled_default: bool) -> None: + """Initialize the ISY994 aux sensor.""" + super().__init__(node) + self._control = control + self._attr_entity_registry_enabled_default = enabled_default + + @property + def device_class(self) -> SensorDeviceClass | str | None: + """Return the device class for the sensor.""" + return ISY_CONTROL_TO_DEVICE_CLASS.get(self._control, super().device_class) + + @property + def target(self) -> Node | NodeProperty: + """Return target for the sensor.""" + return cast(NodeProperty, self._node.aux_properties[self._control]) + + @property + def target_value(self) -> Any: + """Return the target value.""" + return self.target.value + + @property + def unique_id(self) -> str | None: + """Get the unique identifier of the device and aux sensor.""" + if not hasattr(self._node, "address"): + return None + return f"{self._node.isy.configuration['uuid']}_{self._node.address}_{self._control}" + + @property + def name(self) -> str: + """Get the name of the device and aux sensor.""" + base_name = self._name or str(self._node.name) + name = COMMAND_FRIENDLY_NAME.get(self._control, self._control) + return f"{base_name} {name.replace('_', ' ').title()}" + + class ISYSensorVariableEntity(ISYEntity, SensorEntity): """Representation of an ISY994 variable as a sensor device."""