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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 61 additions & 45 deletions homeassistant/components/isy994/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,29 @@
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
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_USERNAME,
CONF_VARIABLES,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
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,
Expand All @@ -36,28 +39,29 @@
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(
{
Expand Down Expand Up @@ -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] = {}
Comment on lines +141 to +146
Copy link
Copy Markdown
Member

@bdraco bdraco Jan 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a future PR: This might be a bit easier to manage as a top level dataclass instead of a dict so you can use named attributes instead. lookin has an example of this in models

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bdraco Good call. I may go ahead and do that before I push the next PR for on level sensors.

Thanks for reviewing all these. I have a few more on deck coming your way while I have time.


isy_config = entry.data
isy_options = entry.options
Expand Down Expand Up @@ -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:
Comment thread
shbatm marked this conversation as resolved.
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.
Expand Down Expand Up @@ -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:
Comment thread
shbatm marked this conversation as resolved.
"""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.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=isy.configuration[ISY_CONF_NAME],
model=isy.configuration[ISY_CONF_MODEL],
sw_version=isy.configuration[ISY_CONF_FIRMWARE],
configuration_url=url,
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,
)


Expand All @@ -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()
Expand All @@ -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
)
65 changes: 43 additions & 22 deletions homeassistant/components/isy994/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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
Expand All @@ -30,9 +31,10 @@
_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,
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -157,37 +167,45 @@ 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
if subnode_id in SUBNODE_TAMPER:
# 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)
Expand Down Expand Up @@ -225,9 +243,10 @@ def __init__(
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
Expand Down Expand Up @@ -260,9 +279,10 @@ def __init__(
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:
Expand Down Expand Up @@ -399,6 +419,7 @@ def __init__(
| ISYBinarySensorEntity
| ISYBinarySensorHeartbeat
| ISYBinarySensorProgramEntity,
device_info: DeviceInfo | None = None,
) -> None:
"""Initialize the ISY binary sensor device.

Expand All @@ -409,7 +430,7 @@ def __init__(
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
Expand Down
Loading