core/homeassistant/components/zwave_js/discovery.py

258 lines
8.1 KiB
Python

"""Map Z-Wave nodes and values to Home Assistant entities."""
from dataclasses import dataclass
from typing import Generator, Optional, Set, Union
from zwave_js_server.const import CommandClass
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import Value as ZwaveValue
from homeassistant.core import callback
@dataclass
class ZwaveDiscoveryInfo:
"""Info discovered from (primary) ZWave Value to create entity."""
node: ZwaveNode # node to which the value(s) belongs
primary_value: ZwaveValue # the value object itself for primary value
platform: str # the home assistant platform for which an entity should be created
platform_hint: Optional[
str
] = "" # hint for the platform about this discovered entity
@property
def value_id(self) -> str:
"""Return the unique value_id belonging to primary value."""
return f"{self.node.node_id}.{self.primary_value.value_id}"
@dataclass
class ZWaveDiscoverySchema:
"""Z-Wave discovery schema.
The (primary) value for an entity must match these conditions.
Use the Z-Wave specifications to find out the values for these parameters:
https://github.com/zwave-js/node-zwave-js/tree/master/specs
"""
# specify the hass platform for which this scheme applies (e.g. light, sensor)
platform: str
# [optional] hint for platform
hint: Optional[str] = None
# [optional] the node's basic device class must match ANY of these values
device_class_basic: Optional[Set[str]] = None
# [optional] the node's generic device class must match ANY of these values
device_class_generic: Optional[Set[str]] = None
# [optional] the node's specific device class must match ANY of these values
device_class_specific: Optional[Set[str]] = None
# [optional] the value's command class must match ANY of these values
command_class: Optional[Set[int]] = None
# [optional] the value's endpoint must match ANY of these values
endpoint: Optional[Set[int]] = None
# [optional] the value's property must match ANY of these values
property: Optional[Set[Union[str, int]]] = None
# [optional] the value's metadata_type must match ANY of these values
type: Optional[Set[str]] = None
DISCOVERY_SCHEMAS = [
# locks
ZWaveDiscoverySchema(
platform="lock",
device_class_generic={"Entry Control"},
device_class_specific={
"Door Lock",
"Advanced Door Lock",
"Secure Keypad Door Lock",
"Secure Lockbox",
},
command_class={
CommandClass.LOCK,
CommandClass.DOOR_LOCK,
},
property={"currentMode", "locked"},
type={"number", "boolean"},
),
# door lock door status
ZWaveDiscoverySchema(
platform="binary_sensor",
hint="property",
device_class_generic={"Entry Control"},
device_class_specific={
"Door Lock",
"Advanced Door Lock",
"Secure Keypad Door Lock",
"Secure Lockbox",
},
command_class={
CommandClass.LOCK,
CommandClass.DOOR_LOCK,
},
property={"doorStatus"},
type={"any"},
),
# climate
ZWaveDiscoverySchema(
platform="climate",
device_class_generic={"Thermostat"},
device_class_specific={
"Setback Thermostat",
"Thermostat General",
"Thermostat General V2",
},
command_class={CommandClass.THERMOSTAT_MODE},
property={"mode"},
type={"number"},
),
# lights
# primary value is the currentValue (brightness)
ZWaveDiscoverySchema(
platform="light",
device_class_generic={"Multilevel Switch", "Remote Switch"},
device_class_specific={
"Multilevel Tunable Color Light",
"Binary Tunable Color Light",
"Multilevel Remote Switch",
"Multilevel Power Switch",
},
command_class={CommandClass.SWITCH_MULTILEVEL},
property={"currentValue"},
type={"number"},
),
# binary sensors
ZWaveDiscoverySchema(
platform="binary_sensor",
hint="boolean",
command_class={
CommandClass.SENSOR_BINARY,
CommandClass.BATTERY,
},
type={"boolean"},
),
ZWaveDiscoverySchema(
platform="binary_sensor",
hint="notification",
command_class={
CommandClass.NOTIFICATION,
},
type={"number"},
),
# generic text sensors
ZWaveDiscoverySchema(
platform="sensor",
hint="string_sensor",
command_class={
CommandClass.ALARM,
CommandClass.SENSOR_ALARM,
CommandClass.INDICATOR,
CommandClass.NOTIFICATION,
},
type={"string"},
),
# generic numeric sensors
ZWaveDiscoverySchema(
platform="sensor",
hint="numeric_sensor",
command_class={
CommandClass.SENSOR_MULTILEVEL,
CommandClass.METER,
CommandClass.ALARM,
CommandClass.SENSOR_ALARM,
CommandClass.INDICATOR,
CommandClass.BATTERY,
CommandClass.NOTIFICATION,
CommandClass.BASIC,
},
type={"number"},
),
# binary switches
ZWaveDiscoverySchema(
platform="switch",
command_class={CommandClass.SWITCH_BINARY},
property={"currentValue"},
),
# cover
ZWaveDiscoverySchema(
platform="cover",
hint="cover",
device_class_generic={"Multilevel Switch"},
device_class_specific={
"Motor Control Class A",
"Motor Control Class B",
"Motor Control Class C",
"Multiposition Motor",
},
command_class={CommandClass.SWITCH_MULTILEVEL},
property={"currentValue"},
type={"number"},
),
# fan
ZWaveDiscoverySchema(
platform="fan",
hint="fan",
device_class_generic={"Multilevel Switch"},
device_class_specific={"Fan Switch"},
command_class={CommandClass.SWITCH_MULTILEVEL},
property={"currentValue"},
type={"number"},
),
]
@callback
def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None, None]:
"""Run discovery on ZWave node and return matching (primary) values."""
for value in node.values.values():
disc_val = async_discover_value(value)
if disc_val:
yield disc_val
@callback
def async_discover_value(value: ZwaveValue) -> Optional[ZwaveDiscoveryInfo]:
"""Run discovery on Z-Wave value and return ZwaveDiscoveryInfo if match found."""
for schema in DISCOVERY_SCHEMAS:
# check device_class_basic
if (
schema.device_class_basic is not None
and value.node.device_class.basic not in schema.device_class_basic
):
continue
# check device_class_generic
if (
schema.device_class_generic is not None
and value.node.device_class.generic not in schema.device_class_generic
):
continue
# check device_class_specific
if (
schema.device_class_specific is not None
and value.node.device_class.specific not in schema.device_class_specific
):
continue
# check command_class
if (
schema.command_class is not None
and value.command_class not in schema.command_class
):
continue
# check endpoint
if schema.endpoint is not None and value.endpoint not in schema.endpoint:
continue
# check property
if schema.property is not None and value.property_ not in schema.property:
continue
# check metadata_type
if schema.type is not None and value.metadata.type not in schema.type:
continue
# all checks passed, this value belongs to an entity
return ZwaveDiscoveryInfo(
node=value.node,
primary_value=value,
platform=schema.platform,
platform_hint=schema.hint,
)
return None