Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion homeassistant/components/isy994/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
MANUFACTURER,
PLATFORMS,
PROGRAM_PLATFORMS,
SENSOR_AUX,
)
from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables
from .services import async_setup_services, async_unload_services
Expand Down Expand Up @@ -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] = []

Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/isy994/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@
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
Expand Down
19 changes: 10 additions & 9 deletions homeassistant/components/isy994/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
EMPTY_TIME,
EVENT_PROPS_IGNORED,
PROTO_GROUP,
PROTO_INSTEON,
PROTO_ZWAVE,
)
from pyisy.helpers import EventListener, NodeProperty
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -86,7 +88,7 @@ def device_info(self) -> DeviceInfo | None:
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.
Expand Down Expand Up @@ -151,11 +153,6 @@ def name(self) -> str:
"""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."""
Expand All @@ -169,9 +166,13 @@ def extra_state_attributes(self) -> dict:
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()

Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/isy994/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
NODE_FILTERS,
PLATFORMS,
PROGRAM_PLATFORMS,
SENSOR_AUX,
SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT,
SUBNODE_EZIO2X4_SENSORS,
Expand Down Expand Up @@ -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.
Expand Down
95 changes: 86 additions & 9 deletions homeassistant/components/isy994/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@

from typing import Any, cast

from pyisy.constants import ISY_VALUE_UNKNOWN

from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity
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,
SensorDeviceClass,
SensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant
Expand All @@ -16,6 +22,7 @@
DOMAIN as ISY994_DOMAIN,
ISY994_NODES,
ISY994_VARIABLES,
SENSOR_AUX,
UOM_DOUBLE_TEMP,
UOM_FRIENDLY_NAME,
UOM_INDEX,
Expand All @@ -25,6 +32,20 @@
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
Expand All @@ -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))

Expand All @@ -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):
Expand All @@ -69,7 +107,7 @@ def raw_unit_of_measurement(self) -> dict | str | None:
@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
Expand All @@ -80,14 +118,14 @@ def native_value(self) -> float | int | str | None:
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):
Expand All @@ -111,6 +149,45 @@ def native_unit_of_measurement(self) -> str | None:
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."""

Expand Down