577 lines
19 KiB
Python
577 lines
19 KiB
Python
"""Map Z-Wave nodes and values to Home Assistant entities."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Generator
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from zwave_js_server.const import CommandClass
|
|
from zwave_js_server.model.device_class import DeviceClassItem
|
|
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 to which the value(s) belongs
|
|
node: ZwaveNode
|
|
# the value object itself for primary value
|
|
primary_value: ZwaveValue
|
|
# the home assistant platform for which an entity should be created
|
|
platform: str
|
|
# hint for the platform about this discovered entity
|
|
platform_hint: str | None = ""
|
|
|
|
|
|
@dataclass
|
|
class ZWaveValueDiscoverySchema:
|
|
"""Z-Wave Value discovery schema.
|
|
|
|
The Z-Wave Value 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
|
|
"""
|
|
|
|
# [optional] the value's command class must match ANY of these values
|
|
command_class: set[int] | None = None
|
|
# [optional] the value's endpoint must match ANY of these values
|
|
endpoint: set[int] | None = None
|
|
# [optional] the value's property must match ANY of these values
|
|
property: set[str | int] | None = None
|
|
# [optional] the value's property name must match ANY of these values
|
|
property_name: set[str] | None = None
|
|
# [optional] the value's property key must match ANY of these values
|
|
property_key: set[str | int] | None = None
|
|
# [optional] the value's property key name must match ANY of these values
|
|
property_key_name: set[str] | None = None
|
|
# [optional] the value's metadata_type must match ANY of these values
|
|
type: set[str] | None = None
|
|
|
|
|
|
@dataclass
|
|
class ZWaveDiscoverySchema:
|
|
"""Z-Wave discovery schema.
|
|
|
|
The Z-Wave node and it's (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
|
|
# primary value belonging to this discovery scheme
|
|
primary_value: ZWaveValueDiscoverySchema
|
|
# [optional] hint for platform
|
|
hint: str | None = None
|
|
# [optional] the node's manufacturer_id must match ANY of these values
|
|
manufacturer_id: set[int] | None = None
|
|
# [optional] the node's product_id must match ANY of these values
|
|
product_id: set[int] | None = None
|
|
# [optional] the node's product_type must match ANY of these values
|
|
product_type: set[int] | None = None
|
|
# [optional] the node's firmware_version must match ANY of these values
|
|
firmware_version: set[str] | None = None
|
|
# [optional] the node's basic device class must match ANY of these values
|
|
device_class_basic: set[str | int] | None = None
|
|
# [optional] the node's generic device class must match ANY of these values
|
|
device_class_generic: set[str | int] | None = None
|
|
# [optional] the node's specific device class must match ANY of these values
|
|
device_class_specific: set[str | int] | None = None
|
|
# [optional] additional values that ALL need to be present on the node for this scheme to pass
|
|
required_values: list[ZWaveValueDiscoverySchema] | None = None
|
|
# [optional] additional values that MAY NOT be present on the node for this scheme to pass
|
|
absent_values: list[ZWaveValueDiscoverySchema] | None = None
|
|
# [optional] bool to specify if this primary value may be discovered by multiple platforms
|
|
allow_multi: bool = False
|
|
|
|
|
|
def get_config_parameter_discovery_schema(
|
|
property_: set[str | int] | None = None,
|
|
property_name: set[str] | None = None,
|
|
property_key: set[str | int] | None = None,
|
|
property_key_name: set[str] | None = None,
|
|
**kwargs: Any,
|
|
) -> ZWaveDiscoverySchema:
|
|
"""
|
|
Return a discovery schema for a config parameter.
|
|
|
|
Supports all keyword arguments to ZWaveValueDiscoverySchema except platform, hint,
|
|
and primary_value.
|
|
"""
|
|
return ZWaveDiscoverySchema(
|
|
platform="sensor",
|
|
hint="config_parameter",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={CommandClass.CONFIGURATION},
|
|
property=property_,
|
|
property_name=property_name,
|
|
property_key=property_key,
|
|
property_key_name=property_key_name,
|
|
type={"number"},
|
|
),
|
|
**kwargs,
|
|
)
|
|
|
|
|
|
SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA = ZWaveValueDiscoverySchema(
|
|
command_class={CommandClass.SWITCH_MULTILEVEL},
|
|
property={"currentValue"},
|
|
type={"number"},
|
|
)
|
|
|
|
# For device class mapping see:
|
|
# https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json
|
|
DISCOVERY_SCHEMAS = [
|
|
# ====== START OF DEVICE SPECIFIC MAPPING SCHEMAS =======
|
|
# Honeywell 39358 In-Wall Fan Control using switch multilevel CC
|
|
ZWaveDiscoverySchema(
|
|
platform="fan",
|
|
manufacturer_id={0x0039},
|
|
product_id={0x3131},
|
|
product_type={0x4944},
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
# GE/Jasco fan controllers using switch multilevel CC
|
|
ZWaveDiscoverySchema(
|
|
platform="fan",
|
|
manufacturer_id={0x0063},
|
|
product_id={0x3034, 0x3131, 0x3138},
|
|
product_type={0x4944},
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
# Leviton ZW4SF fan controllers using switch multilevel CC
|
|
ZWaveDiscoverySchema(
|
|
platform="fan",
|
|
manufacturer_id={0x001D},
|
|
product_id={0x0002},
|
|
product_type={0x0038},
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
# Inovelli LZW36 light / fan controller combo using switch multilevel CC
|
|
# The fan is endpoint 2, the light is endpoint 1.
|
|
ZWaveDiscoverySchema(
|
|
platform="fan",
|
|
manufacturer_id={0x031E},
|
|
product_id={0x0001},
|
|
product_type={0x000E},
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={CommandClass.SWITCH_MULTILEVEL},
|
|
endpoint={2},
|
|
property={"currentValue"},
|
|
type={"number"},
|
|
),
|
|
),
|
|
# Fibaro Shutter Fibaro FGS222
|
|
ZWaveDiscoverySchema(
|
|
platform="cover",
|
|
manufacturer_id={0x010F},
|
|
product_id={0x1000},
|
|
product_type={0x0302},
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
# Qubino flush shutter
|
|
ZWaveDiscoverySchema(
|
|
platform="cover",
|
|
manufacturer_id={0x0159},
|
|
product_id={0x0052},
|
|
product_type={0x0003},
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
# Graber/Bali/Spring Fashion Covers
|
|
ZWaveDiscoverySchema(
|
|
platform="cover",
|
|
manufacturer_id={0x026E},
|
|
product_id={0x5A31},
|
|
product_type={0x4353},
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
# iBlinds v2 window blind motor
|
|
ZWaveDiscoverySchema(
|
|
platform="cover",
|
|
manufacturer_id={0x0287},
|
|
product_id={0x000D},
|
|
product_type={0x0003},
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
# ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS =======
|
|
# Door lock mode config parameter. Functionality equivalent to Notification CC
|
|
# list sensors.
|
|
get_config_parameter_discovery_schema(
|
|
property_name={"Door lock mode"},
|
|
device_class_generic={"Entry Control"},
|
|
device_class_specific={
|
|
"Door Lock",
|
|
"Advanced Door Lock",
|
|
"Secure Keypad Door Lock",
|
|
"Secure Lockbox",
|
|
},
|
|
),
|
|
# ====== START OF GENERIC MAPPING SCHEMAS =======
|
|
# locks
|
|
ZWaveDiscoverySchema(
|
|
platform="lock",
|
|
device_class_generic={"Entry Control"},
|
|
device_class_specific={
|
|
"Door Lock",
|
|
"Advanced Door Lock",
|
|
"Secure Keypad Door Lock",
|
|
"Secure Lockbox",
|
|
},
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
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",
|
|
},
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={
|
|
CommandClass.LOCK,
|
|
CommandClass.DOOR_LOCK,
|
|
},
|
|
property={"doorStatus"},
|
|
type={"any"},
|
|
),
|
|
),
|
|
# climate
|
|
# thermostats supporting mode (and optional setpoint)
|
|
ZWaveDiscoverySchema(
|
|
platform="climate",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={CommandClass.THERMOSTAT_MODE},
|
|
property={"mode"},
|
|
type={"number"},
|
|
),
|
|
),
|
|
# thermostats supporting setpoint only (and thus not mode)
|
|
ZWaveDiscoverySchema(
|
|
platform="climate",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={CommandClass.THERMOSTAT_SETPOINT},
|
|
property={"setpoint"},
|
|
type={"number"},
|
|
),
|
|
absent_values=[ # mode must not be present to prevent dupes
|
|
ZWaveValueDiscoverySchema(
|
|
command_class={CommandClass.THERMOSTAT_MODE},
|
|
property={"mode"},
|
|
type={"number"},
|
|
),
|
|
],
|
|
),
|
|
# binary sensors
|
|
ZWaveDiscoverySchema(
|
|
platform="binary_sensor",
|
|
hint="boolean",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={
|
|
CommandClass.SENSOR_BINARY,
|
|
CommandClass.BATTERY,
|
|
CommandClass.SENSOR_ALARM,
|
|
},
|
|
type={"boolean"},
|
|
),
|
|
),
|
|
ZWaveDiscoverySchema(
|
|
platform="binary_sensor",
|
|
hint="notification",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={
|
|
CommandClass.NOTIFICATION,
|
|
},
|
|
type={"number"},
|
|
),
|
|
allow_multi=True,
|
|
),
|
|
# generic text sensors
|
|
ZWaveDiscoverySchema(
|
|
platform="sensor",
|
|
hint="string_sensor",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={
|
|
CommandClass.SENSOR_ALARM,
|
|
CommandClass.INDICATOR,
|
|
},
|
|
type={"string"},
|
|
),
|
|
),
|
|
# generic numeric sensors
|
|
ZWaveDiscoverySchema(
|
|
platform="sensor",
|
|
hint="numeric_sensor",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={
|
|
CommandClass.SENSOR_MULTILEVEL,
|
|
CommandClass.SENSOR_ALARM,
|
|
CommandClass.INDICATOR,
|
|
CommandClass.BATTERY,
|
|
},
|
|
type={"number"},
|
|
),
|
|
),
|
|
# numeric sensors for Meter CC
|
|
ZWaveDiscoverySchema(
|
|
platform="sensor",
|
|
hint="numeric_sensor",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={
|
|
CommandClass.METER,
|
|
},
|
|
type={"number"},
|
|
property={"value"},
|
|
),
|
|
),
|
|
# special list sensors (Notification CC)
|
|
ZWaveDiscoverySchema(
|
|
platform="sensor",
|
|
hint="list_sensor",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={
|
|
CommandClass.NOTIFICATION,
|
|
},
|
|
type={"number"},
|
|
),
|
|
allow_multi=True,
|
|
),
|
|
# sensor for basic CC
|
|
ZWaveDiscoverySchema(
|
|
platform="sensor",
|
|
hint="numeric_sensor",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={
|
|
CommandClass.BASIC,
|
|
},
|
|
type={"number"},
|
|
property={"currentValue"},
|
|
),
|
|
),
|
|
# binary switches
|
|
ZWaveDiscoverySchema(
|
|
platform="switch",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={CommandClass.SWITCH_BINARY}, property={"currentValue"}
|
|
),
|
|
),
|
|
# binary switch
|
|
# barrier operator signaling states
|
|
ZWaveDiscoverySchema(
|
|
platform="switch",
|
|
hint="barrier_event_signaling_state",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={CommandClass.BARRIER_OPERATOR},
|
|
property={"signalingState"},
|
|
type={"number"},
|
|
),
|
|
),
|
|
# cover
|
|
# window coverings
|
|
ZWaveDiscoverySchema(
|
|
platform="cover",
|
|
hint="window_cover",
|
|
device_class_generic={"Multilevel Switch"},
|
|
device_class_specific={
|
|
"Motor Control Class A",
|
|
"Motor Control Class B",
|
|
"Motor Control Class C",
|
|
"Multiposition Motor",
|
|
},
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
# cover
|
|
# motorized barriers
|
|
ZWaveDiscoverySchema(
|
|
platform="cover",
|
|
hint="motorized_barrier",
|
|
primary_value=ZWaveValueDiscoverySchema(
|
|
command_class={CommandClass.BARRIER_OPERATOR},
|
|
property={"currentState"},
|
|
type={"number"},
|
|
),
|
|
required_values=[
|
|
ZWaveValueDiscoverySchema(
|
|
command_class={CommandClass.BARRIER_OPERATOR},
|
|
property={"targetState"},
|
|
type={"number"},
|
|
),
|
|
],
|
|
),
|
|
# fan
|
|
ZWaveDiscoverySchema(
|
|
platform="fan",
|
|
hint="fan",
|
|
device_class_generic={"Multilevel Switch"},
|
|
device_class_specific={"Fan Switch"},
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
# number platform
|
|
# valve control for thermostats
|
|
ZWaveDiscoverySchema(
|
|
platform="number",
|
|
hint="Valve control",
|
|
device_class_generic={"Thermostat"},
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
# lights
|
|
# primary value is the currentValue (brightness)
|
|
# catch any device with multilevel CC as light
|
|
# NOTE: keep this at the bottom of the discovery scheme,
|
|
# to handle all others that need the multilevel CC first
|
|
ZWaveDiscoverySchema(
|
|
platform="light",
|
|
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
|
),
|
|
]
|
|
|
|
|
|
@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():
|
|
for schema in DISCOVERY_SCHEMAS:
|
|
# check manufacturer_id
|
|
if (
|
|
schema.manufacturer_id is not None
|
|
and value.node.manufacturer_id not in schema.manufacturer_id
|
|
):
|
|
continue
|
|
|
|
# check product_id
|
|
if (
|
|
schema.product_id is not None
|
|
and value.node.product_id not in schema.product_id
|
|
):
|
|
continue
|
|
|
|
# check product_type
|
|
if (
|
|
schema.product_type is not None
|
|
and value.node.product_type not in schema.product_type
|
|
):
|
|
continue
|
|
|
|
# check firmware_version
|
|
if (
|
|
schema.firmware_version is not None
|
|
and value.node.firmware_version not in schema.firmware_version
|
|
):
|
|
continue
|
|
|
|
# check device_class_basic
|
|
if not check_device_class(
|
|
value.node.device_class.basic, schema.device_class_basic
|
|
):
|
|
continue
|
|
|
|
# check device_class_generic
|
|
if not check_device_class(
|
|
value.node.device_class.generic, schema.device_class_generic
|
|
):
|
|
continue
|
|
|
|
# check device_class_specific
|
|
if not check_device_class(
|
|
value.node.device_class.specific, schema.device_class_specific
|
|
):
|
|
continue
|
|
|
|
# check primary value
|
|
if not check_value(value, schema.primary_value):
|
|
continue
|
|
|
|
# check additional required values
|
|
if schema.required_values is not None and not all(
|
|
any(check_value(val, val_scheme) for val in node.values.values())
|
|
for val_scheme in schema.required_values
|
|
):
|
|
continue
|
|
|
|
# check for values that may not be present
|
|
if schema.absent_values is not None and any(
|
|
any(check_value(val, val_scheme) for val in node.values.values())
|
|
for val_scheme in schema.absent_values
|
|
):
|
|
continue
|
|
|
|
# all checks passed, this value belongs to an entity
|
|
yield ZwaveDiscoveryInfo(
|
|
node=value.node,
|
|
primary_value=value,
|
|
platform=schema.platform,
|
|
platform_hint=schema.hint,
|
|
)
|
|
|
|
if not schema.allow_multi:
|
|
# break out of loop, this value may not be discovered by other schemas/platforms
|
|
break
|
|
|
|
|
|
@callback
|
|
def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool:
|
|
"""Check if value matches scheme."""
|
|
# check command_class
|
|
if (
|
|
schema.command_class is not None
|
|
and value.command_class not in schema.command_class
|
|
):
|
|
return False
|
|
# check endpoint
|
|
if schema.endpoint is not None and value.endpoint not in schema.endpoint:
|
|
return False
|
|
# check property
|
|
if schema.property is not None and value.property_ not in schema.property:
|
|
return False
|
|
# check property_name
|
|
if (
|
|
schema.property_name is not None
|
|
and value.property_name not in schema.property_name
|
|
):
|
|
return False
|
|
# check property_key
|
|
if (
|
|
schema.property_key is not None
|
|
and value.property_key not in schema.property_key
|
|
):
|
|
return False
|
|
# check property_key_name
|
|
if (
|
|
schema.property_key_name is not None
|
|
and value.property_key_name not in schema.property_key_name
|
|
):
|
|
return False
|
|
# check metadata_type
|
|
if schema.type is not None and value.metadata.type not in schema.type:
|
|
return False
|
|
return True
|
|
|
|
|
|
@callback
|
|
def check_device_class(
|
|
device_class: DeviceClassItem, required_value: set[str | int] | None
|
|
) -> bool:
|
|
"""Check if device class id or label matches."""
|
|
if required_value is None:
|
|
return True
|
|
for val in required_value:
|
|
if isinstance(val, str) and device_class.label == val:
|
|
return True
|
|
if isinstance(val, int) and device_class.key == val:
|
|
return True
|
|
return False
|