2020-05-03 00:54:16 +00:00
|
|
|
"""Generic Z-Wave Entity Classes."""
|
|
|
|
|
|
|
|
import copy
|
|
|
|
import logging
|
|
|
|
|
|
|
|
from openzwavemqtt.const import (
|
|
|
|
EVENT_INSTANCE_STATUS_CHANGED,
|
|
|
|
EVENT_VALUE_CHANGED,
|
|
|
|
OZW_READY_STATES,
|
2020-08-06 14:50:51 +00:00
|
|
|
CommandClass,
|
|
|
|
ValueIndex,
|
2020-05-03 00:54:16 +00:00
|
|
|
)
|
|
|
|
from openzwavemqtt.models.node import OZWNode
|
|
|
|
from openzwavemqtt.models.value import OZWValue
|
|
|
|
|
|
|
|
from homeassistant.core import callback
|
|
|
|
from homeassistant.helpers.dispatcher import (
|
|
|
|
async_dispatcher_connect,
|
|
|
|
async_dispatcher_send,
|
|
|
|
)
|
|
|
|
from homeassistant.helpers.entity import Entity
|
|
|
|
|
|
|
|
from . import const
|
|
|
|
from .const import DOMAIN, PLATFORMS
|
|
|
|
from .discovery import check_node_schema, check_value_schema
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
2020-08-11 16:23:10 +00:00
|
|
|
OZW_READY_STATES_VALUES = {st.value for st in OZW_READY_STATES}
|
2020-05-03 00:54:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ZWaveDeviceEntityValues:
|
|
|
|
"""Manages entity access to the underlying Z-Wave value objects."""
|
|
|
|
|
|
|
|
def __init__(self, hass, options, schema, primary_value):
|
|
|
|
"""Initialize the values object with the passed entity schema."""
|
|
|
|
self._hass = hass
|
|
|
|
self._entity_created = False
|
|
|
|
self._schema = copy.deepcopy(schema)
|
|
|
|
self._values = {}
|
|
|
|
self.options = options
|
|
|
|
|
|
|
|
# Go through values listed in the discovery schema, initialize them,
|
|
|
|
# and add a check to the schema to make sure the Instance matches.
|
|
|
|
for name, disc_settings in self._schema[const.DISC_VALUES].items():
|
|
|
|
self._values[name] = None
|
2020-05-07 21:52:54 +00:00
|
|
|
disc_settings[const.DISC_INSTANCE] = (primary_value.instance,)
|
2020-05-03 00:54:16 +00:00
|
|
|
|
|
|
|
self._values[const.DISC_PRIMARY] = primary_value
|
|
|
|
self._node = primary_value.node
|
|
|
|
self._schema[const.DISC_NODE_ID] = [self._node.node_id]
|
|
|
|
|
|
|
|
def async_setup(self):
|
|
|
|
"""Set up values instance."""
|
|
|
|
# Check values that have already been discovered for node
|
|
|
|
# and see if they match the schema and need added to the entity.
|
|
|
|
for value in self._node.values():
|
|
|
|
self.async_check_value(value)
|
|
|
|
|
|
|
|
# Check if all the _required_ values in the schema are present and
|
|
|
|
# create the entity.
|
|
|
|
self._async_check_entity_ready()
|
|
|
|
|
|
|
|
def __getattr__(self, name):
|
|
|
|
"""Get the specified value for this entity."""
|
|
|
|
return self._values.get(name, None)
|
|
|
|
|
|
|
|
def __iter__(self):
|
|
|
|
"""Allow iteration over all values."""
|
|
|
|
return iter(self._values.values())
|
|
|
|
|
|
|
|
def __contains__(self, name):
|
|
|
|
"""Check if the specified name/key exists in the values."""
|
|
|
|
return name in self._values
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_check_value(self, value):
|
|
|
|
"""Check if the new value matches a missing value for this entity.
|
|
|
|
|
|
|
|
If a match is found, it is added to the values mapping.
|
|
|
|
"""
|
|
|
|
# Make sure the node matches the schema for this entity.
|
|
|
|
if not check_node_schema(value.node, self._schema):
|
|
|
|
return
|
|
|
|
|
|
|
|
# Go through the possible values for this entity defined by the schema.
|
|
|
|
for name in self._values:
|
|
|
|
# Skip if it's already been added.
|
|
|
|
if self._values[name] is not None:
|
|
|
|
continue
|
|
|
|
# Skip if the value doesn't match the schema.
|
|
|
|
if not check_value_schema(value, self._schema[const.DISC_VALUES][name]):
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Add value to mapping.
|
|
|
|
self._values[name] = value
|
|
|
|
|
|
|
|
# If the entity has already been created, notify it of the new value.
|
|
|
|
if self._entity_created:
|
|
|
|
async_dispatcher_send(
|
|
|
|
self._hass, f"{DOMAIN}_{self.values_id}_value_added"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Check if entity has all required values and create the entity if needed.
|
|
|
|
self._async_check_entity_ready()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_check_entity_ready(self):
|
|
|
|
"""Check if all required values are discovered and create entity."""
|
|
|
|
# Abort if the entity has already been created
|
|
|
|
if self._entity_created:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Go through values defined in the schema and abort if a required value is missing.
|
|
|
|
for name, disc_settings in self._schema[const.DISC_VALUES].items():
|
|
|
|
if self._values[name] is None and not disc_settings.get(
|
|
|
|
const.DISC_OPTIONAL
|
|
|
|
):
|
|
|
|
return
|
|
|
|
|
|
|
|
# We have all the required values, so create the entity.
|
|
|
|
component = self._schema[const.DISC_COMPONENT]
|
|
|
|
|
|
|
|
_LOGGER.debug(
|
|
|
|
"Adding Node_id=%s Generic_command_class=%s, "
|
|
|
|
"Specific_command_class=%s, "
|
|
|
|
"Command_class=%s, Index=%s, Value type=%s, "
|
|
|
|
"Genre=%s as %s",
|
|
|
|
self._node.node_id,
|
|
|
|
self._node.node_generic,
|
|
|
|
self._node.node_specific,
|
|
|
|
self.primary.command_class,
|
|
|
|
self.primary.index,
|
|
|
|
self.primary.type,
|
|
|
|
self.primary.genre,
|
|
|
|
component,
|
|
|
|
)
|
|
|
|
self._entity_created = True
|
|
|
|
|
|
|
|
if component in PLATFORMS:
|
|
|
|
async_dispatcher_send(self._hass, f"{DOMAIN}_new_{component}", self)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def values_id(self):
|
|
|
|
"""Identification for this values collection."""
|
|
|
|
return create_value_id(self.primary)
|
|
|
|
|
|
|
|
|
|
|
|
class ZWaveDeviceEntity(Entity):
|
|
|
|
"""Generic Entity Class for a Z-Wave Device."""
|
|
|
|
|
|
|
|
def __init__(self, values):
|
|
|
|
"""Initialize a generic Z-Wave device entity."""
|
|
|
|
self.values = values
|
|
|
|
self.options = values.options
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def on_value_update(self):
|
|
|
|
"""Call when a value is added/updated in the entity EntityValues Collection.
|
|
|
|
|
|
|
|
To be overridden by platforms needing this event.
|
|
|
|
"""
|
|
|
|
|
|
|
|
async def async_added_to_hass(self):
|
|
|
|
"""Call when entity is added."""
|
2020-08-12 13:17:21 +00:00
|
|
|
# Add dispatcher and OZW listeners callbacks.
|
|
|
|
# Add to on_remove so they will be cleaned up on entity removal.
|
|
|
|
self.async_on_remove(
|
|
|
|
self.options.listen(EVENT_VALUE_CHANGED, self._value_changed)
|
|
|
|
)
|
|
|
|
self.async_on_remove(
|
|
|
|
self.options.listen(EVENT_INSTANCE_STATUS_CHANGED, self._instance_updated)
|
|
|
|
)
|
2020-05-03 00:54:16 +00:00
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass, const.SIGNAL_DELETE_ENTITY, self._delete_callback
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.async_on_remove(
|
|
|
|
async_dispatcher_connect(
|
|
|
|
self.hass,
|
|
|
|
f"{DOMAIN}_{self.values.values_id}_value_added",
|
|
|
|
self._value_added,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_info(self):
|
|
|
|
"""Return device information for the device registry."""
|
|
|
|
node = self.values.primary.node
|
|
|
|
node_instance = self.values.primary.instance
|
|
|
|
dev_id = create_device_id(node, self.values.primary.instance)
|
2020-08-06 14:50:51 +00:00
|
|
|
node_firmware = node.get_value(
|
|
|
|
CommandClass.VERSION, ValueIndex.VERSION_APPLICATION
|
|
|
|
)
|
2020-05-03 00:54:16 +00:00
|
|
|
device_info = {
|
|
|
|
"identifiers": {(DOMAIN, dev_id)},
|
|
|
|
"name": create_device_name(node),
|
|
|
|
"manufacturer": node.node_manufacturer_name,
|
|
|
|
"model": node.node_product_name,
|
|
|
|
}
|
2020-08-06 14:50:51 +00:00
|
|
|
if node_firmware is not None:
|
|
|
|
device_info["sw_version"] = node_firmware.value
|
|
|
|
|
2020-05-03 00:54:16 +00:00
|
|
|
# device with multiple instances is split up into virtual devices for each instance
|
|
|
|
if node_instance > 1:
|
|
|
|
parent_dev_id = create_device_id(node)
|
|
|
|
device_info["name"] += f" - Instance {node_instance}"
|
|
|
|
device_info["via_device"] = (DOMAIN, parent_dev_id)
|
|
|
|
return device_info
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Return the device specific state attributes."""
|
|
|
|
return {const.ATTR_NODE_ID: self.values.primary.node.node_id}
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of the entity."""
|
|
|
|
node = self.values.primary.node
|
|
|
|
return f"{create_device_name(node)}: {self.values.primary.label}"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def unique_id(self):
|
|
|
|
"""Return the unique_id of the entity."""
|
|
|
|
return self.values.values_id
|
|
|
|
|
|
|
|
@property
|
|
|
|
def available(self) -> bool:
|
|
|
|
"""Return entity availability."""
|
|
|
|
# Use OZW Daemon status for availability.
|
|
|
|
instance_status = self.values.primary.ozw_instance.get_status()
|
2020-08-11 16:23:10 +00:00
|
|
|
return instance_status and instance_status.status in OZW_READY_STATES_VALUES
|
2020-05-03 00:54:16 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def _value_changed(self, value):
|
|
|
|
"""Call when a value from ZWaveDeviceEntityValues is changed.
|
|
|
|
|
|
|
|
Should not be overridden by subclasses.
|
|
|
|
"""
|
|
|
|
if value.value_id_key in (v.value_id_key for v in self.values if v):
|
|
|
|
self.on_value_update()
|
|
|
|
self.async_write_ha_state()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _value_added(self):
|
|
|
|
"""Call when a value from ZWaveDeviceEntityValues is added.
|
|
|
|
|
|
|
|
Should not be overridden by subclasses.
|
|
|
|
"""
|
|
|
|
self.on_value_update()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _instance_updated(self, new_status):
|
|
|
|
"""Call when the instance status changes.
|
|
|
|
|
|
|
|
Should not be overridden by subclasses.
|
|
|
|
"""
|
|
|
|
self.on_value_update()
|
|
|
|
self.async_write_ha_state()
|
|
|
|
|
2020-07-20 08:56:22 +00:00
|
|
|
@property
|
|
|
|
def should_poll(self):
|
|
|
|
"""No polling needed."""
|
|
|
|
return False
|
|
|
|
|
2020-05-03 00:54:16 +00:00
|
|
|
async def _delete_callback(self, values_id):
|
|
|
|
"""Remove this entity."""
|
|
|
|
if not self.values:
|
|
|
|
return # race condition: delete already requested
|
|
|
|
if values_id == self.values.values_id:
|
|
|
|
await self.async_remove()
|
|
|
|
|
|
|
|
|
|
|
|
def create_device_name(node: OZWNode):
|
|
|
|
"""Generate sensible (short) default device name from a OZWNode."""
|
2020-05-14 14:33:57 +00:00
|
|
|
# Prefer custom name set by OZWAdmin if present
|
|
|
|
if node.node_name:
|
|
|
|
return node.node_name
|
|
|
|
# Prefer short devicename from metadata if present
|
|
|
|
if node.meta_data and node.meta_data.get("Name"):
|
|
|
|
return node.meta_data["Name"]
|
|
|
|
# Fallback to productname or devicetype strings
|
|
|
|
if node.node_product_name:
|
|
|
|
return node.node_product_name
|
|
|
|
if node.node_device_type_string:
|
|
|
|
return node.node_device_type_string
|
|
|
|
if node.node_specific_string:
|
|
|
|
return node.node_specific_string
|
|
|
|
# Last resort: use Node id (should never happen, but just in case)
|
|
|
|
return f"Node {node.id}"
|
2020-05-03 00:54:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
def create_device_id(node: OZWNode, node_instance: int = 1):
|
|
|
|
"""Generate unique device_id from a OZWNode."""
|
|
|
|
ozw_instance = node.parent.id
|
|
|
|
dev_id = f"{ozw_instance}.{node.node_id}.{node_instance}"
|
|
|
|
return dev_id
|
|
|
|
|
|
|
|
|
|
|
|
def create_value_id(value: OZWValue):
|
|
|
|
"""Generate unique value_id from an OZWValue."""
|
|
|
|
# [OZW_INSTANCE_ID]-[NODE_ID]-[VALUE_ID_KEY]
|
|
|
|
return f"{value.node.parent.id}-{value.node.id}-{value.value_id_key}"
|