Home Assistant Unofficial Reference 2024.12.1
websocket_api.py
Go to the documentation of this file.
1 """Web socket API for Zigbee Home Automation devices."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import logging
7 from typing import TYPE_CHECKING, Any, Literal, NamedTuple, cast
8 
9 import voluptuous as vol
10 from zha.application.const import (
11  ATTR_ARGS,
12  ATTR_ATTRIBUTE,
13  ATTR_CLUSTER_ID,
14  ATTR_CLUSTER_TYPE,
15  ATTR_COMMAND_TYPE,
16  ATTR_ENDPOINT_ID,
17  ATTR_IEEE,
18  ATTR_LEVEL,
19  ATTR_MANUFACTURER,
20  ATTR_MEMBERS,
21  ATTR_PARAMS,
22  ATTR_TYPE,
23  ATTR_VALUE,
24  ATTR_WARNING_DEVICE_DURATION,
25  ATTR_WARNING_DEVICE_MODE,
26  ATTR_WARNING_DEVICE_STROBE,
27  ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE,
28  ATTR_WARNING_DEVICE_STROBE_INTENSITY,
29  CLUSTER_COMMAND_SERVER,
30  CLUSTER_COMMANDS_CLIENT,
31  CLUSTER_COMMANDS_SERVER,
32  CLUSTER_TYPE_IN,
33  CLUSTER_TYPE_OUT,
34  WARNING_DEVICE_MODE_EMERGENCY,
35  WARNING_DEVICE_SOUND_HIGH,
36  WARNING_DEVICE_SQUAWK_MODE_ARMED,
37  WARNING_DEVICE_STROBE_HIGH,
38  WARNING_DEVICE_STROBE_YES,
39  ZHA_CLUSTER_HANDLER_MSG,
40 )
41 from zha.application.gateway import Gateway
42 from zha.application.helpers import (
43  async_is_bindable_target,
44  convert_install_code,
45  get_matched_clusters,
46  qr_to_install_code,
47 )
48 from zha.zigbee.cluster_handlers.const import CLUSTER_HANDLER_IAS_WD
49 from zha.zigbee.device import Device
50 from zha.zigbee.group import GroupMemberReference
51 import zigpy.backups
52 from zigpy.config import CONF_DEVICE
53 from zigpy.config.validators import cv_boolean
54 from zigpy.types.named import EUI64, KeyData
55 from zigpy.zcl.clusters.security import IasAce
56 import zigpy.zdo.types as zdo_types
57 
58 from homeassistant.components import websocket_api
59 from homeassistant.config_entries import ConfigEntry
60 from homeassistant.const import ATTR_COMMAND, ATTR_ID, ATTR_NAME
61 from homeassistant.core import HomeAssistant, ServiceCall, callback
62 from homeassistant.helpers import entity_registry as er
64 from homeassistant.helpers.dispatcher import async_dispatcher_connect
65 from homeassistant.helpers.service import async_register_admin_service
66 from homeassistant.helpers.typing import VolDictType, VolSchemaType
67 
68 from .api import (
69  async_change_channel,
70  async_get_active_network_settings,
71  async_get_radio_type,
72 )
73 from .const import (
74  CUSTOM_CONFIGURATION,
75  DOMAIN,
76  EZSP_OVERWRITE_EUI64,
77  GROUP_ID,
78  GROUP_IDS,
79  GROUP_NAME,
80  MFG_CLUSTER_ID_START,
81  ZHA_ALARM_OPTIONS,
82  ZHA_OPTIONS,
83 )
84 from .helpers import (
85  CONF_ZHA_ALARM_SCHEMA,
86  CONF_ZHA_OPTIONS_SCHEMA,
87  EntityReference,
88  ZHAGatewayProxy,
89  async_cluster_exists,
90  cluster_command_schema_to_vol_schema,
91  get_config_entry,
92  get_zha_gateway,
93  get_zha_gateway_proxy,
94 )
95 
96 if TYPE_CHECKING:
97  from homeassistant.components.websocket_api import ActiveConnection
98 
99 _LOGGER = logging.getLogger(__name__)
100 
101 TYPE = "type"
102 CLIENT = "client"
103 ID = "id"
104 RESPONSE = "response"
105 DEVICE_INFO = "device_info"
106 
107 ATTR_DURATION = "duration"
108 ATTR_GROUP = "group"
109 ATTR_IEEE_ADDRESS = "ieee_address"
110 ATTR_INSTALL_CODE = "install_code"
111 ATTR_NEW_CHANNEL = "new_channel"
112 ATTR_SOURCE_IEEE = "source_ieee"
113 ATTR_TARGET_IEEE = "target_ieee"
114 ATTR_QR_CODE = "qr_code"
115 
116 BINDINGS = "bindings"
117 
118 SERVICE_PERMIT = "permit"
119 SERVICE_REMOVE = "remove"
120 SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = "set_zigbee_cluster_attribute"
121 SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = "issue_zigbee_cluster_command"
122 SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND = "issue_zigbee_group_command"
123 SERVICE_DIRECT_ZIGBEE_BIND = "issue_direct_zigbee_bind"
124 SERVICE_DIRECT_ZIGBEE_UNBIND = "issue_direct_zigbee_unbind"
125 SERVICE_WARNING_DEVICE_SQUAWK = "warning_device_squawk"
126 SERVICE_WARNING_DEVICE_WARN = "warning_device_warn"
127 SERVICE_ZIGBEE_BIND = "service_zigbee_bind"
128 IEEE_SERVICE = "ieee_based_service"
129 
130 IEEE_SCHEMA = vol.All(cv.string, EUI64.convert)
131 
132 
133 def _ensure_list_if_present[_T](value: _T | None) -> list[_T] | list[Any] | None:
134  """Wrap value in list if it is provided and not one."""
135  if value is None:
136  return None
137  return cast("list[_T]", value) if isinstance(value, list) else [value]
138 
139 
140 SERVICE_PERMIT_PARAMS: VolDictType = {
141  vol.Optional(ATTR_IEEE): IEEE_SCHEMA,
142  vol.Optional(ATTR_DURATION, default=60): vol.All(
143  vol.Coerce(int), vol.Range(0, 254)
144  ),
145  vol.Inclusive(ATTR_SOURCE_IEEE, "install_code"): IEEE_SCHEMA,
146  vol.Inclusive(ATTR_INSTALL_CODE, "install_code"): vol.All(
147  cv.string, convert_install_code
148  ),
149  vol.Exclusive(ATTR_QR_CODE, "install_code"): vol.All(cv.string, qr_to_install_code),
150 }
151 
152 SERVICE_SCHEMAS: dict[str, VolSchemaType] = {
153  SERVICE_PERMIT: vol.Schema(
154  vol.All(
155  cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE),
156  SERVICE_PERMIT_PARAMS,
157  )
158  ),
159  IEEE_SERVICE: vol.Schema(
160  vol.All(
161  cv.deprecated(ATTR_IEEE_ADDRESS, replacement_key=ATTR_IEEE),
162  {vol.Required(ATTR_IEEE): IEEE_SCHEMA},
163  )
164  ),
165  SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema(
166  {
167  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
168  vol.Required(ATTR_ENDPOINT_ID): cv.positive_int,
169  vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
170  vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string,
171  vol.Required(ATTR_ATTRIBUTE): vol.Any(cv.positive_int, str),
172  vol.Required(ATTR_VALUE): vol.Any(int, cv.boolean, cv.string),
173  vol.Optional(ATTR_MANUFACTURER): vol.All(
174  vol.Coerce(int), vol.Range(min=-1)
175  ),
176  }
177  ),
178  SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema(
179  {
180  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
181  vol.Optional(
182  ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED
183  ): cv.positive_int,
184  vol.Optional(
185  ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES
186  ): cv.positive_int,
187  vol.Optional(
188  ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH
189  ): cv.positive_int,
190  }
191  ),
192  SERVICE_WARNING_DEVICE_WARN: vol.Schema(
193  {
194  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
195  vol.Optional(
196  ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY
197  ): cv.positive_int,
198  vol.Optional(
199  ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES
200  ): cv.positive_int,
201  vol.Optional(
202  ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH
203  ): cv.positive_int,
204  vol.Optional(ATTR_WARNING_DEVICE_DURATION, default=5): cv.positive_int,
205  vol.Optional(
206  ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, default=0x00
207  ): cv.positive_int,
208  vol.Optional(
209  ATTR_WARNING_DEVICE_STROBE_INTENSITY, default=WARNING_DEVICE_STROBE_HIGH
210  ): cv.positive_int,
211  }
212  ),
213  SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.All(
214  vol.Schema(
215  {
216  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
217  vol.Required(ATTR_ENDPOINT_ID): cv.positive_int,
218  vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
219  vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string,
220  vol.Required(ATTR_COMMAND): cv.positive_int,
221  vol.Required(ATTR_COMMAND_TYPE): cv.string,
222  vol.Exclusive(ATTR_ARGS, "attrs_params"): _ensure_list_if_present,
223  vol.Exclusive(ATTR_PARAMS, "attrs_params"): dict,
224  vol.Optional(ATTR_MANUFACTURER): vol.All(
225  vol.Coerce(int), vol.Range(min=-1)
226  ),
227  }
228  ),
229  cv.deprecated(ATTR_ARGS),
230  cv.has_at_least_one_key(ATTR_ARGS, ATTR_PARAMS),
231  ),
232  SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND: vol.Schema(
233  {
234  vol.Required(ATTR_GROUP): cv.positive_int,
235  vol.Required(ATTR_CLUSTER_ID): cv.positive_int,
236  vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string,
237  vol.Required(ATTR_COMMAND): cv.positive_int,
238  vol.Optional(ATTR_ARGS, default=[]): cv.ensure_list,
239  vol.Optional(ATTR_MANUFACTURER): vol.All(
240  vol.Coerce(int), vol.Range(min=-1)
241  ),
242  }
243  ),
244 }
245 
246 
247 ZHA_CONFIG_SCHEMAS = {
248  ZHA_OPTIONS: CONF_ZHA_OPTIONS_SCHEMA,
249  ZHA_ALARM_OPTIONS: CONF_ZHA_ALARM_SCHEMA,
250 }
251 
252 
253 class ClusterBinding(NamedTuple):
254  """Describes a cluster binding."""
255 
256  name: str
257  type: str
258  id: int
259  endpoint_id: int
260 
261 
262 def _cv_group_member(value: dict[str, Any]) -> GroupMemberReference:
263  """Transform a group member."""
264  return GroupMemberReference(
265  ieee=value[ATTR_IEEE],
266  endpoint_id=value[ATTR_ENDPOINT_ID],
267  )
268 
269 
270 def _cv_cluster_binding(value: dict[str, Any]) -> ClusterBinding:
271  """Transform a cluster binding."""
272  return ClusterBinding(
273  name=value[ATTR_NAME],
274  type=value[ATTR_TYPE],
275  id=value[ATTR_ID],
276  endpoint_id=value[ATTR_ENDPOINT_ID],
277  )
278 
279 
280 def _cv_zigpy_network_backup(value: dict[str, Any]) -> zigpy.backups.NetworkBackup:
281  """Transform a zigpy network backup."""
282 
283  try:
284  return zigpy.backups.NetworkBackup.from_dict(value)
285  except ValueError as err:
286  raise vol.Invalid(str(err)) from err
287 
288 
289 GROUP_MEMBER_SCHEMA = vol.All(
290  vol.Schema(
291  {
292  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
293  vol.Required(ATTR_ENDPOINT_ID): vol.Coerce(int),
294  }
295  ),
296  _cv_group_member,
297 )
298 
299 
300 CLUSTER_BINDING_SCHEMA = vol.All(
301  vol.Schema(
302  {
303  vol.Required(ATTR_NAME): cv.string,
304  vol.Required(ATTR_TYPE): cv.string,
305  vol.Required(ATTR_ID): vol.Coerce(int),
306  vol.Required(ATTR_ENDPOINT_ID): vol.Coerce(int),
307  }
308  ),
309  _cv_cluster_binding,
310 )
311 
312 
313 @websocket_api.require_admin
314 @websocket_api.websocket_command( { vol.Required("type"): "zha/devices/permit",
315  **SERVICE_PERMIT_PARAMS,
316  }
317 )
318 @websocket_api.async_response
320  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
321 ) -> None:
322  """Permit ZHA zigbee devices."""
323  zha_gateway_proxy = get_zha_gateway_proxy(hass)
324  duration: int = msg[ATTR_DURATION]
325  ieee: EUI64 | None = msg.get(ATTR_IEEE)
326 
327  async def forward_messages(data):
328  """Forward events to websocket."""
329  connection.send_message(websocket_api.event_message(msg["id"], data))
330 
331  remove_dispatcher_function = async_dispatcher_connect(
332  hass, "zha_gateway_message", forward_messages
333  )
334 
335  @callback
336  def async_cleanup() -> None:
337  """Remove signal listener and turn off debug mode."""
338  zha_gateway_proxy.async_disable_debug_mode()
339  remove_dispatcher_function()
340 
341  connection.subscriptions[msg["id"]] = async_cleanup
342  zha_gateway_proxy.async_enable_debug_mode()
343  src_ieee: EUI64
344  link_key: KeyData
345  if ATTR_SOURCE_IEEE in msg:
346  src_ieee = msg[ATTR_SOURCE_IEEE]
347  link_key = msg[ATTR_INSTALL_CODE]
348  _LOGGER.debug("Allowing join for %s device with link key", src_ieee)
349  await zha_gateway_proxy.gateway.application_controller.permit_with_link_key(
350  time_s=duration, node=src_ieee, link_key=link_key
351  )
352  elif ATTR_QR_CODE in msg:
353  src_ieee, link_key = msg[ATTR_QR_CODE]
354  _LOGGER.debug("Allowing join for %s device with link key", src_ieee)
355  await zha_gateway_proxy.gateway.application_controller.permit_with_link_key(
356  time_s=duration, node=src_ieee, link_key=link_key
357  )
358  else:
359  await zha_gateway_proxy.gateway.application_controller.permit(
360  time_s=duration, node=ieee
361  )
362  connection.send_result(msg[ID])
363 
364 
365 @websocket_api.require_admin
366 @websocket_api.websocket_command({vol.Required(TYPE): "zha/devices"})
367 @websocket_api.async_response
369  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
370 ) -> None:
371  """Get ZHA devices."""
372  zha_gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass)
373  devices = [
374  device.zha_device_info for device in zha_gateway_proxy.device_proxies.values()
375  ]
376  connection.send_result(msg[ID], devices)
377 
378 
379 @callback
380 def _get_entity_name(zha_gateway: Gateway, entity_ref: EntityReference) -> str | None:
381  entity_registry = er.async_get(zha_gateway.hass)
382  entry = entity_registry.async_get(entity_ref.ha_entity_id)
383  return entry.name if entry else None
384 
385 
386 @callback
388  zha_gateway: Gateway, entity_ref: EntityReference
389 ) -> str | None:
390  entity_registry = er.async_get(zha_gateway.hass)
391  entry = entity_registry.async_get(entity_ref.ha_entity_id)
392  return entry.original_name if entry else None
393 
394 
395 @websocket_api.require_admin
396 @websocket_api.websocket_command({vol.Required(TYPE): "zha/devices/groupable"})
397 @websocket_api.async_response
399  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
400 ) -> None:
401  """Get ZHA devices that can be grouped."""
402  zha_gateway_proxy = get_zha_gateway_proxy(hass)
403 
404  devices = [
405  device
406  for device in zha_gateway_proxy.device_proxies.values()
407  if device.device.is_groupable
408  ]
409  groupable_devices: list[dict[str, Any]] = []
410 
411  for device in devices:
412  entity_refs = zha_gateway_proxy.ha_entity_refs[device.device.ieee]
413  groupable_devices.extend(
414  {
415  "endpoint_id": ep_id,
416  "entities": [
417  {
418  "name": _get_entity_name(zha_gateway_proxy, entity_ref),
419  "original_name": _get_entity_original_name(
420  zha_gateway_proxy, entity_ref
421  ),
422  }
423  for entity_ref in entity_refs
424  if list(entity_ref.entity_data.entity.cluster_handlers.values())[
425  0
426  ].cluster.endpoint.endpoint_id
427  == ep_id
428  ],
429  "device": device.zha_device_info,
430  }
431  for ep_id in device.device.async_get_groupable_endpoints()
432  )
433 
434  connection.send_result(msg[ID], groupable_devices)
435 
436 
437 @websocket_api.require_admin
438 @websocket_api.websocket_command({vol.Required(TYPE): "zha/groups"})
439 @websocket_api.async_response
441  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
442 ) -> None:
443  """Get ZHA groups."""
444  zha_gateway_proxy = get_zha_gateway_proxy(hass)
445  groups = [group.group_info for group in zha_gateway_proxy.group_proxies.values()]
446  connection.send_result(msg[ID], groups)
447 
448 
449 @websocket_api.require_admin
450 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/device",
451  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
452  }
453 )
454 @websocket_api.async_response
455 async def websocket_get_device(
456  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
457 ) -> None:
458  """Get ZHA devices."""
459  zha_gateway_proxy = get_zha_gateway_proxy(hass)
460  ieee: EUI64 = msg[ATTR_IEEE]
461 
462  if not (zha_device := zha_gateway_proxy.device_proxies.get(ieee)):
463  connection.send_message(
464  websocket_api.error_message(
465  msg[ID], websocket_api.ERR_NOT_FOUND, "ZHA Device not found"
466  )
467  )
468  return
469 
470  device_info = zha_device.zha_device_info
471  connection.send_result(msg[ID], device_info)
472 
473 
474 @websocket_api.require_admin
475 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group",
476  vol.Required(GROUP_ID): cv.positive_int,
477  }
478 )
479 @websocket_api.async_response
480 async def websocket_get_group(
481  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
482 ) -> None:
483  """Get ZHA group."""
484  zha_gateway_proxy = get_zha_gateway_proxy(hass)
485  group_id: int = msg[GROUP_ID]
486 
487  if not (zha_group := zha_gateway_proxy.group_proxies.get(group_id)):
488  connection.send_message(
489  websocket_api.error_message(
490  msg[ID], websocket_api.ERR_NOT_FOUND, "ZHA Group not found"
491  )
492  )
493  return
494 
495  group_info = zha_group.group_info
496  connection.send_result(msg[ID], group_info)
497 
498 
499 @websocket_api.require_admin
500 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/add",
501  vol.Required(GROUP_NAME): cv.string,
502  vol.Optional(GROUP_ID): cv.positive_int,
503  vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [GROUP_MEMBER_SCHEMA]),
504  }
505 )
506 @websocket_api.async_response
507 async def websocket_add_group(
508  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
509 ) -> None:
510  """Add a new ZHA group."""
511  zha_gateway = get_zha_gateway_proxy(hass)
512  group_name: str = msg[GROUP_NAME]
513  group_id: int | None = msg.get(GROUP_ID)
514  members: list[GroupMemberReference] | None = msg.get(ATTR_MEMBERS)
515  group = await zha_gateway.gateway.async_create_zigpy_group(
516  group_name, members, group_id
517  )
518  assert group
519  connection.send_result(
520  msg[ID], zha_gateway.group_proxies[group.group_id].group_info
521  )
522 
523 
524 @websocket_api.require_admin
525 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/remove",
526  vol.Required(GROUP_IDS): vol.All(cv.ensure_list, [cv.positive_int]),
527  }
528 )
529 @websocket_api.async_response
530 async def websocket_remove_groups(
531  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
532 ) -> None:
533  """Remove the specified ZHA groups."""
534  zha_gateway = get_zha_gateway_proxy(hass)
535  group_ids: list[int] = msg[GROUP_IDS]
536 
537  if len(group_ids) > 1:
538  tasks = [
539  zha_gateway.gateway.async_remove_zigpy_group(group_id)
540  for group_id in group_ids
541  ]
542  await asyncio.gather(*tasks)
543  else:
544  await zha_gateway.gateway.async_remove_zigpy_group(group_ids[0])
545  ret_groups = [group.group_info for group in zha_gateway.group_proxies.values()]
546  connection.send_result(msg[ID], ret_groups)
547 
548 
549 @websocket_api.require_admin
550 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/members/add",
551  vol.Required(GROUP_ID): cv.positive_int,
552  vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [GROUP_MEMBER_SCHEMA]),
553  }
554 )
555 @websocket_api.async_response
557  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
558 ) -> None:
559  """Add members to a ZHA group."""
560  zha_gateway = get_zha_gateway(hass)
561  zha_gateway_proxy = get_zha_gateway_proxy(hass)
562  group_id: int = msg[GROUP_ID]
563  members: list[GroupMemberReference] = msg[ATTR_MEMBERS]
564 
565  if not (zha_group := zha_gateway.groups.get(group_id)):
566  connection.send_message(
567  websocket_api.error_message(
568  msg[ID], websocket_api.ERR_NOT_FOUND, "ZHA Group not found"
569  )
570  )
571  return
572 
573  await zha_group.async_add_members(members)
574  ret_group = zha_gateway_proxy.get_group_proxy(group_id)
575  assert ret_group
576  connection.send_result(msg[ID], ret_group.group_info)
577 
578 
579 @websocket_api.require_admin
580 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/members/remove",
581  vol.Required(GROUP_ID): cv.positive_int,
582  vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [GROUP_MEMBER_SCHEMA]),
583  }
584 )
585 @websocket_api.async_response
587  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
588 ) -> None:
589  """Remove members from a ZHA group."""
590  zha_gateway = get_zha_gateway(hass)
591  zha_gateway_proxy = get_zha_gateway_proxy(hass)
592  group_id: int = msg[GROUP_ID]
593  members: list[GroupMemberReference] = msg[ATTR_MEMBERS]
594 
595  if not (zha_group := zha_gateway.groups.get(group_id)):
596  connection.send_message(
597  websocket_api.error_message(
598  msg[ID], websocket_api.ERR_NOT_FOUND, "ZHA Group not found"
599  )
600  )
601  return
602 
603  await zha_group.async_remove_members(members)
604  ret_group = zha_gateway_proxy.get_group_proxy(group_id)
605  assert ret_group
606  connection.send_result(msg[ID], ret_group.group_info)
607 
608 
609 @websocket_api.require_admin
610 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/reconfigure",
611  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
612  }
613 )
614 @websocket_api.async_response
616  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
617 ) -> None:
618  """Reconfigure a ZHA nodes entities by its ieee address."""
619  zha_gateway = get_zha_gateway(hass)
620  ieee: EUI64 = msg[ATTR_IEEE]
621  device: Device | None = zha_gateway.get_device(ieee)
622 
623  async def forward_messages(data):
624  """Forward events to websocket."""
625  connection.send_message(websocket_api.event_message(msg["id"], data))
626 
627  remove_dispatcher_function = async_dispatcher_connect(
628  hass, ZHA_CLUSTER_HANDLER_MSG, forward_messages
629  )
630 
631  @callback
632  def async_cleanup() -> None:
633  """Remove signal listener."""
634  remove_dispatcher_function()
635 
636  connection.subscriptions[msg["id"]] = async_cleanup
637 
638  _LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee)
639  assert device
640  hass.async_create_task(device.async_configure())
641 
642 
643 @websocket_api.require_admin
644 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/topology/update",
645  }
646 )
647 @websocket_api.async_response
648 async def websocket_update_topology(
649  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
650 ) -> None:
651  """Update the ZHA network topology."""
652  zha_gateway = get_zha_gateway(hass)
653  hass.async_create_task(zha_gateway.application_controller.topology.scan())
654 
655 
656 @websocket_api.require_admin
657 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters",
658  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
659  }
660 )
661 @websocket_api.async_response
662 async def websocket_device_clusters(
663  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
664 ) -> None:
665  """Return a list of device clusters."""
666  zha_gateway = get_zha_gateway(hass)
667  ieee: EUI64 = msg[ATTR_IEEE]
668  zha_device = zha_gateway.get_device(ieee)
669  response_clusters = []
670  if zha_device is not None:
671  clusters_by_endpoint = zha_device.async_get_clusters()
672  for ep_id, clusters in clusters_by_endpoint.items():
673  for c_id, cluster in clusters[CLUSTER_TYPE_IN].items():
674  response_clusters.append(
675  {
676  TYPE: CLUSTER_TYPE_IN,
677  ID: c_id,
678  ATTR_NAME: cluster.__class__.__name__,
679  "endpoint_id": ep_id,
680  }
681  )
682  for c_id, cluster in clusters[CLUSTER_TYPE_OUT].items():
683  response_clusters.append(
684  {
685  TYPE: CLUSTER_TYPE_OUT,
686  ID: c_id,
687  ATTR_NAME: cluster.__class__.__name__,
688  "endpoint_id": ep_id,
689  }
690  )
691 
692  connection.send_result(msg[ID], response_clusters)
693 
694 
695 @websocket_api.require_admin
696 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes",
697  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
698  vol.Required(ATTR_ENDPOINT_ID): int,
699  vol.Required(ATTR_CLUSTER_ID): int,
700  vol.Required(ATTR_CLUSTER_TYPE): str,
701  }
702 )
703 @websocket_api.async_response
705  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
706 ) -> None:
707  """Return a list of cluster attributes."""
708  zha_gateway = get_zha_gateway(hass)
709  ieee: EUI64 = msg[ATTR_IEEE]
710  endpoint_id: int = msg[ATTR_ENDPOINT_ID]
711  cluster_id: int = msg[ATTR_CLUSTER_ID]
712  cluster_type: str = msg[ATTR_CLUSTER_TYPE]
713  cluster_attributes: list[dict[str, Any]] = []
714  zha_device = zha_gateway.get_device(ieee)
715  attributes = None
716  if zha_device is not None:
717  attributes = zha_device.async_get_cluster_attributes(
718  endpoint_id, cluster_id, cluster_type
719  )
720  if attributes is not None:
721  for attr_id, attr in attributes.items():
722  cluster_attributes.append({ID: attr_id, ATTR_NAME: attr.name})
723  _LOGGER.debug(
724  "Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s",
725  ATTR_CLUSTER_ID,
726  cluster_id,
727  ATTR_CLUSTER_TYPE,
728  cluster_type,
729  ATTR_ENDPOINT_ID,
730  endpoint_id,
731  RESPONSE,
732  cluster_attributes,
733  )
734 
735  connection.send_result(msg[ID], cluster_attributes)
736 
737 
738 @websocket_api.require_admin
739 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/commands",
740  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
741  vol.Required(ATTR_ENDPOINT_ID): int,
742  vol.Required(ATTR_CLUSTER_ID): int,
743  vol.Required(ATTR_CLUSTER_TYPE): str,
744  }
745 )
746 @websocket_api.async_response
748  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
749 ) -> None:
750  """Return a list of cluster commands."""
751  import voluptuous_serialize # pylint: disable=import-outside-toplevel
752 
753  zha_gateway = get_zha_gateway(hass)
754  ieee: EUI64 = msg[ATTR_IEEE]
755  endpoint_id: int = msg[ATTR_ENDPOINT_ID]
756  cluster_id: int = msg[ATTR_CLUSTER_ID]
757  cluster_type: str = msg[ATTR_CLUSTER_TYPE]
758  zha_device = zha_gateway.get_device(ieee)
759  cluster_commands: list[dict[str, Any]] = []
760  commands = None
761  if zha_device is not None:
762  commands = zha_device.async_get_cluster_commands(
763  endpoint_id, cluster_id, cluster_type
764  )
765 
766  if commands is not None:
767  for cmd_id, cmd in commands[CLUSTER_COMMANDS_CLIENT].items():
768  cluster_commands.append(
769  {
770  TYPE: CLIENT,
771  ID: cmd_id,
772  ATTR_NAME: cmd.name,
773  "schema": voluptuous_serialize.convert(
775  custom_serializer=cv.custom_serializer,
776  ),
777  }
778  )
779  for cmd_id, cmd in commands[CLUSTER_COMMANDS_SERVER].items():
780  cluster_commands.append(
781  {
782  TYPE: CLUSTER_COMMAND_SERVER,
783  ID: cmd_id,
784  ATTR_NAME: cmd.name,
785  "schema": voluptuous_serialize.convert(
787  custom_serializer=cv.custom_serializer,
788  ),
789  }
790  )
791  _LOGGER.debug(
792  "Requested commands for: %s: %s, %s: '%s', %s: %s, %s: %s",
793  ATTR_CLUSTER_ID,
794  cluster_id,
795  ATTR_CLUSTER_TYPE,
796  cluster_type,
797  ATTR_ENDPOINT_ID,
798  endpoint_id,
799  RESPONSE,
800  cluster_commands,
801  )
802 
803  connection.send_result(msg[ID], cluster_commands)
804 
805 
806 @websocket_api.require_admin
807 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes/value",
808  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
809  vol.Required(ATTR_ENDPOINT_ID): int,
810  vol.Required(ATTR_CLUSTER_ID): int,
811  vol.Required(ATTR_CLUSTER_TYPE): str,
812  vol.Required(ATTR_ATTRIBUTE): int,
813  vol.Optional(ATTR_MANUFACTURER): cv.positive_int,
814  }
815 )
816 @websocket_api.async_response
818  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
819 ) -> None:
820  """Read zigbee attribute for cluster on ZHA entity."""
821  zha_gateway = get_zha_gateway(hass)
822  ieee: EUI64 = msg[ATTR_IEEE]
823  endpoint_id: int = msg[ATTR_ENDPOINT_ID]
824  cluster_id: int = msg[ATTR_CLUSTER_ID]
825  cluster_type: str = msg[ATTR_CLUSTER_TYPE]
826  attribute: int = msg[ATTR_ATTRIBUTE]
827  manufacturer: int | None = msg.get(ATTR_MANUFACTURER)
828  zha_device = zha_gateway.get_device(ieee)
829  success = {}
830  failure = {}
831  if zha_device is not None:
832  cluster = zha_device.async_get_cluster(
833  endpoint_id, cluster_id, cluster_type=cluster_type
834  )
835  success, failure = await cluster.read_attributes(
836  [attribute], allow_cache=False, only_cache=False, manufacturer=manufacturer
837  )
838  _LOGGER.debug(
839  (
840  "Read attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s]"
841  " %s: [%s],"
842  ),
843  ATTR_CLUSTER_ID,
844  cluster_id,
845  ATTR_CLUSTER_TYPE,
846  cluster_type,
847  ATTR_ENDPOINT_ID,
848  endpoint_id,
849  ATTR_ATTRIBUTE,
850  attribute,
851  ATTR_MANUFACTURER,
852  manufacturer,
853  RESPONSE,
854  str(success.get(attribute)),
855  "failure",
856  failure,
857  )
858  connection.send_result(msg[ID], str(success.get(attribute)))
859 
860 
861 @websocket_api.require_admin
862 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bindable",
863  vol.Required(ATTR_IEEE): IEEE_SCHEMA,
864  }
865 )
866 @websocket_api.async_response
868  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
869 ) -> None:
870  """Directly bind devices."""
871  zha_gateway_proxy = get_zha_gateway_proxy(hass)
872  source_ieee: EUI64 = msg[ATTR_IEEE]
873  source_device = zha_gateway_proxy.device_proxies.get(source_ieee)
874  assert source_device is not None
875 
876  devices = [
877  device.zha_device_info
878  for device in zha_gateway_proxy.device_proxies.values()
879  if async_is_bindable_target(source_device.device, device.device)
880  ]
881 
882  _LOGGER.debug(
883  "Get bindable devices: %s: [%s], %s: [%s]",
884  ATTR_SOURCE_IEEE,
885  source_ieee,
886  "bindable devices",
887  devices,
888  )
889 
890  connection.send_message(websocket_api.result_message(msg[ID], devices))
891 
892 
893 @websocket_api.require_admin
894 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bind",
895  vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA,
896  vol.Required(ATTR_TARGET_IEEE): IEEE_SCHEMA,
897  }
898 )
899 @websocket_api.async_response
900 async def websocket_bind_devices(
901  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
902 ) -> None:
903  """Directly bind devices."""
904  zha_gateway = get_zha_gateway(hass)
905  source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE]
906  target_ieee: EUI64 = msg[ATTR_TARGET_IEEE]
908  zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Bind_req
909  )
910  _LOGGER.info(
911  "Devices bound: %s: [%s] %s: [%s]",
912  ATTR_SOURCE_IEEE,
913  source_ieee,
914  ATTR_TARGET_IEEE,
915  target_ieee,
916  )
917  connection.send_result(msg[ID])
918 
919 
920 @websocket_api.require_admin
921 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/unbind",
922  vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA,
923  vol.Required(ATTR_TARGET_IEEE): IEEE_SCHEMA,
924  }
925 )
926 @websocket_api.async_response
927 async def websocket_unbind_devices(
928  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
929 ) -> None:
930  """Remove a direct binding between devices."""
931  zha_gateway = get_zha_gateway(hass)
932  source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE]
933  target_ieee: EUI64 = msg[ATTR_TARGET_IEEE]
935  zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Unbind_req
936  )
937  _LOGGER.info(
938  "Devices un-bound: %s: [%s] %s: [%s]",
939  ATTR_SOURCE_IEEE,
940  source_ieee,
941  ATTR_TARGET_IEEE,
942  target_ieee,
943  )
944  connection.send_result(msg[ID])
945 
946 
947 @websocket_api.require_admin
948 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/groups/bind",
949  vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA,
950  vol.Required(GROUP_ID): cv.positive_int,
951  vol.Required(BINDINGS): vol.All(cv.ensure_list, [CLUSTER_BINDING_SCHEMA]),
952  }
953 )
954 @websocket_api.async_response
955 async def websocket_bind_group(
956  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
957 ) -> None:
958  """Directly bind a device to a group."""
959  zha_gateway = get_zha_gateway(hass)
960  source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE]
961  group_id: int = msg[GROUP_ID]
962  bindings: list[ClusterBinding] = msg[BINDINGS]
963  source_device = zha_gateway.get_device(source_ieee)
964  assert source_device
965  await source_device.async_bind_to_group(group_id, bindings)
966  connection.send_result(msg[ID])
967 
968 
969 @websocket_api.require_admin
970 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/groups/unbind",
971  vol.Required(ATTR_SOURCE_IEEE): IEEE_SCHEMA,
972  vol.Required(GROUP_ID): cv.positive_int,
973  vol.Required(BINDINGS): vol.All(cv.ensure_list, [CLUSTER_BINDING_SCHEMA]),
974  }
975 )
976 @websocket_api.async_response
977 async def websocket_unbind_group(
978  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
979 ) -> None:
980  """Unbind a device from a group."""
981  zha_gateway = get_zha_gateway(hass)
982  source_ieee: EUI64 = msg[ATTR_SOURCE_IEEE]
983  group_id: int = msg[GROUP_ID]
984  bindings: list[ClusterBinding] = msg[BINDINGS]
985  source_device = zha_gateway.get_device(source_ieee)
986  assert source_device
987  await source_device.async_unbind_from_group(group_id, bindings)
988  connection.send_result(msg[ID])
989 
990 
991 async def async_binding_operation(
992  zha_gateway: Gateway,
993  source_ieee: EUI64,
994  target_ieee: EUI64,
995  operation: zdo_types.ZDOCmd,
996 ) -> None:
997  """Create or remove a direct zigbee binding between 2 devices."""
998 
999  source_device = zha_gateway.get_device(source_ieee)
1000  target_device = zha_gateway.get_device(target_ieee)
1001 
1002  assert source_device
1003  assert target_device
1004  clusters_to_bind = await get_matched_clusters(source_device, target_device)
1005 
1006  zdo = source_device.device.zdo
1007  bind_tasks = []
1008  for binding_pair in clusters_to_bind:
1009  op_msg = "cluster: %s %s --> [%s]"
1010  op_params = (
1011  binding_pair.source_cluster.cluster_id,
1012  operation.name,
1013  target_ieee,
1014  )
1015  zdo.debug(f"processing {op_msg}", *op_params)
1016 
1017  bind_tasks.append(
1018  (
1019  zdo.request(
1020  operation,
1021  source_device.ieee,
1022  binding_pair.source_cluster.endpoint.endpoint_id,
1023  binding_pair.source_cluster.cluster_id,
1024  binding_pair.destination_address,
1025  ),
1026  op_msg,
1027  op_params,
1028  )
1029  )
1030  res = await asyncio.gather(*(t[0] for t in bind_tasks), return_exceptions=True)
1031  for outcome, log_msg in zip(res, bind_tasks, strict=False):
1032  if isinstance(outcome, Exception):
1033  fmt = f"{log_msg[1]} failed: %s"
1034  else:
1035  fmt = f"{log_msg[1]} completed: %s"
1036  zdo.debug(fmt, *(log_msg[2] + (outcome,)))
1037 
1038 
1039 @websocket_api.require_admin
1040 @websocket_api.websocket_command({vol.Required(TYPE): "zha/configuration"})
1041 @websocket_api.async_response
1042 async def websocket_get_configuration(
1043  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
1044 ) -> None:
1045  """Get ZHA configuration."""
1046  config_entry: ConfigEntry = get_config_entry(hass)
1047  import voluptuous_serialize # pylint: disable=import-outside-toplevel
1048 
1049  def custom_serializer(schema: Any) -> Any:
1050  """Serialize additional types for voluptuous_serialize."""
1051  if schema is cv_boolean:
1052  return {"type": "bool"}
1053  if schema is vol.Schema:
1054  return voluptuous_serialize.convert(
1055  schema, custom_serializer=custom_serializer
1056  )
1057 
1058  return cv.custom_serializer(schema)
1059 
1060  data: dict[str, dict[str, Any]] = {"schemas": {}, "data": {}}
1061  for section, schema in ZHA_CONFIG_SCHEMAS.items():
1062  if section == ZHA_ALARM_OPTIONS and not async_cluster_exists(
1063  hass, IasAce.cluster_id
1064  ):
1065  continue
1066  data["schemas"][section] = voluptuous_serialize.convert(
1067  schema, custom_serializer=custom_serializer
1068  )
1069  data["data"][section] = config_entry.options.get(CUSTOM_CONFIGURATION, {}).get(
1070  section, {}
1071  )
1072 
1073  # send default values for unconfigured options
1074  for entry in data["schemas"][section]:
1075  if data["data"][section].get(entry["name"]) is None:
1076  data["data"][section][entry["name"]] = entry["default"]
1077 
1078  connection.send_result(msg[ID], data)
1079 
1080 
1081 @websocket_api.require_admin
1082 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/configuration/update",
1083  vol.Required("data"): ZHA_CONFIG_SCHEMAS,
1084  }
1085 )
1086 @websocket_api.async_response
1088  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
1089 ) -> None:
1090  """Update the ZHA configuration."""
1091  config_entry: ConfigEntry = get_config_entry(hass)
1092  options = config_entry.options
1093  data_to_save = {**options, CUSTOM_CONFIGURATION: msg["data"]}
1094 
1095  for section, schema in ZHA_CONFIG_SCHEMAS.items():
1096  for entry in schema.schema:
1097  # remove options that match defaults
1098  if (
1099  data_to_save[CUSTOM_CONFIGURATION].get(section, {}).get(entry)
1100  == entry.default()
1101  ):
1102  data_to_save[CUSTOM_CONFIGURATION][section].pop(entry)
1103  # remove entire section block if empty
1104  if (
1105  not data_to_save[CUSTOM_CONFIGURATION].get(section)
1106  and section in data_to_save[CUSTOM_CONFIGURATION]
1107  ):
1108  data_to_save[CUSTOM_CONFIGURATION].pop(section)
1109 
1110  # remove entire custom_configuration block if empty
1111  if (
1112  not data_to_save.get(CUSTOM_CONFIGURATION)
1113  and CUSTOM_CONFIGURATION in data_to_save
1114  ):
1115  data_to_save.pop(CUSTOM_CONFIGURATION)
1116 
1117  _LOGGER.info(
1118  "Updating ZHA custom configuration options from %s to %s",
1119  options,
1120  data_to_save,
1121  )
1122 
1123  hass.config_entries.async_update_entry(config_entry, options=data_to_save)
1124  status = await hass.config_entries.async_reload(config_entry.entry_id)
1125  connection.send_result(msg[ID], status)
1126 
1127 
1128 @websocket_api.require_admin
1129 @websocket_api.websocket_command({vol.Required(TYPE): "zha/network/settings"})
1130 @websocket_api.async_response
1132  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
1133 ) -> None:
1134  """Get ZHA network settings."""
1135  backup = async_get_active_network_settings(hass)
1136  zha_gateway = get_zha_gateway(hass)
1137  config_entry: ConfigEntry = get_config_entry(hass)
1138  connection.send_result(
1139  msg[ID],
1140  {
1141  "radio_type": async_get_radio_type(hass, config_entry).name,
1142  "device": zha_gateway.application_controller.config[CONF_DEVICE],
1143  "settings": backup.as_dict(),
1144  },
1145  )
1146 
1147 
1148 @websocket_api.require_admin
1149 @websocket_api.websocket_command({vol.Required(TYPE): "zha/network/backups/list"})
1150 @websocket_api.async_response
1152  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
1153 ) -> None:
1154  """Get ZHA network settings."""
1155  zha_gateway = get_zha_gateway(hass)
1156  application_controller = zha_gateway.application_controller
1157 
1158  # Serialize known backups
1159  connection.send_result(
1160  msg[ID], [backup.as_dict() for backup in application_controller.backups]
1161  )
1162 
1163 
1164 @websocket_api.require_admin
1165 @websocket_api.websocket_command({vol.Required(TYPE): "zha/network/backups/create"})
1166 @websocket_api.async_response
1168  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
1169 ) -> None:
1170  """Create a ZHA network backup."""
1171  zha_gateway = get_zha_gateway(hass)
1172  application_controller = zha_gateway.application_controller
1173 
1174  # This can take 5-30s
1175  backup = await application_controller.backups.create_backup(load_devices=True)
1176  connection.send_result(
1177  msg[ID],
1178  {
1179  "backup": backup.as_dict(),
1180  "is_complete": backup.is_complete(),
1181  },
1182  )
1183 
1184 
1185 @websocket_api.require_admin
1186 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/network/backups/restore",
1187  vol.Required("backup"): _cv_zigpy_network_backup,
1188  vol.Optional("ezsp_force_write_eui64", default=False): cv.boolean,
1189  }
1190 )
1191 @websocket_api.async_response
1193  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
1194 ) -> None:
1195  """Restore a ZHA network backup."""
1196  zha_gateway = get_zha_gateway(hass)
1197  application_controller = zha_gateway.application_controller
1198  backup = msg["backup"]
1199 
1200  if msg["ezsp_force_write_eui64"]:
1201  backup.network_info.stack_specific.setdefault("ezsp", {})[
1202  EZSP_OVERWRITE_EUI64
1203  ] = True
1204 
1205  # This can take 30-40s
1206  try:
1207  await application_controller.backups.restore_backup(backup)
1208  except ValueError as err:
1209  connection.send_error(msg[ID], websocket_api.ERR_INVALID_FORMAT, str(err))
1210  else:
1211  connection.send_result(msg[ID])
1212 
1213 
1214 @websocket_api.require_admin
1215 @websocket_api.websocket_command( { vol.Required(TYPE): "zha/network/change_channel",
1216  vol.Required(ATTR_NEW_CHANNEL): vol.Any("auto", vol.Range(11, 26)),
1217  }
1218 )
1219 @websocket_api.async_response
1220 async def websocket_change_channel(
1221  hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
1222 ) -> None:
1223  """Migrate the Zigbee network to a new channel."""
1224  new_channel = cast(Literal["auto"] | int, msg[ATTR_NEW_CHANNEL])
1225  await async_change_channel(hass, new_channel=new_channel)
1226  connection.send_result(msg[ID])
1227 
1228 
1229 @callback
1230 def async_load_api(hass: HomeAssistant) -> None:
1231  """Set up the web socket API."""
1232  zha_gateway = get_zha_gateway(hass)
1233  application_controller = zha_gateway.application_controller
1234 
1235  async def permit(service: ServiceCall) -> None:
1236  """Allow devices to join this network."""
1237  duration: int = service.data[ATTR_DURATION]
1238  ieee: EUI64 | None = service.data.get(ATTR_IEEE)
1239  src_ieee: EUI64
1240  link_key: KeyData
1241  if ATTR_SOURCE_IEEE in service.data:
1242  src_ieee = service.data[ATTR_SOURCE_IEEE]
1243  link_key = service.data[ATTR_INSTALL_CODE]
1244  _LOGGER.info("Allowing join for %s device with link key", src_ieee)
1245  await application_controller.permit_with_link_key(
1246  time_s=duration, node=src_ieee, link_key=link_key
1247  )
1248  return
1249 
1250  if ATTR_QR_CODE in service.data:
1251  src_ieee, link_key = service.data[ATTR_QR_CODE]
1252  _LOGGER.info("Allowing join for %s device with link key", src_ieee)
1253  await application_controller.permit_with_link_key(
1254  time_s=duration, node=src_ieee, link_key=link_key
1255  )
1256  return
1257 
1258  if ieee:
1259  _LOGGER.info("Permitting joins for %ss on %s device", duration, ieee)
1260  else:
1261  _LOGGER.info("Permitting joins for %ss", duration)
1262  await application_controller.permit(time_s=duration, node=ieee)
1263 
1265  hass, DOMAIN, SERVICE_PERMIT, permit, schema=SERVICE_SCHEMAS[SERVICE_PERMIT]
1266  )
1267 
1268  async def remove(service: ServiceCall) -> None:
1269  """Remove a node from the network."""
1270  zha_gateway = get_zha_gateway(hass)
1271  ieee: EUI64 = service.data[ATTR_IEEE]
1272  _LOGGER.info("Removing node %s", ieee)
1273  await zha_gateway.async_remove_device(ieee)
1274 
1276  hass, DOMAIN, SERVICE_REMOVE, remove, schema=SERVICE_SCHEMAS[IEEE_SERVICE]
1277  )
1278 
1279  async def set_zigbee_cluster_attributes(service: ServiceCall) -> None:
1280  """Set zigbee attribute for cluster on zha entity."""
1281  ieee: EUI64 = service.data[ATTR_IEEE]
1282  endpoint_id: int = service.data[ATTR_ENDPOINT_ID]
1283  cluster_id: int = service.data[ATTR_CLUSTER_ID]
1284  cluster_type: str = service.data[ATTR_CLUSTER_TYPE]
1285  attribute: int | str = service.data[ATTR_ATTRIBUTE]
1286  value: int | bool | str = service.data[ATTR_VALUE]
1287  manufacturer: int | None = service.data.get(ATTR_MANUFACTURER)
1288  zha_device = zha_gateway.get_device(ieee)
1289  response = None
1290  if zha_device is not None:
1291  response = await zha_device.write_zigbee_attribute(
1292  endpoint_id,
1293  cluster_id,
1294  attribute,
1295  value,
1296  cluster_type=cluster_type,
1297  manufacturer=manufacturer,
1298  )
1299  else:
1300  raise ValueError(f"Device with IEEE {ieee!s} not found")
1301 
1302  _LOGGER.debug(
1303  (
1304  "Set attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s:"
1305  " [%s] %s: [%s]"
1306  ),
1307  ATTR_CLUSTER_ID,
1308  cluster_id,
1309  ATTR_CLUSTER_TYPE,
1310  cluster_type,
1311  ATTR_ENDPOINT_ID,
1312  endpoint_id,
1313  ATTR_ATTRIBUTE,
1314  attribute,
1315  ATTR_VALUE,
1316  value,
1317  ATTR_MANUFACTURER,
1318  manufacturer,
1319  RESPONSE,
1320  response,
1321  )
1322 
1324  hass,
1325  DOMAIN,
1326  SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE,
1327  set_zigbee_cluster_attributes,
1328  schema=SERVICE_SCHEMAS[SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE],
1329  )
1330 
1331  async def issue_zigbee_cluster_command(service: ServiceCall) -> None:
1332  """Issue command on zigbee cluster on ZHA entity."""
1333  ieee: EUI64 = service.data[ATTR_IEEE]
1334  endpoint_id: int = service.data[ATTR_ENDPOINT_ID]
1335  cluster_id: int = service.data[ATTR_CLUSTER_ID]
1336  cluster_type: str = service.data[ATTR_CLUSTER_TYPE]
1337  command: int = service.data[ATTR_COMMAND]
1338  command_type: str = service.data[ATTR_COMMAND_TYPE]
1339  args: list | None = service.data.get(ATTR_ARGS)
1340  params: dict | None = service.data.get(ATTR_PARAMS)
1341  manufacturer: int | None = service.data.get(ATTR_MANUFACTURER)
1342  zha_device = zha_gateway.get_device(ieee)
1343  if zha_device is not None:
1344  if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None:
1345  manufacturer = zha_device.manufacturer_code
1346 
1347  await zha_device.issue_cluster_command(
1348  endpoint_id,
1349  cluster_id,
1350  command,
1351  command_type,
1352  args,
1353  params,
1354  cluster_type=cluster_type,
1355  manufacturer=manufacturer,
1356  )
1357  _LOGGER.debug(
1358  (
1359  "Issued command for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s]"
1360  " %s: [%s] %s: [%s] %s: [%s]"
1361  ),
1362  ATTR_CLUSTER_ID,
1363  cluster_id,
1364  ATTR_CLUSTER_TYPE,
1365  cluster_type,
1366  ATTR_ENDPOINT_ID,
1367  endpoint_id,
1368  ATTR_COMMAND,
1369  command,
1370  ATTR_COMMAND_TYPE,
1371  command_type,
1372  ATTR_ARGS,
1373  args,
1374  ATTR_PARAMS,
1375  params,
1376  ATTR_MANUFACTURER,
1377  manufacturer,
1378  )
1379  else:
1380  raise ValueError(f"Device with IEEE {ieee!s} not found")
1381 
1383  hass,
1384  DOMAIN,
1385  SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND,
1386  issue_zigbee_cluster_command,
1387  schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND],
1388  )
1389 
1390  async def issue_zigbee_group_command(service: ServiceCall) -> None:
1391  """Issue command on zigbee cluster on a zigbee group."""
1392  group_id: int = service.data[ATTR_GROUP]
1393  cluster_id: int = service.data[ATTR_CLUSTER_ID]
1394  command: int = service.data[ATTR_COMMAND]
1395  args: list = service.data[ATTR_ARGS]
1396  manufacturer: int | None = service.data.get(ATTR_MANUFACTURER)
1397  group = zha_gateway.get_group(group_id)
1398  if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None:
1399  _LOGGER.error("Missing manufacturer attribute for cluster: %d", cluster_id)
1400  response = None
1401  if group is not None:
1402  cluster = group.endpoint[cluster_id]
1403  response = await cluster.command(
1404  command, *args, manufacturer=manufacturer, expect_reply=True
1405  )
1406  _LOGGER.debug(
1407  "Issued group command for: %s: [%s] %s: [%s] %s: %s %s: [%s] %s: %s",
1408  ATTR_CLUSTER_ID,
1409  cluster_id,
1410  ATTR_COMMAND,
1411  command,
1412  ATTR_ARGS,
1413  args,
1414  ATTR_MANUFACTURER,
1415  manufacturer,
1416  RESPONSE,
1417  response,
1418  )
1419 
1421  hass,
1422  DOMAIN,
1423  SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND,
1424  issue_zigbee_group_command,
1425  schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND],
1426  )
1427 
1428  def _get_ias_wd_cluster_handler(zha_device):
1429  """Get the IASWD cluster handler for a device."""
1430  cluster_handlers = {
1431  ch.name: ch
1432  for endpoint in zha_device.endpoints.values()
1433  for ch in endpoint.claimed_cluster_handlers.values()
1434  }
1435  return cluster_handlers.get(CLUSTER_HANDLER_IAS_WD)
1436 
1437  async def warning_device_squawk(service: ServiceCall) -> None:
1438  """Issue the squawk command for an IAS warning device."""
1439  ieee: EUI64 = service.data[ATTR_IEEE]
1440  mode: int = service.data[ATTR_WARNING_DEVICE_MODE]
1441  strobe: int = service.data[ATTR_WARNING_DEVICE_STROBE]
1442  level: int = service.data[ATTR_LEVEL]
1443 
1444  if (zha_device := zha_gateway.get_device(ieee)) is not None:
1445  if cluster_handler := _get_ias_wd_cluster_handler(zha_device):
1446  await cluster_handler.issue_squawk(mode, strobe, level)
1447  else:
1448  _LOGGER.error(
1449  "Squawking IASWD: %s: [%s] is missing the required IASWD cluster handler!",
1450  ATTR_IEEE,
1451  str(ieee),
1452  )
1453  else:
1454  _LOGGER.error(
1455  "Squawking IASWD: %s: [%s] could not be found!", ATTR_IEEE, str(ieee)
1456  )
1457  _LOGGER.debug(
1458  "Squawking IASWD: %s: [%s] %s: [%s] %s: [%s] %s: [%s]",
1459  ATTR_IEEE,
1460  str(ieee),
1461  ATTR_WARNING_DEVICE_MODE,
1462  mode,
1463  ATTR_WARNING_DEVICE_STROBE,
1464  strobe,
1465  ATTR_LEVEL,
1466  level,
1467  )
1468 
1470  hass,
1471  DOMAIN,
1472  SERVICE_WARNING_DEVICE_SQUAWK,
1473  warning_device_squawk,
1474  schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_SQUAWK],
1475  )
1476 
1477  async def warning_device_warn(service: ServiceCall) -> None:
1478  """Issue the warning command for an IAS warning device."""
1479  ieee: EUI64 = service.data[ATTR_IEEE]
1480  mode: int = service.data[ATTR_WARNING_DEVICE_MODE]
1481  strobe: int = service.data[ATTR_WARNING_DEVICE_STROBE]
1482  level: int = service.data[ATTR_LEVEL]
1483  duration: int = service.data[ATTR_WARNING_DEVICE_DURATION]
1484  duty_mode: int = service.data[ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE]
1485  intensity: int = service.data[ATTR_WARNING_DEVICE_STROBE_INTENSITY]
1486 
1487  if (zha_device := zha_gateway.get_device(ieee)) is not None:
1488  if cluster_handler := _get_ias_wd_cluster_handler(zha_device):
1489  await cluster_handler.issue_start_warning(
1490  mode, strobe, level, duration, duty_mode, intensity
1491  )
1492  else:
1493  _LOGGER.error(
1494  "Warning IASWD: %s: [%s] is missing the required IASWD cluster handler!",
1495  ATTR_IEEE,
1496  str(ieee),
1497  )
1498  else:
1499  _LOGGER.error(
1500  "Warning IASWD: %s: [%s] could not be found!", ATTR_IEEE, str(ieee)
1501  )
1502  _LOGGER.debug(
1503  "Warning IASWD: %s: [%s] %s: [%s] %s: [%s] %s: [%s]",
1504  ATTR_IEEE,
1505  str(ieee),
1506  ATTR_WARNING_DEVICE_MODE,
1507  mode,
1508  ATTR_WARNING_DEVICE_STROBE,
1509  strobe,
1510  ATTR_LEVEL,
1511  level,
1512  )
1513 
1515  hass,
1516  DOMAIN,
1517  SERVICE_WARNING_DEVICE_WARN,
1518  warning_device_warn,
1519  schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_WARN],
1520  )
1521 
1522  websocket_api.async_register_command(hass, websocket_permit_devices)
1523  websocket_api.async_register_command(hass, websocket_get_devices)
1524  websocket_api.async_register_command(hass, websocket_get_groupable_devices)
1525  websocket_api.async_register_command(hass, websocket_get_groups)
1526  websocket_api.async_register_command(hass, websocket_get_device)
1527  websocket_api.async_register_command(hass, websocket_get_group)
1528  websocket_api.async_register_command(hass, websocket_add_group)
1529  websocket_api.async_register_command(hass, websocket_remove_groups)
1530  websocket_api.async_register_command(hass, websocket_add_group_members)
1531  websocket_api.async_register_command(hass, websocket_remove_group_members)
1532  websocket_api.async_register_command(hass, websocket_bind_group)
1533  websocket_api.async_register_command(hass, websocket_unbind_group)
1534  websocket_api.async_register_command(hass, websocket_reconfigure_node)
1535  websocket_api.async_register_command(hass, websocket_device_clusters)
1536  websocket_api.async_register_command(hass, websocket_device_cluster_attributes)
1537  websocket_api.async_register_command(hass, websocket_device_cluster_commands)
1538  websocket_api.async_register_command(hass, websocket_read_zigbee_cluster_attributes)
1539  websocket_api.async_register_command(hass, websocket_get_bindable_devices)
1540  websocket_api.async_register_command(hass, websocket_bind_devices)
1541  websocket_api.async_register_command(hass, websocket_unbind_devices)
1542  websocket_api.async_register_command(hass, websocket_update_topology)
1543  websocket_api.async_register_command(hass, websocket_get_configuration)
1544  websocket_api.async_register_command(hass, websocket_update_zha_configuration)
1545  websocket_api.async_register_command(hass, websocket_get_network_settings)
1546  websocket_api.async_register_command(hass, websocket_list_network_backups)
1547  websocket_api.async_register_command(hass, websocket_create_network_backup)
1548  websocket_api.async_register_command(hass, websocket_restore_network_backup)
1549  websocket_api.async_register_command(hass, websocket_change_channel)
1550 
1551 
1552 @callback
1553 def async_unload_api(hass: HomeAssistant) -> None:
1554  """Unload the ZHA API."""
1555  hass.services.async_remove(DOMAIN, SERVICE_PERMIT)
1556  hass.services.async_remove(DOMAIN, SERVICE_REMOVE)
1557  hass.services.async_remove(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE)
1558  hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND)
1559  hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND)
1560  hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_SQUAWK)
1561  hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_WARN)
1562 
bool remove(self, _T matcher)
Definition: match.py:214
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
HabiticaConfigEntry get_config_entry(HomeAssistant hass, str entry_id)
Definition: services.py:92
None async_change_channel(HomeAssistant hass, OTBRData data, int channel, float delay)
NetworkBackup async_get_active_network_settings(HomeAssistant hass)
Definition: api.py:40
RadioType async_get_radio_type(HomeAssistant hass, ConfigEntry|None config_entry=None)
Definition: api.py:81
Gateway get_zha_gateway(HomeAssistant hass)
Definition: helpers.py:1028
vol.Schema cluster_command_schema_to_vol_schema(CommandSchema schema)
Definition: helpers.py:1070
ZHAGatewayProxy get_zha_gateway_proxy(HomeAssistant hass)
Definition: helpers.py:1036
def async_cluster_exists(HomeAssistant hass, cluster_id, skip_coordinator=True)
Definition: helpers.py:1138
None websocket_reconfigure_node(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_update_topology(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_get_devices(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_get_device(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_unbind_devices(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_remove_groups(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
str|None _get_entity_name(Gateway zha_gateway, EntityReference entity_ref)
GroupMemberReference _cv_group_member(dict[str, Any] value)
None websocket_bind_group(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_change_channel(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_get_network_settings(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_get_group(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
zigpy.backups.NetworkBackup _cv_zigpy_network_backup(dict[str, Any] value)
None websocket_device_clusters(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_unbind_group(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
str|None _get_entity_original_name(Gateway zha_gateway, EntityReference entity_ref)
None websocket_get_groups(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_get_bindable_devices(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_restore_network_backup(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
ClusterBinding _cv_cluster_binding(dict[str, Any] value)
None websocket_bind_devices(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_permit_devices(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_device_cluster_attributes(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_remove_group_members(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_create_network_backup(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_read_zigbee_cluster_attributes(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_list_network_backups(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_update_zha_configuration(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_get_configuration(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_add_group(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_get_groupable_devices(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None websocket_add_group_members(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None async_binding_operation(Gateway zha_gateway, EUI64 source_ieee, EUI64 target_ieee, zdo_types.ZDOCmd operation)
None websocket_device_cluster_commands(HomeAssistant hass, ActiveConnection connection, dict[str, Any] msg)
None async_cleanup(HomeAssistant hass, DeviceRegistry dev_reg, entity_registry.EntityRegistry ent_reg)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))
Definition: service.py:1121