Home Assistant Unofficial Reference 2024.12.1
helpers.py
Go to the documentation of this file.
1 """Helper functions for the ZHA integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import collections
7 from collections.abc import Awaitable, Callable, Coroutine, Mapping
8 import copy
9 import dataclasses
10 import enum
11 import functools
12 import itertools
13 import logging
14 import re
15 import time
16 from types import MappingProxyType
17 from typing import TYPE_CHECKING, Any, Concatenate, NamedTuple, cast
18 from zoneinfo import ZoneInfo
19 
20 import voluptuous as vol
21 from zha.application.const import (
22  ATTR_CLUSTER_ID,
23  ATTR_DEVICE_IEEE,
24  ATTR_TYPE,
25  ATTR_UNIQUE_ID,
26  CLUSTER_TYPE_IN,
27  CLUSTER_TYPE_OUT,
28  CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY,
29  CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS,
30  UNKNOWN_MANUFACTURER,
31  UNKNOWN_MODEL,
32  ZHA_CLUSTER_HANDLER_CFG_DONE,
33  ZHA_CLUSTER_HANDLER_MSG,
34  ZHA_CLUSTER_HANDLER_MSG_BIND,
35  ZHA_CLUSTER_HANDLER_MSG_CFG_RPT,
36  ZHA_CLUSTER_HANDLER_MSG_DATA,
37  ZHA_EVENT,
38  ZHA_GW_MSG,
39  ZHA_GW_MSG_DEVICE_FULL_INIT,
40  ZHA_GW_MSG_DEVICE_INFO,
41  ZHA_GW_MSG_DEVICE_JOINED,
42  ZHA_GW_MSG_DEVICE_REMOVED,
43  ZHA_GW_MSG_GROUP_ADDED,
44  ZHA_GW_MSG_GROUP_INFO,
45  ZHA_GW_MSG_GROUP_MEMBER_ADDED,
46  ZHA_GW_MSG_GROUP_MEMBER_REMOVED,
47  ZHA_GW_MSG_GROUP_REMOVED,
48  ZHA_GW_MSG_RAW_INIT,
49  RadioType,
50 )
51 from zha.application.gateway import (
52  ConnectionLostEvent,
53  DeviceFullInitEvent,
54  DeviceJoinedEvent,
55  DeviceLeftEvent,
56  DeviceRemovedEvent,
57  Gateway,
58  GroupEvent,
59  RawDeviceInitializedEvent,
60 )
61 from zha.application.helpers import (
62  AlarmControlPanelOptions,
63  CoordinatorConfiguration,
64  DeviceOptions,
65  DeviceOverridesConfiguration,
66  LightOptions,
67  QuirksConfiguration,
68  ZHAConfiguration,
69  ZHAData,
70 )
71 from zha.application.platforms import GroupEntity, PlatformEntity
72 from zha.event import EventBase
73 from zha.exceptions import ZHAException
74 from zha.mixins import LogMixin
75 from zha.zigbee.cluster_handlers import ClusterBindEvent, ClusterConfigureReportingEvent
76 from zha.zigbee.device import ClusterHandlerConfigurationComplete, Device, ZHAEvent
77 from zha.zigbee.group import Group, GroupInfo, GroupMember
78 from zigpy.config import (
79  CONF_DATABASE,
80  CONF_DEVICE,
81  CONF_DEVICE_PATH,
82  CONF_NWK,
83  CONF_NWK_CHANNEL,
84 )
85 import zigpy.exceptions
86 from zigpy.profiles import PROFILES
87 import zigpy.types
88 from zigpy.types import EUI64
89 import zigpy.util
90 import zigpy.zcl
91 from zigpy.zcl.foundation import CommandSchema
92 
93 from homeassistant import __path__ as HOMEASSISTANT_PATH
95  is_multiprotocol_url,
96 )
97 from homeassistant.components.system_log import LogEntry
98 from homeassistant.config_entries import ConfigEntry
99 from homeassistant.const import (
100  ATTR_AREA_ID,
101  ATTR_DEVICE_ID,
102  ATTR_ENTITY_ID,
103  ATTR_MODEL,
104  ATTR_NAME,
105  Platform,
106 )
107 from homeassistant.core import Event, HomeAssistant, callback
108 from homeassistant.exceptions import HomeAssistantError
109 from homeassistant.helpers import (
110  config_validation as cv,
111  device_registry as dr,
112  entity_registry as er,
113 )
114 from homeassistant.helpers.dispatcher import async_dispatcher_send
115 from homeassistant.helpers.entity_platform import AddEntitiesCallback
116 from homeassistant.helpers.typing import ConfigType
117 
118 from .const import (
119  ATTR_ACTIVE_COORDINATOR,
120  ATTR_ATTRIBUTES,
121  ATTR_AVAILABLE,
122  ATTR_CLUSTER_NAME,
123  ATTR_DEVICE_TYPE,
124  ATTR_ENDPOINT_NAMES,
125  ATTR_IEEE,
126  ATTR_LAST_SEEN,
127  ATTR_LQI,
128  ATTR_MANUFACTURER,
129  ATTR_MANUFACTURER_CODE,
130  ATTR_NEIGHBORS,
131  ATTR_NWK,
132  ATTR_POWER_SOURCE,
133  ATTR_QUIRK_APPLIED,
134  ATTR_QUIRK_CLASS,
135  ATTR_QUIRK_ID,
136  ATTR_ROUTES,
137  ATTR_RSSI,
138  ATTR_SIGNATURE,
139  ATTR_SUCCESS,
140  CONF_ALARM_ARM_REQUIRES_CODE,
141  CONF_ALARM_FAILED_TRIES,
142  CONF_ALARM_MASTER_CODE,
143  CONF_BAUDRATE,
144  CONF_CONSIDER_UNAVAILABLE_BATTERY,
145  CONF_CONSIDER_UNAVAILABLE_MAINS,
146  CONF_CUSTOM_QUIRKS_PATH,
147  CONF_DEFAULT_LIGHT_TRANSITION,
148  CONF_DEVICE_CONFIG,
149  CONF_ENABLE_ENHANCED_LIGHT_TRANSITION,
150  CONF_ENABLE_IDENTIFY_ON_JOIN,
151  CONF_ENABLE_LIGHT_TRANSITIONING_FLAG,
152  CONF_ENABLE_MAINS_STARTUP_POLLING,
153  CONF_ENABLE_QUIRKS,
154  CONF_FLOW_CONTROL,
155  CONF_GROUP_MEMBERS_ASSUME_STATE,
156  CONF_RADIO_TYPE,
157  CONF_ZIGPY,
158  CUSTOM_CONFIGURATION,
159  DATA_ZHA,
160  DEFAULT_DATABASE_NAME,
161  DEVICE_PAIRING_STATUS,
162  DOMAIN,
163  ZHA_ALARM_OPTIONS,
164  ZHA_OPTIONS,
165 )
166 
167 if TYPE_CHECKING:
168  from logging import Filter, LogRecord
169 
170  from .entity import ZHAEntity
171  from .update import ZHAFirmwareUpdateCoordinator
172 
173  type _LogFilterType = Filter | Callable[[LogRecord], bool]
174 
175 _LOGGER = logging.getLogger(__name__)
176 
177 DEBUG_COMP_BELLOWS = "bellows"
178 DEBUG_COMP_ZHA = "homeassistant.components.zha"
179 DEBUG_LIB_ZHA = "zha"
180 DEBUG_COMP_ZIGPY = "zigpy"
181 DEBUG_COMP_ZIGPY_ZNP = "zigpy_znp"
182 DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz"
183 DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee"
184 DEBUG_COMP_ZIGPY_ZIGATE = "zigpy_zigate"
185 DEBUG_LEVEL_CURRENT = "current"
186 DEBUG_LEVEL_ORIGINAL = "original"
187 DEBUG_LEVELS = {
188  DEBUG_COMP_BELLOWS: logging.DEBUG,
189  DEBUG_COMP_ZHA: logging.DEBUG,
190  DEBUG_COMP_ZIGPY: logging.DEBUG,
191  DEBUG_COMP_ZIGPY_ZNP: logging.DEBUG,
192  DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG,
193  DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG,
194  DEBUG_COMP_ZIGPY_ZIGATE: logging.DEBUG,
195  DEBUG_LIB_ZHA: logging.DEBUG,
196 }
197 DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY, DEBUG_LIB_ZHA]
198 ZHA_GW_MSG_LOG_ENTRY = "log_entry"
199 ZHA_GW_MSG_LOG_OUTPUT = "log_output"
200 SIGNAL_REMOVE_ENTITIES = "zha_remove_entities"
201 GROUP_ENTITY_DOMAINS = [Platform.LIGHT, Platform.SWITCH, Platform.FAN]
202 SIGNAL_ADD_ENTITIES = "zha_add_entities"
203 ENTITIES = "entities"
204 
205 RX_ON_WHEN_IDLE = "rx_on_when_idle"
206 RELATIONSHIP = "relationship"
207 EXTENDED_PAN_ID = "extended_pan_id"
208 PERMIT_JOINING = "permit_joining"
209 DEPTH = "depth"
210 
211 DEST_NWK = "dest_nwk"
212 ROUTE_STATUS = "route_status"
213 MEMORY_CONSTRAINED = "memory_constrained"
214 MANY_TO_ONE = "many_to_one"
215 ROUTE_RECORD_REQUIRED = "route_record_required"
216 NEXT_HOP = "next_hop"
217 
218 USER_GIVEN_NAME = "user_given_name"
219 DEVICE_REG_ID = "device_reg_id"
220 
221 
222 class GroupEntityReference(NamedTuple):
223  """Reference to a group entity."""
224 
225  name: str | None
226  original_name: str | None
227  entity_id: str
228 
229 
230 class ZHAGroupProxy(LogMixin):
231  """Proxy class to interact with the ZHA group instances."""
232 
233  def __init__(self, group: Group, gateway_proxy: ZHAGatewayProxy) -> None:
234  """Initialize the gateway proxy."""
235  self.group: Group = group
236  self.gateway_proxy: ZHAGatewayProxy = gateway_proxy
237 
238  @property
239  def group_info(self) -> dict[str, Any]:
240  """Return a group description for group."""
241  return {
242  "name": self.group.name,
243  "group_id": self.group.group_id,
244  "members": [
245  {
246  "endpoint_id": member.endpoint_id,
247  "device": self.gateway_proxy.device_proxies[
248  member.device.ieee
249  ].zha_device_info,
250  "entities": [e._asdict() for e in self.associated_entitiesassociated_entities(member)],
251  }
252  for member in self.group.members
253  ],
254  }
255 
256  def associated_entities(self, member: GroupMember) -> list[GroupEntityReference]:
257  """Return the list of entities that were derived from this endpoint."""
258  entity_registry = er.async_get(self.gateway_proxy.hass)
259  entity_refs: collections.defaultdict[EUI64, list[EntityReference]] = (
260  self.gateway_proxy.ha_entity_refs
261  )
262 
263  entity_info = []
264 
265  for entity_ref in entity_refs.get(member.device.ieee): # type: ignore[union-attr]
266  if not entity_ref.entity_data.is_group_entity:
267  continue
268  entity = entity_registry.async_get(entity_ref.ha_entity_id)
269 
270  if (
271  entity is None
272  or entity_ref.entity_data.group_proxy is None
273  or entity_ref.entity_data.group_proxy.group.group_id
274  != member.group.group_id
275  ):
276  continue
277 
278  entity_info.append(
280  name=entity.name,
281  original_name=entity.original_name,
282  entity_id=entity_ref.ha_entity_id,
283  )
284  )
285 
286  return entity_info
287 
288  def log(self, level: int, msg: str, *args: Any, **kwargs) -> None:
289  """Log a message."""
290  msg = f"[%s](%s): {msg}"
291  args = (
292  f"0x{self.group.group_id:04x}",
293  self.group.endpoint.endpoint_id,
294  *args,
295  )
296  _LOGGER.log(level, msg, *args, **kwargs)
297 
298 
299 class ZHADeviceProxy(EventBase):
300  """Proxy class to interact with the ZHA device instances."""
301 
302  _ha_device_id: str
303 
304  def __init__(self, device: Device, gateway_proxy: ZHAGatewayProxy) -> None:
305  """Initialize the gateway proxy."""
306  super().__init__()
307  self.devicedevice = device
308  self.gateway_proxygateway_proxy = gateway_proxy
309  self._unsubs: list[Callable[[], None]] = []
310  self._unsubs.append(self.devicedevice.on_all_events(self._handle_event_protocol))
311 
312  @property
313  def device_id(self) -> str:
314  """Return the HA device registry device id."""
315  return self._ha_device_id_ha_device_id
316 
317  @device_id.setter
318  def device_id(self, device_id: str) -> None:
319  """Set the HA device registry device id."""
320  self._ha_device_id_ha_device_id = device_id
321 
322  @property
323  def device_info(self) -> dict[str, Any]:
324  """Return a device description for device."""
325  ieee = str(self.devicedevice.ieee)
326  time_struct = time.localtime(self.devicedevice.last_seen)
327  update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
328  return {
329  ATTR_IEEE: ieee,
330  ATTR_NWK: self.devicedevice.nwk,
331  ATTR_MANUFACTURER: self.devicedevice.manufacturer,
332  ATTR_MODEL: self.devicedevice.model,
333  ATTR_NAME: self.devicedevice.name or ieee,
334  ATTR_QUIRK_APPLIED: self.devicedevice.quirk_applied,
335  ATTR_QUIRK_CLASS: self.devicedevice.quirk_class,
336  ATTR_QUIRK_ID: self.devicedevice.quirk_id,
337  ATTR_MANUFACTURER_CODE: self.devicedevice.manufacturer_code,
338  ATTR_POWER_SOURCE: self.devicedevice.power_source,
339  ATTR_LQI: self.devicedevice.lqi,
340  ATTR_RSSI: self.devicedevice.rssi,
341  ATTR_LAST_SEEN: update_time,
342  ATTR_AVAILABLE: self.devicedevice.available,
343  ATTR_DEVICE_TYPE: self.devicedevice.device_type,
344  ATTR_SIGNATURE: self.devicedevice.zigbee_signature,
345  }
346 
347  @property
348  def zha_device_info(self) -> dict[str, Any]:
349  """Get ZHA device information."""
350  device_info: dict[str, Any] = {}
351  device_info.update(self.device_infodevice_info)
352  device_info[ATTR_ACTIVE_COORDINATOR] = self.devicedevice.is_active_coordinator
353  device_info[ENTITIES] = [
354  {
355  ATTR_ENTITY_ID: entity_ref.ha_entity_id,
356  ATTR_NAME: entity_ref.ha_device_info[ATTR_NAME],
357  }
358  for entity_ref in self.gateway_proxygateway_proxy.ha_entity_refs[self.devicedevice.ieee]
359  ]
360 
361  topology = self.gateway_proxygateway_proxy.gateway.application_controller.topology
362  device_info[ATTR_NEIGHBORS] = [
363  {
364  ATTR_DEVICE_TYPE: neighbor.device_type.name,
365  RX_ON_WHEN_IDLE: neighbor.rx_on_when_idle.name,
366  RELATIONSHIP: neighbor.relationship.name,
367  EXTENDED_PAN_ID: str(neighbor.extended_pan_id),
368  ATTR_IEEE: str(neighbor.ieee),
369  ATTR_NWK: str(neighbor.nwk),
370  PERMIT_JOINING: neighbor.permit_joining.name,
371  DEPTH: str(neighbor.depth),
372  ATTR_LQI: str(neighbor.lqi),
373  }
374  for neighbor in topology.neighbors[self.devicedevice.ieee]
375  ]
376 
377  device_info[ATTR_ROUTES] = [
378  {
379  DEST_NWK: str(route.DstNWK),
380  ROUTE_STATUS: str(route.RouteStatus.name),
381  MEMORY_CONSTRAINED: bool(route.MemoryConstrained),
382  MANY_TO_ONE: bool(route.ManyToOne),
383  ROUTE_RECORD_REQUIRED: bool(route.RouteRecordRequired),
384  NEXT_HOP: str(route.NextHop),
385  }
386  for route in topology.routes[self.devicedevice.ieee]
387  ]
388 
389  # Return endpoint device type Names
390  names: list[dict[str, str]] = []
391  for endpoint in (
392  ep for epid, ep in self.devicedevice.device.endpoints.items() if epid
393  ):
394  profile = PROFILES.get(endpoint.profile_id)
395  if profile and endpoint.device_type is not None:
396  # DeviceType provides undefined enums
397  names.append({ATTR_NAME: profile.DeviceType(endpoint.device_type).name})
398  else:
399  names.append(
400  {
401  ATTR_NAME: (
402  f"unknown {endpoint.device_type} device_type "
403  f"of 0x{(endpoint.profile_id or 0xFFFF):04x} profile id"
404  )
405  }
406  )
407  device_info[ATTR_ENDPOINT_NAMES] = names
408 
409  device_registry = dr.async_get(self.gateway_proxygateway_proxy.hass)
410  reg_device = device_registry.async_get(self.device_iddevice_iddevice_id)
411  if reg_device is not None:
412  device_info[USER_GIVEN_NAME] = reg_device.name_by_user
413  device_info[DEVICE_REG_ID] = reg_device.id
414  device_info[ATTR_AREA_ID] = reg_device.area_id
415  return device_info
416 
417  @callback
418  def handle_zha_event(self, zha_event: ZHAEvent) -> None:
419  """Handle a ZHA event."""
420  self.gateway_proxygateway_proxy.hass.bus.async_fire(
421  ZHA_EVENT,
422  {
423  ATTR_DEVICE_IEEE: str(zha_event.device_ieee),
424  ATTR_UNIQUE_ID: zha_event.unique_id,
425  ATTR_DEVICE_ID: self.device_iddevice_iddevice_id,
426  **zha_event.data,
427  },
428  )
429 
430  @callback
432  self, event: ClusterConfigureReportingEvent
433  ) -> None:
434  """Handle a ZHA cluster configure reporting event."""
436  self.gateway_proxygateway_proxy.hass,
437  ZHA_CLUSTER_HANDLER_MSG,
438  {
439  ATTR_TYPE: ZHA_CLUSTER_HANDLER_MSG_CFG_RPT,
440  ZHA_CLUSTER_HANDLER_MSG_DATA: {
441  ATTR_CLUSTER_NAME: event.cluster_name,
442  ATTR_CLUSTER_ID: event.cluster_id,
443  ATTR_ATTRIBUTES: event.attributes,
444  },
445  },
446  )
447 
448  @callback
450  self, event: ClusterHandlerConfigurationComplete
451  ) -> None:
452  """Handle a ZHA cluster configure reporting event."""
454  self.gateway_proxygateway_proxy.hass,
455  ZHA_CLUSTER_HANDLER_MSG,
456  {
457  ATTR_TYPE: ZHA_CLUSTER_HANDLER_CFG_DONE,
458  },
459  )
460 
461  @callback
462  def handle_zha_channel_bind(self, event: ClusterBindEvent) -> None:
463  """Handle a ZHA cluster bind event."""
465  self.gateway_proxygateway_proxy.hass,
466  ZHA_CLUSTER_HANDLER_MSG,
467  {
468  ATTR_TYPE: ZHA_CLUSTER_HANDLER_MSG_BIND,
469  ZHA_CLUSTER_HANDLER_MSG_DATA: {
470  ATTR_CLUSTER_NAME: event.cluster_name,
471  ATTR_CLUSTER_ID: event.cluster_id,
472  ATTR_SUCCESS: event.success,
473  },
474  },
475  )
476 
477 
478 class EntityReference(NamedTuple):
479  """Describes an entity reference."""
480 
481  ha_entity_id: str
482  entity_data: EntityData
483  ha_device_info: dr.DeviceInfo
484  remove_future: asyncio.Future[Any]
485 
486 
487 class ZHAGatewayProxy(EventBase):
488  """Proxy class to interact with the ZHA gateway."""
489 
490  def __init__(
491  self, hass: HomeAssistant, config_entry: ConfigEntry, gateway: Gateway
492  ) -> None:
493  """Initialize the gateway proxy."""
494  super().__init__()
495  self.hasshass = hass
496  self.config_entryconfig_entry = config_entry
497  self.gatewaygateway = gateway
498  self.device_proxies: dict[EUI64, ZHADeviceProxy] = {}
499  self.group_proxies: dict[int, ZHAGroupProxy] = {}
500  self._ha_entity_refs: collections.defaultdict[EUI64, list[EntityReference]] = (
501  collections.defaultdict(list)
502  )
503  self._log_levels: dict[str, dict[str, int]] = {
504  DEBUG_LEVEL_ORIGINAL: async_capture_log_levels(),
505  DEBUG_LEVEL_CURRENT: async_capture_log_levels(),
506  }
507  self.debug_enableddebug_enabled: bool = False
508  self._log_relay_handler: LogRelayHandler = LogRelayHandler(hass, self)
509  self._unsubs: list[Callable[[], None]] = []
510  self._unsubs.append(self.gatewaygateway.on_all_events(self._handle_event_protocol))
511  self._reload_task_reload_task: asyncio.Task | None = None
512  config_entry.async_on_unload(
513  self.hasshass.bus.async_listen(
514  er.EVENT_ENTITY_REGISTRY_UPDATED,
515  self._handle_entity_registry_updated_handle_entity_registry_updated,
516  )
517  )
518 
519  @property
520  def ha_entity_refs(self) -> collections.defaultdict[EUI64, list[EntityReference]]:
521  """Return entities by ieee."""
522  return self._ha_entity_refs
523 
525  self,
526  ha_entity_id: str,
527  entity_data: EntityData,
528  ha_device_info: dr.DeviceInfo,
529  remove_future: asyncio.Future[Any],
530  ) -> None:
531  """Record the creation of a hass entity associated with ieee."""
532  self._ha_entity_refs[entity_data.device_proxy.device.ieee].append(
534  ha_entity_id=ha_entity_id,
535  entity_data=entity_data,
536  ha_device_info=ha_device_info,
537  remove_future=remove_future,
538  )
539  )
540 
542  self, event: Event[er.EventEntityRegistryUpdatedData]
543  ) -> None:
544  """Handle when entity registry updated."""
545  entity_id = event.data["entity_id"]
546  entity_entry: er.RegistryEntry | None = er.async_get(self.hasshass).async_get(
547  entity_id
548  )
549  if (
550  entity_entry is None
551  or entity_entry.config_entry_id != self.config_entryconfig_entry.entry_id
552  or entity_entry.device_id is None
553  ):
554  return
555  device_entry: dr.DeviceEntry | None = dr.async_get(self.hasshass).async_get(
556  entity_entry.device_id
557  )
558  assert device_entry
559 
560  ieee_address = next(
561  identifier
562  for domain, identifier in device_entry.identifiers
563  if domain == DOMAIN
564  )
565  assert ieee_address
566 
567  ieee = EUI64.convert(ieee_address)
568 
569  assert ieee in self.device_proxies
570 
571  zha_device_proxy = self.device_proxies[ieee]
572  entity_key = (entity_entry.domain, entity_entry.unique_id)
573  if entity_key not in zha_device_proxy.device.platform_entities:
574  return
575  platform_entity = zha_device_proxy.device.platform_entities[entity_key]
576  if entity_entry.disabled:
577  platform_entity.disable()
578  else:
579  platform_entity.enable()
580 
581  async def async_initialize_devices_and_entities(self) -> None:
582  """Initialize devices and entities."""
583  for device in self.gatewaygateway.devices.values():
584  device_proxy = self._async_get_or_create_device_proxy_async_get_or_create_device_proxy(device)
585  self._create_entity_metadata_create_entity_metadata(device_proxy)
586  for group in self.gatewaygateway.groups.values():
587  group_proxy = self._async_get_or_create_group_proxy_async_get_or_create_group_proxy(group)
588  self._create_entity_metadata_create_entity_metadata(group_proxy)
589 
591 
592  @callback
593  def handle_connection_lost(self, event: ConnectionLostEvent) -> None:
594  """Handle a connection lost event."""
595 
596  _LOGGER.debug("Connection to the radio was lost: %r", event)
597 
598  # Ensure we do not queue up multiple resets
599  if self._reload_task_reload_task is not None:
600  _LOGGER.debug("Ignoring reset, one is already running")
601  return
602 
603  self._reload_task_reload_task = self.hasshass.async_create_task(
604  self.hasshass.config_entries.async_reload(self.config_entryconfig_entry.entry_id),
605  )
606 
607  @callback
608  def handle_device_joined(self, event: DeviceJoinedEvent) -> None:
609  """Handle a device joined event."""
611  self.hasshass,
612  ZHA_GW_MSG,
613  {
614  ATTR_TYPE: ZHA_GW_MSG_DEVICE_JOINED,
615  ZHA_GW_MSG_DEVICE_INFO: {
616  ATTR_NWK: event.device_info.nwk,
617  ATTR_IEEE: str(event.device_info.ieee),
618  DEVICE_PAIRING_STATUS: event.device_info.pairing_status.name,
619  },
620  },
621  )
622 
623  @callback
624  def handle_device_removed(self, event: DeviceRemovedEvent) -> None:
625  """Handle a device removed event."""
626  zha_device_proxy = self.device_proxies.pop(event.device_info.ieee, None)
627  entity_refs = self._ha_entity_refs.pop(event.device_info.ieee, None)
628  if zha_device_proxy is not None:
629  device_info = zha_device_proxy.zha_device_info
630  # zha_device_proxy.async_cleanup_handles()
632  self.hasshass,
633  f"{SIGNAL_REMOVE_ENTITIES}_{zha_device_proxy.device.ieee!s}",
634  )
635  self.hasshass.async_create_task(
636  self._async_remove_device_async_remove_device(zha_device_proxy, entity_refs),
637  "ZHAGateway._async_remove_device",
638  )
639  if device_info is not None:
641  self.hasshass,
642  ZHA_GW_MSG,
643  {
644  ATTR_TYPE: ZHA_GW_MSG_DEVICE_REMOVED,
645  ZHA_GW_MSG_DEVICE_INFO: device_info,
646  },
647  )
648 
649  @callback
650  def handle_device_left(self, event: DeviceLeftEvent) -> None:
651  """Handle a device left event."""
652 
653  @callback
654  def handle_raw_device_initialized(self, event: RawDeviceInitializedEvent) -> None:
655  """Handle a raw device initialized event."""
656  manuf = event.device_info.manufacturer
658  self.hasshass,
659  ZHA_GW_MSG,
660  {
661  ATTR_TYPE: ZHA_GW_MSG_RAW_INIT,
662  ZHA_GW_MSG_DEVICE_INFO: {
663  ATTR_NWK: str(event.device_info.nwk),
664  ATTR_IEEE: str(event.device_info.ieee),
665  DEVICE_PAIRING_STATUS: event.device_info.pairing_status.name,
666  ATTR_MODEL: (
667  event.device_info.model
668  if event.device_info.model
669  else UNKNOWN_MODEL
670  ),
671  ATTR_MANUFACTURER: manuf if manuf else UNKNOWN_MANUFACTURER,
672  ATTR_SIGNATURE: event.device_info.signature,
673  },
674  },
675  )
676 
677  @callback
678  def handle_device_fully_initialized(self, event: DeviceFullInitEvent) -> None:
679  """Handle a device fully initialized event."""
680  zha_device = self.gatewaygateway.get_device(event.device_info.ieee)
681  zha_device_proxy = self._async_get_or_create_device_proxy_async_get_or_create_device_proxy(zha_device)
682 
683  device_info = zha_device_proxy.zha_device_info
684  device_info[DEVICE_PAIRING_STATUS] = event.device_info.pairing_status.name
685  if event.new_join:
686  self._create_entity_metadata_create_entity_metadata(zha_device_proxy)
687  async_dispatcher_send(self.hasshass, SIGNAL_ADD_ENTITIES)
689  self.hasshass,
690  ZHA_GW_MSG,
691  {
692  ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT,
693  ZHA_GW_MSG_DEVICE_INFO: device_info,
694  },
695  )
696 
697  @callback
698  def handle_group_member_removed(self, event: GroupEvent) -> None:
699  """Handle a group member removed event."""
700  zha_group_proxy = self._async_get_or_create_group_proxy_async_get_or_create_group_proxy(event.group_info)
701  zha_group_proxy.info("group_member_removed - group_info: %s", event.group_info)
702  self._update_group_entities_update_group_entities(event)
703  self._send_group_gateway_message_send_group_gateway_message(
704  zha_group_proxy, ZHA_GW_MSG_GROUP_MEMBER_REMOVED
705  )
706 
707  @callback
708  def handle_group_member_added(self, event: GroupEvent) -> None:
709  """Handle a group member added event."""
710  zha_group_proxy = self._async_get_or_create_group_proxy_async_get_or_create_group_proxy(event.group_info)
711  zha_group_proxy.info("group_member_added - group_info: %s", event.group_info)
712  self._send_group_gateway_message_send_group_gateway_message(zha_group_proxy, ZHA_GW_MSG_GROUP_MEMBER_ADDED)
713  self._update_group_entities_update_group_entities(event)
714 
715  @callback
716  def handle_group_added(self, event: GroupEvent) -> None:
717  """Handle a group added event."""
718  zha_group_proxy = self._async_get_or_create_group_proxy_async_get_or_create_group_proxy(event.group_info)
719  zha_group_proxy.info("group_added")
720  self._update_group_entities_update_group_entities(event)
721  self._send_group_gateway_message_send_group_gateway_message(zha_group_proxy, ZHA_GW_MSG_GROUP_ADDED)
722 
723  @callback
724  def handle_group_removed(self, event: GroupEvent) -> None:
725  """Handle a group removed event."""
726  zha_group_proxy = self.group_proxies.pop(event.group_info.group_id)
727  self._send_group_gateway_message_send_group_gateway_message(zha_group_proxy, ZHA_GW_MSG_GROUP_REMOVED)
728  zha_group_proxy.info("group_removed")
729  self._cleanup_group_entity_registry_entries_cleanup_group_entity_registry_entries(zha_group_proxy)
730 
731  @callback
732  def async_enable_debug_mode(self, filterer: _LogFilterType | None = None) -> None:
733  """Enable debug mode for ZHA."""
734  self._log_levels[DEBUG_LEVEL_ORIGINAL] = async_capture_log_levels()
735  async_set_logger_levels(DEBUG_LEVELS)
736  self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels()
737 
738  if filterer:
739  self._log_relay_handler.addFilter(filterer)
740 
741  for logger_name in DEBUG_RELAY_LOGGERS:
742  logging.getLogger(logger_name).addHandler(self._log_relay_handler)
743 
744  self.debug_enableddebug_enabled = True
745 
746  @callback
747  def async_disable_debug_mode(self, filterer: _LogFilterType | None = None) -> None:
748  """Disable debug mode for ZHA."""
749  async_set_logger_levels(self._log_levels[DEBUG_LEVEL_ORIGINAL])
750  self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels()
751  for logger_name in DEBUG_RELAY_LOGGERS:
752  logging.getLogger(logger_name).removeHandler(self._log_relay_handler)
753  if filterer:
754  self._log_relay_handler.removeFilter(filterer)
755  self.debug_enableddebug_enabled = False
756 
757  async def shutdown(self) -> None:
758  """Shutdown the gateway proxy."""
759  for unsub in self._unsubs:
760  unsub()
761  await self.gatewaygateway.shutdown()
762 
763  def get_device_proxy(self, ieee: EUI64) -> ZHADeviceProxy | None:
764  """Return ZHADevice for given ieee."""
765  return self.device_proxies.get(ieee)
766 
767  def get_group_proxy(self, group_id: int | str) -> ZHAGroupProxy | None:
768  """Return Group for given group id."""
769  if isinstance(group_id, str):
770  for group_proxy in self.group_proxies.values():
771  if group_proxy.group.name == group_id:
772  return group_proxy
773  return None
774  return self.group_proxies.get(group_id)
775 
776  def get_entity_reference(self, entity_id: str) -> EntityReference | None:
777  """Return entity reference for given entity_id if found."""
778  for entity_reference in itertools.chain.from_iterable(
779  self.ha_entity_refsha_entity_refs.values()
780  ):
781  if entity_id == entity_reference.ha_entity_id:
782  return entity_reference
783  return None
784 
785  def remove_entity_reference(self, entity: ZHAEntity) -> None:
786  """Remove entity reference for given entity_id if found."""
787  if entity.zha_device.ieee in self.ha_entity_refsha_entity_refs:
788  entity_refs = self.ha_entity_refsha_entity_refs.get(entity.zha_device.ieee)
789  self.ha_entity_refsha_entity_refs[entity.zha_device.ieee] = [
790  e
791  for e in entity_refs # type: ignore[union-attr]
792  if e.ha_entity_id != entity.entity_id
793  ]
794 
795  def _async_get_or_create_device_proxy(self, zha_device: Device) -> ZHADeviceProxy:
796  """Get or create a ZHA device."""
797  if (zha_device_proxy := self.device_proxies.get(zha_device.ieee)) is None:
798  zha_device_proxy = ZHADeviceProxy(zha_device, self)
799  self.device_proxies[zha_device_proxy.device.ieee] = zha_device_proxy
800 
801  device_registry = dr.async_get(self.hasshass)
802  device_registry_device = device_registry.async_get_or_create(
803  config_entry_id=self.config_entryconfig_entry.entry_id,
804  connections={(dr.CONNECTION_ZIGBEE, str(zha_device.ieee))},
805  identifiers={(DOMAIN, str(zha_device.ieee))},
806  name=zha_device.name,
807  manufacturer=zha_device.manufacturer,
808  model=zha_device.model,
809  )
810  zha_device_proxy.device_id = device_registry_device.id
811  return zha_device_proxy
812 
813  def _async_get_or_create_group_proxy(self, group_info: GroupInfo) -> ZHAGroupProxy:
814  """Get or create a ZHA group."""
815  zha_group_proxy = self.group_proxies.get(group_info.group_id)
816  if zha_group_proxy is None:
817  zha_group_proxy = ZHAGroupProxy(
818  self.gatewaygateway.groups[group_info.group_id], self
819  )
820  self.group_proxies[group_info.group_id] = zha_group_proxy
821  return zha_group_proxy
822 
824  self, proxy_object: ZHADeviceProxy | ZHAGroupProxy
825  ) -> None:
826  """Create HA entity metadata."""
827  ha_zha_data = get_zha_data(self.hasshass)
828  coordinator_proxy = self.device_proxies[
829  self.gatewaygateway.coordinator_zha_device.ieee
830  ]
831 
832  if isinstance(proxy_object, ZHADeviceProxy):
833  for entity in proxy_object.device.platform_entities.values():
834  ha_zha_data.platforms[Platform(entity.PLATFORM)].append(
835  EntityData(
836  entity=entity, device_proxy=proxy_object, group_proxy=None
837  )
838  )
839  else:
840  for entity in proxy_object.group.group_entities.values():
841  ha_zha_data.platforms[Platform(entity.PLATFORM)].append(
842  EntityData(
843  entity=entity,
844  device_proxy=coordinator_proxy,
845  group_proxy=proxy_object,
846  )
847  )
848 
850  self, zha_group_proxy: ZHAGroupProxy
851  ) -> None:
852  """Remove entity registry entries for group entities when the groups are removed from HA."""
853  # first we collect the potential unique ids for entities that could be created from this group
854  possible_entity_unique_ids = [
855  f"{domain}_zha_group_0x{zha_group_proxy.group.group_id:04x}"
856  for domain in GROUP_ENTITY_DOMAINS
857  ]
858 
859  # then we get all group entity entries tied to the coordinator
860  entity_registry = er.async_get(self.hasshass)
861  assert self.gatewaygateway.coordinator_zha_device
862  coordinator_proxy = self.device_proxies[
863  self.gatewaygateway.coordinator_zha_device.ieee
864  ]
865  all_group_entity_entries = er.async_entries_for_device(
866  entity_registry,
867  coordinator_proxy.device_id,
868  include_disabled_entities=True,
869  )
870 
871  # then we get the entity entries for this specific group
872  # by getting the entries that match
873  entries_to_remove = [
874  entry
875  for entry in all_group_entity_entries
876  if entry.unique_id in possible_entity_unique_ids
877  ]
878 
879  # then we remove the entries from the entity registry
880  for entry in entries_to_remove:
881  _LOGGER.debug(
882  "cleaning up entity registry entry for entity: %s", entry.entity_id
883  )
884  entity_registry.async_remove(entry.entity_id)
885 
886  def _update_group_entities(self, group_event: GroupEvent) -> None:
887  """Update group entities when a group event is received."""
889  self.hasshass,
890  f"{SIGNAL_REMOVE_ENTITIES}_group_{group_event.group_info.group_id}",
891  )
892  self._create_entity_metadata_create_entity_metadata(
893  self.group_proxies[group_event.group_info.group_id]
894  )
895  async_dispatcher_send(self.hasshass, SIGNAL_ADD_ENTITIES)
896 
898  self, zha_group_proxy: ZHAGroupProxy, gateway_message_type: str
899  ) -> None:
900  """Send the gateway event for a zigpy group event."""
902  self.hasshass,
903  ZHA_GW_MSG,
904  {
905  ATTR_TYPE: gateway_message_type,
906  ZHA_GW_MSG_GROUP_INFO: zha_group_proxy.group_info,
907  },
908  )
909 
911  self, device: ZHADeviceProxy, entity_refs: list[EntityReference] | None
912  ) -> None:
913  if entity_refs is not None:
914  remove_tasks: list[asyncio.Future[Any]] = [
915  entity_ref.remove_future for entity_ref in entity_refs
916  ]
917  if remove_tasks:
918  await asyncio.wait(remove_tasks)
919 
920  device_registry = dr.async_get(self.hasshass)
921  reg_device = device_registry.async_get(device.device_id)
922  if reg_device is not None:
923  device_registry.async_remove_device(reg_device.id)
924 
925 
926 @callback
927 def async_capture_log_levels() -> dict[str, int]:
928  """Capture current logger levels for ZHA."""
929  return {
930  DEBUG_COMP_BELLOWS: logging.getLogger(DEBUG_COMP_BELLOWS).getEffectiveLevel(),
931  DEBUG_COMP_ZHA: logging.getLogger(DEBUG_COMP_ZHA).getEffectiveLevel(),
932  DEBUG_COMP_ZIGPY: logging.getLogger(DEBUG_COMP_ZIGPY).getEffectiveLevel(),
933  DEBUG_COMP_ZIGPY_ZNP: logging.getLogger(
934  DEBUG_COMP_ZIGPY_ZNP
935  ).getEffectiveLevel(),
936  DEBUG_COMP_ZIGPY_DECONZ: logging.getLogger(
937  DEBUG_COMP_ZIGPY_DECONZ
938  ).getEffectiveLevel(),
939  DEBUG_COMP_ZIGPY_XBEE: logging.getLogger(
940  DEBUG_COMP_ZIGPY_XBEE
941  ).getEffectiveLevel(),
942  DEBUG_COMP_ZIGPY_ZIGATE: logging.getLogger(
943  DEBUG_COMP_ZIGPY_ZIGATE
944  ).getEffectiveLevel(),
945  DEBUG_LIB_ZHA: logging.getLogger(DEBUG_LIB_ZHA).getEffectiveLevel(),
946  }
947 
948 
949 @callback
950 def async_set_logger_levels(levels: dict[str, int]) -> None:
951  """Set logger levels for ZHA."""
952  logging.getLogger(DEBUG_COMP_BELLOWS).setLevel(levels[DEBUG_COMP_BELLOWS])
953  logging.getLogger(DEBUG_COMP_ZHA).setLevel(levels[DEBUG_COMP_ZHA])
954  logging.getLogger(DEBUG_COMP_ZIGPY).setLevel(levels[DEBUG_COMP_ZIGPY])
955  logging.getLogger(DEBUG_COMP_ZIGPY_ZNP).setLevel(levels[DEBUG_COMP_ZIGPY_ZNP])
956  logging.getLogger(DEBUG_COMP_ZIGPY_DECONZ).setLevel(levels[DEBUG_COMP_ZIGPY_DECONZ])
957  logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE])
958  logging.getLogger(DEBUG_COMP_ZIGPY_ZIGATE).setLevel(levels[DEBUG_COMP_ZIGPY_ZIGATE])
959  logging.getLogger(DEBUG_LIB_ZHA).setLevel(levels[DEBUG_LIB_ZHA])
960 
961 
962 class LogRelayHandler(logging.Handler):
963  """Log handler for error messages."""
964 
965  def __init__(self, hass: HomeAssistant, gateway: ZHAGatewayProxy) -> None:
966  """Initialize a new LogErrorHandler."""
967  super().__init__()
968  self.hasshass = hass
969  self.gatewaygateway = gateway
970  hass_path: str = HOMEASSISTANT_PATH[0]
971  config_dir = self.hasshass.config.config_dir
972  self.paths_repaths_re = re.compile(
973  rf"(?:{re.escape(hass_path)}|{re.escape(config_dir)})/(.*)"
974  )
975 
976  def emit(self, record: LogRecord) -> None:
977  """Relay log message via dispatcher."""
978  entry = LogEntry(
979  record, self.paths_repaths_re, figure_out_source=record.levelno >= logging.WARNING
980  )
982  self.hasshass,
983  ZHA_GW_MSG,
984  {ATTR_TYPE: ZHA_GW_MSG_LOG_OUTPUT, ZHA_GW_MSG_LOG_ENTRY: entry.to_dict()},
985  )
986 
987 
988 @dataclasses.dataclass(kw_only=True, slots=True)
989 class HAZHAData:
990  """ZHA data stored in `hass.data`."""
991 
992  yaml_config: ConfigType = dataclasses.field(default_factory=dict)
993  config_entry: ConfigEntry | None = dataclasses.field(default=None)
994  device_trigger_cache: dict[str, tuple[str, dict]] = dataclasses.field(
995  default_factory=dict
996  )
997  gateway_proxy: ZHAGatewayProxy | None = dataclasses.field(default=None)
998  platforms: collections.defaultdict[Platform, list] = dataclasses.field(
999  default_factory=lambda: collections.defaultdict(list)
1000  )
1001  update_coordinator: ZHAFirmwareUpdateCoordinator | None = dataclasses.field(
1002  default=None
1003  )
1004 
1005 
1006 @dataclasses.dataclass(kw_only=True, slots=True)
1008  """ZHA entity data."""
1009 
1010  entity: PlatformEntity | GroupEntity
1011  device_proxy: ZHADeviceProxy
1012  group_proxy: ZHAGroupProxy | None = dataclasses.field(default=None)
1013 
1014  @property
1015  def is_group_entity(self) -> bool:
1016  """Return if this is a group entity."""
1017  return self.group_proxy is not None and isinstance(self.entity, GroupEntity)
1018 
1019 
1020 def get_zha_data(hass: HomeAssistant) -> HAZHAData:
1021  """Get the global ZHA data object."""
1022  if DATA_ZHA not in hass.data:
1023  hass.data[DATA_ZHA] = HAZHAData()
1024 
1025  return hass.data[DATA_ZHA]
1026 
1027 
1028 def get_zha_gateway(hass: HomeAssistant) -> Gateway:
1029  """Get the ZHA gateway object."""
1030  if (gateway_proxy := get_zha_data(hass).gateway_proxy) is None:
1031  raise ValueError("No gateway object exists")
1032 
1033  return gateway_proxy.gateway
1034 
1035 
1036 def get_zha_gateway_proxy(hass: HomeAssistant) -> ZHAGatewayProxy:
1037  """Get the ZHA gateway object."""
1038  if (gateway_proxy := get_zha_data(hass).gateway_proxy) is None:
1039  raise ValueError("No gateway object exists")
1040 
1041  return gateway_proxy
1042 
1043 
1044 def get_config_entry(hass: HomeAssistant) -> ConfigEntry:
1045  """Get the ZHA gateway object."""
1046  if (gateway_proxy := get_zha_data(hass).gateway_proxy) is None:
1047  raise ValueError("No gateway object exists to retrieve the config entry from.")
1048 
1049  return gateway_proxy.config_entry
1050 
1051 
1052 @callback
1053 def async_get_zha_device_proxy(hass: HomeAssistant, device_id: str) -> ZHADeviceProxy:
1054  """Get a ZHA device for the given device registry id."""
1055  device_registry = dr.async_get(hass)
1056  registry_device = device_registry.async_get(device_id)
1057  if not registry_device:
1058  _LOGGER.error("Device id `%s` not found in registry", device_id)
1059  raise KeyError(f"Device id `{device_id}` not found in registry.")
1060  zha_gateway_proxy = get_zha_gateway_proxy(hass)
1061  ieee_address = next(
1062  identifier
1063  for domain, identifier in registry_device.identifiers
1064  if domain == DOMAIN
1065  )
1066  ieee = EUI64.convert(ieee_address)
1067  return zha_gateway_proxy.device_proxies[ieee]
1068 
1069 
1070 def cluster_command_schema_to_vol_schema(schema: CommandSchema) -> vol.Schema:
1071  """Convert a cluster command schema to a voluptuous schema."""
1072  return vol.Schema(
1073  {
1074  (
1075  vol.Optional(field.name) if field.optional else vol.Required(field.name)
1076  ): schema_type_to_vol(field.type)
1077  for field in schema.fields
1078  }
1079  )
1080 
1081 
1082 def schema_type_to_vol(field_type: Any) -> Any:
1083  """Convert a schema type to a voluptuous type."""
1084  if issubclass(field_type, enum.Flag) and field_type.__members__:
1085  return cv.multi_select(
1086  [key.replace("_", " ") for key in field_type.__members__]
1087  )
1088  if issubclass(field_type, enum.Enum) and field_type.__members__:
1089  return vol.In([key.replace("_", " ") for key in field_type.__members__])
1090  if (
1091  issubclass(field_type, zigpy.types.FixedIntType)
1092  or issubclass(field_type, enum.Flag)
1093  or issubclass(field_type, enum.Enum)
1094  ):
1095  return vol.All(
1096  vol.Coerce(int), vol.Range(field_type.min_value, field_type.max_value)
1097  )
1098  return str
1099 
1100 
1102  fields: dict[str, Any], schema: CommandSchema
1103 ) -> dict[str, Any]:
1104  """Convert user input to ZCL values."""
1105  converted_fields: dict[str, Any] = {}
1106  for field in schema.fields:
1107  if field.name not in fields:
1108  continue
1109  value = fields[field.name]
1110  if issubclass(field.type, enum.Flag) and isinstance(value, list):
1111  new_value = 0
1112 
1113  for flag in value:
1114  if isinstance(flag, str):
1115  new_value |= field.type[flag.replace(" ", "_")]
1116  else:
1117  new_value |= flag
1118 
1119  value = field.type(new_value)
1120  elif issubclass(field.type, enum.Enum):
1121  value = (
1122  field.type[value.replace(" ", "_")]
1123  if isinstance(value, str)
1124  else field.type(value)
1125  )
1126  else:
1127  value = field.type(value)
1128  _LOGGER.debug(
1129  "Converted ZCL schema field(%s) value from: %s to: %s",
1130  field.name,
1131  fields[field.name],
1132  value,
1133  )
1134  converted_fields[field.name] = value
1135  return converted_fields
1136 
1137 
1138 def async_cluster_exists(hass: HomeAssistant, cluster_id, skip_coordinator=True):
1139  """Determine if a device containing the specified in cluster is paired."""
1140  zha_gateway = get_zha_gateway(hass)
1141  zha_devices = zha_gateway.devices.values()
1142  for zha_device in zha_devices:
1143  if skip_coordinator and zha_device.is_coordinator:
1144  continue
1145  clusters_by_endpoint = zha_device.async_get_clusters()
1146  for clusters in clusters_by_endpoint.values():
1147  if (
1148  cluster_id in clusters[CLUSTER_TYPE_IN]
1149  or cluster_id in clusters[CLUSTER_TYPE_OUT]
1150  ):
1151  return True
1152  return False
1153 
1154 
1155 @callback
1157  _async_add_entities: AddEntitiesCallback,
1158  entity_class: type[ZHAEntity],
1159  entities: list[EntityData],
1160  **kwargs,
1161 ) -> None:
1162  """Add entities helper."""
1163  if not entities:
1164  return
1165 
1166  entities_to_add: list[ZHAEntity] = []
1167  for entity_data in entities:
1168  try:
1169  entities_to_add.append(entity_class(entity_data))
1170  # broad exception to prevent a single entity from preventing an entire platform from loading
1171  # this can potentially be caused by a misbehaving device or a bad quirk. Not ideal but the
1172  # alternative is adding try/catch to each entity class __init__ method with a specific exception
1173  except Exception: # noqa: BLE001
1174  _LOGGER.exception(
1175  "Error while adding entity from entity data: %s", entity_data
1176  )
1177  _async_add_entities(entities_to_add, update_before_add=False)
1178  for entity in entities_to_add:
1179  if not entity.enabled:
1180  entity.entity_data.entity.disable()
1181  entities.clear()
1182 
1183 
1184 def _clean_serial_port_path(path: str) -> str:
1185  """Clean the serial port path, applying corrections where necessary."""
1186 
1187  if path.startswith("socket://"):
1188  path = path.strip()
1189 
1190  # Removes extraneous brackets from IP addresses (they don't parse in CPython 3.11.4)
1191  if re.match(r"^socket://\[\d+\.\d+\.\d+\.\d+\]:\d+$", path):
1192  path = path.replace("[", "").replace("]", "")
1193 
1194  return path
1195 
1196 
1197 CONF_ZHA_OPTIONS_SCHEMA = vol.Schema(
1198  {
1199  vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION, default=0): vol.All(
1200  vol.Coerce(float), vol.Range(min=0, max=2**16 / 10)
1201  ),
1202  vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean,
1203  vol.Required(CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, default=True): cv.boolean,
1204  vol.Required(CONF_GROUP_MEMBERS_ASSUME_STATE, default=True): cv.boolean,
1205  vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean,
1206  vol.Optional(
1207  CONF_CONSIDER_UNAVAILABLE_MAINS,
1208  default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS,
1209  ): cv.positive_int,
1210  vol.Optional(
1211  CONF_CONSIDER_UNAVAILABLE_BATTERY,
1212  default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY,
1213  ): cv.positive_int,
1214  vol.Required(CONF_ENABLE_MAINS_STARTUP_POLLING, default=True): cv.boolean,
1215  },
1216  extra=vol.REMOVE_EXTRA,
1217 )
1218 
1219 CONF_ZHA_ALARM_SCHEMA = vol.Schema(
1220  {
1221  vol.Required(CONF_ALARM_MASTER_CODE, default="1234"): cv.string,
1222  vol.Required(CONF_ALARM_FAILED_TRIES, default=3): cv.positive_int,
1223  vol.Required(CONF_ALARM_ARM_REQUIRES_CODE, default=False): cv.boolean,
1224  }
1225 )
1226 
1227 
1228 def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData:
1229  """Create ZHA lib configuration from HA config objects."""
1230 
1231  # ensure that we have the necessary HA configuration data
1232  assert ha_zha_data.config_entry is not None
1233  assert ha_zha_data.yaml_config is not None
1234 
1235  # Remove brackets around IP addresses, this no longer works in CPython 3.11.4
1236  # This will be removed in 2023.11.0
1237  path = ha_zha_data.config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
1238  cleaned_path = _clean_serial_port_path(path)
1239 
1240  if path != cleaned_path:
1241  _LOGGER.debug("Cleaned serial port path %r -> %r", path, cleaned_path)
1242  ha_zha_data.config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] = cleaned_path
1243  hass.config_entries.async_update_entry(
1244  ha_zha_data.config_entry, data=ha_zha_data.config_entry.data
1245  )
1246 
1247  # deep copy the yaml config to avoid modifying the original and to safely
1248  # pass it to the ZHA library
1249  app_config = copy.deepcopy(ha_zha_data.yaml_config.get(CONF_ZIGPY, {}))
1250  database = ha_zha_data.yaml_config.get(
1251  CONF_DATABASE,
1252  hass.config.path(DEFAULT_DATABASE_NAME),
1253  )
1254  app_config[CONF_DATABASE] = database
1255  app_config[CONF_DEVICE] = ha_zha_data.config_entry.data[CONF_DEVICE]
1256 
1257  radio_type = RadioType[ha_zha_data.config_entry.data[CONF_RADIO_TYPE]]
1258 
1259  # Until we have a way to coordinate channels with the Thread half of multi-PAN,
1260  # stick to the old zigpy default of channel 15 instead of dynamically scanning
1261  if (
1262  is_multiprotocol_url(app_config[CONF_DEVICE][CONF_DEVICE_PATH])
1263  and app_config.get(CONF_NWK, {}).get(CONF_NWK_CHANNEL) is None
1264  ):
1265  app_config.setdefault(CONF_NWK, {})[CONF_NWK_CHANNEL] = 15
1266 
1267  options: MappingProxyType[str, Any] = ha_zha_data.config_entry.options.get(
1268  CUSTOM_CONFIGURATION, {}
1269  )
1270  zha_options = CONF_ZHA_OPTIONS_SCHEMA(options.get(ZHA_OPTIONS, {}))
1271  ha_acp_options = CONF_ZHA_ALARM_SCHEMA(options.get(ZHA_ALARM_OPTIONS, {}))
1272  light_options: LightOptions = LightOptions(
1273  default_light_transition=zha_options.get(CONF_DEFAULT_LIGHT_TRANSITION),
1274  enable_enhanced_light_transition=zha_options.get(
1275  CONF_ENABLE_ENHANCED_LIGHT_TRANSITION
1276  ),
1277  enable_light_transitioning_flag=zha_options.get(
1278  CONF_ENABLE_LIGHT_TRANSITIONING_FLAG
1279  ),
1280  group_members_assume_state=zha_options.get(CONF_GROUP_MEMBERS_ASSUME_STATE),
1281  )
1282  device_options: DeviceOptions = DeviceOptions(
1283  enable_identify_on_join=zha_options.get(CONF_ENABLE_IDENTIFY_ON_JOIN),
1284  consider_unavailable_mains=zha_options.get(CONF_CONSIDER_UNAVAILABLE_MAINS),
1285  consider_unavailable_battery=zha_options.get(CONF_CONSIDER_UNAVAILABLE_BATTERY),
1286  enable_mains_startup_polling=zha_options.get(CONF_ENABLE_MAINS_STARTUP_POLLING),
1287  )
1288  acp_options: AlarmControlPanelOptions = AlarmControlPanelOptions(
1289  master_code=ha_acp_options.get(CONF_ALARM_MASTER_CODE),
1290  failed_tries=ha_acp_options.get(CONF_ALARM_FAILED_TRIES),
1291  arm_requires_code=ha_acp_options.get(CONF_ALARM_ARM_REQUIRES_CODE),
1292  )
1293  coord_config: CoordinatorConfiguration = CoordinatorConfiguration(
1294  path=app_config[CONF_DEVICE][CONF_DEVICE_PATH],
1295  baudrate=app_config[CONF_DEVICE][CONF_BAUDRATE],
1296  flow_control=app_config[CONF_DEVICE][CONF_FLOW_CONTROL],
1297  radio_type=radio_type.name,
1298  )
1299  quirks_config: QuirksConfiguration = QuirksConfiguration(
1300  enabled=ha_zha_data.yaml_config.get(CONF_ENABLE_QUIRKS, True),
1301  custom_quirks_path=ha_zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH),
1302  )
1303  overrides_config: dict[str, DeviceOverridesConfiguration] = {}
1304  overrides: dict[str, dict[str, Any]] = cast(
1305  dict[str, dict[str, Any]], ha_zha_data.yaml_config.get(CONF_DEVICE_CONFIG)
1306  )
1307  if overrides is not None:
1308  for unique_id, override in overrides.items():
1309  overrides_config[unique_id] = DeviceOverridesConfiguration(
1310  type=override["type"],
1311  )
1312 
1313  return ZHAData(
1314  zigpy_config=app_config,
1315  config=ZHAConfiguration(
1316  light_options=light_options,
1317  device_options=device_options,
1318  alarm_control_panel_options=acp_options,
1319  coordinator_configuration=coord_config,
1320  quirks_configuration=quirks_config,
1321  device_overrides=overrides_config,
1322  ),
1323  local_timezone=ZoneInfo(hass.config.time_zone),
1324  )
1325 
1326 
1327 def convert_zha_error_to_ha_error[**_P, _EntityT: ZHAEntity](
1328  func: Callable[Concatenate[_EntityT, _P], Awaitable[None]],
1329 ) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
1330  """Decorate ZHA commands and re-raises ZHAException as HomeAssistantError."""
1331 
1332  @functools.wraps(func)
1333  async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
1334  try:
1335  return await func(self, *args, **kwargs)
1336  except ZHAException as err:
1337  raise HomeAssistantError(err) from err
1338 
1339  return handler
1340 
1341 
1342 def exclude_none_values(obj: Mapping[str, Any]) -> dict[str, Any]:
1343  """Return a new dictionary excluding keys with None values."""
1344  return {k: v for k, v in obj.items() if v is not None}
None __init__(self, HomeAssistant hass, ZHAGatewayProxy gateway)
Definition: helpers.py:965
None handle_zha_channel_cfg_done(self, ClusterHandlerConfigurationComplete event)
Definition: helpers.py:451
None __init__(self, Device device, ZHAGatewayProxy gateway_proxy)
Definition: helpers.py:304
None handle_zha_event(self, ZHAEvent zha_event)
Definition: helpers.py:418
None handle_zha_channel_bind(self, ClusterBindEvent event)
Definition: helpers.py:462
None handle_zha_channel_configure_reporting(self, ClusterConfigureReportingEvent event)
Definition: helpers.py:433
EntityReference|None get_entity_reference(self, str entity_id)
Definition: helpers.py:776
None _cleanup_group_entity_registry_entries(self, ZHAGroupProxy zha_group_proxy)
Definition: helpers.py:851
None async_enable_debug_mode(self, _LogFilterType|None filterer=None)
Definition: helpers.py:732
None handle_group_member_added(self, GroupEvent event)
Definition: helpers.py:708
None handle_group_removed(self, GroupEvent event)
Definition: helpers.py:724
None _handle_entity_registry_updated(self, Event[er.EventEntityRegistryUpdatedData] event)
Definition: helpers.py:543
None _update_group_entities(self, GroupEvent group_event)
Definition: helpers.py:886
None handle_device_removed(self, DeviceRemovedEvent event)
Definition: helpers.py:624
collections.defaultdict[EUI64, list[EntityReference]] ha_entity_refs(self)
Definition: helpers.py:520
ZHAGroupProxy _async_get_or_create_group_proxy(self, GroupInfo group_info)
Definition: helpers.py:813
None handle_raw_device_initialized(self, RawDeviceInitializedEvent event)
Definition: helpers.py:654
None handle_group_member_removed(self, GroupEvent event)
Definition: helpers.py:698
None handle_device_fully_initialized(self, DeviceFullInitEvent event)
Definition: helpers.py:678
None async_disable_debug_mode(self, _LogFilterType|None filterer=None)
Definition: helpers.py:747
ZHADeviceProxy _async_get_or_create_device_proxy(self, Device zha_device)
Definition: helpers.py:795
None handle_device_left(self, DeviceLeftEvent event)
Definition: helpers.py:650
None _create_entity_metadata(self, ZHADeviceProxy|ZHAGroupProxy proxy_object)
Definition: helpers.py:825
None handle_connection_lost(self, ConnectionLostEvent event)
Definition: helpers.py:593
None register_entity_reference(self, str ha_entity_id, EntityData entity_data, dr.DeviceInfo ha_device_info, asyncio.Future[Any] remove_future)
Definition: helpers.py:530
None remove_entity_reference(self, ZHAEntity entity)
Definition: helpers.py:785
None handle_group_added(self, GroupEvent event)
Definition: helpers.py:716
None _async_remove_device(self, ZHADeviceProxy device, list[EntityReference]|None entity_refs)
Definition: helpers.py:912
ZHADeviceProxy|None get_device_proxy(self, EUI64 ieee)
Definition: helpers.py:763
ZHAGroupProxy|None get_group_proxy(self, int|str group_id)
Definition: helpers.py:767
None handle_device_joined(self, DeviceJoinedEvent event)
Definition: helpers.py:608
None __init__(self, HomeAssistant hass, ConfigEntry config_entry, Gateway gateway)
Definition: helpers.py:492
None _send_group_gateway_message(self, ZHAGroupProxy zha_group_proxy, str gateway_message_type)
Definition: helpers.py:899
None __init__(self, Group group, ZHAGatewayProxy gateway_proxy)
Definition: helpers.py:233
None log(self, int level, str msg, *Any args, **kwargs)
Definition: helpers.py:288
list[GroupEntityReference] associated_entities(self, GroupMember member)
Definition: helpers.py:256
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None _async_add_entities(AvmWrapper avm_wrapper, AddEntitiesCallback async_add_entities, FritzData data_fritz)
DeviceModel|None get_device(int device_id, list[DeviceModel] devices)
Definition: helpers.py:8
Gateway get_zha_gateway(HomeAssistant hass)
Definition: helpers.py:1028
None async_set_logger_levels(dict[str, int] levels)
Definition: helpers.py:950
vol.Schema cluster_command_schema_to_vol_schema(CommandSchema schema)
Definition: helpers.py:1070
dict[str, int] async_capture_log_levels()
Definition: helpers.py:927
Any schema_type_to_vol(Any field_type)
Definition: helpers.py:1082
dict[str, Any] convert_to_zcl_values(dict[str, Any] fields, CommandSchema schema)
Definition: helpers.py:1103
ZHAData create_zha_config(HomeAssistant hass, HAZHAData ha_zha_data)
Definition: helpers.py:1228
ZHAGatewayProxy get_zha_gateway_proxy(HomeAssistant hass)
Definition: helpers.py:1036
ZHADeviceProxy async_get_zha_device_proxy(HomeAssistant hass, str device_id)
Definition: helpers.py:1053
ConfigEntry get_config_entry(HomeAssistant hass)
Definition: helpers.py:1044
None async_add_entities(AddEntitiesCallback _async_add_entities, type[ZHAEntity] entity_class, list[EntityData] entities, **kwargs)
Definition: helpers.py:1161
HAZHAData get_zha_data(HomeAssistant hass)
Definition: helpers.py:1020
def async_cluster_exists(HomeAssistant hass, cluster_id, skip_coordinator=True)
Definition: helpers.py:1138
dict[str, Any] exclude_none_values(Mapping[str, Any] obj)
Definition: helpers.py:1342
AreaRegistry async_get(HomeAssistant hass)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193