Consolidate device info and clean-up ISY994 code base (#85657)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
shbatm 2023-01-11 22:07:44 -06:00 committed by GitHub
parent 43cc8a1ebf
commit 255a8362a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 481 additions and 446 deletions

View file

@ -7,7 +7,6 @@ from urllib.parse import urlparse
from aiohttp import CookieJar
import async_timeout
from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError
from pyisy.constants import PROTO_NETWORK_RESOURCE
import voluptuous as vol
from homeassistant import config_entries
@ -15,6 +14,7 @@ from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
CONF_VARIABLES,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
@ -22,11 +22,14 @@ from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import ConfigType
from .const import (
_LOGGER,
CONF_IGNORE_STRING,
CONF_NETWORK,
CONF_RESTORE_LIGHT_STATE,
CONF_SENSOR_STRING,
CONF_TLS_VER,
@ -36,28 +39,29 @@ from .const import (
DEFAULT_SENSOR_STRING,
DEFAULT_VAR_SENSOR_STRING,
DOMAIN,
ISY994_ISY,
ISY994_NODES,
ISY994_PROGRAMS,
ISY994_VARIABLES,
ISY_CONF_FIRMWARE,
ISY_CONF_MODEL,
ISY_CONF_NAME,
ISY_CONF_NETWORKING,
ISY_CONF_UUID,
ISY_CONN_ADDRESS,
ISY_CONN_PORT,
ISY_CONN_TLS,
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 .services import async_setup_services, async_unload_services
from .util import unique_ids_for_config_entry_id
CONFIG_SCHEMA = vol.Schema(
{
@ -134,17 +138,12 @@ async def async_setup_entry(
hass.data[DOMAIN][entry.entry_id] = {}
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
hass_isy_data[ISY994_NODES] = {SENSOR_AUX: [], PROTO_NETWORK_RESOURCE: []}
for platform in PLATFORMS:
hass_isy_data[ISY994_NODES][platform] = []
hass_isy_data[ISY994_PROGRAMS] = {}
for platform in PROGRAM_PLATFORMS:
hass_isy_data[ISY994_PROGRAMS][platform] = []
hass_isy_data[ISY994_VARIABLES] = {}
hass_isy_data[ISY994_VARIABLES][Platform.NUMBER] = []
hass_isy_data[ISY994_VARIABLES][Platform.SENSOR] = []
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_options = entry.options
@ -218,17 +217,24 @@ async def async_setup_entry(
# Categorize variables call to be removed with variable sensors in 2023.5.0
_categorize_variables(hass_isy_data, isy.variables, variable_identifier)
# Gather ISY Variables to be added. Identifier used to enable by default.
numbers = hass_isy_data[ISY994_VARIABLES][Platform.NUMBER]
for vtype, vname, vid in isy.variables.children:
numbers.append((isy.variables[vtype][vid], variable_identifier in vname))
if isy.configuration[ISY_CONF_NETWORKING]:
if len(isy.variables.children) > 0:
hass_isy_data[ISY_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))
if isy.conf[ISY_CONF_NETWORKING]:
hass_isy_data[ISY_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[ISY994_NODES][PROTO_NETWORK_RESOURCE].append(resource)
hass_isy_data[ISY_NET_RES].append(resource)
# Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs
_LOGGER.info(repr(isy.clock))
hass_isy_data[ISY994_ISY] = isy
hass_isy_data[ISY_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.
@ -280,29 +286,39 @@ def _async_import_options_from_data_if_missing(
hass.config_entries.async_update_entry(entry, options=options)
@callback
def _async_isy_to_configuration_url(isy: ISY) -> str:
"""Extract the configuration url from the isy."""
connection_info = isy.conn.connection_info
proto = SCHEME_HTTPS if ISY_CONN_TLS in connection_info else SCHEME_HTTP
return f"{proto}://{connection_info[ISY_CONN_ADDRESS]}:{connection_info[ISY_CONN_PORT]}"
@callback
def _async_get_or_create_isy_device_in_registry(
hass: HomeAssistant, entry: config_entries.ConfigEntry, isy: ISY
) -> None:
device_registry = dr.async_get(hass)
url = _async_isy_to_configuration_url(isy)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration[ISY_CONF_UUID])},
identifiers={(DOMAIN, isy.configuration[ISY_CONF_UUID])},
connections={(dr.CONNECTION_NETWORK_MAC, isy.uuid)},
identifiers={(DOMAIN, isy.uuid)},
manufacturer=MANUFACTURER,
name=isy.configuration[ISY_CONF_NAME],
model=isy.configuration[ISY_CONF_MODEL],
sw_version=isy.configuration[ISY_CONF_FIRMWARE],
configuration_url=url,
name=isy.conf[ISY_CONF_NAME],
model=isy.conf[ISY_CONF_MODEL],
sw_version=isy.conf[ISY_CONF_FIRMWARE],
configuration_url=isy.conn.url,
)
def _create_service_device_info(isy: ISY, name: str, unique_id: str) -> DeviceInfo:
"""Create device info for ISY service devices."""
return DeviceInfo(
identifiers={
(
DOMAIN,
f"{isy.uuid}_{unique_id}",
)
},
manufacturer=MANUFACTURER,
name=f"{isy.conf[ISY_CONF_NAME]} {name}",
model=isy.conf[ISY_CONF_MODEL],
sw_version=isy.conf[ISY_CONF_FIRMWARE],
configuration_url=isy.conn.url,
via_device=(DOMAIN, isy.uuid),
entry_type=DeviceEntryType.SERVICE,
)
@ -314,7 +330,7 @@ async def async_unload_entry(
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
isy: ISY = hass_isy_data[ISY994_ISY]
isy: ISY = hass_isy_data[ISY_ROOT]
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
isy.websocket.stop()
@ -333,7 +349,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]
return not device_entry.identifiers.intersection(
(DOMAIN, unique_id)
for unique_id in unique_ids_for_config_entry_id(hass, config_entry.entry_id)
(DOMAIN, unique_id) for unique_id in hass_isy_devices
)

View file

@ -21,6 +21,7 @@ from homeassistant.components.binary_sensor import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_ON, Platform
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.restore_state import RestoreEntity
@ -30,9 +31,10 @@ from .const import (
_LOGGER,
BINARY_SENSOR_DEVICE_TYPES_ISY,
BINARY_SENSOR_DEVICE_TYPES_ZWAVE,
DOMAIN as ISY994_DOMAIN,
ISY994_NODES,
ISY994_PROGRAMS,
DOMAIN,
ISY_DEVICES,
ISY_NODES,
ISY_PROGRAMS,
SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT,
SUBNODE_DUSK_DAWN,
@ -70,27 +72,33 @@ async def async_setup_entry(
| ISYBinarySensorHeartbeat
| ISYBinarySensorProgramEntity,
] = {}
child_nodes: list[tuple[Node, BinarySensorDeviceClass | None, str | None]] = []
child_nodes: list[
tuple[Node, BinarySensorDeviceClass | None, str | None, DeviceInfo | None]
] = []
entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
for node in hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR]:
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]:
assert isinstance(node, Node)
device_info = devices.get(node.primary_node)
device_class, device_type = _detect_device_type_and_class(node)
if node.protocol == PROTO_INSTEON:
if node.parent_node is not None:
# We'll process the Insteon child nodes last, to ensure all parent
# nodes have been processed
child_nodes.append((node, device_class, device_type))
child_nodes.append((node, device_class, device_type, device_info))
continue
entity = ISYInsteonBinarySensorEntity(node, device_class)
entity = ISYInsteonBinarySensorEntity(
node, device_class, device_info=device_info
)
else:
entity = ISYBinarySensorEntity(node, device_class)
entity = ISYBinarySensorEntity(node, device_class, device_info=device_info)
entities.append(entity)
entities_by_address[node.address] = entity
# Handle some special child node cases for Insteon Devices
for (node, device_class, device_type) in child_nodes:
for (node, device_class, device_type, device_info) in child_nodes:
subnode_id = int(node.address.split(" ")[-1], 16)
# Handle Insteon Thermostats
if device_type is not None and device_type.startswith(TYPE_CATEGORY_CLIMATE):
@ -101,13 +109,13 @@ async def async_setup_entry(
# As soon as the ISY Event Stream connects if it has a
# valid state, it will be set.
entity = ISYInsteonBinarySensorEntity(
node, BinarySensorDeviceClass.COLD, False
node, BinarySensorDeviceClass.COLD, False, device_info=device_info
)
entities.append(entity)
elif subnode_id == SUBNODE_CLIMATE_HEAT:
# Subnode 3 is the "Heat Control" sensor
entity = ISYInsteonBinarySensorEntity(
node, BinarySensorDeviceClass.HEAT, False
node, BinarySensorDeviceClass.HEAT, False, device_info=device_info
)
entities.append(entity)
continue
@ -138,7 +146,9 @@ async def async_setup_entry(
assert isinstance(parent_entity, ISYInsteonBinarySensorEntity)
# Subnode 4 is the heartbeat node, which we will
# represent as a separate binary_sensor
entity = ISYBinarySensorHeartbeat(node, parent_entity)
entity = ISYBinarySensorHeartbeat(
node, parent_entity, device_info=device_info
)
parent_entity.add_heartbeat_device(entity)
entities.append(entity)
continue
@ -157,14 +167,17 @@ async def async_setup_entry(
if subnode_id == SUBNODE_DUSK_DAWN:
# Subnode 2 is the Dusk/Dawn sensor
entity = ISYInsteonBinarySensorEntity(
node, BinarySensorDeviceClass.LIGHT
node, BinarySensorDeviceClass.LIGHT, device_info=device_info
)
entities.append(entity)
continue
if subnode_id == SUBNODE_LOW_BATTERY:
# Subnode 3 is the low battery node
entity = ISYInsteonBinarySensorEntity(
node, BinarySensorDeviceClass.BATTERY, initial_state
node,
BinarySensorDeviceClass.BATTERY,
initial_state,
device_info=device_info,
)
entities.append(entity)
continue
@ -172,22 +185,27 @@ async def async_setup_entry(
# Tamper Sub-node for MS II. Sometimes reported as "A" sometimes
# reported as "10", which translate from Hex to 10 and 16 resp.
entity = ISYInsteonBinarySensorEntity(
node, BinarySensorDeviceClass.PROBLEM, initial_state
node,
BinarySensorDeviceClass.PROBLEM,
initial_state,
device_info=device_info,
)
entities.append(entity)
continue
if subnode_id in SUBNODE_MOTION_DISABLED:
# Motion Disabled Sub-node for MS II ("D" or "13")
entity = ISYInsteonBinarySensorEntity(node)
entity = ISYInsteonBinarySensorEntity(node, device_info=device_info)
entities.append(entity)
continue
# We don't yet have any special logic for other sensor
# types, so add the nodes as individual devices
entity = ISYBinarySensorEntity(node, device_class)
entity = ISYBinarySensorEntity(
node, force_device_class=device_class, device_info=device_info
)
entities.append(entity)
for name, status, _ in hass_isy_data[ISY994_PROGRAMS][Platform.BINARY_SENSOR]:
for name, status, _ in hass_isy_data[ISY_PROGRAMS][Platform.BINARY_SENSOR]:
entities.append(ISYBinarySensorProgramEntity(name, status))
async_add_entities(entities)
@ -225,9 +243,10 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity):
node: Node,
force_device_class: BinarySensorDeviceClass | None = None,
unknown_state: bool | None = None,
device_info: DeviceInfo | None = None,
) -> None:
"""Initialize the ISY binary sensor device."""
super().__init__(node)
super().__init__(node, device_info=device_info)
self._device_class = force_device_class
@property
@ -260,9 +279,10 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity):
node: Node,
force_device_class: BinarySensorDeviceClass | None = None,
unknown_state: bool | None = None,
device_info: DeviceInfo | None = None,
) -> None:
"""Initialize the ISY binary sensor device."""
super().__init__(node, force_device_class)
super().__init__(node, force_device_class, device_info=device_info)
self._negative_node: Node | None = None
self._heartbeat_device: ISYBinarySensorHeartbeat | None = None
if self._node.status == ISY_VALUE_UNKNOWN:
@ -399,6 +419,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity, RestoreEntity)
| ISYBinarySensorEntity
| ISYBinarySensorHeartbeat
| ISYBinarySensorProgramEntity,
device_info: DeviceInfo | None = None,
) -> None:
"""Initialize the ISY binary sensor device.
@ -409,7 +430,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity, RestoreEntity)
If the heartbeat is not received in 25 hours then the computed state is
set to ON (Low Battery).
"""
super().__init__(node)
super().__init__(node, device_info=device_info)
self._parent_device = parent_device
self._heartbeat_timer: CALLBACK_TYPE | None = None
self._computed_state: bool | None = None

View file

@ -2,28 +2,24 @@
from __future__ import annotations
from pyisy import ISY
from pyisy.constants import PROTO_INSTEON, PROTO_NETWORK_RESOURCE
from pyisy.constants import PROTO_INSTEON
from pyisy.networking import NetworkCommand
from pyisy.nodes import Node
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import _async_isy_to_configuration_url
from .const import (
DOMAIN as ISY994_DOMAIN,
ISY994_ISY,
ISY994_NODES,
ISY_CONF_FIRMWARE,
ISY_CONF_MODEL,
ISY_CONF_NAME,
ISY_CONF_NETWORKING,
ISY_CONF_UUID,
MANUFACTURER,
CONF_NETWORK,
DOMAIN,
ISY_DEVICES,
ISY_NET_RES,
ISY_ROOT,
ISY_ROOT_NODES,
)
@ -33,107 +29,104 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up ISY/IoX button from config entry."""
hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id]
isy: ISY = hass_isy_data[ISY994_ISY]
uuid = isy.configuration[ISY_CONF_UUID]
hass_isy_data = hass.data[DOMAIN][config_entry.entry_id]
isy: ISY = hass_isy_data[ISY_ROOT]
device_info = hass_isy_data[ISY_DEVICES]
entities: list[
ISYNodeQueryButtonEntity
| ISYNodeBeepButtonEntity
| ISYNetworkResourceButtonEntity
] = []
nodes: dict = hass_isy_data[ISY994_NODES]
for node in nodes[Platform.BUTTON]:
entities.append(ISYNodeQueryButtonEntity(node, f"{uuid}_{node.address}"))
if node.protocol == PROTO_INSTEON:
entities.append(ISYNodeBeepButtonEntity(node, f"{uuid}_{node.address}"))
for node in nodes[PROTO_NETWORK_RESOURCE]:
for node in hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON]:
entities.append(
ISYNetworkResourceButtonEntity(node, f"{uuid}_{PROTO_NETWORK_RESOURCE}")
ISYNodeQueryButtonEntity(
node=node,
name="Query",
unique_id=f"{isy.uuid}_{node.address}_query",
entity_category=EntityCategory.DIAGNOSTIC,
device_info=device_info[node.address],
)
)
if node.protocol == PROTO_INSTEON:
entities.append(
ISYNodeBeepButtonEntity(
node=node,
name="Beep",
unique_id=f"{isy.uuid}_{node.address}_beep",
entity_category=EntityCategory.DIAGNOSTIC,
device_info=device_info[node.address],
)
)
for node in hass_isy_data[ISY_NET_RES]:
entities.append(
ISYNetworkResourceButtonEntity(
node=node,
name=node.name,
unique_id=f"{isy.uuid}_{CONF_NETWORK}_{node.address}",
device_info=device_info[CONF_NETWORK],
)
)
# Add entity to query full system
entities.append(ISYNodeQueryButtonEntity(isy, uuid))
entities.append(
ISYNodeQueryButtonEntity(
node=isy,
name="Query",
unique_id=isy.uuid,
device_info=DeviceInfo(identifiers={(DOMAIN, isy.uuid)}),
entity_category=EntityCategory.DIAGNOSTIC,
)
)
async_add_entities(entities)
class ISYNodeQueryButtonEntity(ButtonEntity):
"""Representation of a device query button entity."""
class ISYNodeButtonEntity(ButtonEntity):
"""Representation of an ISY/IoX device button entity."""
_attr_should_poll = False
_attr_entity_category = EntityCategory.CONFIG
_attr_has_entity_name = True
def __init__(self, node: Node | ISY, base_unique_id: str) -> None:
def __init__(
self,
node: Node | ISY | NetworkCommand,
name: str,
unique_id: str,
device_info: DeviceInfo,
entity_category: EntityCategory | None = None,
) -> None:
"""Initialize a query ISY device button entity."""
self._node = node
# Entity class attributes
self._attr_name = "Query"
self._attr_unique_id = f"{base_unique_id}_query"
self._attr_device_info = DeviceInfo(
identifiers={(ISY994_DOMAIN, base_unique_id)}
)
self._attr_name = name
self._attr_entity_category = entity_category
self._attr_unique_id = unique_id
self._attr_device_info = device_info
class ISYNodeQueryButtonEntity(ISYNodeButtonEntity):
"""Representation of a device query button entity."""
async def async_press(self) -> None:
"""Press the button."""
await self._node.query()
class ISYNodeBeepButtonEntity(ButtonEntity):
class ISYNodeBeepButtonEntity(ISYNodeButtonEntity):
"""Representation of a device beep button entity."""
_attr_should_poll = False
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_has_entity_name = True
def __init__(self, node: Node, base_unique_id: str) -> None:
"""Initialize a beep Insteon device button entity."""
self._node = node
# Entity class attributes
self._attr_name = "Beep"
self._attr_unique_id = f"{base_unique_id}_beep"
self._attr_device_info = DeviceInfo(
identifiers={(ISY994_DOMAIN, base_unique_id)}
)
async def async_press(self) -> None:
"""Press the button."""
await self._node.beep()
class ISYNetworkResourceButtonEntity(ButtonEntity):
class ISYNetworkResourceButtonEntity(ISYNodeButtonEntity):
"""Representation of an ISY/IoX Network Resource button entity."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(self, node: Node, base_unique_id: str) -> None:
"""Initialize an ISY network resource button entity."""
self._node = node
# Entity class attributes
self._attr_name = node.name
self._attr_unique_id = f"{base_unique_id}_{node.address}"
url = _async_isy_to_configuration_url(node.isy)
config = node.isy.configuration
self._attr_device_info = DeviceInfo(
identifiers={
(
ISY994_DOMAIN,
f"{config[ISY_CONF_UUID]}_{PROTO_NETWORK_RESOURCE}",
)
},
manufacturer=MANUFACTURER,
name=f"{config[ISY_CONF_NAME]} {ISY_CONF_NETWORKING}",
model=config[ISY_CONF_MODEL],
sw_version=config[ISY_CONF_FIRMWARE],
configuration_url=url,
via_device=(ISY994_DOMAIN, config[ISY_CONF_UUID]),
entry_type=DeviceEntryType.SERVICE,
)
_attr_has_entity_name = False
async def async_press(self) -> None:
"""Press the button."""

View file

@ -35,15 +35,17 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
_LOGGER,
DOMAIN as ISY994_DOMAIN,
DOMAIN,
HA_FAN_TO_ISY,
HA_HVAC_TO_ISY,
ISY994_NODES,
ISY_DEVICES,
ISY_HVAC_MODES,
ISY_NODES,
UOM_FAN_MODES,
UOM_HVAC_ACTIONS,
UOM_HVAC_MODE_GENERIC,
@ -63,9 +65,10 @@ async def async_setup_entry(
"""Set up the ISY thermostat platform."""
entities = []
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
for node in hass_isy_data[ISY994_NODES][Platform.CLIMATE]:
entities.append(ISYThermostatEntity(node))
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]:
entities.append(ISYThermostatEntity(node, devices.get(node.primary_node)))
async_add_entities(entities)
@ -81,9 +84,9 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
def __init__(self, node: Node) -> None:
def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None:
"""Initialize the ISY Thermostat entity."""
super().__init__(node)
super().__init__(node, device_info=device_info)
self._uom = self._node.uom
if isinstance(self._uom, list):
self._uom = self._node.uom[0]

View file

@ -58,6 +58,7 @@ DOMAIN = "isy994"
MANUFACTURER = "Universal Devices, Inc"
CONF_NETWORK = "network"
CONF_IGNORE_STRING = "ignore_string"
CONF_SENSOR_STRING = "sensor_string"
CONF_VAR_SENSOR_STRING = "variable_sensor_string"
@ -74,15 +75,13 @@ DEFAULT_VAR_SENSOR_STRING = "HA."
KEY_ACTIONS = "actions"
KEY_STATUS = "status"
PLATFORMS = [
NODE_PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.FAN,
Platform.LIGHT,
Platform.LOCK,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
]
@ -93,6 +92,16 @@ PROGRAM_PLATFORMS = [
Platform.LOCK,
Platform.SWITCH,
]
ROOT_NODE_PLATFORMS = [Platform.BUTTON]
VARIABLE_PLATFORMS = [Platform.NUMBER, Platform.SENSOR]
# Set of all platforms used by integration
PLATFORMS = {
*NODE_PLATFORMS,
*PROGRAM_PLATFORMS,
*ROOT_NODE_PLATFORMS,
*VARIABLE_PLATFORMS,
}
SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"]
@ -100,10 +109,13 @@ SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"]
# (they can turn off, and report their state)
ISY_GROUP_PLATFORM = Platform.SWITCH
ISY994_ISY = "isy"
ISY994_NODES = "isy994_nodes"
ISY994_PROGRAMS = "isy994_programs"
ISY994_VARIABLES = "isy994_variables"
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"
@ -201,14 +213,6 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
], # Does a startswith() match; include the dot
FILTER_ZWAVE_CAT: (["104", "112", "138"] + list(map(str, range(148, 180)))),
},
Platform.BUTTON: {
# No devices automatically sorted as buttons at this time. Query buttons added elsewhere.
FILTER_UOM: [],
FILTER_STATES: [],
FILTER_NODE_DEF_ID: [],
FILTER_INSTEON_TYPE: [],
FILTER_ZWAVE_CAT: [],
},
Platform.SENSOR: {
# This is just a more-readable way of including MOST uoms between 1-100
# (Remember that range() is non-inclusive of the stop value)
@ -308,14 +312,6 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
FILTER_INSTEON_TYPE: ["4.8", TYPE_CATEGORY_CLIMATE],
FILTER_ZWAVE_CAT: ["140"],
},
Platform.NUMBER: {
# No devices automatically sorted as numbers at this time.
FILTER_UOM: [],
FILTER_STATES: [],
FILTER_NODE_DEF_ID: [],
FILTER_INSTEON_TYPE: [],
FILTER_ZWAVE_CAT: [],
},
}
UOM_FRIENDLY_NAME = {

View file

@ -13,13 +13,15 @@ from homeassistant.components.cover import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
_LOGGER,
DOMAIN as ISY994_DOMAIN,
ISY994_NODES,
ISY994_PROGRAMS,
DOMAIN,
ISY_DEVICES,
ISY_NODES,
ISY_PROGRAMS,
UOM_8_BIT_RANGE,
UOM_BARRIER,
)
@ -30,12 +32,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[ISY994_DOMAIN][entry.entry_id]
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
entities: list[ISYCoverEntity | ISYCoverProgramEntity] = []
for node in hass_isy_data[ISY994_NODES][Platform.COVER]:
entities.append(ISYCoverEntity(node))
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
for node in hass_isy_data[ISY_NODES][Platform.COVER]:
entities.append(ISYCoverEntity(node, devices.get(node.primary_node)))
for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.COVER]:
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.COVER]:
entities.append(ISYCoverProgramEntity(name, status, actions))
async_add_entities(entities)

View file

@ -7,32 +7,37 @@ from pyisy.constants import (
COMMAND_FRIENDLY_NAME,
EMPTY_TIME,
EVENT_PROPS_IGNORED,
PROTO_GROUP,
PROTO_INSTEON,
PROTO_ZWAVE,
)
from pyisy.helpers import EventListener, NodeProperty
from pyisy.nodes import Node
from pyisy.programs import Program
from pyisy.variables import Variable
from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, STATE_OFF, STATE_ON
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo, Entity
from . import _async_isy_to_configuration_url
from .const import DOMAIN, ISY_CONF_UUID
from .const import DOMAIN
class ISYEntity(Entity):
"""Representation of an ISY device."""
_name: str | None = None
_attr_has_entity_name = False
_attr_should_poll = False
_node: Node | Program | Variable
def __init__(self, node: Node) -> None:
def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None:
"""Initialize the insteon device."""
self._node = node
self._attr_name = node.name
if device_info is None:
device_info = DeviceInfo(identifiers={(DOMAIN, node.isy.uuid)})
self._attr_device_info = device_info
self._attr_unique_id = f"{node.isy.uuid}_{node.address}"
self._attrs: dict[str, Any] = {}
self._change_handler: EventListener | None = None
self._control_handler: EventListener | None = None
@ -69,72 +74,6 @@ class ISYEntity(Entity):
self.hass.bus.async_fire("isy994_control", event_data)
@property
def device_info(self) -> DeviceInfo | None:
"""Return the device_info of the device."""
isy = self._node.isy
uuid = isy.configuration[ISY_CONF_UUID]
node = self._node
url = _async_isy_to_configuration_url(isy)
basename = self._name or str(self._node.name)
if node.protocol == PROTO_GROUP and len(node.controllers) == 1:
# If Group has only 1 Controller, link to that device instead of the hub
node = isy.nodes.get_by_id(node.controllers[0])
basename = node.name
if hasattr(node, "parent_node"): # Verify this is a Node class
if node.parent_node is not None:
# This is not the parent node, get the parent node.
node = node.parent_node
basename = node.name
else:
# Default to the hub device if parent node is not a physical device
return DeviceInfo(identifiers={(DOMAIN, uuid)})
device_info = DeviceInfo(
identifiers={(DOMAIN, f"{uuid}_{node.address}")},
manufacturer=node.protocol,
name=f"{basename} ({(str(node.address).rpartition(' ')[0] or node.address)})",
via_device=(DOMAIN, uuid),
configuration_url=url,
suggested_area=node.folder,
)
# ISYv5 Device Types can provide model and manufacturer
model: str = "Unknown"
if node.node_def_id is not None:
model = str(node.node_def_id)
# Numerical Device Type
if node.type is not None:
model += f" ({node.type})"
# Get extra information for Z-Wave Devices
if node.protocol == PROTO_ZWAVE:
device_info[ATTR_MANUFACTURER] = f"Z-Wave MfrID:{node.zwave_props.mfr_id}"
model += (
f" Type:{node.zwave_props.devtype_gen} "
f"ProductTypeID:{node.zwave_props.prod_type_id} "
f"ProductID:{node.zwave_props.product_id}"
)
device_info[ATTR_MODEL] = model
return device_info
@property
def unique_id(self) -> str | None:
"""Get the unique identifier of the device."""
if hasattr(self._node, "address"):
return f"{self._node.isy.configuration[ISY_CONF_UUID]}_{self._node.address}"
return None
@property
def name(self) -> str:
"""Get the name of the device."""
return self._name or str(self._node.name)
class ISYNodeEntity(ISYEntity):
"""Representation of a ISY Nodebase (Node/Group) entity."""
@ -219,7 +158,7 @@ class ISYProgramEntity(ISYEntity):
def __init__(self, name: str, status: Any | None, actions: Program = None) -> None:
"""Initialize the ISY program-based entity."""
super().__init__(status)
self._name = name
self._attr_name = name
self._actions = actions
@property

View file

@ -10,6 +10,7 @@ from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.percentage import (
int_states_in_range,
@ -17,7 +18,7 @@ from homeassistant.util.percentage import (
ranged_value_to_percentage,
)
from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS
from .entity import ISYNodeEntity, ISYProgramEntity
SPEED_RANGE = (1, 255) # off is not included
@ -27,13 +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[ISY994_DOMAIN][entry.entry_id]
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
entities: list[ISYFanEntity | ISYFanProgramEntity] = []
for node in hass_isy_data[ISY994_NODES][Platform.FAN]:
entities.append(ISYFanEntity(node))
for node in hass_isy_data[ISY_NODES][Platform.FAN]:
entities.append(ISYFanEntity(node, devices.get(node.primary_node)))
for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.FAN]:
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.FAN]:
entities.append(ISYFanProgramEntity(name, status, actions))
async_add_entities(entities)

View file

@ -15,24 +15,28 @@ from pyisy.nodes import Group, Node, Nodes
from pyisy.programs import Programs
from pyisy.variables import Variables
from homeassistant.const import Platform
from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, Platform
from homeassistant.helpers.entity import DeviceInfo
from .const import (
_LOGGER,
DEFAULT_PROGRAM_STRING,
DOMAIN,
FILTER_INSTEON_TYPE,
FILTER_NODE_DEF_ID,
FILTER_STATES,
FILTER_UOM,
FILTER_ZWAVE_CAT,
ISY994_NODES,
ISY994_PROGRAMS,
ISY994_VARIABLES,
ISY_DEVICES,
ISY_GROUP_PLATFORM,
ISY_NODES,
ISY_PROGRAMS,
ISY_ROOT_NODES,
ISY_VARIABLES,
KEY_ACTIONS,
KEY_STATUS,
NODE_FILTERS,
PLATFORMS,
NODE_PLATFORMS,
PROGRAM_PLATFORMS,
SENSOR_AUX,
SUBNODE_CLIMATE_COOL,
@ -64,10 +68,10 @@ def _check_for_node_def(
node_def_id = node.node_def_id
platforms = PLATFORMS if not single_platform else [single_platform]
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[ISY994_NODES][platform].append(node)
hass_isy_data[ISY_NODES][platform].append(node)
return True
return False
@ -89,7 +93,7 @@ def _check_for_insteon_type(
return False
device_type = node.type
platforms = PLATFORMS if not single_platform else [single_platform]
platforms = NODE_PLATFORMS if not single_platform else [single_platform]
for platform in platforms:
if any(
device_type.startswith(t)
@ -103,7 +107,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[ISY994_NODES][Platform.LIGHT].append(node)
hass_isy_data[ISY_NODES][Platform.LIGHT].append(node)
return True
# Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3
@ -111,7 +115,7 @@ def _check_for_insteon_type(
SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT,
):
hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR].append(node)
hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR].append(node)
return True
# IOLincs which have a sensor and relay on 2 different nodes
@ -120,7 +124,7 @@ def _check_for_insteon_type(
and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS)
and subnode_id == SUBNODE_IOLINC_RELAY
):
hass_isy_data[ISY994_NODES][Platform.SWITCH].append(node)
hass_isy_data[ISY_NODES][Platform.SWITCH].append(node)
return True
# Smartenit EZIO2X4
@ -129,10 +133,10 @@ def _check_for_insteon_type(
and device_type.startswith(TYPE_EZIO2X4)
and subnode_id in SUBNODE_EZIO2X4_SENSORS
):
hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR].append(node)
hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR].append(node)
return True
hass_isy_data[ISY994_NODES][platform].append(node)
hass_isy_data[ISY_NODES][platform].append(node)
return True
return False
@ -154,13 +158,13 @@ def _check_for_zwave_cat(
return False
device_type = node.zwave_props.category
platforms = PLATFORMS if not single_platform else [single_platform]
platforms = NODE_PLATFORMS if not single_platform else [single_platform]
for platform in platforms:
if any(
device_type.startswith(t)
for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT])
):
hass_isy_data[ISY994_NODES][platform].append(node)
hass_isy_data[ISY_NODES][platform].append(node)
return True
return False
@ -188,14 +192,14 @@ def _check_for_uom_id(
if uom_list:
if node_uom in uom_list:
hass_isy_data[ISY994_NODES][single_platform].append(node)
hass_isy_data[ISY_NODES][single_platform].append(node)
return True
return False
platforms = PLATFORMS if not single_platform else [single_platform]
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[ISY994_NODES][platform].append(node)
hass_isy_data[ISY_NODES][platform].append(node)
return True
return False
@ -225,14 +229,14 @@ def _check_for_states_in_uom(
if states_list:
if node_uom == set(states_list):
hass_isy_data[ISY994_NODES][single_platform].append(node)
hass_isy_data[ISY_NODES][single_platform].append(node)
return True
return False
platforms = PLATFORMS if not single_platform else [single_platform]
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[ISY994_NODES][platform].append(node)
hass_isy_data[ISY_NODES][platform].append(node)
return True
return False
@ -269,6 +273,41 @@ def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool:
return False
def _generate_device_info(node: Node) -> DeviceInfo:
"""Generate the device info for a root node device."""
isy = node.isy
basename = node.name
device_info = DeviceInfo(
identifiers={(DOMAIN, f"{isy.uuid}_{node.address}")},
manufacturer=node.protocol,
name=f"{basename} ({(str(node.address).rpartition(' ')[0] or node.address)})",
via_device=(DOMAIN, isy.uuid),
configuration_url=isy.conn.url,
suggested_area=node.folder,
)
# ISYv5 Device Types can provide model and manufacturer
model: str = "Unknown"
if node.node_def_id is not None:
model = str(node.node_def_id)
# Numerical Device Type
if node.type is not None:
model += f" ({node.type})"
# Get extra information for Z-Wave Devices
if node.protocol == PROTO_ZWAVE:
device_info[ATTR_MANUFACTURER] = f"Z-Wave MfrID:{node.zwave_props.mfr_id}"
model += (
f" Type:{node.zwave_props.devtype_gen} "
f"ProductTypeID:{node.zwave_props.prod_type_id} "
f"ProductID:{node.zwave_props.product_id}"
)
device_info[ATTR_MODEL] = model
return device_info
def _categorize_nodes(
hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str
) -> None:
@ -280,23 +319,24 @@ def _categorize_nodes(
continue
if hasattr(node, "parent_node") and node.parent_node is None:
# This is a physical device / parent node, add a query button
hass_isy_data[ISY994_NODES][Platform.BUTTON].append(node)
# 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)
if node.protocol == PROTO_GROUP:
hass_isy_data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node)
hass_isy_data[ISY_NODES][ISY_GROUP_PLATFORM].append(node)
continue
if node.protocol == PROTO_INSTEON:
for control in node.aux_properties:
hass_isy_data[ISY994_NODES][SENSOR_AUX].append((node, control))
hass_isy_data[ISY_NODES][SENSOR_AUX].append((node, control))
if sensor_identifier in path or sensor_identifier in node.name:
# User has specified to treat this as a sensor. First we need to
# determine if it should be a binary_sensor.
if _is_sensor_a_binary_sensor(hass_isy_data, node):
continue
hass_isy_data[ISY994_NODES][Platform.SENSOR].append(node)
hass_isy_data[ISY_NODES][Platform.SENSOR].append(node)
continue
# We have a bunch of different methods for determining the device type,
@ -314,7 +354,7 @@ def _categorize_nodes(
continue
# Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes.
hass_isy_data[ISY994_NODES][Platform.SENSOR].append(node)
hass_isy_data[ISY_NODES][Platform.SENSOR].append(node)
def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
@ -353,7 +393,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
continue
entity = (entity_folder.name, status, actions)
hass_isy_data[ISY994_PROGRAMS][platform].append(entity)
hass_isy_data[ISY_PROGRAMS][platform].append(entity)
def _categorize_variables(
@ -369,7 +409,7 @@ def _categorize_variables(
except KeyError as err:
_LOGGER.error("Error adding ISY Variables: %s", err)
return
variable_entities = hass_isy_data[ISY994_VARIABLES]
variable_entities = hass_isy_data[ISY_VARIABLES]
for vtype, vname, vid in var_to_add:
variable_entities[Platform.SENSOR].append((vname, variables[vtype][vid]))

View file

@ -11,14 +11,16 @@ from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
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 as ISY994_DOMAIN,
ISY994_NODES,
DOMAIN,
ISY_DEVICES,
ISY_NODES,
UOM_PERCENTAGE,
)
from .entity import ISYNodeEntity
@ -31,13 +33,16 @@ 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[ISY994_DOMAIN][entry.entry_id]
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
isy_options = entry.options
restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False)
entities = []
for node in hass_isy_data[ISY994_NODES][Platform.LIGHT]:
entities.append(ISYLightEntity(node, restore_light_state))
for node in hass_isy_data[ISY_NODES][Platform.LIGHT]:
entities.append(
ISYLightEntity(node, restore_light_state, devices.get(node.primary_node))
)
async_add_entities(entities)
async_setup_light_services(hass)
@ -49,9 +54,14 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
_attr_color_mode = ColorMode.BRIGHTNESS
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
def __init__(self, node: Node, restore_light_state: bool) -> None:
def __init__(
self,
node: Node,
restore_light_state: bool,
device_info: DeviceInfo | None = None,
) -> None:
"""Initialize the ISY light device."""
super().__init__(node)
super().__init__(node, device_info=device_info)
self._last_brightness: int | None = None
self._restore_light_state = restore_light_state

View file

@ -9,9 +9,10 @@ from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS
from .entity import ISYNodeEntity, ISYProgramEntity
VALUE_TO_STATE = {0: False, 100: True}
@ -21,12 +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[ISY994_DOMAIN][entry.entry_id]
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
entities: list[ISYLockEntity | ISYLockProgramEntity] = []
for node in hass_isy_data[ISY994_NODES][Platform.LOCK]:
entities.append(ISYLockEntity(node))
for node in hass_isy_data[ISY_NODES][Platform.LOCK]:
entities.append(ISYLockEntity(node, devices.get(node.primary_node)))
for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.LOCK]:
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.LOCK]:
entities.append(ISYLockProgramEntity(name, status, actions))
async_add_entities(entities)

View file

@ -3,7 +3,7 @@
"name": "Universal Devices ISY/IoX",
"integration_type": "hub",
"documentation": "https://www.home-assistant.io/integrations/isy994",
"requirements": ["pyisy==3.0.12"],
"requirements": ["pyisy==3.1.3"],
"codeowners": ["@bdraco", "@shbatm"],
"config_flow": true,
"ssdp": [

View file

@ -9,23 +9,12 @@ from pyisy.variables import Variable
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.const import CONF_VARIABLES, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import _async_isy_to_configuration_url
from .const import (
DOMAIN as ISY994_DOMAIN,
ISY994_ISY,
ISY994_VARIABLES,
ISY_CONF_FIRMWARE,
ISY_CONF_MODEL,
ISY_CONF_NAME,
ISY_CONF_UUID,
MANUFACTURER,
)
from .const import DOMAIN, ISY_DEVICES, ISY_ROOT, ISY_VARIABLES
from .helpers import convert_isy_value_to_hass
ISY_MAX_SIZE = (2**32) / 2
@ -37,12 +26,12 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up ISY/IoX number entities from config entry."""
hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id]
isy: ISY = hass_isy_data[ISY994_ISY]
uuid = isy.configuration[ISY_CONF_UUID]
hass_isy_data = hass.data[DOMAIN][config_entry.entry_id]
isy: ISY = hass_isy_data[ISY_ROOT]
device_info = hass_isy_data[ISY_DEVICES]
entities: list[ISYVariableNumberEntity] = []
for node, enable_by_default in hass_isy_data[ISY994_VARIABLES][Platform.NUMBER]:
for node, enable_by_default in hass_isy_data[ISY_VARIABLES][Platform.NUMBER]:
step = 10 ** (-1 * node.prec)
min_max = ISY_MAX_SIZE / (10**node.prec)
description = NumberEntityDescription(
@ -70,15 +59,17 @@ async def async_setup_entry(
entities.append(
ISYVariableNumberEntity(
node,
unique_id=f"{uuid}_{node.address}",
unique_id=f"{isy.uuid}_{node.address}",
description=description,
device_info=device_info[CONF_VARIABLES],
)
)
entities.append(
ISYVariableNumberEntity(
node=node,
unique_id=f"{uuid}_{node.address}_init",
unique_id=f"{isy.uuid}_{node.address}_init",
description=description_init,
device_info=device_info[CONF_VARIABLES],
init_entity=True,
)
)
@ -89,7 +80,7 @@ async def async_setup_entry(
class ISYVariableNumberEntity(NumberEntity):
"""Representation of an ISY variable as a number entity device."""
_attr_has_entity_name = True
_attr_has_entity_name = False
_attr_should_poll = False
_init_entity: bool
_node: Variable
@ -100,37 +91,19 @@ class ISYVariableNumberEntity(NumberEntity):
node: Variable,
unique_id: str,
description: NumberEntityDescription,
device_info: DeviceInfo,
init_entity: bool = False,
) -> None:
"""Initialize the ISY variable number."""
self._node = node
self._name = description.name
self.entity_description = description
self._change_handler: EventListener | None = None
# Two entities are created for each variable, one for current value and one for initial.
# Initial value entities are disabled by default
self._init_entity = init_entity
self._attr_unique_id = unique_id
url = _async_isy_to_configuration_url(node.isy)
config = node.isy.configuration
self._attr_device_info = DeviceInfo(
identifiers={
(
ISY994_DOMAIN,
f"{config[ISY_CONF_UUID]}_variables",
)
},
manufacturer=MANUFACTURER,
name=f"{config[ISY_CONF_NAME]} Variables",
model=config[ISY_CONF_MODEL],
sw_version=config[ISY_CONF_FIRMWARE],
configuration_url=url,
via_device=(ISY994_DOMAIN, config[ISY_CONF_UUID]),
entry_type=DeviceEntryType.SERVICE,
)
self._attr_device_info = device_info
async def async_added_to_hass(self) -> None:
"""Subscribe to the node change events."""

View file

@ -28,15 +28,15 @@ from homeassistant.components.sensor import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
_LOGGER,
DOMAIN as ISY994_DOMAIN,
ISY994_NODES,
ISY994_VARIABLES,
ISY_CONF_UUID,
DOMAIN,
ISY_DEVICES,
ISY_NODES,
ISY_VARIABLES,
SENSOR_AUX,
UOM_DOUBLE_TEMP,
UOM_FRIENDLY_NAME,
@ -110,15 +110,16 @@ 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[ISY994_DOMAIN][entry.entry_id]
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
entities: list[ISYSensorEntity | ISYSensorVariableEntity] = []
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
for node in hass_isy_data[ISY994_NODES][Platform.SENSOR]:
for node in hass_isy_data[ISY_NODES][Platform.SENSOR]:
_LOGGER.debug("Loading %s", node.name)
entities.append(ISYSensorEntity(node))
entities.append(ISYSensorEntity(node, devices.get(node.primary_node)))
aux_nodes = set()
for node, control in hass_isy_data[ISY994_NODES][SENSOR_AUX]:
for node, control in hass_isy_data[ISY_NODES][SENSOR_AUX]:
aux_nodes.add(node)
if control in SKIP_AUX_PROPERTIES:
continue
@ -126,13 +127,27 @@ async def async_setup_entry(
enabled_default = control not in AUX_DISABLED_BY_DEFAULT_EXACT and not any(
control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT_MATCH
)
entities.append(ISYAuxSensorEntity(node, control, enabled_default))
entities.append(
ISYAuxSensorEntity(
node=node,
control=control,
enabled_default=enabled_default,
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, PROP_COMMS_ERROR, False))
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[ISY994_VARIABLES][Platform.SENSOR]:
for vname, vobj in hass_isy_data[ISY_VARIABLES][Platform.SENSOR]:
entities.append(ISYSensorVariableEntity(vname, vobj))
async_add_entities(entities)
@ -228,15 +243,26 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity):
class ISYAuxSensorEntity(ISYSensorEntity):
"""Representation of an ISY aux sensor device."""
def __init__(self, node: Node, control: str, enabled_default: bool) -> None:
def __init__(
self,
node: Node,
control: str,
enabled_default: bool,
device_info: DeviceInfo | None = None,
) -> None:
"""Initialize the ISY aux sensor."""
super().__init__(node)
super().__init__(node, device_info=device_info)
self._control = control
self._attr_entity_registry_enabled_default = enabled_default
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)
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."""
@ -250,25 +276,11 @@ class ISYAuxSensorEntity(ISYSensorEntity):
"""Return the target value."""
return None if self.target is None else self.target.value
@property
def unique_id(self) -> str | None:
"""Get the unique identifier of the device and aux sensor."""
if not hasattr(self._node, "address"):
return None
return f"{self._node.isy.configuration[ISY_CONF_UUID]}_{self._node.address}_{self._control}"
@property
def name(self) -> str:
"""Get the name of the device and aux sensor."""
base_name = self._name or str(self._node.name)
name = COMMAND_FRIENDLY_NAME.get(self._control, self._control)
return f"{base_name} {name.replace('_', ' ').title()}"
class ISYSensorVariableEntity(ISYEntity, SensorEntity):
"""Representation of an ISY variable as a sensor device."""
# Depreceted sensors, will be removed in 2023.5.0
# Deprecated sensors, will be removed in 2023.5.0
_attr_entity_registry_enabled_default = False
def __init__(self, vname: str, vobj: object) -> None:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from pyisy.constants import COMMAND_FRIENDLY_NAME, PROTO_NETWORK_RESOURCE
from pyisy.constants import COMMAND_FRIENDLY_NAME
import voluptuous as vol
from homeassistant.const import (
@ -25,11 +25,11 @@ from homeassistant.helpers.service import entity_service_call
from .const import (
_LOGGER,
CONF_NETWORK,
DOMAIN,
ISY994_ISY,
ISY_CONF_NAME,
ISY_CONF_NETWORKING,
ISY_CONF_UUID,
ISY_ROOT,
)
from .util import unique_ids_for_config_entry_id
@ -192,8 +192,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][ISY994_ISY]
if isy_name and isy_name != isy.configuration["name"]:
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT]
if isy_name and isy_name != isy.conf["name"]:
continue
# If an address is provided, make sure we query the correct ISY.
# Otherwise, query the whole system on all ISY's connected.
@ -201,7 +201,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
_LOGGER.debug(
"Requesting query of device %s on ISY %s",
address,
isy.configuration[ISY_CONF_UUID],
isy.uuid,
)
await isy.query(address)
async_log_deprecated_service_call(
@ -211,21 +211,19 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
alternate_target=entity_registry.async_get_entity_id(
Platform.BUTTON,
DOMAIN,
f"{isy.configuration[ISY_CONF_UUID]}_{address}_query",
f"{isy.uuid}_{address}_query",
),
breaks_in_ha_version="2023.5.0",
)
return
_LOGGER.debug(
"Requesting system query of ISY %s", isy.configuration[ISY_CONF_UUID]
)
_LOGGER.debug("Requesting system query of ISY %s", isy.uuid)
await isy.query()
async_log_deprecated_service_call(
hass,
call=service,
alternate_service="button.press",
alternate_target=entity_registry.async_get_entity_id(
Platform.BUTTON, DOMAIN, f"{isy.configuration[ISY_CONF_UUID]}_query"
Platform.BUTTON, DOMAIN, f"{isy.uuid}_query"
),
breaks_in_ha_version="2023.5.0",
)
@ -237,10 +235,10 @@ 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][ISY994_ISY]
if isy_name and isy_name != isy.configuration[ISY_CONF_NAME]:
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT]
if isy_name and isy_name != isy.conf[ISY_CONF_NAME]:
continue
if isy.networking is None or not isy.configuration[ISY_CONF_NETWORKING]:
if isy.networking is None or not isy.conf[ISY_CONF_NETWORKING]:
continue
command = None
if address:
@ -257,7 +255,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
alternate_target=entity_registry.async_get_entity_id(
Platform.BUTTON,
DOMAIN,
f"{isy.configuration[ISY_CONF_UUID]}_{PROTO_NETWORK_RESOURCE}_{address}",
f"{isy.uuid}_{CONF_NETWORK}_{address}",
),
breaks_in_ha_version="2023.5.0",
)
@ -274,8 +272,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][ISY994_ISY]
if isy_name and isy_name != isy.configuration["name"]:
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT]
if isy_name and isy_name != isy.conf["name"]:
continue
program = None
if address:
@ -297,8 +295,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][ISY994_ISY]
if isy_name and isy_name != isy.configuration["name"]:
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT]
if isy_name and isy_name != isy.conf["name"]:
continue
variable = None
if name:
@ -315,7 +313,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
alternate_target=entity_registry.async_get_entity_id(
Platform.NUMBER,
DOMAIN,
f"{isy.configuration[ISY_CONF_UUID]}_{address}{'_init' if init else ''}",
f"{isy.uuid}_{address}{'_init' if init else ''}",
),
breaks_in_ha_version="2023.5.0",
)
@ -326,40 +324,31 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
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)
config_ids = []
current_unique_ids: set[str] = set()
for config_entry_id in hass.data[DOMAIN]:
entries_for_this_config = er.async_entries_for_config_entry(
entity_registry, config_entry_id
)
config_ids.extend(
[
(entity.unique_id, entity.entity_id)
for entity in entries_for_this_config
]
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)
)
current_unique_ids |= unique_ids_for_config_entry_id(hass, config_entry_id)
extra_entities = [
entity_id
for unique_id, entity_id in config_ids
if unique_id not in current_unique_ids
]
for entity in extra_entities:
if entity_registry.async_is_registered(entities[entity]):
entity_registry.async_remove(entities[entity])
for entity_id in extra_entities:
if entity_registry.async_is_registered(entity_id):
entity_registry.async_remove(entity_id)
_LOGGER.debug(
(
"Cleaning up ISY Entities and devices: Config Entries: %s, Current"
" Entries: %s, Extra Entries Removed: %s"
),
len(config_ids),
len(current_unique_ids),
len(extra_entities),
)
_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:
"""Trigger a reload of all ISY config entries."""

View file

@ -9,9 +9,10 @@ from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS
from .entity import ISYNodeEntity, ISYProgramEntity
@ -19,12 +20,18 @@ 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[ISY994_DOMAIN][entry.entry_id]
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = []
for node in hass_isy_data[ISY994_NODES][Platform.SWITCH]:
entities.append(ISYSwitchEntity(node))
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
for node in hass_isy_data[ISY_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
primary = node.isy.nodes.get_by_id(node.controllers[0]).primary_node
for name, status, actions in hass_isy_data[ISY994_PROGRAMS][Platform.SWITCH]:
entities.append(ISYSwitchEntity(node, devices.get(primary)))
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.SWITCH]:
entities.append(ISYSwitchProgramEntity(name, status, actions))
async_add_entities(entities)

View file

@ -10,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN, ISY994_ISY, ISY_URL_POSTFIX
from .const import DOMAIN, ISY_ROOT, ISY_URL_POSTFIX
@callback
@ -28,7 +28,7 @@ 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][ISY994_ISY]
isy: ISY = hass.data[DOMAIN][config_entry_id][ISY_ROOT]
entry = hass.config_entries.async_get_entry(config_entry_id)
assert isinstance(entry, ConfigEntry)

View file

@ -1,40 +1,69 @@
"""ISY utils."""
from __future__ import annotations
from pyisy.constants import PROP_COMMS_ERROR, PROTO_INSTEON
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import (
CONF_NETWORK,
DOMAIN,
ISY994_ISY,
ISY994_NODES,
ISY994_PROGRAMS,
ISY994_VARIABLES,
ISY_CONF_UUID,
PLATFORMS,
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(
hass: HomeAssistant, config_entry_id: str
) -> set[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]
uuid = hass_isy_data[ISY994_ISY].configuration[ISY_CONF_UUID]
current_unique_ids: set[str] = {uuid}
isy = hass_isy_data[ISY_ROOT]
current_unique_ids: set[tuple[Platform | str, str]] = {
(Platform.BUTTON, f"{isy.uuid}_query")
}
for platform in PLATFORMS:
for node in hass_isy_data[ISY994_NODES][platform]:
if hasattr(node, "address"):
current_unique_ids.add(f"{uuid}_{node.address}")
# 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}"))
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 platform in PROGRAM_PLATFORMS:
for _, node, _ in hass_isy_data[ISY994_PROGRAMS][platform]:
if hasattr(node, "address"):
current_unique_ids.add(f"{uuid}_{node.address}")
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[ISY994_VARIABLES]:
if hasattr(node, "address"):
current_unique_ids.add(f"{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

View file

@ -1696,7 +1696,7 @@ pyirishrail==0.0.2
pyiss==1.0.1
# homeassistant.components.isy994
pyisy==3.0.12
pyisy==3.1.3
# homeassistant.components.itach
pyitachip2ir==0.0.7

View file

@ -1215,7 +1215,7 @@ pyiqvia==2022.04.0
pyiss==1.0.1
# homeassistant.components.isy994
pyisy==3.0.12
pyisy==3.1.3
# homeassistant.components.kaleidescape
pykaleidescape==1.0.1

View file

@ -4,7 +4,7 @@ from unittest.mock import Mock
from aiohttp import ClientError
from homeassistant.components.isy994.const import DOMAIN, ISY994_ISY, ISY_URL_POSTFIX
from homeassistant.components.isy994.const import DOMAIN, ISY_ROOT, ISY_URL_POSTFIX
from homeassistant.const import CONF_HOST
from homeassistant.setup import async_setup_component
@ -33,7 +33,7 @@ async def test_system_health(hass, aioclient_mock):
hass.data[DOMAIN] = {}
hass.data[DOMAIN][MOCK_ENTRY_ID] = {}
hass.data[DOMAIN][MOCK_ENTRY_ID][ISY994_ISY] = Mock(
hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock(
connected=True,
websocket=Mock(
last_heartbeat=MOCK_HEARTBEAT,
@ -69,7 +69,7 @@ async def test_system_health_failed_connect(hass, aioclient_mock):
hass.data[DOMAIN] = {}
hass.data[DOMAIN][MOCK_ENTRY_ID] = {}
hass.data[DOMAIN][MOCK_ENTRY_ID][ISY994_ISY] = Mock(
hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock(
connected=True,
websocket=Mock(
last_heartbeat=MOCK_HEARTBEAT,