Files
core/homeassistant/components/zwave_js/models.py
2025-08-27 12:01:34 +02:00

250 lines
9.2 KiB
Python

"""Provide models for the Z-Wave integration."""
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import asdict, dataclass, field
from enum import StrEnum
from typing import TYPE_CHECKING, Any
from awesomeversion import AwesomeVersion
from zwave_js_server.const import LogLevel
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import (
ConfigurationValue as ZwaveConfigurationValue,
Value as ZwaveValue,
get_value_id_str,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory, Platform
from homeassistant.helpers.entity import EntityDescription
if TYPE_CHECKING:
from _typeshed import DataclassInstance
from zwave_js_server.client import Client as ZwaveClient
from . import DriverEvents
@dataclass
class ZwaveJSData:
"""Data for zwave_js runtime data."""
client: ZwaveClient
driver_events: DriverEvents
old_server_log_level: LogLevel | None = None
type ZwaveJSConfigEntry = ConfigEntry[ZwaveJSData]
@dataclass
class ZwaveValueID:
"""Class to represent a value ID."""
property_: str | int
command_class: int
endpoint: int | None = None
property_key: str | int | None = None
class ValueType(StrEnum):
"""Enum with all value types."""
ANY = "any"
BOOLEAN = "boolean"
NUMBER = "number"
STRING = "string"
class DataclassMustHaveAtLeastOne:
"""A dataclass that must have at least one input parameter that is not None."""
def __post_init__(self: DataclassInstance) -> None:
"""Post dataclass initialization."""
if all(val is None for val in asdict(self).values()):
raise ValueError("At least one input parameter must not be None")
@dataclass
class FirmwareVersionRange(DataclassMustHaveAtLeastOne):
"""Firmware version range dictionary."""
min: str | None = None
max: str | None = None
min_ver: AwesomeVersion | None = field(default=None, init=False)
max_ver: AwesomeVersion | None = field(default=None, init=False)
def __post_init__(self) -> None:
"""Post dataclass initialization."""
super().__post_init__()
if self.min:
self.min_ver = AwesomeVersion(self.min)
if self.max:
self.max_ver = AwesomeVersion(self.max)
@dataclass
class PlatformZwaveDiscoveryInfo:
"""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
# bool to specify whether state is assumed and events should be fired on value
# update
assumed_state: bool
# the home assistant platform for which an entity should be created
platform: Platform
# additional values that need to be watched by entity
additional_value_ids_to_watch: set[str]
@dataclass
class ZwaveDiscoveryInfo(PlatformZwaveDiscoveryInfo):
"""Info discovered from (primary) ZWave Value to create entity."""
# helper data to use in platform setup
platform_data: Any = None
# data template to use in platform logic
platform_data_template: BaseDiscoverySchemaDataTemplate | None = None
# hint for the platform about this discovered entity
platform_hint: str | None = ""
# bool to specify whether entity should be enabled by default
entity_registry_enabled_default: bool = True
# the entity category for the discovered entity
entity_category: EntityCategory | None = None
@dataclass
class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne):
"""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/specs/tree/master
"""
# [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 = None
# [optional] the value's property key must NOT match ANY of these values
not_property_key: set[str | int | None] | None = None
# [optional] the value's metadata_type must match ANY of these values
type: set[str] | None = None
# [optional] the value's metadata_readable must match this value
readable: bool | None = None
# [optional] the value's metadata_writeable must match this value
writeable: bool | None = None
# [optional] the value's states map must include ANY of these key/value pairs
any_available_states: set[tuple[int, str]] | None = None
# [optional] the value's states map must include ANY of these keys
any_available_states_keys: set[int] | None = None
# [optional] the value's cc specific map must include ANY of these key/value pairs
any_available_cc_specific: set[tuple[Any, Any]] | None = None
# [optional] the value's value must match this value
value: Any | None = None
# [optional] the value's metadata_stateful must match this value
stateful: bool | None = None
@dataclass
class NewZWaveDiscoverySchema:
"""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: Platform
# platform-specific entity description
entity_description: EntityDescription
# entity class to use to instantiate the entity
entity_class: type
# primary value belonging to this discovery scheme
primary_value: ZWaveValueDiscoverySchema
# [optional] template to generate platform specific data to use in setup
data_template: BaseDiscoverySchemaDataTemplate | 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 be within this range
firmware_version_range: FirmwareVersionRange | 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
# [optional] bool to specify whether state is assumed
# and events should be fired on value update
assumed_state: bool = False
@dataclass
class BaseDiscoverySchemaDataTemplate:
"""Base class for discovery schema data templates."""
static_data: Any | None = None
def resolve_data(self, value: ZwaveValue) -> Any:
"""Resolve helper class data for a discovered value.
Can optionally be implemented by subclasses if input data needs to be
transformed once discovered Value is available.
"""
return {}
def values_to_watch(self, resolved_data: Any) -> Iterable[ZwaveValue | None]:
"""Return list of all ZwaveValues resolved by helper that should be watched.
Should be implemented by subclasses only if there are values to watch.
"""
return []
def value_ids_to_watch(self, resolved_data: Any) -> set[str]:
"""Return list of all Value IDs resolved by helper that should be watched.
Not to be overwritten by subclasses.
"""
return {val.value_id for val in self.values_to_watch(resolved_data) if val}
@staticmethod
def _get_value_from_id(
node: ZwaveNode, value_id_obj: ZwaveValueID
) -> ZwaveValue | ZwaveConfigurationValue | None:
"""Get a ZwaveValue from a node using a ZwaveValueDict."""
value_id = get_value_id_str(
node,
value_id_obj.command_class,
value_id_obj.property_,
endpoint=value_id_obj.endpoint,
property_key=value_id_obj.property_key,
)
return node.values.get(value_id)