Home Assistant Unofficial Reference 2024.12.1
services.py
Go to the documentation of this file.
1 """Methods and classes related to executing Z-Wave commands."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Collection, Generator, Sequence
7 import logging
8 import math
9 from typing import Any
10 
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,
20  ValueDataType,
21  get_value_id_str,
22 )
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,
27 )
28 
29 from homeassistant.const import ATTR_AREA_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID
30 from homeassistant.core import HomeAssistant, ServiceCall, callback
31 from homeassistant.exceptions import HomeAssistantError
32 from homeassistant.helpers import device_registry as dr, entity_registry as er
34 from homeassistant.helpers.dispatcher import async_dispatcher_send
35 from homeassistant.helpers.group import expand_entity_ids
36 
37 from . import const
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,
45 )
46 
47 _LOGGER = logging.getLogger(__name__)
48 
49 type _NodeOrEndpointType = ZwaveNode | Endpoint
50 
51 TARGET_VALIDATORS = {
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,
55 }
56 
57 
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."""
62  if (
63  isinstance(val[const.ATTR_CONFIG_PARAMETER], str)
64  and const.ATTR_CONFIG_PARAMETER_BITMASK in val
65  ):
66  raise vol.Invalid(
67  "Don't include a bitmask when a parameter name is specified",
68  path=[const.ATTR_CONFIG_PARAMETER, const.ATTR_CONFIG_PARAMETER_BITMASK],
69  )
70  return val
71 
72 
73 def check_base_2(val: int) -> int:
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.")
77  return val
78 
79 
80 def broadcast_command(val: dict[str, Any]) -> dict[str, Any]:
81  """Validate that the service call is for a broadcast command."""
82  if val.get(const.ATTR_BROADCAST):
83  return val
84  raise vol.Invalid(
85  "Either `broadcast` must be set to True or multiple devices/entities must be "
86  "specified"
87  )
88 
89 
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
97 
98 
100  zwave_objects: Sequence[_NodeOrEndpointType], results: Sequence[Any]
101 ) -> None:
102  """Raise list of exceptions from a list of results."""
103  errors: Sequence[tuple[_NodeOrEndpointType, Any]]
104  if errors := [
105  tup
106  for tup in zip(zwave_objects, results, strict=True)
107  if isinstance(tup[1], Exception)
108  ]:
109  lines = [
110  *(
111  f"{zwave_object} - {error.__class__.__name__}: {error.args[0]}"
112  for zwave_object, error in errors
113  )
114  ]
115  if len(lines) > 1:
116  lines.insert(0, f"{len(errors)} error(s):")
117  raise HomeAssistantError("\n".join(lines))
118 
119 
121  nodes_or_endpoints: Collection[_NodeOrEndpointType],
122  command_class: CommandClass,
123  method_name: str,
124  *args: Any,
125 ) -> None:
126  """Invoke the CC API on a node endpoint."""
127  nodes_or_endpoints_list = list(nodes_or_endpoints)
128  results = await asyncio.gather(
129  *(
130  node_or_endpoint.async_invoke_cc_api(command_class, method_name, *args)
131  for node_or_endpoint in nodes_or_endpoints_list
132  ),
133  return_exceptions=True,
134  )
135  for node_or_endpoint, result in get_valid_responses_from_results(
136  nodes_or_endpoints_list, results
137  ):
138  if isinstance(node_or_endpoint, ZwaveNode):
139  _LOGGER.info(
140  (
141  "Invoked %s CC API method %s on node %s with the following result: "
142  "%s"
143  ),
144  command_class.name,
145  method_name,
146  node_or_endpoint,
147  result,
148  )
149  else:
150  _LOGGER.info(
151  (
152  "Invoked %s CC API method %s on endpoint %s with the following "
153  "result: %s"
154  ),
155  command_class.name,
156  method_name,
157  node_or_endpoint,
158  result,
159  )
160  raise_exceptions_from_results(nodes_or_endpoints_list, results)
161 
162 
164  """Class that holds our services (Zwave Commands).
165 
166  Services that should be published to hass.
167  """
168 
169  def __init__(
170  self,
171  hass: HomeAssistant,
172  ent_reg: er.EntityRegistry,
173  dev_reg: dr.DeviceRegistry,
174  ) -> None:
175  """Initialize with hass object."""
176  self._hass_hass = hass
177  self._ent_reg_ent_reg = ent_reg
178  self._dev_reg_dev_reg = dev_reg
179 
180  @callback
181  def async_register(self) -> None:
182  """Register all our services."""
183 
184  @callback
185  def get_nodes_from_service_data(val: dict[str, Any]) -> dict[str, Any]:
186  """Get nodes set from service data."""
187  val[const.ATTR_NODES] = async_get_nodes_from_targets(
188  self._hass_hass, val, self._ent_reg_ent_reg, self._dev_reg_dev_reg, _LOGGER
189  )
190  return val
191 
192  @callback
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")
197  return val
198 
199  @callback
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]
204 
205  if not broadcast:
206  has_at_least_one_node(val)
207 
208  # User must specify a node if they are attempting a broadcast and have more
209  # than one zwave-js network.
210  if (
211  broadcast
212  and not nodes
213  and len(self._hass_hass.config_entries.async_entries(const.DOMAIN)) > 1
214  ):
215  raise vol.Invalid(
216  "You must include at least one entity or device in the service call"
217  )
218 
219  first_node = next((node for node in nodes), None)
220 
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}")
223 
224  # If any nodes don't have matching home IDs, we can't run the command
225  # because we can't multicast across multiple networks
226  if (
227  first_node
228  and first_node.client.driver # We checked the driver was ready above.
229  and any(
230  node.client.driver.controller.home_id
231  != first_node.client.driver.controller.home_id
232  for node in nodes
233  if node.client.driver is not None
234  )
235  ):
236  raise vol.Invalid(
237  "Multicast commands only work on devices in the same network"
238  )
239 
240  return val
241 
242  @callback
243  def validate_entities(val: dict[str, Any]) -> dict[str, Any]:
244  """Validate entities exist and are from the zwave_js platform."""
245  val[ATTR_ENTITY_ID] = expand_entity_ids(self._hass_hass, val[ATTR_ENTITY_ID])
246  invalid_entities = []
247  for entity_id in val[ATTR_ENTITY_ID]:
248  entry = self._ent_reg_ent_reg.async_get(entity_id)
249  if entry is None or entry.platform != const.DOMAIN:
250  _LOGGER.info(
251  "Entity %s is not a valid %s entity", entity_id, const.DOMAIN
252  )
253  invalid_entities.append(entity_id)
254 
255  # Remove invalid entities
256  val[ATTR_ENTITY_ID] = list(set(val[ATTR_ENTITY_ID]) - set(invalid_entities))
257 
258  if not val[ATTR_ENTITY_ID]:
259  raise vol.Invalid(f"No {const.DOMAIN} entities found in service call")
260 
261  return val
262 
263  self._hass_hass.services.async_register(
264  const.DOMAIN,
265  const.SERVICE_SET_CONFIG_PARAMETER,
266  self.async_set_config_parameterasync_set_config_parameter,
267  schema=vol.Schema(
268  vol.All(
269  {
270  **TARGET_VALIDATORS,
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
274  ),
275  vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(
276  vol.Coerce(int), BITMASK_SCHEMA
277  ),
278  vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
279  vol.Coerce(int), BITMASK_SCHEMA, cv.string
280  ),
281  vol.Inclusive(const.ATTR_VALUE_SIZE, "raw"): vol.All(
282  vol.Coerce(int), vol.Range(min=1, max=4), check_base_2
283  ),
284  vol.Inclusive(const.ATTR_VALUE_FORMAT, "raw"): vol.Coerce(
285  ConfigurationValueFormat
286  ),
287  },
288  cv.has_at_least_one_key(
289  ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
290  ),
291  cv.has_at_most_one_key(
292  const.ATTR_CONFIG_PARAMETER_BITMASK, const.ATTR_VALUE_SIZE
293  ),
294  parameter_name_does_not_need_bitmask,
295  get_nodes_from_service_data,
296  has_at_least_one_node,
297  ),
298  ),
299  )
300 
301  self._hass_hass.services.async_register(
302  const.DOMAIN,
303  const.SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS,
304  self.async_bulk_set_partial_config_parametersasync_bulk_set_partial_config_parameters,
305  schema=vol.Schema(
306  vol.All(
307  {
308  **TARGET_VALIDATORS,
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(
312  vol.Coerce(int),
313  {
314  vol.Any(
315  vol.Coerce(int), BITMASK_SCHEMA, cv.string
316  ): vol.Any(vol.Coerce(int), BITMASK_SCHEMA, cv.string)
317  },
318  ),
319  },
320  cv.has_at_least_one_key(
321  ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
322  ),
323  get_nodes_from_service_data,
324  has_at_least_one_node,
325  ),
326  ),
327  )
328 
329  self._hass_hass.services.async_register(
330  const.DOMAIN,
331  const.SERVICE_REFRESH_VALUE,
332  self.async_poll_valueasync_poll_value,
333  schema=vol.Schema(
334  vol.All(
335  {
336  vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
337  vol.Optional(
338  const.ATTR_REFRESH_ALL_VALUES, default=False
339  ): cv.boolean,
340  },
341  validate_entities,
342  )
343  ),
344  )
345 
346  self._hass_hass.services.async_register(
347  const.DOMAIN,
348  const.SERVICE_SET_VALUE,
349  self.async_set_valueasync_set_value,
350  schema=vol.Schema(
351  vol.All(
352  {
353  **TARGET_VALIDATORS,
354  vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int),
355  vol.Required(const.ATTR_PROPERTY): vol.Any(
356  vol.Coerce(int), str
357  ),
358  vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any(
359  vol.Coerce(int), str
360  ),
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},
365  },
366  cv.has_at_least_one_key(
367  ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
368  ),
369  get_nodes_from_service_data,
370  has_at_least_one_node,
371  ),
372  ),
373  )
374 
375  self._hass_hass.services.async_register(
376  const.DOMAIN,
377  const.SERVICE_MULTICAST_SET_VALUE,
378  self.async_multicast_set_valueasync_multicast_set_value,
379  schema=vol.Schema(
380  vol.All(
381  {
382  **TARGET_VALIDATORS,
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(
386  vol.Coerce(int), str
387  ),
388  vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any(
389  vol.Coerce(int), str
390  ),
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},
394  },
395  vol.Any(
396  cv.has_at_least_one_key(
397  ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
398  ),
399  broadcast_command,
400  ),
401  get_nodes_from_service_data,
402  validate_multicast_nodes,
403  ),
404  ),
405  )
406 
407  self._hass_hass.services.async_register(
408  const.DOMAIN,
409  const.SERVICE_PING,
410  self.async_pingasync_ping,
411  schema=vol.Schema(
412  vol.All(
413  TARGET_VALIDATORS,
414  cv.has_at_least_one_key(
415  ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
416  ),
417  get_nodes_from_service_data,
418  has_at_least_one_node,
419  ),
420  ),
421  )
422 
423  self._hass_hass.services.async_register(
424  const.DOMAIN,
425  const.SERVICE_INVOKE_CC_API,
426  self.async_invoke_cc_apiasync_invoke_cc_api,
427  schema=vol.Schema(
428  vol.All(
429  {
430  **TARGET_VALIDATORS,
431  vol.Required(const.ATTR_COMMAND_CLASS): vol.All(
432  vol.Coerce(int), vol.Coerce(CommandClass)
433  ),
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,
437  },
438  cv.has_at_least_one_key(
439  ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
440  ),
441  get_nodes_from_service_data,
442  has_at_least_one_node,
443  ),
444  ),
445  )
446 
447  self._hass_hass.services.async_register(
448  const.DOMAIN,
449  const.SERVICE_REFRESH_NOTIFICATIONS,
450  self.async_refresh_notificationsasync_refresh_notifications,
451  schema=vol.Schema(
452  vol.All(
453  {
454  **TARGET_VALIDATORS,
455  vol.Required(const.ATTR_NOTIFICATION_TYPE): vol.All(
456  vol.Coerce(int), vol.Coerce(NotificationType)
457  ),
458  vol.Optional(const.ATTR_NOTIFICATION_EVENT): vol.Coerce(int),
459  },
460  cv.has_at_least_one_key(
461  ATTR_DEVICE_ID, ATTR_ENTITY_ID, ATTR_AREA_ID
462  ),
463  get_nodes_from_service_data,
464  has_at_least_one_node,
465  ),
466  ),
467  )
468 
469  async def async_set_config_parameter(self, service: ServiceCall) -> None:
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)
478 
479  nodes_without_endpoints: set[ZwaveNode] = set()
480  # Remove nodes that don't have the specified endpoint
481  for node in nodes:
482  if endpoint not in node.endpoints:
483  nodes_without_endpoints.add(node)
484  nodes = nodes.difference(nodes_without_endpoints)
485  if not nodes:
486  raise HomeAssistantError(
487  "None of the specified nodes have the specified endpoint"
488  )
489  if nodes_without_endpoints and _LOGGER.isEnabledFor(logging.WARNING):
490  _LOGGER.warning(
491  (
492  "The following nodes do not have endpoint %x and will be "
493  "skipped: %s"
494  ),
495  endpoint,
496  nodes_without_endpoints,
497  )
498 
499  # If value_size isn't provided, we will use the utility function which includes
500  # additional checks and protections. If it is provided, we will use the
501  # node.async_set_raw_config_parameter_value method which calls the
502  # Configuration CC set API.
503  results = await asyncio.gather(
504  *(
506  node,
507  new_value,
508  property_or_property_name,
509  property_key=property_key,
510  endpoint=endpoint,
511  )
512  if value_size is None
513  else node.endpoints[endpoint].async_set_raw_config_parameter_value(
514  new_value,
515  property_or_property_name,
516  property_key=property_key,
517  value_size=value_size,
518  value_format=value_format,
519  )
520  for node in nodes
521  ),
522  return_exceptions=True,
523  )
524 
525  def process_results(
526  nodes_or_endpoints_list: Sequence[_NodeOrEndpointType], _results: list[Any]
527  ) -> None:
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
531  ):
532  if value_size is None:
533  # async_set_config_parameter still returns (Value, SetConfigParameterResult)
534  zwave_value = result[0]
535  cmd_status = result[1]
536  else:
537  # async_set_raw_config_parameter_value now returns just SetConfigParameterResult
538  cmd_status = result
539  zwave_value = f"parameter {property_or_property_name}"
540 
541  if cmd_status.status == CommandStatus.ACCEPTED:
542  msg = "Set configuration parameter %s on Node %s with value %s"
543  else:
544  msg = (
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"
547  )
548  _LOGGER.info(msg, zwave_value, node_or_endpoint, new_value)
549  raise_exceptions_from_results(nodes_or_endpoints_list, _results)
550 
551  if value_size is None:
552  process_results(list(nodes), results)
553  else:
554  process_results([node.endpoints[endpoint] for node in nodes], results)
555 
557  self, service: ServiceCall
558  ) -> None:
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]
564 
565  results = await asyncio.gather(
566  *(
568  node,
569  property_,
570  new_value,
571  endpoint=endpoint,
572  )
573  for node in nodes
574  ),
575  return_exceptions=True,
576  )
577 
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"
582  else:
583  msg = (
584  "Queued command to bulk set partials for configuration parameter "
585  "%s on Node %s"
586  )
587 
588  _LOGGER.info(msg, property_, node)
589 
590  raise_exceptions_from_results(nodes_list, results)
591 
592  async def async_poll_value(self, service: ServiceCall) -> None:
593  """Poll value on a node."""
594  for entity_id in service.data[ATTR_ENTITY_ID]:
595  entry = self._ent_reg_ent_reg.async_get(entity_id)
596  assert entry # Schema validation would have failed if we can't do this
598  self._hass_hass,
599  f"{const.DOMAIN}_{entry.unique_id}_poll_value",
600  service.data[const.ATTR_REFRESH_ALL_VALUES],
601  )
602 
603  async def async_set_value(self, service: ServiceCall) -> None:
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)
613 
614  coros = []
615  for node in nodes:
616  value_id = get_value_id_str(
617  node,
618  command_class,
619  property_,
620  endpoint=endpoint,
621  property_key=property_key,
622  )
623  # If value has a string type but the new value is not a string, we need to
624  # convert it to one. We use new variable `new_value_` to convert the data
625  # so we can preserve the original `new_value` for every node.
626  if (
627  value_id in node.values
628  and node.values[value_id].metadata.type == "string"
629  and not isinstance(new_value, str)
630  ):
631  new_value_ = str(new_value)
632  else:
633  new_value_ = new_value
634  coros.append(
635  node.async_set_value(
636  value_id,
637  new_value_,
638  options=options,
639  wait_for_result=wait_for_result,
640  )
641  )
642 
643  results = await asyncio.gather(*coros, return_exceptions=True)
644  nodes_list = list(nodes)
645  # multiple set_values my fail so we will track the entire list
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:
650  # If we failed to set a value, add node to exception list
651  set_value_failed_nodes_list.append(node_)
652  set_value_failed_error_list.append(
653  SetValueFailed(f"{result.status} {result.message}")
654  )
655 
656  # Add the exception to the results and the nodes to the node list. No-op if
657  # no set value commands failed
659  (*nodes_list, *set_value_failed_nodes_list),
660  (*results, *set_value_failed_error_list),
661  )
662 
663  async def async_multicast_set_value(self, service: ServiceCall) -> None:
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)
668 
669  if not broadcast and len(nodes) == 1:
670  _LOGGER.info(
671  "Passing the zwave_js.multicast_set_value service call to the "
672  "zwave_js.set_value service since only one node was targeted"
673  )
674  await self.async_set_valueasync_set_value(service)
675  return
676 
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)
681 
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
687 
688  new_value = service.data[const.ATTR_VALUE]
689 
690  # If there are no nodes, we can assume there is only one config entry due to
691  # schema validation and can use that to get the client, otherwise we can just
692  # get the client from the node.
693  client: ZwaveClient
694  first_node: ZwaveNode
695  try:
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]
701  assert client.driver
702  first_node = next(
703  node
704  for node in client.driver.controller.nodes.values()
705  if get_value_id_str(
706  node, command_class, property_, endpoint, property_key
707  )
708  in node.values
709  )
710 
711  # If value has a string type but the new value is not a string, we need to
712  # convert it to one
713  value_id = get_value_id_str(
714  first_node, command_class, property_, endpoint, property_key
715  )
716  if (
717  value_id in first_node.values
718  and first_node.values[value_id].metadata.type == "string"
719  and not isinstance(new_value, str)
720  ):
721  new_value = str(new_value)
722 
723  try:
724  result = await async_multicast_set_value(
725  client=client,
726  new_value=new_value,
727  value_data=value,
728  nodes=None if broadcast else list(nodes),
729  options=options,
730  )
731  except FailedZWaveCommand as err:
732  raise HomeAssistantError("Unable to set value via multicast") from err
733 
734  if result.status not in SET_VALUE_SUCCESS:
735  raise HomeAssistantError(
736  "Unable to set value via multicast"
737  ) from SetValueFailed(f"{result.status} {result.message}")
738 
739  async def async_ping(self, service: ServiceCall) -> None:
740  """Ping node(s)."""
741  _LOGGER.warning(
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 "
744  "future release"
745  )
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
749  )
750  raise_exceptions_from_results(nodes, results)
751 
752  async def async_invoke_cc_api(self, service: ServiceCall) -> None:
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]
757 
758  # If an endpoint is provided, we assume the user wants to call the CC API on
759  # that endpoint for all target nodes
760  if (endpoint := service.data.get(const.ATTR_ENDPOINT)) is not None:
761  await _async_invoke_cc_api(
762  {node.endpoints[endpoint] for node in service.data[const.ATTR_NODES]},
763  command_class,
764  method_name,
765  *parameters,
766  )
767  return
768 
769  # If no endpoint is provided, we target endpoint 0 for all device and area
770  # nodes and we target the endpoint of the primary value for all entities
771  # specified.
772  endpoints: set[Endpoint] = set()
773  for area_id in service.data.get(ATTR_AREA_ID, []):
774  for node in async_get_nodes_from_area_id(
775  self._hass_hass, area_id, self._ent_reg_ent_reg, self._dev_reg_dev_reg
776  ):
777  endpoints.add(node.endpoints[0])
778 
779  for device_id in service.data.get(ATTR_DEVICE_ID, []):
780  try:
782  self._hass_hass, device_id, self._dev_reg_dev_reg
783  )
784  except ValueError as err:
785  _LOGGER.warning(err.args[0])
786  continue
787  endpoints.add(node.endpoints[0])
788 
789  for entity_id in service.data.get(ATTR_ENTITY_ID, []):
790  if (
791  not (entity_entry := self._ent_reg_ent_reg.async_get(entity_id))
792  or entity_entry.platform != const.DOMAIN
793  ):
794  _LOGGER.warning(
795  "Skipping entity %s as it is not a valid %s entity",
796  entity_id,
797  const.DOMAIN,
798  )
799  continue
801  self._hass_hass, entity_id, self._ent_reg_ent_reg, self._dev_reg_dev_reg
802  )
803  if (
804  value_id := get_value_id_from_unique_id(entity_entry.unique_id)
805  ) is None:
806  _LOGGER.warning("Skipping entity %s as it has no value ID", entity_id)
807  continue
808 
809  endpoint_idx = node.values[value_id].endpoint
810  endpoints.add(
811  node.endpoints[endpoint_idx if endpoint_idx is not None else 0]
812  )
813 
814  await _async_invoke_cc_api(endpoints, command_class, method_name, *parameters)
815 
816  async def async_refresh_notifications(self, service: ServiceCall) -> None:
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
824  await _async_invoke_cc_api(nodes, CommandClass.NOTIFICATION, "get", param)
None async_poll_value(self, ServiceCall service)
Definition: services.py:592
None async_invoke_cc_api(self, ServiceCall service)
Definition: services.py:752
None async_refresh_notifications(self, ServiceCall service)
Definition: services.py:816
None async_set_value(self, ServiceCall service)
Definition: services.py:603
None __init__(self, HomeAssistant hass, er.EntityRegistry ent_reg, dr.DeviceRegistry dev_reg)
Definition: services.py:174
None async_set_config_parameter(self, ServiceCall service)
Definition: services.py:469
None async_multicast_set_value(self, ServiceCall service)
Definition: services.py:663
None async_bulk_set_partial_config_parameters(self, ServiceCall service)
Definition: services.py:558
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)
Definition: helpers.py:330
str|None get_value_id_from_unique_id(str unique_id)
Definition: helpers.py:104
ZwaveNode async_get_node_from_entity_id(HomeAssistant hass, str entity_id, er.EntityRegistry|None ent_reg=None, dr.DeviceRegistry|None dev_reg=None)
Definition: helpers.py:306
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)
Definition: helpers.py:369
ZwaveNode async_get_node_from_device_id(HomeAssistant hass, str device_id, dr.DeviceRegistry|None dev_reg=None)
Definition: helpers.py:253
None _async_invoke_cc_api(Collection[_NodeOrEndpointType] nodes_or_endpoints, CommandClass command_class, str method_name, *Any args)
Definition: services.py:125
None raise_exceptions_from_results(Sequence[_NodeOrEndpointType] zwave_objects, Sequence[Any] results)
Definition: services.py:101
dict[str, Any] broadcast_command(dict[str, Any] val)
Definition: services.py:80
dict[str, int|str|list[str]] parameter_name_does_not_need_bitmask(dict[str, int|str|list[str]] val)
Definition: services.py:60
AreaRegistry async_get(HomeAssistant hass)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193