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/light.py
homeassistant/components/isy994/lock.py
homeassistant/components/isy994/models.py
homeassistant/components/isy994/number.py
homeassistant/components/isy994/sensor.py
homeassistant/components/isy994/services.py

View file

@ -43,25 +43,15 @@ from .const import (
ISY_CONF_MODEL,
ISY_CONF_NAME,
ISY_CONF_NETWORKING,
ISY_DEVICES,
ISY_NET_RES,
ISY_NODES,
ISY_PROGRAMS,
ISY_ROOT,
ISY_ROOT_NODES,
ISY_VARIABLES,
MANUFACTURER,
NODE_PLATFORMS,
PLATFORMS,
PROGRAM_PLATFORMS,
ROOT_NODE_PLATFORMS,
SCHEME_HTTP,
SCHEME_HTTPS,
SENSOR_AUX,
VARIABLE_PLATFORMS,
)
from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables
from .models import IsyData
from .services import async_setup_services, async_unload_services
from .util import _async_cleanup_registry_entries
CONFIG_SCHEMA = vol.Schema(
{
@ -135,15 +125,7 @@ async def async_setup_entry(
# they are missing from the options
_async_import_options_from_data_if_missing(hass, entry)
hass.data[DOMAIN][entry.entry_id] = {}
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_data = hass.data[DOMAIN][entry.entry_id] = IsyData()
isy_config = entry.data
isy_options = entry.options
@ -212,34 +194,37 @@ async def async_setup_entry(
f"Invalid response ISY, device is likely still starting: {err}"
) from err
_categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier)
_categorize_programs(hass_isy_data, isy.programs)
_categorize_nodes(isy_data, isy.nodes, ignore_identifier, sensor_identifier)
_categorize_programs(isy_data, isy.programs)
# 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.
if len(isy.variables.children) > 0:
hass_isy_data[ISY_DEVICES][CONF_VARIABLES] = _create_service_device_info(
if isy.variables.children:
isy_data.devices[CONF_VARIABLES] = _create_service_device_info(
isy, name=CONF_VARIABLES.title(), unique_id=CONF_VARIABLES
)
numbers = hass_isy_data[ISY_VARIABLES][Platform.NUMBER]
for vtype, vname, vid in isy.variables.children:
numbers.append((isy.variables[vtype][vid], variable_identifier in vname))
numbers = isy_data.variables[Platform.NUMBER]
for vtype, _, vid in isy.variables.children:
numbers.append(isy.variables[vtype][vid])
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
)
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
_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)
# Load platforms for the devices in the ISY controller that we support.
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
def _async_stop_auto_update(event: Event) -> None:
"""Stop the isy auto update on Home Assistant Shutdown."""
@ -328,9 +313,9 @@ async def async_unload_entry(
"""Unload a config entry."""
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")
isy.websocket.stop()
@ -349,7 +334,7 @@ async def async_remove_config_entry_device(
device_entry: dr.DeviceEntry,
) -> bool:
"""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(
(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_ZWAVE,
DOMAIN,
ISY_DEVICES,
ISY_NODES,
ISY_PROGRAMS,
SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT,
SUBNODE_DUSK_DAWN,
@ -77,9 +74,9 @@ async def async_setup_entry(
] = []
entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
for node in hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR]:
isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = isy_data.devices
for node in isy_data.nodes[Platform.BINARY_SENSOR]:
assert isinstance(node, Node)
device_info = devices.get(node.primary_node)
device_class, device_type = _detect_device_type_and_class(node)
@ -205,7 +202,7 @@ async def async_setup_entry(
)
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))
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_platform import AddEntitiesCallback
from .const import (
CONF_NETWORK,
DOMAIN,
ISY_DEVICES,
ISY_NET_RES,
ISY_ROOT,
ISY_ROOT_NODES,
)
from .const import CONF_NETWORK, DOMAIN
async def async_setup_entry(
@ -29,21 +22,21 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up ISY/IoX button from config entry."""
hass_isy_data = hass.data[DOMAIN][config_entry.entry_id]
isy: ISY = hass_isy_data[ISY_ROOT]
device_info = hass_isy_data[ISY_DEVICES]
isy_data = hass.data[DOMAIN][config_entry.entry_id]
isy: ISY = isy_data.root
device_info = isy_data.devices
entities: list[
ISYNodeQueryButtonEntity
| ISYNodeBeepButtonEntity
| ISYNetworkResourceButtonEntity
] = []
for node in hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON]:
for node in isy_data.root_nodes[Platform.BUTTON]:
entities.append(
ISYNodeQueryButtonEntity(
node=node,
name="Query",
unique_id=f"{isy.uuid}_{node.address}_query",
unique_id=f"{isy_data.uid_base(node)}_query",
entity_category=EntityCategory.DIAGNOSTIC,
device_info=device_info[node.address],
)
@ -53,18 +46,18 @@ async def async_setup_entry(
ISYNodeBeepButtonEntity(
node=node,
name="Beep",
unique_id=f"{isy.uuid}_{node.address}_beep",
unique_id=f"{isy_data.uid_base(node)}_beep",
entity_category=EntityCategory.DIAGNOSTIC,
device_info=device_info[node.address],
)
)
for node in hass_isy_data[ISY_NET_RES]:
for node in isy_data.net_resources:
entities.append(
ISYNetworkResourceButtonEntity(
node=node,
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],
)
)

View file

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

View file

@ -1,6 +1,8 @@
"""Constants for the ISY Platform."""
import logging
from pyisy.constants import PROP_ON_LEVEL, PROP_RAMP_RATE
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.climate import (
FAN_AUTO,
@ -85,6 +87,7 @@ NODE_PLATFORMS = [
Platform.SENSOR,
Platform.SWITCH,
]
NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR]
PROGRAM_PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.COVER,
@ -98,6 +101,7 @@ VARIABLE_PLATFORMS = [Platform.NUMBER, Platform.SENSOR]
# Set of all platforms used by integration
PLATFORMS = {
*NODE_PLATFORMS,
*NODE_AUX_PROP_PLATFORMS,
*PROGRAM_PLATFORMS,
*ROOT_NODE_PLATFORMS,
*VARIABLE_PLATFORMS,
@ -109,14 +113,6 @@ SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"]
# (they can turn off, and report their state)
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_UUID = "uuid"
ISY_CONF_NAME = "name"
@ -186,8 +182,6 @@ 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
@ -313,6 +307,10 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
FILTER_ZWAVE_CAT: ["140"],
},
}
NODE_AUX_FILTERS: dict[str, Platform] = {
PROP_ON_LEVEL: Platform.SENSOR,
PROP_RAMP_RATE: Platform.SENSOR,
}
UOM_FRIENDLY_NAME = {
"1": UnitOfElectricCurrent.AMPERE,

View file

@ -16,15 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
_LOGGER,
DOMAIN,
ISY_DEVICES,
ISY_NODES,
ISY_PROGRAMS,
UOM_8_BIT_RANGE,
UOM_BARRIER,
)
from .const import _LOGGER, DOMAIN, UOM_8_BIT_RANGE, UOM_BARRIER
from .entity import ISYNodeEntity, ISYProgramEntity
@ -32,13 +24,13 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""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] = []
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
for node in hass_isy_data[ISY_NODES][Platform.COVER]:
devices: dict[str, DeviceInfo] = isy_data.devices
for node in isy_data.nodes[Platform.COVER]:
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))
async_add_entities(entities)

View file

@ -11,7 +11,7 @@ from pyisy.constants import (
PROTO_ZWAVE,
)
from pyisy.helpers import EventListener, NodeProperty
from pyisy.nodes import Node
from pyisy.nodes import Group, Node
from pyisy.programs import Program
from pyisy.variables import Variable
@ -30,7 +30,11 @@ class ISYEntity(Entity):
_attr_should_poll = False
_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."""
self._node = node
self._attr_name = node.name
@ -89,10 +93,7 @@ class ISYNodeEntity(ISYEntity):
attr = {}
node = self._node
# Insteon aux_properties are now their own sensors
if (
hasattr(self._node, "aux_properties")
and getattr(node, "protocol", None) != PROTO_INSTEON
):
if hasattr(self._node, "aux_properties") and node.protocol != 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()
@ -128,7 +129,7 @@ class ISYNodeEntity(ISYEntity):
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."""
if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE:
if self._node.protocol != PROTO_ZWAVE:
raise HomeAssistantError(
"Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave"
f" device {self.entity_id}"
@ -139,7 +140,7 @@ class ISYNodeEntity(ISYEntity):
self, parameter: Any, value: Any | None, size: int | None
) -> None:
"""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(
"Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave"
f" device {self.entity_id}"
@ -155,7 +156,10 @@ class ISYNodeEntity(ISYEntity):
class ISYProgramEntity(ISYEntity):
"""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."""
super().__init__(status)
self._attr_name = name

View file

@ -18,7 +18,7 @@ from homeassistant.util.percentage import (
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
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
) -> None:
"""Set up the ISY fan platform."""
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = isy_data.devices
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)))
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))
async_add_entities(entities)

View file

@ -5,6 +5,11 @@ from typing import cast
from pyisy.constants import (
ISY_VALUE_UNKNOWN,
PROP_BUSY,
PROP_COMMS_ERROR,
PROP_ON_LEVEL,
PROP_RAMP_RATE,
PROP_STATUS,
PROTO_GROUP,
PROTO_INSTEON,
PROTO_PROGRAM,
@ -27,18 +32,12 @@ from .const import (
FILTER_STATES,
FILTER_UOM,
FILTER_ZWAVE_CAT,
ISY_DEVICES,
ISY_GROUP_PLATFORM,
ISY_NODES,
ISY_PROGRAMS,
ISY_ROOT_NODES,
ISY_VARIABLES,
KEY_ACTIONS,
KEY_STATUS,
NODE_FILTERS,
NODE_PLATFORMS,
PROGRAM_PLATFORMS,
SENSOR_AUX,
SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT,
SUBNODE_EZIO2X4_SENSORS,
@ -49,13 +48,19 @@ from .const import (
UOM_DOUBLE_TEMP,
UOM_ISYV4_DEGREES,
)
from .models import IsyData
BINARY_SENSOR_UOMS = ["2", "78"]
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(
hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None
isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None
) -> bool:
"""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]
for platform in platforms:
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 False
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:
"""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.
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
# 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_HEAT,
):
hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR].append(node)
isy_data.nodes[Platform.BINARY_SENSOR].append(node)
return True
# 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 subnode_id == SUBNODE_IOLINC_RELAY
):
hass_isy_data[ISY_NODES][Platform.SWITCH].append(node)
isy_data.nodes[Platform.SWITCH].append(node)
return True
# Smartenit EZIO2X4
@ -133,17 +138,17 @@ def _check_for_insteon_type(
and device_type.startswith(TYPE_EZIO2X4)
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
hass_isy_data[ISY_NODES][platform].append(node)
isy_data.nodes[platform].append(node)
return True
return False
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:
"""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)
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 False
def _check_for_uom_id(
hass_isy_data: dict,
isy_data: IsyData,
node: Group | Node,
single_platform: Platform | None = None,
uom_list: list[str] | None = None,
@ -190,23 +195,23 @@ def _check_for_uom_id(
if isinstance(node.uom, list):
node_uom = node.uom[0]
if uom_list:
if uom_list and single_platform:
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 False
platforms = NODE_PLATFORMS if not single_platform else [single_platform]
for platform in platforms:
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 False
def _check_for_states_in_uom(
hass_isy_data: dict,
isy_data: IsyData,
node: Group | Node,
single_platform: Platform | 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))
if states_list:
if states_list and single_platform:
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 False
platforms = NODE_PLATFORMS if not single_platform else [single_platform]
for platform in platforms:
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 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."""
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
if _check_for_insteon_type(
hass_isy_data, node, single_platform=Platform.BINARY_SENSOR
):
if _check_for_insteon_type(isy_data, node, single_platform=Platform.BINARY_SENSOR):
return True
# 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
# sensor device.
if _check_for_uom_id(
hass_isy_data,
isy_data,
node,
single_platform=Platform.BINARY_SENSOR,
uom_list=BINARY_SENSOR_UOMS,
):
return True
if _check_for_states_in_uom(
hass_isy_data,
isy_data,
node,
single_platform=Platform.BINARY_SENSOR,
states_list=BINARY_SENSOR_ISY_STATES,
@ -309,7 +312,7 @@ def _generate_device_info(node: Node) -> DeviceInfo:
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:
"""Sort the nodes to their proper platforms."""
for path, node in nodes:
@ -320,44 +323,53 @@ def _categorize_nodes(
if hasattr(node, "parent_node") and node.parent_node is None:
# This is a physical device / parent node
hass_isy_data[ISY_DEVICES][node.address] = _generate_device_info(node)
hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON].append(node)
isy_data.devices[node.address] = _generate_device_info(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:
hass_isy_data[ISY_NODES][ISY_GROUP_PLATFORM].append(node)
isy_data.nodes[ISY_GROUP_PLATFORM].append(node)
continue
if node.protocol == PROTO_INSTEON:
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:
# User has specified to treat this as a sensor. First we need to
# 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
hass_isy_data[ISY_NODES][Platform.SENSOR].append(node)
isy_data.nodes[Platform.SENSOR].append(node)
continue
# We have a bunch of different methods for determining the device type,
# each of which works with different ISY firmware versions or device
# 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
if _check_for_insteon_type(hass_isy_data, node):
if _check_for_insteon_type(isy_data, node):
continue
if _check_for_zwave_cat(hass_isy_data, node):
if _check_for_zwave_cat(isy_data, node):
continue
if _check_for_uom_id(hass_isy_data, node):
if _check_for_uom_id(isy_data, node):
continue
if _check_for_states_in_uom(hass_isy_data, node):
if _check_for_states_in_uom(isy_data, node):
continue
# 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."""
for platform in PROGRAM_PLATFORMS:
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
entity = (entity_folder.name, status, actions)
hass_isy_data[ISY_PROGRAMS][platform].append(entity)
isy_data.programs[platform].append(entity)
def _categorize_variables(
hass_isy_data: dict, variables: Variables, identifier: str
isy_data: IsyData, variables: Variables, identifier: str
) -> None:
"""Gather the ISY Variables to be added as sensors."""
try:
var_to_add = [
(vtype, vname, vid)
isy_data.variables[Platform.SENSOR] = [
variables[vtype][vid]
for (vtype, vname, vid) in variables.children
if identifier in vname
]
except KeyError as 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(

View file

@ -15,14 +15,7 @@ from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .const import (
_LOGGER,
CONF_RESTORE_LIGHT_STATE,
DOMAIN,
ISY_DEVICES,
ISY_NODES,
UOM_PERCENTAGE,
)
from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE
from .entity import ISYNodeEntity
from .services import async_setup_light_services
@ -33,13 +26,13 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the ISY light platform."""
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = isy_data.devices
isy_options = entry.options
restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False)
entities = []
for node in hass_isy_data[ISY_NODES][Platform.LIGHT]:
for node in isy_data.nodes[Platform.LIGHT]:
entities.append(
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_platform import AddEntitiesCallback
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS
from .const import _LOGGER, DOMAIN
from .entity import ISYNodeEntity, ISYProgramEntity
VALUE_TO_STATE = {0: False, 100: True}
@ -22,13 +22,13 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the ISY lock platform."""
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = isy_data.devices
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)))
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))
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 pyisy import ISY
from pyisy.helpers import EventListener, NodeProperty
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_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
ISY_MAX_SIZE = (2**32) / 2
@ -26,19 +25,19 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up ISY/IoX number entities from config entry."""
hass_isy_data = hass.data[DOMAIN][config_entry.entry_id]
isy: ISY = hass_isy_data[ISY_ROOT]
device_info = hass_isy_data[ISY_DEVICES]
isy_data = hass.data[DOMAIN][config_entry.entry_id]
device_info = isy_data.devices
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)
min_max = ISY_MAX_SIZE / (10**node.prec)
description = NumberEntityDescription(
key=node.address,
name=node.name,
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_step=step,
native_min_value=-min_max,
@ -59,7 +58,7 @@ async def async_setup_entry(
entities.append(
ISYVariableNumberEntity(
node,
unique_id=f"{isy.uuid}_{node.address}",
unique_id=isy_data.uid_base(node),
description=description,
device_info=device_info[CONF_VARIABLES],
)
@ -67,7 +66,7 @@ async def async_setup_entry(
entities.append(
ISYVariableNumberEntity(
node=node,
unique_id=f"{isy.uuid}_{node.address}_init",
unique_id=f"{isy_data.uid_base(node)}_init",
description=description_init,
device_info=device_info[CONF_VARIABLES],
init_entity=True,

View file

@ -19,6 +19,7 @@ from pyisy.constants import (
)
from pyisy.helpers import NodeProperty
from pyisy.nodes import Node
from pyisy.variables import Variable
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -34,10 +35,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
_LOGGER,
DOMAIN,
ISY_DEVICES,
ISY_NODES,
ISY_VARIABLES,
SENSOR_AUX,
UOM_DOUBLE_TEMP,
UOM_FRIENDLY_NAME,
UOM_INDEX,
@ -110,20 +107,17 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""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] = []
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)
entities.append(ISYSensorEntity(node, devices.get(node.primary_node)))
aux_nodes = set()
for node, control in hass_isy_data[ISY_NODES][SENSOR_AUX]:
aux_nodes.add(node)
if control in SKIP_AUX_PROPERTIES:
continue
_LOGGER.debug("Loading %s %s", node.name, node.aux_properties[control])
aux_sensors_list = isy_data.aux_properties[Platform.SENSOR]
for node, control in aux_sensors_list:
_LOGGER.debug("Loading %s %s", node.name, COMMAND_FRIENDLY_NAME.get(control))
enabled_default = control not in AUX_DISABLED_BY_DEFAULT_EXACT and not any(
control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT_MATCH
)
@ -132,23 +126,13 @@ async def async_setup_entry(
node=node,
control=control,
enabled_default=enabled_default,
unique_id=f"{isy_data.uid_base(node)}_{control}",
device_info=devices.get(node.primary_node),
)
)
for node in aux_nodes:
# Any node in SENSOR_AUX can potentially have communication errors
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))
for variable in isy_data.variables[Platform.SENSOR]:
entities.append(ISYSensorVariableEntity(variable))
async_add_entities(entities)
@ -248,6 +232,7 @@ class ISYAuxSensorEntity(ISYSensorEntity):
node: Node,
control: str,
enabled_default: bool,
unique_id: str,
device_info: DeviceInfo | None = None,
) -> None:
"""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_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
name = COMMAND_FRIENDLY_NAME.get(self._control, self._control)
self._attr_name = f"{node.name} {name.replace('_', ' ').title()}"
self._attr_unique_id = f"{node.isy.uuid}_{node.address}_{control}"
@property
def target(self) -> Node | NodeProperty | None:
"""Return target for the sensor."""
@ -283,10 +267,10 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity):
# Deprecated sensors, will be removed in 2023.5.0
_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."""
super().__init__(vobj)
self._name = vname
super().__init__(variable_node)
self._name = variable_node.name
@property
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.service import entity_service_call
from .const import (
_LOGGER,
CONF_NETWORK,
DOMAIN,
ISY_CONF_NAME,
ISY_CONF_NETWORKING,
ISY_ROOT,
)
from .util import unique_ids_for_config_entry_id
from .const import _LOGGER, CONF_NETWORK, DOMAIN, ISY_CONF_NAME, ISY_CONF_NETWORKING
from .util import _async_cleanup_registry_entries
# Common Services for All Platforms:
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)
entity_registry = er.async_get(hass)
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"]:
continue
# 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)
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]:
continue
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)
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"]:
continue
program = None
@ -295,7 +291,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
isy_name = service.data.get(CONF_ISY)
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"]:
continue
variable = None
@ -323,32 +320,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
@callback
def async_cleanup_registry_entries(service: ServiceCall) -> None:
"""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]:
entries_for_this_config = er.async_entries_for_config_entry(
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_cleanup_registry_entries(hass, config_entry_id)
async def async_reload_config_entries(service: ServiceCall) -> None:
"""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_platform import AddEntitiesCallback
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS
from .const import _LOGGER, DOMAIN
from .entity import ISYNodeEntity, ISYProgramEntity
@ -20,10 +20,10 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""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] = []
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
for node in hass_isy_data[ISY_NODES][Platform.SWITCH]:
devices: dict[str, DeviceInfo] = 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
@ -31,7 +31,7 @@ async def async_setup_entry(
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))
async_add_entities(entities)

View file

@ -10,7 +10,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
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
@ -28,7 +29,8 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
config_entry_id = next(
iter(hass.data[DOMAIN])
) # 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)
assert isinstance(entry, ConfigEntry)

View file

@ -1,69 +1,34 @@
"""ISY utils."""
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 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,
)
from .const import _LOGGER, DOMAIN
def unique_ids_for_config_entry_id(
hass: HomeAssistant, config_entry_id: str
) -> set[tuple[Platform | str, str]]:
"""Find all the unique ids for a config entry id."""
hass_isy_data = hass.data[DOMAIN][config_entry_id]
isy = hass_isy_data[ISY_ROOT]
current_unique_ids: set[tuple[Platform | str, str]] = {
(Platform.BUTTON, f"{isy.uuid}_query")
@callback
def _async_cleanup_registry_entries(hass: HomeAssistant, entry_id: str) -> None:
"""Remove extra entities that are no longer part of the integration."""
entity_registry = er.async_get(hass)
isy_data = hass.data[DOMAIN][entry_id]
existing_entries = er.async_entries_for_config_entry(entity_registry, entry_id)
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
for platform in NODE_PLATFORMS:
for node in hass_isy_data[ISY_NODES][platform]:
current_unique_ids.add((platform, f"{isy.uuid}_{node.address}"))
extra_entities = set(entities.keys()).difference(isy_data.unique_ids)
if not extra_entities:
return
for node, control in hass_isy_data[ISY_NODES][SENSOR_AUX]:
current_unique_ids.add(
(Platform.SENSOR, f"{isy.uuid}_{node.address}_{control}")
)
current_unique_ids.add(
(Platform.SENSOR, f"{isy.uuid}_{node.address}_{PROP_COMMS_ERROR}")
)
for entity in extra_entities:
if entity_registry.async_is_registered(entities[entity]):
entity_registry.async_remove(entities[entity])
for platform in PROGRAM_PLATFORMS:
for _, node, _ in hass_isy_data[ISY_PROGRAMS][platform]:
current_unique_ids.add((platform, f"{isy.uuid}_{node.address}"))
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
_LOGGER.debug(
("Cleaning up ISY entities: removed %s extra entities for config entry %s"),
len(extra_entities),
entry_id,
)

View file

@ -4,7 +4,7 @@ from unittest.mock import Mock
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.setup import async_setup_component
@ -31,15 +31,16 @@ async def test_system_health(hass, aioclient_mock):
unique_id=MOCK_UUID,
).add_to_hass(hass)
hass.data[DOMAIN] = {}
hass.data[DOMAIN][MOCK_ENTRY_ID] = {}
hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock(
connected=True,
websocket=Mock(
last_heartbeat=MOCK_HEARTBEAT,
status=MOCK_CONNECTED,
),
isy_data = Mock(
root=Mock(
connected=True,
websocket=Mock(
last_heartbeat=MOCK_HEARTBEAT,
status=MOCK_CONNECTED,
),
)
)
hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data}
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,
).add_to_hass(hass)
hass.data[DOMAIN] = {}
hass.data[DOMAIN][MOCK_ENTRY_ID] = {}
hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock(
connected=True,
websocket=Mock(
last_heartbeat=MOCK_HEARTBEAT,
status=MOCK_CONNECTED,
),
isy_data = Mock(
root=Mock(
connected=True,
websocket=Mock(
last_heartbeat=MOCK_HEARTBEAT,
status=MOCK_CONNECTED,
),
)
)
hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data}
info = await get_system_health_info(hass, DOMAIN)