265 lines
9.9 KiB
Python
265 lines
9.9 KiB
Python
"""Representation of ISYEntity Types."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, cast
|
|
|
|
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 homeassistant.const import (
|
|
ATTR_IDENTIFIERS,
|
|
ATTR_MANUFACTURER,
|
|
ATTR_MODEL,
|
|
ATTR_NAME,
|
|
ATTR_SUGGESTED_AREA,
|
|
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
|
|
|
|
|
|
class ISYEntity(Entity):
|
|
"""Representation of an ISY994 device."""
|
|
|
|
_name: str | None = None
|
|
_attr_should_poll = False
|
|
|
|
def __init__(self, node: Node) -> None:
|
|
"""Initialize the insteon device."""
|
|
self._node = node
|
|
self._attrs: dict[str, Any] = {}
|
|
self._change_handler: EventListener | None = None
|
|
self._control_handler: EventListener | None = None
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Subscribe to the node change events."""
|
|
self._change_handler = self._node.status_events.subscribe(self.async_on_update)
|
|
|
|
if hasattr(self._node, "control_events"):
|
|
self._control_handler = self._node.control_events.subscribe(
|
|
self.async_on_control
|
|
)
|
|
|
|
@callback
|
|
def async_on_update(self, event: NodeProperty) -> None:
|
|
"""Handle the update event from the ISY994 Node."""
|
|
self.async_write_ha_state()
|
|
|
|
@callback
|
|
def async_on_control(self, event: NodeProperty) -> None:
|
|
"""Handle a control event from the ISY994 Node."""
|
|
event_data = {
|
|
"entity_id": self.entity_id,
|
|
"control": event.control,
|
|
"value": event.value,
|
|
"formatted": event.formatted,
|
|
"uom": event.uom,
|
|
"precision": event.prec,
|
|
}
|
|
|
|
if event.control not in EVENT_PROPS_IGNORED:
|
|
# New state attributes may be available, update the state.
|
|
self.async_write_ha_state()
|
|
|
|
self.hass.bus.async_fire("isy994_control", event_data)
|
|
|
|
@property
|
|
def device_info(self) -> DeviceInfo | None:
|
|
"""Return the device_info of the device."""
|
|
if hasattr(self._node, "protocol") and self._node.protocol == PROTO_GROUP:
|
|
# not a device
|
|
return None
|
|
isy = self._node.isy
|
|
uuid = isy.configuration["uuid"]
|
|
node = self._node
|
|
url = _async_isy_to_configuration_url(isy)
|
|
|
|
basename = self._name or str(self._node.name)
|
|
|
|
if hasattr(self._node, "parent_node") and self._node.parent_node is not None:
|
|
# This is not the parent node, get the parent node.
|
|
node = self._node.parent_node
|
|
basename = node.name
|
|
|
|
device_info = DeviceInfo(
|
|
manufacturer="Unknown",
|
|
model="Unknown",
|
|
name=basename,
|
|
via_device=(DOMAIN, uuid),
|
|
configuration_url=url,
|
|
)
|
|
|
|
if hasattr(node, "address"):
|
|
assert isinstance(node.address, str)
|
|
device_info[ATTR_NAME] = f"{basename} ({node.address})"
|
|
if hasattr(node, "primary_node"):
|
|
device_info[ATTR_IDENTIFIERS] = {(DOMAIN, f"{uuid}_{node.address}")}
|
|
# ISYv5 Device Types
|
|
if hasattr(node, "node_def_id") and node.node_def_id is not None:
|
|
model: str = str(node.node_def_id)
|
|
# Numerical Device Type
|
|
if hasattr(node, "type") and node.type is not None:
|
|
model += f" {node.type}"
|
|
device_info[ATTR_MODEL] = model
|
|
if hasattr(node, "protocol"):
|
|
model = str(device_info[ATTR_MODEL])
|
|
manufacturer = str(node.protocol)
|
|
if node.protocol == PROTO_ZWAVE:
|
|
# Get extra information for Z-Wave Devices
|
|
manufacturer += f" 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_MANUFACTURER] = manufacturer
|
|
device_info[ATTR_MODEL] = model
|
|
if hasattr(node, "folder") and node.folder is not None:
|
|
device_info[ATTR_SUGGESTED_AREA] = node.folder
|
|
# Note: sw_version is not exposed by the ISY for the individual devices.
|
|
|
|
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['uuid']}_{self._node.address}"
|
|
return None
|
|
|
|
@property
|
|
def old_unique_id(self) -> str | None:
|
|
"""Get the old unique identifier of the device."""
|
|
if hasattr(self._node, "address"):
|
|
return cast(str, 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."""
|
|
|
|
@property
|
|
def extra_state_attributes(self) -> dict:
|
|
"""Get the state attributes for the device.
|
|
|
|
The 'aux_properties' in the pyisy Node class are combined with the
|
|
other attributes which have been picked up from the event stream and
|
|
the combined result are returned as the device state attributes.
|
|
"""
|
|
attr = {}
|
|
node = self._node
|
|
# Insteon aux_properties are now their own sensors
|
|
if (
|
|
hasattr(self._node, "aux_properties")
|
|
and getattr(node, "protocol", None) != PROTO_INSTEON
|
|
):
|
|
for name, value in self._node.aux_properties.items():
|
|
attr_name = COMMAND_FRIENDLY_NAME.get(name, name)
|
|
attr[attr_name] = str(value.formatted).lower()
|
|
|
|
# If a Group/Scene, set a property if the entire scene is on/off
|
|
if hasattr(self._node, "group_all_on"):
|
|
attr["group_all_on"] = STATE_ON if self._node.group_all_on else STATE_OFF
|
|
|
|
self._attrs.update(attr)
|
|
return self._attrs
|
|
|
|
async def async_send_node_command(self, command: str) -> None:
|
|
"""Respond to an entity service command call."""
|
|
if not hasattr(self._node, command):
|
|
raise HomeAssistantError(
|
|
f"Invalid service call: {command} for device {self.entity_id}"
|
|
)
|
|
await getattr(self._node, command)()
|
|
|
|
async def async_send_raw_node_command(
|
|
self,
|
|
command: str,
|
|
value: Any | None = None,
|
|
unit_of_measurement: str | None = None,
|
|
parameters: Any | None = None,
|
|
) -> None:
|
|
"""Respond to an entity service raw command call."""
|
|
if not hasattr(self._node, "send_cmd"):
|
|
raise HomeAssistantError(
|
|
f"Invalid service call: {command} for device {self.entity_id}"
|
|
)
|
|
await self._node.send_cmd(command, value, unit_of_measurement, parameters)
|
|
|
|
async def async_get_zwave_parameter(self, parameter: Any) -> None:
|
|
"""Respond to an entity service command to request a Z-Wave device parameter from the ISY."""
|
|
if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE:
|
|
raise HomeAssistantError(
|
|
f"Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave device {self.entity_id}"
|
|
)
|
|
await self._node.get_zwave_parameter(parameter)
|
|
|
|
async def async_set_zwave_parameter(
|
|
self, parameter: Any, value: Any | None, size: int | None
|
|
) -> None:
|
|
"""Respond to an entity service command to set a Z-Wave device parameter via the ISY."""
|
|
if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE:
|
|
raise HomeAssistantError(
|
|
f"Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave device {self.entity_id}"
|
|
)
|
|
await self._node.set_zwave_parameter(parameter, value, size)
|
|
await self._node.get_zwave_parameter(parameter)
|
|
|
|
async def async_rename_node(self, name: str) -> None:
|
|
"""Respond to an entity service command to rename a node on the ISY."""
|
|
await self._node.rename(name)
|
|
|
|
|
|
class ISYProgramEntity(ISYEntity):
|
|
"""Representation of an ISY994 program base."""
|
|
|
|
def __init__(self, name: str, status: Any | None, actions: Program = None) -> None:
|
|
"""Initialize the ISY994 program-based entity."""
|
|
super().__init__(status)
|
|
self._name = name
|
|
self._actions = actions
|
|
|
|
@property
|
|
def extra_state_attributes(self) -> dict:
|
|
"""Get the state attributes for the device."""
|
|
attr = {}
|
|
if self._actions:
|
|
attr["actions_enabled"] = self._actions.enabled
|
|
if self._actions.last_finished != EMPTY_TIME:
|
|
attr["actions_last_finished"] = self._actions.last_finished
|
|
if self._actions.last_run != EMPTY_TIME:
|
|
attr["actions_last_run"] = self._actions.last_run
|
|
if self._actions.last_update != EMPTY_TIME:
|
|
attr["actions_last_update"] = self._actions.last_update
|
|
attr["ran_else"] = self._actions.ran_else
|
|
attr["ran_then"] = self._actions.ran_then
|
|
attr["run_at_startup"] = self._actions.run_at_startup
|
|
attr["running"] = self._actions.running
|
|
attr["status_enabled"] = self._node.enabled
|
|
if self._node.last_finished != EMPTY_TIME:
|
|
attr["status_last_finished"] = self._node.last_finished
|
|
if self._node.last_run != EMPTY_TIME:
|
|
attr["status_last_run"] = self._node.last_run
|
|
if self._node.last_update != EMPTY_TIME:
|
|
attr["status_last_update"] = self._node.last_update
|
|
return attr
|