1 """Helper functions for Z-Wave JS integration."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from dataclasses
import astuple, dataclass
8 from typing
import Any, cast
10 import voluptuous
as vol
11 from zwave_js_server.client
import Client
as ZwaveClient
12 from zwave_js_server.const
import (
15 ConfigurationValueType,
18 from zwave_js_server.model.controller
import Controller
19 from zwave_js_server.model.driver
import Driver
20 from zwave_js_server.model.log_config
import LogConfig
21 from zwave_js_server.model.node
import Node
as ZwaveNode
22 from zwave_js_server.model.value
import (
36 __version__
as HA_VERSION,
51 DATA_OLD_SERVER_LOG_LEVEL,
60 """Class to represent a value ID."""
64 endpoint: int |
None =
None
65 property_key: str | int |
None =
None
70 """Class to allow matching a Z-Wave Value."""
72 property_: str | int |
None =
None
73 command_class: int |
None =
None
74 endpoint: int |
None =
None
75 property_key: str | int |
None =
None
78 """Post initialization check."""
79 if all(val
is None for val
in astuple(self)):
80 raise ValueError(
"At least one of the fields must be set.")
84 matcher: ZwaveValueMatcher, value_data: ValueDataType
86 """Return whether value matches matcher."""
88 if "commandClass" in value_data:
89 command_class = CommandClass(value_data[
"commandClass"])
91 property_=value_data.get(
"property"),
92 command_class=command_class,
93 endpoint=value_data.get(
"endpoint"),
94 property_key=value_data.get(
"propertyKey"),
97 redacted_field_val
is None or redacted_field_val == zwave_value_field_val
98 for redacted_field_val, zwave_value_field_val
in zip(
99 astuple(matcher), astuple(zwave_value_id), strict=
False
105 """Get the value ID and optional state key from a unique ID.
109 split_unique_id = unique_id.split(
".")
112 if "-" in (value_id := split_unique_id[1]):
118 """Get the state key from a unique ID."""
121 if len(split_unique_id := unique_id.split(
".")) > 2:
123 return int(split_unique_id[-1])
130 """Return the value of a ZwaveValue."""
131 return value.value
if value
else None
135 """Enable statistics on the driver."""
136 await driver.async_enable_statistics(
"Home Assistant", HA_VERSION)
140 hass: HomeAssistant, entry: ConfigEntry, driver: Driver
142 """Enable logging of zwave-js-server in the lib."""
149 or not driver.client.connected
150 or driver.client.server_logging_enabled
154 LOGGER.info(
"Enabling zwave-js-server logging")
155 if (curr_server_log_level := driver.log_config.level)
and (
156 LOG_LEVEL_MAP[curr_server_log_level]
157 ) > (lib_log_level := LIB_LOGGER.getEffectiveLevel()):
158 entry_data = entry.runtime_data
161 "Server logging is set to %s and is currently less verbose "
162 "than library logging, setting server log level to %s to match"
164 curr_server_log_level,
165 logging.getLevelName(lib_log_level),
167 entry_data[DATA_OLD_SERVER_LOG_LEVEL] = curr_server_log_level
168 await driver.async_update_log_config(LogConfig(level=LogLevel.DEBUG))
169 await driver.client.enable_server_logging()
170 LOGGER.info(
"Zwave-js-server logging is enabled")
174 hass: HomeAssistant, entry: ConfigEntry, driver: Driver
176 """Disable logging of zwave-js-server in the lib if still connected to server."""
179 or not driver.client.connected
180 or not driver.client.server_logging_enabled
183 LOGGER.info(
"Disabling zwave_js server logging")
185 DATA_OLD_SERVER_LOG_LEVEL
in entry.runtime_data
186 and (old_server_log_level := entry.runtime_data.pop(DATA_OLD_SERVER_LOG_LEVEL))
187 != driver.log_config.level
191 "Server logging is currently set to %s as a result of server logging "
192 "being enabled. It is now being reset to %s"
194 driver.log_config.level,
195 old_server_log_level,
197 await driver.async_update_log_config(LogConfig(level=old_server_log_level))
198 await driver.client.disable_server_logging()
199 LOGGER.info(
"Zwave-js-server logging is enabled")
203 """Return the base unique ID for an entity that is not based on a value."""
204 return f
"{driver.controller.home_id}.{node.node_id}"
208 """Get unique ID from client and value ID."""
209 return f
"{driver.controller.home_id}.{value_id}"
213 """Get device registry identifier for Z-Wave node."""
214 return (DOMAIN, f
"{driver.controller.home_id}-{node.node_id}")
218 """Get extended device registry identifier for Z-Wave node."""
219 if None in (node.manufacturer_id, node.product_type, node.product_id):
225 f
"{dev_id}-{node.manufacturer_id}:{node.product_type}:{node.product_id}",
230 device_entry: dr.DeviceEntry,
231 ) -> tuple[str, int] |
None:
232 """Get home ID and node ID for Z-Wave device registry entry.
234 Returns (home_id, node_id) or None if not found.
239 for identifier
in device_entry.identifiers
240 if identifier[0] == DOMAIN
244 if device_id
is None:
246 id_ = device_id.split(
"-")
247 return (id_[0],
int(id_[1]))
252 hass: HomeAssistant, device_id: str, dev_reg: dr.DeviceRegistry |
None =
None
254 """Get node from a device ID.
256 Raises ValueError if device is invalid or node can't be found.
259 dev_reg = dr.async_get(hass)
261 if not (device_entry := dev_reg.async_get(device_id)):
262 raise ValueError(f
"Device ID {device_id} is not valid")
266 config_entry_ids = device_entry.config_entries
270 for entry
in hass.config_entries.async_entries(DOMAIN)
271 if entry.entry_id
in config_entry_ids
275 if entry
and entry.state != ConfigEntryState.LOADED:
276 raise ValueError(f
"Device {device_id} config entry is not loaded")
279 f
"Device {device_id} is not from an existing zwave_js config entry"
282 client: ZwaveClient = entry.runtime_data[DATA_CLIENT]
283 driver = client.driver
286 raise ValueError(
"Driver is not ready.")
292 node_id = identifiers[1]
if identifiers
else None
294 if node_id
is None or node_id
not in driver.controller.nodes:
295 raise ValueError(f
"Node for device {device_id} can't be found")
297 return driver.controller.nodes[node_id]
304 ent_reg: er.EntityRegistry |
None =
None,
305 dev_reg: dr.DeviceRegistry |
None =
None,
307 """Get node from an entity ID.
309 Raises ValueError if entity is invalid.
312 ent_reg = er.async_get(hass)
313 entity_entry = ent_reg.async_get(entity_id)
315 if entity_entry
is None or entity_entry.platform != DOMAIN:
316 raise ValueError(f
"Entity {entity_id} is not a valid {DOMAIN} entity")
320 assert entity_entry.device_id
328 ent_reg: er.EntityRegistry |
None =
None,
329 dev_reg: dr.DeviceRegistry |
None =
None,
331 """Get nodes for all Z-Wave JS devices and entities that are in an area."""
332 nodes: set[ZwaveNode] = set()
334 ent_reg = er.async_get(hass)
336 dev_reg = dr.async_get(hass)
341 for entity
in er.async_entries_for_area(ent_reg, area_id)
342 if entity.platform == DOMAIN
and entity.device_id
is not None
348 for device
in dr.async_entries_for_area(dev_reg, area_id)
352 hass.config_entries.async_get_entry(config_entry_id),
355 for config_entry_id
in device.config_entries
366 ent_reg: er.EntityRegistry |
None =
None,
367 dev_reg: dr.DeviceRegistry |
None =
None,
368 logger: logging.Logger = LOGGER,
370 """Get nodes for all targets.
372 Supports entity_id with group expansion, area_id, and device_id.
374 nodes: set[ZwaveNode] = set()
379 except ValueError
as err:
380 logger.warning(err.args[0])
383 for area_id
in val.get(ATTR_AREA_ID, []):
387 for device_id
in val.get(ATTR_DEVICE_ID, []):
390 except ValueError
as err:
391 logger.warning(err.args[0])
397 """Get a Z-Wave JS Value from a config."""
399 if config.get(ATTR_ENDPOINT):
400 endpoint = config[ATTR_ENDPOINT]
402 if config.get(ATTR_PROPERTY_KEY):
403 property_key = config[ATTR_PROPERTY_KEY]
404 value_id = get_value_id_str(
406 config[ATTR_COMMAND_CLASS],
407 config[ATTR_PROPERTY],
411 if value_id
not in node.values:
412 raise vol.Invalid(f
"Value {value_id} can't be found on node {node}")
413 return node.values[value_id]
417 """Find zwave_js config entry from a device."""
418 for entry_id
in device.config_entries:
419 entry = hass.config_entries.async_get_entry(entry_id)
420 if entry
and entry.domain == DOMAIN:
429 ent_reg: er.EntityRegistry |
None =
None,
430 dev_reg: dr.DeviceRegistry |
None =
None,
432 """Get the node status sensor entity ID for a given Z-Wave JS device."""
434 ent_reg = er.async_get(hass)
436 dev_reg = dr.async_get(hass)
437 if not (device := dev_reg.async_get(device_id)):
443 entry = hass.config_entries.async_get_entry(entry_id)
445 client = entry.runtime_data[DATA_CLIENT]
447 return ent_reg.async_get_entity_id(
450 f
"{client.driver.controller.home_id}.{node.node_id}.node_status",
455 """Remove keys from config where the value is an empty string or None."""
456 return {key: value
for key, value
in config.items()
if value
not in (
"",
None)}
460 schema_map: dict[str, vol.Schema],
461 ) -> Callable[[ConfigType], ConfigType]:
462 """Check type specific schema against config."""
464 def _check_type_schema(config: ConfigType) -> ConfigType:
465 """Check type specific schema against config."""
466 return cast(ConfigType, schema_map[
str(config[CONF_TYPE])](config))
468 return _check_type_schema
472 input_dict: dict[str, Any], output_dict: dict[str, Any], params: list[str]
474 """Copy available params from input into output."""
476 {param: input_dict[param]
for param
in params
if param
in input_dict}
482 ) -> VolSchemaType | vol.Coerce | vol.In |
None:
483 """Return device automation schema for a config entry."""
484 if isinstance(value, ConfigurationValue):
485 min_ = value.metadata.min
486 max_ = value.metadata.max
487 if value.configuration_value_type
in (
488 ConfigurationValueType.RANGE,
489 ConfigurationValueType.MANUAL_ENTRY,
491 return vol.All(vol.Coerce(int), vol.Range(min=min_, max=max_))
493 if value.configuration_value_type == ConfigurationValueType.BOOLEAN:
494 return vol.Coerce(bool)
496 if value.configuration_value_type == ConfigurationValueType.ENUMERATED:
497 return vol.In({
int(k): v
for k, v
in value.metadata.states.items()})
501 if value.metadata.states:
502 return vol.In({
int(k): v
for k, v
in value.metadata.states.items()})
506 vol.Range(min=value.metadata.min, max=value.metadata.max),
511 """Get DeviceInfo for node."""
514 sw_version=node.firmware_version,
515 name=node.name
or node.device_config.description
or f
"Node {node.node_id}",
516 model=node.device_config.label,
517 manufacturer=node.device_config.manufacturer,
518 suggested_area=node.location
if node.location
else None,
523 hass: HomeAssistant, config_entry: ConfigEntry, controller: Controller
525 """Return the network identifier string for persistent notifications."""
526 home_id =
str(controller.home_id)
527 if len(hass.config_entries.async_entries(DOMAIN)) > 1:
528 if str(home_id) != config_entry.title:
529 return f
"`{config_entry.title}`, with the home ID `{home_id}`,"
530 return f
"with the home ID `{home_id}`"
tuple[str, str] get_device_id(Driver driver, ZwaveNode node)
set[ZwaveNode] async_get_nodes_from_area_id(HomeAssistant hass, str area_id, er.EntityRegistry|None ent_reg=None, dr.DeviceRegistry|None dev_reg=None)
tuple[str, int]|None get_home_and_node_id_from_device_entry(dr.DeviceEntry device_entry)
int|None get_state_key_from_unique_id(str unique_id)
tuple[str, str]|None get_device_id_ext(Driver driver, ZwaveNode node)
str get_valueless_base_unique_id(Driver driver, ZwaveNode node)
str|None get_value_id_from_unique_id(str unique_id)
ConfigType remove_keys_with_empty_values(ConfigType config)
None async_enable_statistics(Driver driver)
str|None async_get_node_status_sensor_entity_id(HomeAssistant hass, str device_id, er.EntityRegistry|None ent_reg=None, dr.DeviceRegistry|None dev_reg=None)
ZwaveNode async_get_node_from_entity_id(HomeAssistant hass, str entity_id, er.EntityRegistry|None ent_reg=None, dr.DeviceRegistry|None dev_reg=None)
bool value_matches_matcher(ZwaveValueMatcher matcher, ValueDataType value_data)
None copy_available_params(dict[str, Any] input_dict, dict[str, Any] output_dict, list[str] params)
Callable[[ConfigType], ConfigType] check_type_schema_map(dict[str, vol.Schema] schema_map)
DeviceInfo get_device_info(Driver driver, ZwaveNode node)
str get_unique_id(Driver driver, str value_id)
None async_enable_server_logging_if_needed(HomeAssistant hass, ConfigEntry entry, Driver driver)
set[ZwaveNode] async_get_nodes_from_targets(HomeAssistant hass, dict[str, Any] val, er.EntityRegistry|None ent_reg=None, dr.DeviceRegistry|None dev_reg=None, logging.Logger logger=LOGGER)
str get_network_identifier_for_notification(HomeAssistant hass, ConfigEntry config_entry, Controller controller)
Any|None get_value_of_zwave_value(ZwaveValue|None value)
None async_disable_server_logging_if_needed(HomeAssistant hass, ConfigEntry entry, Driver driver)
VolSchemaType|vol.Coerce|vol.In|None get_value_state_schema(ZwaveValue value)
ZwaveValue get_zwave_value_from_config(ZwaveNode node, ConfigType config)
ZwaveNode async_get_node_from_device_id(HomeAssistant hass, str device_id, dr.DeviceRegistry|None dev_reg=None)
str|None _zwave_js_config_entry(HomeAssistant hass, dr.DeviceEntry device)