Consolidate device info and clean-up ISY994 code base (#85657)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
43cc8a1ebf
commit
255a8362a1
21 changed files with 481 additions and 446 deletions
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue