Move ISY994 data to dataclass and remove bad entities (#85744)

This commit is contained in:
shbatm 2023-01-12 17:09:04 -06:00 committed by GitHub
parent f941864308
commit 28bea53afe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 321 additions and 331 deletions

View file

@ -610,6 +610,7 @@ omit =
homeassistant/components/isy994/helpers.py homeassistant/components/isy994/helpers.py
homeassistant/components/isy994/light.py homeassistant/components/isy994/light.py
homeassistant/components/isy994/lock.py homeassistant/components/isy994/lock.py
homeassistant/components/isy994/models.py
homeassistant/components/isy994/number.py homeassistant/components/isy994/number.py
homeassistant/components/isy994/sensor.py homeassistant/components/isy994/sensor.py
homeassistant/components/isy994/services.py homeassistant/components/isy994/services.py

View file

@ -43,25 +43,15 @@ from .const import (
ISY_CONF_MODEL, ISY_CONF_MODEL,
ISY_CONF_NAME, ISY_CONF_NAME,
ISY_CONF_NETWORKING, ISY_CONF_NETWORKING,
ISY_DEVICES,
ISY_NET_RES,
ISY_NODES,
ISY_PROGRAMS,
ISY_ROOT,
ISY_ROOT_NODES,
ISY_VARIABLES,
MANUFACTURER, MANUFACTURER,
NODE_PLATFORMS,
PLATFORMS, PLATFORMS,
PROGRAM_PLATFORMS,
ROOT_NODE_PLATFORMS,
SCHEME_HTTP, SCHEME_HTTP,
SCHEME_HTTPS, SCHEME_HTTPS,
SENSOR_AUX,
VARIABLE_PLATFORMS,
) )
from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables
from .models import IsyData
from .services import async_setup_services, async_unload_services from .services import async_setup_services, async_unload_services
from .util import _async_cleanup_registry_entries
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
@ -135,15 +125,7 @@ async def async_setup_entry(
# they are missing from the options # they are missing from the options
_async_import_options_from_data_if_missing(hass, entry) _async_import_options_from_data_if_missing(hass, entry)
hass.data[DOMAIN][entry.entry_id] = {} isy_data = hass.data[DOMAIN][entry.entry_id] = IsyData()
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
hass_isy_data[ISY_NODES] = {p: [] for p in (NODE_PLATFORMS + [SENSOR_AUX])}
hass_isy_data[ISY_ROOT_NODES] = {p: [] for p in ROOT_NODE_PLATFORMS}
hass_isy_data[ISY_PROGRAMS] = {p: [] for p in PROGRAM_PLATFORMS}
hass_isy_data[ISY_VARIABLES] = {p: [] for p in VARIABLE_PLATFORMS}
hass_isy_data[ISY_NET_RES] = []
hass_isy_data[ISY_DEVICES] = {}
isy_config = entry.data isy_config = entry.data
isy_options = entry.options isy_options = entry.options
@ -212,34 +194,37 @@ async def async_setup_entry(
f"Invalid response ISY, device is likely still starting: {err}" f"Invalid response ISY, device is likely still starting: {err}"
) from err ) from err
_categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) _categorize_nodes(isy_data, isy.nodes, ignore_identifier, sensor_identifier)
_categorize_programs(hass_isy_data, isy.programs) _categorize_programs(isy_data, isy.programs)
# Categorize variables call to be removed with variable sensors in 2023.5.0 # Categorize variables call to be removed with variable sensors in 2023.5.0
_categorize_variables(hass_isy_data, isy.variables, variable_identifier) _categorize_variables(isy_data, isy.variables, variable_identifier)
# Gather ISY Variables to be added. Identifier used to enable by default. # Gather ISY Variables to be added. Identifier used to enable by default.
if len(isy.variables.children) > 0: if isy.variables.children:
hass_isy_data[ISY_DEVICES][CONF_VARIABLES] = _create_service_device_info( isy_data.devices[CONF_VARIABLES] = _create_service_device_info(
isy, name=CONF_VARIABLES.title(), unique_id=CONF_VARIABLES isy, name=CONF_VARIABLES.title(), unique_id=CONF_VARIABLES
) )
numbers = hass_isy_data[ISY_VARIABLES][Platform.NUMBER] numbers = isy_data.variables[Platform.NUMBER]
for vtype, vname, vid in isy.variables.children: for vtype, _, vid in isy.variables.children:
numbers.append((isy.variables[vtype][vid], variable_identifier in vname)) numbers.append(isy.variables[vtype][vid])
if isy.conf[ISY_CONF_NETWORKING]: if isy.conf[ISY_CONF_NETWORKING]:
hass_isy_data[ISY_DEVICES][CONF_NETWORK] = _create_service_device_info( isy_data.devices[CONF_NETWORK] = _create_service_device_info(
isy, name=ISY_CONF_NETWORKING, unique_id=CONF_NETWORK isy, name=ISY_CONF_NETWORKING, unique_id=CONF_NETWORK
) )
for resource in isy.networking.nobjs: for resource in isy.networking.nobjs:
hass_isy_data[ISY_NET_RES].append(resource) isy_data.net_resources.append(resource)
# Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs # Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs
_LOGGER.info(repr(isy.clock)) _LOGGER.info(repr(isy.clock))
hass_isy_data[ISY_ROOT] = isy isy_data.root = isy
_async_get_or_create_isy_device_in_registry(hass, entry, isy) _async_get_or_create_isy_device_in_registry(hass, entry, isy)
# Load platforms for the devices in the ISY controller that we support. # Load platforms for the devices in the ISY controller that we support.
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Clean-up any old entities that we no longer provide.
_async_cleanup_registry_entries(hass, entry.entry_id)
@callback @callback
def _async_stop_auto_update(event: Event) -> None: def _async_stop_auto_update(event: Event) -> None:
"""Stop the isy auto update on Home Assistant Shutdown.""" """Stop the isy auto update on Home Assistant Shutdown."""
@ -328,9 +313,9 @@ async def async_unload_entry(
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass_isy_data = hass.data[DOMAIN][entry.entry_id] isy_data = hass.data[DOMAIN][entry.entry_id]
isy: ISY = hass_isy_data[ISY_ROOT] isy: ISY = isy_data.root
_LOGGER.debug("ISY Stopping Event Stream and automatic updates") _LOGGER.debug("ISY Stopping Event Stream and automatic updates")
isy.websocket.stop() isy.websocket.stop()
@ -349,7 +334,7 @@ async def async_remove_config_entry_device(
device_entry: dr.DeviceEntry, device_entry: dr.DeviceEntry,
) -> bool: ) -> bool:
"""Remove ISY config entry from a device.""" """Remove ISY config entry from a device."""
hass_isy_devices = hass.data[DOMAIN][config_entry.entry_id][ISY_DEVICES] isy_data = hass.data[DOMAIN][config_entry.entry_id]
return not device_entry.identifiers.intersection( return not device_entry.identifiers.intersection(
(DOMAIN, unique_id) for unique_id in hass_isy_devices (DOMAIN, unique_id) for unique_id in isy_data.devices
) )

View file

@ -32,9 +32,6 @@ from .const import (
BINARY_SENSOR_DEVICE_TYPES_ISY, BINARY_SENSOR_DEVICE_TYPES_ISY,
BINARY_SENSOR_DEVICE_TYPES_ZWAVE, BINARY_SENSOR_DEVICE_TYPES_ZWAVE,
DOMAIN, DOMAIN,
ISY_DEVICES,
ISY_NODES,
ISY_PROGRAMS,
SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT, SUBNODE_CLIMATE_HEAT,
SUBNODE_DUSK_DAWN, SUBNODE_DUSK_DAWN,
@ -77,9 +74,9 @@ async def async_setup_entry(
] = [] ] = []
entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity
hass_isy_data = hass.data[DOMAIN][entry.entry_id] isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] devices: dict[str, DeviceInfo] = isy_data.devices
for node in hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR]: for node in isy_data.nodes[Platform.BINARY_SENSOR]:
assert isinstance(node, Node) assert isinstance(node, Node)
device_info = devices.get(node.primary_node) device_info = devices.get(node.primary_node)
device_class, device_type = _detect_device_type_and_class(node) device_class, device_type = _detect_device_type_and_class(node)
@ -205,7 +202,7 @@ async def async_setup_entry(
) )
entities.append(entity) entities.append(entity)
for name, status, _ in hass_isy_data[ISY_PROGRAMS][Platform.BINARY_SENSOR]: for name, status, _ in isy_data.programs[Platform.BINARY_SENSOR]:
entities.append(ISYBinarySensorProgramEntity(name, status)) entities.append(ISYBinarySensorProgramEntity(name, status))
async_add_entities(entities) async_add_entities(entities)

View file

@ -13,14 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import CONF_NETWORK, DOMAIN
CONF_NETWORK,
DOMAIN,
ISY_DEVICES,
ISY_NET_RES,
ISY_ROOT,
ISY_ROOT_NODES,
)
async def async_setup_entry( async def async_setup_entry(
@ -29,21 +22,21 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up ISY/IoX button from config entry.""" """Set up ISY/IoX button from config entry."""
hass_isy_data = hass.data[DOMAIN][config_entry.entry_id] isy_data = hass.data[DOMAIN][config_entry.entry_id]
isy: ISY = hass_isy_data[ISY_ROOT] isy: ISY = isy_data.root
device_info = hass_isy_data[ISY_DEVICES] device_info = isy_data.devices
entities: list[ entities: list[
ISYNodeQueryButtonEntity ISYNodeQueryButtonEntity
| ISYNodeBeepButtonEntity | ISYNodeBeepButtonEntity
| ISYNetworkResourceButtonEntity | ISYNetworkResourceButtonEntity
] = [] ] = []
for node in hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON]: for node in isy_data.root_nodes[Platform.BUTTON]:
entities.append( entities.append(
ISYNodeQueryButtonEntity( ISYNodeQueryButtonEntity(
node=node, node=node,
name="Query", name="Query",
unique_id=f"{isy.uuid}_{node.address}_query", unique_id=f"{isy_data.uid_base(node)}_query",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
device_info=device_info[node.address], device_info=device_info[node.address],
) )
@ -53,18 +46,18 @@ async def async_setup_entry(
ISYNodeBeepButtonEntity( ISYNodeBeepButtonEntity(
node=node, node=node,
name="Beep", name="Beep",
unique_id=f"{isy.uuid}_{node.address}_beep", unique_id=f"{isy_data.uid_base(node)}_beep",
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
device_info=device_info[node.address], device_info=device_info[node.address],
) )
) )
for node in hass_isy_data[ISY_NET_RES]: for node in isy_data.net_resources:
entities.append( entities.append(
ISYNetworkResourceButtonEntity( ISYNetworkResourceButtonEntity(
node=node, node=node,
name=node.name, name=node.name,
unique_id=f"{isy.uuid}_{CONF_NETWORK}_{node.address}", unique_id=isy_data.uid_base(node),
device_info=device_info[CONF_NETWORK], device_info=device_info[CONF_NETWORK],
) )
) )

View file

@ -43,9 +43,7 @@ from .const import (
DOMAIN, DOMAIN,
HA_FAN_TO_ISY, HA_FAN_TO_ISY,
HA_HVAC_TO_ISY, HA_HVAC_TO_ISY,
ISY_DEVICES,
ISY_HVAC_MODES, ISY_HVAC_MODES,
ISY_NODES,
UOM_FAN_MODES, UOM_FAN_MODES,
UOM_HVAC_ACTIONS, UOM_HVAC_ACTIONS,
UOM_HVAC_MODE_GENERIC, UOM_HVAC_MODE_GENERIC,
@ -65,9 +63,9 @@ async def async_setup_entry(
"""Set up the ISY thermostat platform.""" """Set up the ISY thermostat platform."""
entities = [] entities = []
hass_isy_data = hass.data[DOMAIN][entry.entry_id] isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] devices: dict[str, DeviceInfo] = isy_data.devices
for node in hass_isy_data[ISY_NODES][Platform.CLIMATE]: for node in isy_data.nodes[Platform.CLIMATE]:
entities.append(ISYThermostatEntity(node, devices.get(node.primary_node))) entities.append(ISYThermostatEntity(node, devices.get(node.primary_node)))
async_add_entities(entities) async_add_entities(entities)

View file

@ -1,6 +1,8 @@
"""Constants for the ISY Platform.""" """Constants for the ISY Platform."""
import logging import logging
from pyisy.constants import PROP_ON_LEVEL, PROP_RAMP_RATE
from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.climate import ( from homeassistant.components.climate import (
FAN_AUTO, FAN_AUTO,
@ -85,6 +87,7 @@ NODE_PLATFORMS = [
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
] ]
NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR]
PROGRAM_PLATFORMS = [ PROGRAM_PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.COVER, Platform.COVER,
@ -98,6 +101,7 @@ VARIABLE_PLATFORMS = [Platform.NUMBER, Platform.SENSOR]
# Set of all platforms used by integration # Set of all platforms used by integration
PLATFORMS = { PLATFORMS = {
*NODE_PLATFORMS, *NODE_PLATFORMS,
*NODE_AUX_PROP_PLATFORMS,
*PROGRAM_PLATFORMS, *PROGRAM_PLATFORMS,
*ROOT_NODE_PLATFORMS, *ROOT_NODE_PLATFORMS,
*VARIABLE_PLATFORMS, *VARIABLE_PLATFORMS,
@ -109,14 +113,6 @@ SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"]
# (they can turn off, and report their state) # (they can turn off, and report their state)
ISY_GROUP_PLATFORM = Platform.SWITCH ISY_GROUP_PLATFORM = Platform.SWITCH
ISY_ROOT = "isy"
ISY_ROOT_NODES = "isy_root_nodes"
ISY_NET_RES = "isy_net_res"
ISY_NODES = "isy_nodes"
ISY_PROGRAMS = "isy_programs"
ISY_VARIABLES = "isy_variables"
ISY_DEVICES = "isy_devices"
ISY_CONF_NETWORKING = "Networking Module" ISY_CONF_NETWORKING = "Networking Module"
ISY_CONF_UUID = "uuid" ISY_CONF_UUID = "uuid"
ISY_CONF_NAME = "name" ISY_CONF_NAME = "name"
@ -186,8 +182,6 @@ UOM_INDEX = "25"
UOM_ON_OFF = "2" UOM_ON_OFF = "2"
UOM_PERCENTAGE = "51" UOM_PERCENTAGE = "51"
SENSOR_AUX = "sensor_aux"
# Do not use the Home Assistant consts for the states here - we're matching exact API # Do not use the Home Assistant consts for the states here - we're matching exact API
# responses, not using them for Home Assistant states # responses, not using them for Home Assistant states
# Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml # Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml
@ -313,6 +307,10 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
FILTER_ZWAVE_CAT: ["140"], FILTER_ZWAVE_CAT: ["140"],
}, },
} }
NODE_AUX_FILTERS: dict[str, Platform] = {
PROP_ON_LEVEL: Platform.SENSOR,
PROP_RAMP_RATE: Platform.SENSOR,
}
UOM_FRIENDLY_NAME = { UOM_FRIENDLY_NAME = {
"1": UnitOfElectricCurrent.AMPERE, "1": UnitOfElectricCurrent.AMPERE,

View file

@ -16,15 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import _LOGGER, DOMAIN, UOM_8_BIT_RANGE, UOM_BARRIER
_LOGGER,
DOMAIN,
ISY_DEVICES,
ISY_NODES,
ISY_PROGRAMS,
UOM_8_BIT_RANGE,
UOM_BARRIER,
)
from .entity import ISYNodeEntity, ISYProgramEntity from .entity import ISYNodeEntity, ISYProgramEntity
@ -32,13 +24,13 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY cover platform.""" """Set up the ISY cover platform."""
hass_isy_data = hass.data[DOMAIN][entry.entry_id] isy_data = hass.data[DOMAIN][entry.entry_id]
entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] entities: list[ISYCoverEntity | ISYCoverProgramEntity] = []
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] devices: dict[str, DeviceInfo] = isy_data.devices
for node in hass_isy_data[ISY_NODES][Platform.COVER]: for node in isy_data.nodes[Platform.COVER]:
entities.append(ISYCoverEntity(node, devices.get(node.primary_node))) entities.append(ISYCoverEntity(node, devices.get(node.primary_node)))
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.COVER]: for name, status, actions in isy_data.programs[Platform.COVER]:
entities.append(ISYCoverProgramEntity(name, status, actions)) entities.append(ISYCoverProgramEntity(name, status, actions))
async_add_entities(entities) async_add_entities(entities)

View file

@ -11,7 +11,7 @@ from pyisy.constants import (
PROTO_ZWAVE, PROTO_ZWAVE,
) )
from pyisy.helpers import EventListener, NodeProperty from pyisy.helpers import EventListener, NodeProperty
from pyisy.nodes import Node from pyisy.nodes import Group, Node
from pyisy.programs import Program from pyisy.programs import Program
from pyisy.variables import Variable from pyisy.variables import Variable
@ -30,7 +30,11 @@ class ISYEntity(Entity):
_attr_should_poll = False _attr_should_poll = False
_node: Node | Program | Variable _node: Node | Program | Variable
def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None: def __init__(
self,
node: Node | Group | Variable | Program,
device_info: DeviceInfo | None = None,
) -> None:
"""Initialize the insteon device.""" """Initialize the insteon device."""
self._node = node self._node = node
self._attr_name = node.name self._attr_name = node.name
@ -89,10 +93,7 @@ class ISYNodeEntity(ISYEntity):
attr = {} attr = {}
node = self._node node = self._node
# Insteon aux_properties are now their own sensors # Insteon aux_properties are now their own sensors
if ( if hasattr(self._node, "aux_properties") and node.protocol != PROTO_INSTEON:
hasattr(self._node, "aux_properties")
and getattr(node, "protocol", None) != PROTO_INSTEON
):
for name, value in self._node.aux_properties.items(): for name, value in self._node.aux_properties.items():
attr_name = COMMAND_FRIENDLY_NAME.get(name, name) attr_name = COMMAND_FRIENDLY_NAME.get(name, name)
attr[attr_name] = str(value.formatted).lower() attr[attr_name] = str(value.formatted).lower()
@ -128,7 +129,7 @@ class ISYNodeEntity(ISYEntity):
async def async_get_zwave_parameter(self, parameter: Any) -> None: async def async_get_zwave_parameter(self, parameter: Any) -> None:
"""Respond to an entity service command to request a Z-Wave device parameter from the ISY.""" """Respond to an entity service command to request a Z-Wave device parameter from the ISY."""
if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: if self._node.protocol != PROTO_ZWAVE:
raise HomeAssistantError( raise HomeAssistantError(
"Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave" "Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave"
f" device {self.entity_id}" f" device {self.entity_id}"
@ -139,7 +140,7 @@ class ISYNodeEntity(ISYEntity):
self, parameter: Any, value: Any | None, size: int | None self, parameter: Any, value: Any | None, size: int | None
) -> None: ) -> None:
"""Respond to an entity service command to set a Z-Wave device parameter via the ISY.""" """Respond to an entity service command to set a Z-Wave device parameter via the ISY."""
if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE: if self._node.protocol != PROTO_ZWAVE:
raise HomeAssistantError( raise HomeAssistantError(
"Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave" "Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave"
f" device {self.entity_id}" f" device {self.entity_id}"
@ -155,7 +156,10 @@ class ISYNodeEntity(ISYEntity):
class ISYProgramEntity(ISYEntity): class ISYProgramEntity(ISYEntity):
"""Representation of an ISY program base.""" """Representation of an ISY program base."""
def __init__(self, name: str, status: Any | None, actions: Program = None) -> None: _actions: Program
_status: Program
def __init__(self, name: str, status: Program, actions: Program = None) -> None:
"""Initialize the ISY program-based entity.""" """Initialize the ISY program-based entity."""
super().__init__(status) super().__init__(status)
self._attr_name = name self._attr_name = name

View file

@ -18,7 +18,7 @@ from homeassistant.util.percentage import (
ranged_value_to_percentage, ranged_value_to_percentage,
) )
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS from .const import _LOGGER, DOMAIN
from .entity import ISYNodeEntity, ISYProgramEntity from .entity import ISYNodeEntity, ISYProgramEntity
SPEED_RANGE = (1, 255) # off is not included SPEED_RANGE = (1, 255) # off is not included
@ -28,14 +28,14 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY fan platform.""" """Set up the ISY fan platform."""
hass_isy_data = hass.data[DOMAIN][entry.entry_id] isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] devices: dict[str, DeviceInfo] = isy_data.devices
entities: list[ISYFanEntity | ISYFanProgramEntity] = [] entities: list[ISYFanEntity | ISYFanProgramEntity] = []
for node in hass_isy_data[ISY_NODES][Platform.FAN]: for node in isy_data.nodes[Platform.FAN]:
entities.append(ISYFanEntity(node, devices.get(node.primary_node))) entities.append(ISYFanEntity(node, devices.get(node.primary_node)))
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.FAN]: for name, status, actions in isy_data.programs[Platform.FAN]:
entities.append(ISYFanProgramEntity(name, status, actions)) entities.append(ISYFanProgramEntity(name, status, actions))
async_add_entities(entities) async_add_entities(entities)

View file

@ -5,6 +5,11 @@ from typing import cast
from pyisy.constants import ( from pyisy.constants import (
ISY_VALUE_UNKNOWN, ISY_VALUE_UNKNOWN,
PROP_BUSY,
PROP_COMMS_ERROR,
PROP_ON_LEVEL,
PROP_RAMP_RATE,
PROP_STATUS,
PROTO_GROUP, PROTO_GROUP,
PROTO_INSTEON, PROTO_INSTEON,
PROTO_PROGRAM, PROTO_PROGRAM,
@ -27,18 +32,12 @@ from .const import (
FILTER_STATES, FILTER_STATES,
FILTER_UOM, FILTER_UOM,
FILTER_ZWAVE_CAT, FILTER_ZWAVE_CAT,
ISY_DEVICES,
ISY_GROUP_PLATFORM, ISY_GROUP_PLATFORM,
ISY_NODES,
ISY_PROGRAMS,
ISY_ROOT_NODES,
ISY_VARIABLES,
KEY_ACTIONS, KEY_ACTIONS,
KEY_STATUS, KEY_STATUS,
NODE_FILTERS, NODE_FILTERS,
NODE_PLATFORMS, NODE_PLATFORMS,
PROGRAM_PLATFORMS, PROGRAM_PLATFORMS,
SENSOR_AUX,
SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT, SUBNODE_CLIMATE_HEAT,
SUBNODE_EZIO2X4_SENSORS, SUBNODE_EZIO2X4_SENSORS,
@ -49,13 +48,19 @@ from .const import (
UOM_DOUBLE_TEMP, UOM_DOUBLE_TEMP,
UOM_ISYV4_DEGREES, UOM_ISYV4_DEGREES,
) )
from .models import IsyData
BINARY_SENSOR_UOMS = ["2", "78"] BINARY_SENSOR_UOMS = ["2", "78"]
BINARY_SENSOR_ISY_STATES = ["on", "off"] BINARY_SENSOR_ISY_STATES = ["on", "off"]
ROOT_AUX_CONTROLS = {
PROP_ON_LEVEL,
PROP_RAMP_RATE,
}
SKIP_AUX_PROPS = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS, *ROOT_AUX_CONTROLS}
def _check_for_node_def( def _check_for_node_def(
hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None
) -> bool: ) -> bool:
"""Check if the node matches the node_def_id for any platforms. """Check if the node matches the node_def_id for any platforms.
@ -71,14 +76,14 @@ def _check_for_node_def(
platforms = NODE_PLATFORMS if not single_platform else [single_platform] platforms = NODE_PLATFORMS if not single_platform else [single_platform]
for platform in platforms: for platform in platforms:
if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]: if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]:
hass_isy_data[ISY_NODES][platform].append(node) isy_data.nodes[platform].append(node)
return True return True
return False return False
def _check_for_insteon_type( def _check_for_insteon_type(
hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None
) -> bool: ) -> bool:
"""Check if the node matches the Insteon type for any platforms. """Check if the node matches the Insteon type for any platforms.
@ -107,7 +112,7 @@ def _check_for_insteon_type(
# FanLinc, which has a light module as one of its nodes. # FanLinc, which has a light module as one of its nodes.
if platform == Platform.FAN and subnode_id == SUBNODE_FANLINC_LIGHT: if platform == Platform.FAN and subnode_id == SUBNODE_FANLINC_LIGHT:
hass_isy_data[ISY_NODES][Platform.LIGHT].append(node) isy_data.nodes[Platform.LIGHT].append(node)
return True return True
# Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3 # Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3
@ -115,7 +120,7 @@ def _check_for_insteon_type(
SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT, SUBNODE_CLIMATE_HEAT,
): ):
hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR].append(node) isy_data.nodes[Platform.BINARY_SENSOR].append(node)
return True return True
# IOLincs which have a sensor and relay on 2 different nodes # IOLincs which have a sensor and relay on 2 different nodes
@ -124,7 +129,7 @@ def _check_for_insteon_type(
and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS) and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS)
and subnode_id == SUBNODE_IOLINC_RELAY and subnode_id == SUBNODE_IOLINC_RELAY
): ):
hass_isy_data[ISY_NODES][Platform.SWITCH].append(node) isy_data.nodes[Platform.SWITCH].append(node)
return True return True
# Smartenit EZIO2X4 # Smartenit EZIO2X4
@ -133,17 +138,17 @@ def _check_for_insteon_type(
and device_type.startswith(TYPE_EZIO2X4) and device_type.startswith(TYPE_EZIO2X4)
and subnode_id in SUBNODE_EZIO2X4_SENSORS and subnode_id in SUBNODE_EZIO2X4_SENSORS
): ):
hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR].append(node) isy_data.nodes[Platform.BINARY_SENSOR].append(node)
return True return True
hass_isy_data[ISY_NODES][platform].append(node) isy_data.nodes[platform].append(node)
return True return True
return False return False
def _check_for_zwave_cat( def _check_for_zwave_cat(
hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None
) -> bool: ) -> bool:
"""Check if the node matches the ISY Z-Wave Category for any platforms. """Check if the node matches the ISY Z-Wave Category for any platforms.
@ -164,14 +169,14 @@ def _check_for_zwave_cat(
device_type.startswith(t) device_type.startswith(t)
for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT]) for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT])
): ):
hass_isy_data[ISY_NODES][platform].append(node) isy_data.nodes[platform].append(node)
return True return True
return False return False
def _check_for_uom_id( def _check_for_uom_id(
hass_isy_data: dict, isy_data: IsyData,
node: Group | Node, node: Group | Node,
single_platform: Platform | None = None, single_platform: Platform | None = None,
uom_list: list[str] | None = None, uom_list: list[str] | None = None,
@ -190,23 +195,23 @@ def _check_for_uom_id(
if isinstance(node.uom, list): if isinstance(node.uom, list):
node_uom = node.uom[0] node_uom = node.uom[0]
if uom_list: if uom_list and single_platform:
if node_uom in uom_list: if node_uom in uom_list:
hass_isy_data[ISY_NODES][single_platform].append(node) isy_data.nodes[single_platform].append(node)
return True return True
return False return False
platforms = NODE_PLATFORMS if not single_platform else [single_platform] platforms = NODE_PLATFORMS if not single_platform else [single_platform]
for platform in platforms: for platform in platforms:
if node_uom in NODE_FILTERS[platform][FILTER_UOM]: if node_uom in NODE_FILTERS[platform][FILTER_UOM]:
hass_isy_data[ISY_NODES][platform].append(node) isy_data.nodes[platform].append(node)
return True return True
return False return False
def _check_for_states_in_uom( def _check_for_states_in_uom(
hass_isy_data: dict, isy_data: IsyData,
node: Group | Node, node: Group | Node,
single_platform: Platform | None = None, single_platform: Platform | None = None,
states_list: list[str] | None = None, states_list: list[str] | None = None,
@ -227,28 +232,26 @@ def _check_for_states_in_uom(
node_uom = set(map(str.lower, node.uom)) node_uom = set(map(str.lower, node.uom))
if states_list: if states_list and single_platform:
if node_uom == set(states_list): if node_uom == set(states_list):
hass_isy_data[ISY_NODES][single_platform].append(node) isy_data.nodes[single_platform].append(node)
return True return True
return False return False
platforms = NODE_PLATFORMS if not single_platform else [single_platform] platforms = NODE_PLATFORMS if not single_platform else [single_platform]
for platform in platforms: for platform in platforms:
if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]): if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]):
hass_isy_data[ISY_NODES][platform].append(node) isy_data.nodes[platform].append(node)
return True return True
return False return False
def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: def _is_sensor_a_binary_sensor(isy_data: IsyData, node: Group | Node) -> bool:
"""Determine if the given sensor node should be a binary_sensor.""" """Determine if the given sensor node should be a binary_sensor."""
if _check_for_node_def(hass_isy_data, node, single_platform=Platform.BINARY_SENSOR): if _check_for_node_def(isy_data, node, single_platform=Platform.BINARY_SENSOR):
return True return True
if _check_for_insteon_type( if _check_for_insteon_type(isy_data, node, single_platform=Platform.BINARY_SENSOR):
hass_isy_data, node, single_platform=Platform.BINARY_SENSOR
):
return True return True
# For the next two checks, we're providing our own set of uoms that # For the next two checks, we're providing our own set of uoms that
@ -256,14 +259,14 @@ def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool:
# checks in the context of already knowing that this is definitely a # checks in the context of already knowing that this is definitely a
# sensor device. # sensor device.
if _check_for_uom_id( if _check_for_uom_id(
hass_isy_data, isy_data,
node, node,
single_platform=Platform.BINARY_SENSOR, single_platform=Platform.BINARY_SENSOR,
uom_list=BINARY_SENSOR_UOMS, uom_list=BINARY_SENSOR_UOMS,
): ):
return True return True
if _check_for_states_in_uom( if _check_for_states_in_uom(
hass_isy_data, isy_data,
node, node,
single_platform=Platform.BINARY_SENSOR, single_platform=Platform.BINARY_SENSOR,
states_list=BINARY_SENSOR_ISY_STATES, states_list=BINARY_SENSOR_ISY_STATES,
@ -309,7 +312,7 @@ def _generate_device_info(node: Node) -> DeviceInfo:
def _categorize_nodes( def _categorize_nodes(
hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str isy_data: IsyData, nodes: Nodes, ignore_identifier: str, sensor_identifier: str
) -> None: ) -> None:
"""Sort the nodes to their proper platforms.""" """Sort the nodes to their proper platforms."""
for path, node in nodes: for path, node in nodes:
@ -320,44 +323,53 @@ def _categorize_nodes(
if hasattr(node, "parent_node") and node.parent_node is None: if hasattr(node, "parent_node") and node.parent_node is None:
# This is a physical device / parent node # This is a physical device / parent node
hass_isy_data[ISY_DEVICES][node.address] = _generate_device_info(node) isy_data.devices[node.address] = _generate_device_info(node)
hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON].append(node) isy_data.root_nodes[Platform.BUTTON].append(node)
# Any parent node can have communication errors:
isy_data.aux_properties[Platform.SENSOR].append((node, PROP_COMMS_ERROR))
# Add Ramp Rate and On Levels for Dimmable Load devices
if getattr(node, "is_dimmable", False):
aux_controls = ROOT_AUX_CONTROLS.intersection(node.aux_properties)
for control in aux_controls:
isy_data.aux_properties[Platform.SENSOR].append((node, control))
if node.protocol == PROTO_GROUP: if node.protocol == PROTO_GROUP:
hass_isy_data[ISY_NODES][ISY_GROUP_PLATFORM].append(node) isy_data.nodes[ISY_GROUP_PLATFORM].append(node)
continue continue
if node.protocol == PROTO_INSTEON: if node.protocol == PROTO_INSTEON:
for control in node.aux_properties: for control in node.aux_properties:
hass_isy_data[ISY_NODES][SENSOR_AUX].append((node, control)) if control in SKIP_AUX_PROPS:
continue
isy_data.aux_properties[Platform.SENSOR].append((node, control))
if sensor_identifier in path or sensor_identifier in node.name: if sensor_identifier in path or sensor_identifier in node.name:
# User has specified to treat this as a sensor. First we need to # User has specified to treat this as a sensor. First we need to
# determine if it should be a binary_sensor. # determine if it should be a binary_sensor.
if _is_sensor_a_binary_sensor(hass_isy_data, node): if _is_sensor_a_binary_sensor(isy_data, node):
continue continue
hass_isy_data[ISY_NODES][Platform.SENSOR].append(node) isy_data.nodes[Platform.SENSOR].append(node)
continue continue
# We have a bunch of different methods for determining the device type, # We have a bunch of different methods for determining the device type,
# each of which works with different ISY firmware versions or device # each of which works with different ISY firmware versions or device
# family. The order here is important, from most reliable to least. # family. The order here is important, from most reliable to least.
if _check_for_node_def(hass_isy_data, node): if _check_for_node_def(isy_data, node):
continue continue
if _check_for_insteon_type(hass_isy_data, node): if _check_for_insteon_type(isy_data, node):
continue continue
if _check_for_zwave_cat(hass_isy_data, node): if _check_for_zwave_cat(isy_data, node):
continue continue
if _check_for_uom_id(hass_isy_data, node): if _check_for_uom_id(isy_data, node):
continue continue
if _check_for_states_in_uom(hass_isy_data, node): if _check_for_states_in_uom(isy_data, node):
continue continue
# Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes. # Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes.
hass_isy_data[ISY_NODES][Platform.SENSOR].append(node) isy_data.nodes[Platform.SENSOR].append(node)
def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: def _categorize_programs(isy_data: IsyData, programs: Programs) -> None:
"""Categorize the ISY programs.""" """Categorize the ISY programs."""
for platform in PROGRAM_PLATFORMS: for platform in PROGRAM_PLATFORMS:
folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}") folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}")
@ -393,25 +405,21 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
continue continue
entity = (entity_folder.name, status, actions) entity = (entity_folder.name, status, actions)
hass_isy_data[ISY_PROGRAMS][platform].append(entity) isy_data.programs[platform].append(entity)
def _categorize_variables( def _categorize_variables(
hass_isy_data: dict, variables: Variables, identifier: str isy_data: IsyData, variables: Variables, identifier: str
) -> None: ) -> None:
"""Gather the ISY Variables to be added as sensors.""" """Gather the ISY Variables to be added as sensors."""
try: try:
var_to_add = [ isy_data.variables[Platform.SENSOR] = [
(vtype, vname, vid) variables[vtype][vid]
for (vtype, vname, vid) in variables.children for (vtype, vname, vid) in variables.children
if identifier in vname if identifier in vname
] ]
except KeyError as err: except KeyError as err:
_LOGGER.error("Error adding ISY Variables: %s", err) _LOGGER.error("Error adding ISY Variables: %s", err)
return
variable_entities = hass_isy_data[ISY_VARIABLES]
for vtype, vname, vid in var_to_add:
variable_entities[Platform.SENSOR].append((vname, variables[vtype][vid]))
def convert_isy_value_to_hass( def convert_isy_value_to_hass(

View file

@ -15,14 +15,7 @@ from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from .const import ( from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE
_LOGGER,
CONF_RESTORE_LIGHT_STATE,
DOMAIN,
ISY_DEVICES,
ISY_NODES,
UOM_PERCENTAGE,
)
from .entity import ISYNodeEntity from .entity import ISYNodeEntity
from .services import async_setup_light_services from .services import async_setup_light_services
@ -33,13 +26,13 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY light platform.""" """Set up the ISY light platform."""
hass_isy_data = hass.data[DOMAIN][entry.entry_id] isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] devices: dict[str, DeviceInfo] = isy_data.devices
isy_options = entry.options isy_options = entry.options
restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False)
entities = [] entities = []
for node in hass_isy_data[ISY_NODES][Platform.LIGHT]: for node in isy_data.nodes[Platform.LIGHT]:
entities.append( entities.append(
ISYLightEntity(node, restore_light_state, devices.get(node.primary_node)) ISYLightEntity(node, restore_light_state, devices.get(node.primary_node))
) )

View file

@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS from .const import _LOGGER, DOMAIN
from .entity import ISYNodeEntity, ISYProgramEntity from .entity import ISYNodeEntity, ISYProgramEntity
VALUE_TO_STATE = {0: False, 100: True} VALUE_TO_STATE = {0: False, 100: True}
@ -22,13 +22,13 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY lock platform.""" """Set up the ISY lock platform."""
hass_isy_data = hass.data[DOMAIN][entry.entry_id] isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] devices: dict[str, DeviceInfo] = isy_data.devices
entities: list[ISYLockEntity | ISYLockProgramEntity] = [] entities: list[ISYLockEntity | ISYLockProgramEntity] = []
for node in hass_isy_data[ISY_NODES][Platform.LOCK]: for node in isy_data.nodes[Platform.LOCK]:
entities.append(ISYLockEntity(node, devices.get(node.primary_node))) entities.append(ISYLockEntity(node, devices.get(node.primary_node)))
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.LOCK]: for name, status, actions in isy_data.programs[Platform.LOCK]:
entities.append(ISYLockProgramEntity(name, status, actions)) entities.append(ISYLockProgramEntity(name, status, actions))
async_add_entities(entities) async_add_entities(entities)

View file

@ -0,0 +1,96 @@
"""The ISY/IoX integration data models."""
from __future__ import annotations
from dataclasses import dataclass
from typing import cast
from pyisy import ISY
from pyisy.constants import PROTO_INSTEON
from pyisy.networking import NetworkCommand
from pyisy.nodes import Group, Node
from pyisy.programs import Program
from pyisy.variables import Variable
from homeassistant.const import Platform
from homeassistant.helpers.entity import DeviceInfo
from .const import (
CONF_NETWORK,
NODE_AUX_PROP_PLATFORMS,
NODE_PLATFORMS,
PROGRAM_PLATFORMS,
ROOT_NODE_PLATFORMS,
VARIABLE_PLATFORMS,
)
@dataclass
class IsyData:
"""Data for the ISY/IoX integration."""
root: ISY
nodes: dict[Platform, list[Node | Group]]
root_nodes: dict[Platform, list[Node]]
variables: dict[Platform, list[Variable]]
programs: dict[Platform, list[tuple[str, Program, Program]]]
net_resources: list[NetworkCommand]
devices: dict[str, DeviceInfo]
aux_properties: dict[Platform, list[tuple[Node, str]]]
def __init__(self) -> None:
"""Initialize an empty ISY data class."""
self.nodes = {p: [] for p in NODE_PLATFORMS}
self.root_nodes = {p: [] for p in ROOT_NODE_PLATFORMS}
self.aux_properties = {p: [] for p in NODE_AUX_PROP_PLATFORMS}
self.programs = {p: [] for p in PROGRAM_PLATFORMS}
self.variables = {p: [] for p in VARIABLE_PLATFORMS}
self.net_resources = []
self.devices = {}
@property
def uuid(self) -> str:
"""Return the ISY UUID identification."""
return cast(str, self.root.uuid)
def uid_base(self, node: Node | Group | Variable | Program | NetworkCommand) -> str:
"""Return the unique id base string for a given node."""
if isinstance(node, NetworkCommand):
return f"{self.uuid}_{CONF_NETWORK}_{node.address}"
return f"{self.uuid}_{node.address}"
@property
def unique_ids(self) -> set[tuple[Platform, str]]:
"""Return all the unique ids for a config entry id."""
current_unique_ids: set[tuple[Platform, str]] = {
(Platform.BUTTON, f"{self.uuid}_query")
}
# Structure and prefixes here must match what's added in __init__ and helpers
for platform in NODE_PLATFORMS:
for node in self.nodes[platform]:
current_unique_ids.add((platform, self.uid_base(node)))
for platform in NODE_AUX_PROP_PLATFORMS:
for node, control in self.aux_properties[platform]:
current_unique_ids.add((platform, f"{self.uid_base(node)}_{control}"))
for platform in PROGRAM_PLATFORMS:
for _, node, _ in self.programs[platform]:
current_unique_ids.add((platform, self.uid_base(node)))
for platform in VARIABLE_PLATFORMS:
for node in self.variables[platform]:
current_unique_ids.add((platform, self.uid_base(node)))
if platform == Platform.NUMBER:
current_unique_ids.add((platform, f"{self.uid_base(node)}_init"))
for platform in ROOT_NODE_PLATFORMS:
for node in self.root_nodes[platform]:
current_unique_ids.add((platform, f"{self.uid_base(node)}_query"))
if platform == Platform.BUTTON and node.protocol == PROTO_INSTEON:
current_unique_ids.add((platform, f"{self.uid_base(node)}_beep"))
for node in self.net_resources:
current_unique_ids.add((Platform.BUTTON, self.uid_base(node)))
return current_unique_ids

View file

@ -3,7 +3,6 @@ from __future__ import annotations
from typing import Any from typing import Any
from pyisy import ISY
from pyisy.helpers import EventListener, NodeProperty from pyisy.helpers import EventListener, NodeProperty
from pyisy.variables import Variable from pyisy.variables import Variable
@ -14,7 +13,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, ISY_DEVICES, ISY_ROOT, ISY_VARIABLES from .const import CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING, DOMAIN
from .helpers import convert_isy_value_to_hass from .helpers import convert_isy_value_to_hass
ISY_MAX_SIZE = (2**32) / 2 ISY_MAX_SIZE = (2**32) / 2
@ -26,19 +25,19 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up ISY/IoX number entities from config entry.""" """Set up ISY/IoX number entities from config entry."""
hass_isy_data = hass.data[DOMAIN][config_entry.entry_id] isy_data = hass.data[DOMAIN][config_entry.entry_id]
isy: ISY = hass_isy_data[ISY_ROOT] device_info = isy_data.devices
device_info = hass_isy_data[ISY_DEVICES]
entities: list[ISYVariableNumberEntity] = [] entities: list[ISYVariableNumberEntity] = []
var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING)
for node, enable_by_default in hass_isy_data[ISY_VARIABLES][Platform.NUMBER]: for node in isy_data.variables[Platform.NUMBER]:
step = 10 ** (-1 * node.prec) step = 10 ** (-1 * node.prec)
min_max = ISY_MAX_SIZE / (10**node.prec) min_max = ISY_MAX_SIZE / (10**node.prec)
description = NumberEntityDescription( description = NumberEntityDescription(
key=node.address, key=node.address,
name=node.name, name=node.name,
icon="mdi:counter", icon="mdi:counter",
entity_registry_enabled_default=enable_by_default, entity_registry_enabled_default=var_id in node.name,
native_unit_of_measurement=None, native_unit_of_measurement=None,
native_step=step, native_step=step,
native_min_value=-min_max, native_min_value=-min_max,
@ -59,7 +58,7 @@ async def async_setup_entry(
entities.append( entities.append(
ISYVariableNumberEntity( ISYVariableNumberEntity(
node, node,
unique_id=f"{isy.uuid}_{node.address}", unique_id=isy_data.uid_base(node),
description=description, description=description,
device_info=device_info[CONF_VARIABLES], device_info=device_info[CONF_VARIABLES],
) )
@ -67,7 +66,7 @@ async def async_setup_entry(
entities.append( entities.append(
ISYVariableNumberEntity( ISYVariableNumberEntity(
node=node, node=node,
unique_id=f"{isy.uuid}_{node.address}_init", unique_id=f"{isy_data.uid_base(node)}_init",
description=description_init, description=description_init,
device_info=device_info[CONF_VARIABLES], device_info=device_info[CONF_VARIABLES],
init_entity=True, init_entity=True,

View file

@ -19,6 +19,7 @@ from pyisy.constants import (
) )
from pyisy.helpers import NodeProperty from pyisy.helpers import NodeProperty
from pyisy.nodes import Node from pyisy.nodes import Node
from pyisy.variables import Variable
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -34,10 +35,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
_LOGGER, _LOGGER,
DOMAIN, DOMAIN,
ISY_DEVICES,
ISY_NODES,
ISY_VARIABLES,
SENSOR_AUX,
UOM_DOUBLE_TEMP, UOM_DOUBLE_TEMP,
UOM_FRIENDLY_NAME, UOM_FRIENDLY_NAME,
UOM_INDEX, UOM_INDEX,
@ -110,20 +107,17 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY sensor platform.""" """Set up the ISY sensor platform."""
hass_isy_data = hass.data[DOMAIN][entry.entry_id] isy_data = hass.data[DOMAIN][entry.entry_id]
entities: list[ISYSensorEntity | ISYSensorVariableEntity] = [] entities: list[ISYSensorEntity | ISYSensorVariableEntity] = []
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] devices: dict[str, DeviceInfo] = isy_data.devices
for node in hass_isy_data[ISY_NODES][Platform.SENSOR]: for node in isy_data.nodes[Platform.SENSOR]:
_LOGGER.debug("Loading %s", node.name) _LOGGER.debug("Loading %s", node.name)
entities.append(ISYSensorEntity(node, devices.get(node.primary_node))) entities.append(ISYSensorEntity(node, devices.get(node.primary_node)))
aux_nodes = set() aux_sensors_list = isy_data.aux_properties[Platform.SENSOR]
for node, control in hass_isy_data[ISY_NODES][SENSOR_AUX]: for node, control in aux_sensors_list:
aux_nodes.add(node) _LOGGER.debug("Loading %s %s", node.name, COMMAND_FRIENDLY_NAME.get(control))
if control in SKIP_AUX_PROPERTIES:
continue
_LOGGER.debug("Loading %s %s", node.name, node.aux_properties[control])
enabled_default = control not in AUX_DISABLED_BY_DEFAULT_EXACT and not any( enabled_default = control not in AUX_DISABLED_BY_DEFAULT_EXACT and not any(
control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT_MATCH control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT_MATCH
) )
@ -132,23 +126,13 @@ async def async_setup_entry(
node=node, node=node,
control=control, control=control,
enabled_default=enabled_default, enabled_default=enabled_default,
unique_id=f"{isy_data.uid_base(node)}_{control}",
device_info=devices.get(node.primary_node), device_info=devices.get(node.primary_node),
) )
) )
for node in aux_nodes: for variable in isy_data.variables[Platform.SENSOR]:
# Any node in SENSOR_AUX can potentially have communication errors entities.append(ISYSensorVariableEntity(variable))
entities.append(
ISYAuxSensorEntity(
node=node,
control=PROP_COMMS_ERROR,
enabled_default=False,
device_info=devices.get(node.primary_node),
)
)
for vname, vobj in hass_isy_data[ISY_VARIABLES][Platform.SENSOR]:
entities.append(ISYSensorVariableEntity(vname, vobj))
async_add_entities(entities) async_add_entities(entities)
@ -248,6 +232,7 @@ class ISYAuxSensorEntity(ISYSensorEntity):
node: Node, node: Node,
control: str, control: str,
enabled_default: bool, enabled_default: bool,
unique_id: str,
device_info: DeviceInfo | None = None, device_info: DeviceInfo | None = None,
) -> None: ) -> None:
"""Initialize the ISY aux sensor.""" """Initialize the ISY aux sensor."""
@ -257,12 +242,11 @@ class ISYAuxSensorEntity(ISYSensorEntity):
self._attr_entity_category = ISY_CONTROL_TO_ENTITY_CATEGORY.get(control) self._attr_entity_category = ISY_CONTROL_TO_ENTITY_CATEGORY.get(control)
self._attr_device_class = ISY_CONTROL_TO_DEVICE_CLASS.get(control) self._attr_device_class = ISY_CONTROL_TO_DEVICE_CLASS.get(control)
self._attr_state_class = ISY_CONTROL_TO_STATE_CLASS.get(control) self._attr_state_class = ISY_CONTROL_TO_STATE_CLASS.get(control)
self._attr_unique_id = unique_id
name = COMMAND_FRIENDLY_NAME.get(self._control, self._control) name = COMMAND_FRIENDLY_NAME.get(self._control, self._control)
self._attr_name = f"{node.name} {name.replace('_', ' ').title()}" self._attr_name = f"{node.name} {name.replace('_', ' ').title()}"
self._attr_unique_id = f"{node.isy.uuid}_{node.address}_{control}"
@property @property
def target(self) -> Node | NodeProperty | None: def target(self) -> Node | NodeProperty | None:
"""Return target for the sensor.""" """Return target for the sensor."""
@ -283,10 +267,10 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity):
# Deprecated sensors, will be removed in 2023.5.0 # Deprecated sensors, will be removed in 2023.5.0
_attr_entity_registry_enabled_default = False _attr_entity_registry_enabled_default = False
def __init__(self, vname: str, vobj: object) -> None: def __init__(self, variable_node: Variable) -> None:
"""Initialize the ISY binary sensor program.""" """Initialize the ISY binary sensor program."""
super().__init__(vobj) super().__init__(variable_node)
self._name = vname self._name = variable_node.name
@property @property
def native_value(self) -> float | int | None: def native_value(self) -> float | int | None:

View file

@ -23,15 +23,8 @@ import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.service import entity_service_call from homeassistant.helpers.service import entity_service_call
from .const import ( from .const import _LOGGER, CONF_NETWORK, DOMAIN, ISY_CONF_NAME, ISY_CONF_NETWORKING
_LOGGER, from .util import _async_cleanup_registry_entries
CONF_NETWORK,
DOMAIN,
ISY_CONF_NAME,
ISY_CONF_NETWORKING,
ISY_ROOT,
)
from .util import unique_ids_for_config_entry_id
# Common Services for All Platforms: # Common Services for All Platforms:
SERVICE_SYSTEM_QUERY = "system_query" SERVICE_SYSTEM_QUERY = "system_query"
@ -192,7 +185,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
isy_name = service.data.get(CONF_ISY) isy_name = service.data.get(CONF_ISY)
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
for config_entry_id in hass.data[DOMAIN]: for config_entry_id in hass.data[DOMAIN]:
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] isy_data = hass.data[DOMAIN][config_entry_id]
isy = isy_data.root
if isy_name and isy_name != isy.conf["name"]: if isy_name and isy_name != isy.conf["name"]:
continue continue
# If an address is provided, make sure we query the correct ISY. # If an address is provided, make sure we query the correct ISY.
@ -235,7 +229,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
isy_name = service.data.get(CONF_ISY) isy_name = service.data.get(CONF_ISY)
for config_entry_id in hass.data[DOMAIN]: for config_entry_id in hass.data[DOMAIN]:
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] isy_data = hass.data[DOMAIN][config_entry_id]
isy = isy_data.root
if isy_name and isy_name != isy.conf[ISY_CONF_NAME]: if isy_name and isy_name != isy.conf[ISY_CONF_NAME]:
continue continue
if isy.networking is None or not isy.conf[ISY_CONF_NETWORKING]: if isy.networking is None or not isy.conf[ISY_CONF_NETWORKING]:
@ -272,7 +267,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
isy_name = service.data.get(CONF_ISY) isy_name = service.data.get(CONF_ISY)
for config_entry_id in hass.data[DOMAIN]: for config_entry_id in hass.data[DOMAIN]:
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] isy_data = hass.data[DOMAIN][config_entry_id]
isy = isy_data.root
if isy_name and isy_name != isy.conf["name"]: if isy_name and isy_name != isy.conf["name"]:
continue continue
program = None program = None
@ -295,7 +291,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
isy_name = service.data.get(CONF_ISY) isy_name = service.data.get(CONF_ISY)
for config_entry_id in hass.data[DOMAIN]: for config_entry_id in hass.data[DOMAIN]:
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT] isy_data = hass.data[DOMAIN][config_entry_id]
isy = isy_data.root
if isy_name and isy_name != isy.conf["name"]: if isy_name and isy_name != isy.conf["name"]:
continue continue
variable = None variable = None
@ -323,32 +320,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
@callback @callback
def async_cleanup_registry_entries(service: ServiceCall) -> None: def async_cleanup_registry_entries(service: ServiceCall) -> None:
"""Remove extra entities that are no longer part of the integration.""" """Remove extra entities that are no longer part of the integration."""
entity_registry = er.async_get(hass)
for config_entry_id in hass.data[DOMAIN]: for config_entry_id in hass.data[DOMAIN]:
entries_for_this_config = er.async_entries_for_config_entry( _async_cleanup_registry_entries(hass, config_entry_id)
entity_registry, config_entry_id
)
entities = {
(entity.domain, entity.unique_id): entity.entity_id
for entity in entries_for_this_config
}
extra_entities = set(entities.keys()).difference(
unique_ids_for_config_entry_id(hass, config_entry_id)
)
for entity in extra_entities:
if entity_registry.async_is_registered(entities[entity]):
entity_registry.async_remove(entities[entity])
_LOGGER.debug(
(
"Cleaning up ISY entities: removed %s extra entities for config entry: %s"
),
len(extra_entities),
len(config_entry_id),
)
async def async_reload_config_entries(service: ServiceCall) -> None: async def async_reload_config_entries(service: ServiceCall) -> None:
"""Trigger a reload of all ISY config entries.""" """Trigger a reload of all ISY config entries."""

View file

@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS from .const import _LOGGER, DOMAIN
from .entity import ISYNodeEntity, ISYProgramEntity from .entity import ISYNodeEntity, ISYProgramEntity
@ -20,10 +20,10 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up the ISY switch platform.""" """Set up the ISY switch platform."""
hass_isy_data = hass.data[DOMAIN][entry.entry_id] isy_data = hass.data[DOMAIN][entry.entry_id]
entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = []
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES] devices: dict[str, DeviceInfo] = isy_data.devices
for node in hass_isy_data[ISY_NODES][Platform.SWITCH]: for node in isy_data.nodes[Platform.SWITCH]:
primary = node.primary_node primary = node.primary_node
if node.protocol == PROTO_GROUP and len(node.controllers) == 1: if node.protocol == PROTO_GROUP and len(node.controllers) == 1:
# If Group has only 1 Controller, link to that device instead of the hub # If Group has only 1 Controller, link to that device instead of the hub
@ -31,7 +31,7 @@ async def async_setup_entry(
entities.append(ISYSwitchEntity(node, devices.get(primary))) entities.append(ISYSwitchEntity(node, devices.get(primary)))
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.SWITCH]: for name, status, actions in isy_data.programs[Platform.SWITCH]:
entities.append(ISYSwitchProgramEntity(name, status, actions)) entities.append(ISYSwitchProgramEntity(name, status, actions))
async_add_entities(entities) async_add_entities(entities)

View file

@ -10,7 +10,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN, ISY_ROOT, ISY_URL_POSTFIX from .const import DOMAIN, ISY_URL_POSTFIX
from .models import IsyData
@callback @callback
@ -28,7 +29,8 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
config_entry_id = next( config_entry_id = next(
iter(hass.data[DOMAIN]) iter(hass.data[DOMAIN])
) # Only first ISY is supported for now ) # Only first ISY is supported for now
isy: ISY = hass.data[DOMAIN][config_entry_id][ISY_ROOT] isy_data: IsyData = hass.data[DOMAIN][config_entry_id]
isy: ISY = isy_data.root
entry = hass.config_entries.async_get_entry(config_entry_id) entry = hass.config_entries.async_get_entry(config_entry_id)
assert isinstance(entry, ConfigEntry) assert isinstance(entry, ConfigEntry)

View file

@ -1,69 +1,34 @@
"""ISY utils.""" """ISY utils."""
from __future__ import annotations from __future__ import annotations
from pyisy.constants import PROP_COMMS_ERROR, PROTO_INSTEON from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.entity_registry as er
from homeassistant.const import Platform from .const import _LOGGER, DOMAIN
from homeassistant.core import HomeAssistant
from .const import (
CONF_NETWORK,
DOMAIN,
ISY_NET_RES,
ISY_NODES,
ISY_PROGRAMS,
ISY_ROOT,
ISY_ROOT_NODES,
ISY_VARIABLES,
NODE_PLATFORMS,
PROGRAM_PLATFORMS,
ROOT_NODE_PLATFORMS,
SENSOR_AUX,
)
def unique_ids_for_config_entry_id( @callback
hass: HomeAssistant, config_entry_id: str def _async_cleanup_registry_entries(hass: HomeAssistant, entry_id: str) -> None:
) -> set[tuple[Platform | str, str]]: """Remove extra entities that are no longer part of the integration."""
"""Find all the unique ids for a config entry id.""" entity_registry = er.async_get(hass)
hass_isy_data = hass.data[DOMAIN][config_entry_id] isy_data = hass.data[DOMAIN][entry_id]
isy = hass_isy_data[ISY_ROOT]
current_unique_ids: set[tuple[Platform | str, str]] = { existing_entries = er.async_entries_for_config_entry(entity_registry, entry_id)
(Platform.BUTTON, f"{isy.uuid}_query") entities = {
(entity.domain, entity.unique_id): entity.entity_id
for entity in existing_entries
} }
# Structure and prefixes here must match what's added in __init__ and helpers extra_entities = set(entities.keys()).difference(isy_data.unique_ids)
for platform in NODE_PLATFORMS: if not extra_entities:
for node in hass_isy_data[ISY_NODES][platform]: return
current_unique_ids.add((platform, f"{isy.uuid}_{node.address}"))
for node, control in hass_isy_data[ISY_NODES][SENSOR_AUX]: for entity in extra_entities:
current_unique_ids.add( if entity_registry.async_is_registered(entities[entity]):
(Platform.SENSOR, f"{isy.uuid}_{node.address}_{control}") entity_registry.async_remove(entities[entity])
)
current_unique_ids.add(
(Platform.SENSOR, f"{isy.uuid}_{node.address}_{PROP_COMMS_ERROR}")
)
for platform in PROGRAM_PLATFORMS: _LOGGER.debug(
for _, node, _ in hass_isy_data[ISY_PROGRAMS][platform]: ("Cleaning up ISY entities: removed %s extra entities for config entry %s"),
current_unique_ids.add((platform, f"{isy.uuid}_{node.address}")) len(extra_entities),
entry_id,
for node, _ in hass_isy_data[ISY_VARIABLES][Platform.NUMBER]: )
current_unique_ids.add((Platform.NUMBER, f"{isy.uuid}_{node.address}"))
current_unique_ids.add((Platform.NUMBER, f"{isy.uuid}_{node.address}_init"))
for _, node in hass_isy_data[ISY_VARIABLES][Platform.SENSOR]:
current_unique_ids.add((Platform.SENSOR, f"{isy.uuid}_{node.address}"))
for platform in ROOT_NODE_PLATFORMS:
for node in hass_isy_data[ISY_ROOT_NODES][platform]:
current_unique_ids.add((platform, f"{isy.uuid}_{node.address}_query"))
if platform == Platform.BUTTON and node.protocol == PROTO_INSTEON:
current_unique_ids.add((platform, f"{isy.uuid}_{node.address}_beep"))
for node in hass_isy_data[ISY_NET_RES]:
current_unique_ids.add(
(Platform.BUTTON, f"{isy.uuid}_{CONF_NETWORK}_{node.address}")
)
return current_unique_ids

View file

@ -4,7 +4,7 @@ from unittest.mock import Mock
from aiohttp import ClientError from aiohttp import ClientError
from homeassistant.components.isy994.const import DOMAIN, ISY_ROOT, ISY_URL_POSTFIX from homeassistant.components.isy994.const import DOMAIN, ISY_URL_POSTFIX
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -31,15 +31,16 @@ async def test_system_health(hass, aioclient_mock):
unique_id=MOCK_UUID, unique_id=MOCK_UUID,
).add_to_hass(hass) ).add_to_hass(hass)
hass.data[DOMAIN] = {} isy_data = Mock(
hass.data[DOMAIN][MOCK_ENTRY_ID] = {} root=Mock(
hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock( connected=True,
connected=True, websocket=Mock(
websocket=Mock( last_heartbeat=MOCK_HEARTBEAT,
last_heartbeat=MOCK_HEARTBEAT, status=MOCK_CONNECTED,
status=MOCK_CONNECTED, ),
), )
) )
hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data}
info = await get_system_health_info(hass, DOMAIN) info = await get_system_health_info(hass, DOMAIN)
@ -67,15 +68,16 @@ async def test_system_health_failed_connect(hass, aioclient_mock):
unique_id=MOCK_UUID, unique_id=MOCK_UUID,
).add_to_hass(hass) ).add_to_hass(hass)
hass.data[DOMAIN] = {} isy_data = Mock(
hass.data[DOMAIN][MOCK_ENTRY_ID] = {} root=Mock(
hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock( connected=True,
connected=True, websocket=Mock(
websocket=Mock( last_heartbeat=MOCK_HEARTBEAT,
last_heartbeat=MOCK_HEARTBEAT, status=MOCK_CONNECTED,
status=MOCK_CONNECTED, ),
), )
) )
hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data}
info = await get_system_health_info(hass, DOMAIN) info = await get_system_health_info(hass, DOMAIN)