1 """Methods and classes related to executing Z-Wave commands."""
3 from __future__
import annotations
6 from collections.abc
import Collection, Generator, Sequence
11 import voluptuous
as vol
12 from zwave_js_server.client
import Client
as ZwaveClient
13 from zwave_js_server.const
import SET_VALUE_SUCCESS, CommandClass, CommandStatus
14 from zwave_js_server.const.command_class.notification
import NotificationType
15 from zwave_js_server.exceptions
import FailedZWaveCommand, SetValueFailed
16 from zwave_js_server.model.endpoint
import Endpoint
17 from zwave_js_server.model.node
import Node
as ZwaveNode
18 from zwave_js_server.model.value
import (
19 ConfigurationValueFormat,
23 from zwave_js_server.util.multicast
import async_multicast_set_value
24 from zwave_js_server.util.node
import (
25 async_bulk_set_partial_config_parameters,
26 async_set_config_parameter,
38 from .config_validation
import BITMASK_SCHEMA, VALUE_SCHEMA
39 from .helpers
import (
40 async_get_node_from_device_id,
41 async_get_node_from_entity_id,
42 async_get_nodes_from_area_id,
43 async_get_nodes_from_targets,
44 get_value_id_from_unique_id,
47 _LOGGER = logging.getLogger(__name__)
49 type _NodeOrEndpointType = ZwaveNode | Endpoint
52 vol.Optional(ATTR_AREA_ID): vol.All(cv.ensure_list, [cv.string]),
53 vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
54 vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
59 val: dict[str, int | str | list[str]],
60 ) -> dict[str, int | str | list[str]]:
61 """Validate that if a parameter name is provided, bitmask is not as well."""
63 isinstance(val[const.ATTR_CONFIG_PARAMETER], str)
64 and const.ATTR_CONFIG_PARAMETER_BITMASK
in val
67 "Don't include a bitmask when a parameter name is specified",
68 path=[const.ATTR_CONFIG_PARAMETER, const.ATTR_CONFIG_PARAMETER_BITMASK],
74 """Check if value is a power of 2."""
75 if not math.log2(val).is_integer():
76 raise vol.Invalid(
"Value must be a power of 2.")
81 """Validate that the service call is for a broadcast command."""
82 if val.get(const.ATTR_BROADCAST):
85 "Either `broadcast` must be set to True or multiple devices/entities must be "
90 def get_valid_responses_from_results[_T: ZwaveNode | Endpoint](
91 zwave_objects: Sequence[_T], results: Sequence[Any]
92 ) -> Generator[tuple[_T, Any]]:
93 """Return valid responses from a list of results."""
94 for zwave_object, result
in zip(zwave_objects, results, strict=
False):
95 if not isinstance(result, Exception):
96 yield zwave_object, result
100 zwave_objects: Sequence[_NodeOrEndpointType], results: Sequence[Any]
102 """Raise list of exceptions from a list of results."""
103 errors: Sequence[tuple[_NodeOrEndpointType, Any]]
106 for tup
in zip(zwave_objects, results, strict=
True)
107 if isinstance(tup[1], Exception)
111 f
"{zwave_object} - {error.__class__.__name__}: {error.args[0]}"
112 for zwave_object, error
in errors
116 lines.insert(0, f
"{len(errors)} error(s):")
121 nodes_or_endpoints: Collection[_NodeOrEndpointType],
122 command_class: CommandClass,
126 """Invoke the CC API on a node endpoint."""
127 nodes_or_endpoints_list =
list(nodes_or_endpoints)
128 results = await asyncio.gather(
130 node_or_endpoint.async_invoke_cc_api(command_class, method_name, *args)
131 for node_or_endpoint
in nodes_or_endpoints_list
133 return_exceptions=
True,
135 for node_or_endpoint, result
in get_valid_responses_from_results(
136 nodes_or_endpoints_list, results
138 if isinstance(node_or_endpoint, ZwaveNode):
141 "Invoked %s CC API method %s on node %s with the following result: "
152 "Invoked %s CC API method %s on endpoint %s with the following "
164 """Class that holds our services (Zwave Commands).
166 Services that should be published to hass.
172 ent_reg: er.EntityRegistry,
173 dev_reg: dr.DeviceRegistry,
175 """Initialize with hass object."""
182 """Register all our services."""
185 def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]:
186 """Get nodes set from service data."""
193 def has_at_least_one_node(val: dict[str, Any]) -> dict[str, Any]:
194 """Validate that at least one node is specified."""
195 if not val.get(const.ATTR_NODES):
196 raise vol.Invalid(f
"No {const.DOMAIN} nodes found for given targets")
200 def validate_multicast_nodes(val: dict[str, Any]) -> dict[str, Any]:
201 """Validate the input nodes for multicast."""
202 nodes: set[ZwaveNode] = val[const.ATTR_NODES]
203 broadcast: bool = val[const.ATTR_BROADCAST]
206 has_at_least_one_node(val)
213 and len(self.
_hass_hass.config_entries.async_entries(const.DOMAIN)) > 1
216 "You must include at least one entity or device in the service call"
219 first_node = next((node
for node
in nodes),
None)
221 if first_node
and not all(node.client.driver
is not None for node
in nodes):
222 raise vol.Invalid(f
"Driver not ready for all nodes: {nodes}")
228 and first_node.client.driver
230 node.client.driver.controller.home_id
231 != first_node.client.driver.controller.home_id
233 if node.client.driver
is not None
237 "Multicast commands only work on devices in the same network"
243 def validate_entities(val: dict[str, Any]) -> dict[str, Any]:
244 """Validate entities exist and are from the zwave_js platform."""
246 invalid_entities = []
247 for entity_id
in val[ATTR_ENTITY_ID]:
249 if entry
is None or entry.platform != const.DOMAIN:
251 "Entity %s is not a valid %s entity", entity_id, const.DOMAIN
253 invalid_entities.append(entity_id)
256 val[ATTR_ENTITY_ID] =
list(set(val[ATTR_ENTITY_ID]) - set(invalid_entities))
258 if not val[ATTR_ENTITY_ID]:
259 raise vol.Invalid(f
"No {const.DOMAIN} entities found in service call")
263 self.
_hass_hass.services.async_register(
265 const.SERVICE_SET_CONFIG_PARAMETER,
271 vol.Optional(const.ATTR_ENDPOINT, default=0): vol.Coerce(int),
272 vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any(
273 vol.Coerce(int), cv.string
275 vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(
276 vol.Coerce(int), BITMASK_SCHEMA
278 vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
279 vol.Coerce(int), BITMASK_SCHEMA, cv.string
281 vol.Inclusive(const.ATTR_VALUE_SIZE,
"raw"): vol.All(
282 vol.Coerce(int), vol.Range(min=1, max=4), check_base_2
284 vol.Inclusive(const.ATTR_VALUE_FORMAT,
"raw"): vol.Coerce(
285 ConfigurationValueFormat
288 cv.has_at_least_one_key(
289 ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
291 cv.has_at_most_one_key(
292 const.ATTR_CONFIG_PARAMETER_BITMASK, const.ATTR_VALUE_SIZE
294 parameter_name_does_not_need_bitmask,
295 get_nodes_from_service_data,
296 has_at_least_one_node,
301 self.
_hass_hass.services.async_register(
303 const.SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
309 vol.Optional(const.ATTR_ENDPOINT, default=0): vol.Coerce(int),
310 vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int),
311 vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
315 vol.Coerce(int), BITMASK_SCHEMA, cv.string
316 ): vol.Any(vol.Coerce(int), BITMASK_SCHEMA, cv.string)
320 cv.has_at_least_one_key(
321 ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
323 get_nodes_from_service_data,
324 has_at_least_one_node,
329 self.
_hass_hass.services.async_register(
331 const.SERVICE_REFRESH_VALUE,
336 vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
338 const.ATTR_REFRESH_ALL_VALUES, default=
False
346 self.
_hass_hass.services.async_register(
348 const.SERVICE_SET_VALUE,
354 vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int),
355 vol.Required(const.ATTR_PROPERTY): vol.Any(
358 vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any(
361 vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int),
362 vol.Required(const.ATTR_VALUE): VALUE_SCHEMA,
363 vol.Optional(const.ATTR_WAIT_FOR_RESULT): cv.boolean,
364 vol.Optional(const.ATTR_OPTIONS): {cv.string: VALUE_SCHEMA},
366 cv.has_at_least_one_key(
367 ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
369 get_nodes_from_service_data,
370 has_at_least_one_node,
375 self.
_hass_hass.services.async_register(
377 const.SERVICE_MULTICAST_SET_VALUE,
383 vol.Optional(const.ATTR_BROADCAST, default=
False): cv.boolean,
384 vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int),
385 vol.Required(const.ATTR_PROPERTY): vol.Any(
388 vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any(
391 vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int),
392 vol.Required(const.ATTR_VALUE): VALUE_SCHEMA,
393 vol.Optional(const.ATTR_OPTIONS): {cv.string: VALUE_SCHEMA},
396 cv.has_at_least_one_key(
397 ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
401 get_nodes_from_service_data,
402 validate_multicast_nodes,
407 self.
_hass_hass.services.async_register(
414 cv.has_at_least_one_key(
415 ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
417 get_nodes_from_service_data,
418 has_at_least_one_node,
423 self.
_hass_hass.services.async_register(
425 const.SERVICE_INVOKE_CC_API,
431 vol.Required(const.ATTR_COMMAND_CLASS): vol.All(
432 vol.Coerce(int), vol.Coerce(CommandClass)
434 vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int),
435 vol.Required(const.ATTR_METHOD_NAME): cv.string,
436 vol.Required(const.ATTR_PARAMETERS): list,
438 cv.has_at_least_one_key(
439 ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
441 get_nodes_from_service_data,
442 has_at_least_one_node,
447 self.
_hass_hass.services.async_register(
449 const.SERVICE_REFRESH_NOTIFICATIONS,
455 vol.Required(const.ATTR_NOTIFICATION_TYPE): vol.All(
456 vol.Coerce(int), vol.Coerce(NotificationType)
458 vol.Optional(const.ATTR_NOTIFICATION_EVENT): vol.Coerce(int),
460 cv.has_at_least_one_key(
461 ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
463 get_nodes_from_service_data,
464 has_at_least_one_node,
470 """Set a config value on a node."""
471 nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
472 endpoint = service.data[const.ATTR_ENDPOINT]
473 property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER]
474 property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
475 new_value = service.data[const.ATTR_CONFIG_VALUE]
476 value_size = service.data.get(const.ATTR_VALUE_SIZE)
477 value_format = service.data.get(const.ATTR_VALUE_FORMAT)
479 nodes_without_endpoints: set[ZwaveNode] = set()
482 if endpoint
not in node.endpoints:
483 nodes_without_endpoints.add(node)
484 nodes = nodes.difference(nodes_without_endpoints)
487 "None of the specified nodes have the specified endpoint"
489 if nodes_without_endpoints
and _LOGGER.isEnabledFor(logging.WARNING):
492 "The following nodes do not have endpoint %x and will be "
496 nodes_without_endpoints,
503 results = await asyncio.gather(
508 property_or_property_name,
509 property_key=property_key,
512 if value_size
is None
513 else node.endpoints[endpoint].async_set_raw_config_parameter_value(
515 property_or_property_name,
516 property_key=property_key,
517 value_size=value_size,
518 value_format=value_format,
522 return_exceptions=
True,
526 nodes_or_endpoints_list: Sequence[_NodeOrEndpointType], _results: list[Any]
528 """Process results for given nodes or endpoints."""
529 for node_or_endpoint, result
in get_valid_responses_from_results(
530 nodes_or_endpoints_list, _results
532 if value_size
is None:
534 zwave_value = result[0]
535 cmd_status = result[1]
539 zwave_value = f
"parameter {property_or_property_name}"
541 if cmd_status.status == CommandStatus.ACCEPTED:
542 msg =
"Set configuration parameter %s on Node %s with value %s"
545 "Added command to queue to set configuration parameter %s on %s "
546 "with value %s. Parameter will be set when the device wakes up"
548 _LOGGER.info(msg, zwave_value, node_or_endpoint, new_value)
551 if value_size
is None:
552 process_results(
list(nodes), results)
554 process_results([node.endpoints[endpoint]
for node
in nodes], results)
557 self, service: ServiceCall
559 """Bulk set multiple partial config values on a node."""
560 nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
561 endpoint = service.data[const.ATTR_ENDPOINT]
562 property_ = service.data[const.ATTR_CONFIG_PARAMETER]
563 new_value = service.data[const.ATTR_CONFIG_VALUE]
565 results = await asyncio.gather(
575 return_exceptions=
True,
578 nodes_list =
list(nodes)
579 for node, cmd_status
in get_valid_responses_from_results(nodes_list, results):
580 if cmd_status == CommandStatus.ACCEPTED:
581 msg =
"Bulk set partials for configuration parameter %s on Node %s"
584 "Queued command to bulk set partials for configuration parameter "
588 _LOGGER.info(msg, property_, node)
593 """Poll value on a node."""
594 for entity_id
in service.data[ATTR_ENTITY_ID]:
599 f
"{const.DOMAIN}_{entry.unique_id}_poll_value",
600 service.data[const.ATTR_REFRESH_ALL_VALUES],
604 """Set a value on a node."""
605 nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
606 command_class: CommandClass = service.data[const.ATTR_COMMAND_CLASS]
607 property_: int | str = service.data[const.ATTR_PROPERTY]
608 property_key: int | str |
None = service.data.get(const.ATTR_PROPERTY_KEY)
609 endpoint: int |
None = service.data.get(const.ATTR_ENDPOINT)
610 new_value = service.data[const.ATTR_VALUE]
611 wait_for_result = service.data.get(const.ATTR_WAIT_FOR_RESULT)
612 options = service.data.get(const.ATTR_OPTIONS)
616 value_id = get_value_id_str(
621 property_key=property_key,
627 value_id
in node.values
628 and node.values[value_id].metadata.type ==
"string"
629 and not isinstance(new_value, str)
631 new_value_ =
str(new_value)
633 new_value_ = new_value
635 node.async_set_value(
639 wait_for_result=wait_for_result,
643 results = await asyncio.gather(*coros, return_exceptions=
True)
644 nodes_list =
list(nodes)
646 set_value_failed_nodes_list: list[ZwaveNode] = []
647 set_value_failed_error_list: list[SetValueFailed] = []
648 for node_, result
in get_valid_responses_from_results(nodes_list, results):
649 if result
and result.status
not in SET_VALUE_SUCCESS:
651 set_value_failed_nodes_list.append(node_)
652 set_value_failed_error_list.append(
653 SetValueFailed(f
"{result.status} {result.message}")
659 (*nodes_list, *set_value_failed_nodes_list),
660 (*results, *set_value_failed_error_list),
664 """Set a value via multicast to multiple nodes."""
665 nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
666 broadcast: bool = service.data[const.ATTR_BROADCAST]
667 options = service.data.get(const.ATTR_OPTIONS)
669 if not broadcast
and len(nodes) == 1:
671 "Passing the zwave_js.multicast_set_value service call to the "
672 "zwave_js.set_value service since only one node was targeted"
677 command_class: CommandClass = service.data[const.ATTR_COMMAND_CLASS]
678 property_: int | str = service.data[const.ATTR_PROPERTY]
679 property_key: int | str |
None = service.data.get(const.ATTR_PROPERTY_KEY)
680 endpoint: int |
None = service.data.get(const.ATTR_ENDPOINT)
682 value = ValueDataType(commandClass=command_class, property=property_)
683 if property_key
is not None:
684 value[
"propertyKey"] = property_key
685 if endpoint
is not None:
686 value[
"endpoint"] = endpoint
688 new_value = service.data[const.ATTR_VALUE]
694 first_node: ZwaveNode
696 first_node = next(node
for node
in nodes)
697 client = first_node.client
698 except StopIteration:
699 data = self.
_hass_hass.config_entries.async_entries(const.DOMAIN)[0].runtime_data
700 client = data[const.DATA_CLIENT]
704 for node
in client.driver.controller.nodes.values()
706 node, command_class, property_, endpoint, property_key
713 value_id = get_value_id_str(
714 first_node, command_class, property_, endpoint, property_key
717 value_id
in first_node.values
718 and first_node.values[value_id].metadata.type ==
"string"
719 and not isinstance(new_value, str)
721 new_value =
str(new_value)
728 nodes=
None if broadcast
else list(nodes),
731 except FailedZWaveCommand
as err:
734 if result.status
not in SET_VALUE_SUCCESS:
736 "Unable to set value via multicast"
737 )
from SetValueFailed(f
"{result.status} {result.message}")
742 "This service is deprecated in favor of the ping button entity. Service "
743 "calls will still work for now but the service will be removed in a "
746 nodes: list[ZwaveNode] =
list(service.data[const.ATTR_NODES])
747 results = await asyncio.gather(
748 *(node.async_ping()
for node
in nodes), return_exceptions=
True
753 """Invoke a command class API."""
754 command_class: CommandClass = service.data[const.ATTR_COMMAND_CLASS]
755 method_name: str = service.data[const.ATTR_METHOD_NAME]
756 parameters: list[Any] = service.data[const.ATTR_PARAMETERS]
760 if (endpoint := service.data.get(const.ATTR_ENDPOINT))
is not None:
762 {node.endpoints[endpoint]
for node
in service.data[const.ATTR_NODES]},
772 endpoints: set[Endpoint] = set()
773 for area_id
in service.data.get(ATTR_AREA_ID, []):
777 endpoints.add(node.endpoints[0])
779 for device_id
in service.data.get(ATTR_DEVICE_ID, []):
784 except ValueError
as err:
785 _LOGGER.warning(err.args[0])
787 endpoints.add(node.endpoints[0])
789 for entity_id
in service.data.get(ATTR_ENTITY_ID, []):
792 or entity_entry.platform != const.DOMAIN
795 "Skipping entity %s as it is not a valid %s entity",
806 _LOGGER.warning(
"Skipping entity %s as it has no value ID", entity_id)
809 endpoint_idx = node.values[value_id].endpoint
811 node.endpoints[endpoint_idx
if endpoint_idx
is not None else 0]
817 """Refresh notifications on a node."""
818 nodes: set[ZwaveNode] = service.data[const.ATTR_NODES]
819 notification_type: NotificationType = service.data[const.ATTR_NOTIFICATION_TYPE]
820 notification_event: int |
None = service.data.get(const.ATTR_NOTIFICATION_EVENT)
821 param: dict[str, int] = {
"notificationType": notification_type.value}
822 if notification_event
is not None:
823 param[
"notificationEvent"] = notification_event
None async_poll_value(self, ServiceCall service)
None async_invoke_cc_api(self, ServiceCall service)
None async_ping(self, ServiceCall service)
None async_refresh_notifications(self, ServiceCall service)
None async_register(self)
None async_set_value(self, ServiceCall service)
None __init__(self, HomeAssistant hass, er.EntityRegistry ent_reg, dr.DeviceRegistry dev_reg)
None async_set_config_parameter(self, ServiceCall service)
None async_multicast_set_value(self, ServiceCall service)
None async_bulk_set_partial_config_parameters(self, ServiceCall service)
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)
str|None get_value_id_from_unique_id(str unique_id)
ZwaveNode async_get_node_from_entity_id(HomeAssistant hass, str entity_id, er.EntityRegistry|None ent_reg=None, dr.DeviceRegistry|None dev_reg=None)
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)
ZwaveNode async_get_node_from_device_id(HomeAssistant hass, str device_id, dr.DeviceRegistry|None dev_reg=None)
None _async_invoke_cc_api(Collection[_NodeOrEndpointType] nodes_or_endpoints, CommandClass command_class, str method_name, *Any args)
None raise_exceptions_from_results(Sequence[_NodeOrEndpointType] zwave_objects, Sequence[Any] results)
dict[str, Any] broadcast_command(dict[str, Any] val)
int check_base_2(int val)
dict[str, int|str|list[str]] parameter_name_does_not_need_bitmask(dict[str, int|str|list[str]] val)
AreaRegistry async_get(HomeAssistant hass)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)