Create ISY auxiliary sensors as sensor entities instead of attributes (#71254)

This commit is contained in:
J. Nick Koston 2022-05-03 11:49:52 -05:00 committed by GitHub
parent 60bfcc6be4
commit 8d40d9df85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 18 deletions

View File

@ -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] = []

View File

@ -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

View File

@ -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()

View File

@ -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.

View File

@ -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."""