Home Assistant Unofficial Reference 2024.12.1
accessories.py
Go to the documentation of this file.
1 """Extend the basic Accessory and Bridge functions."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, cast
7 from uuid import UUID
8 
9 from pyhap.accessory import Accessory, Bridge
10 from pyhap.accessory_driver import AccessoryDriver
11 from pyhap.characteristic import Characteristic
12 from pyhap.const import CATEGORY_OTHER
13 from pyhap.iid_manager import IIDManager
14 from pyhap.service import Service
15 from pyhap.util import callback as pyhap_callback
16 
17 from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature
18 from homeassistant.components.media_player import MediaPlayerDeviceClass
19 from homeassistant.components.remote import RemoteEntityFeature
20 from homeassistant.components.sensor import SensorDeviceClass
21 from homeassistant.components.switch import SwitchDeviceClass
22 from homeassistant.const import (
23  ATTR_BATTERY_CHARGING,
24  ATTR_BATTERY_LEVEL,
25  ATTR_DEVICE_CLASS,
26  ATTR_ENTITY_ID,
27  ATTR_HW_VERSION,
28  ATTR_MANUFACTURER,
29  ATTR_MODEL,
30  ATTR_SERVICE,
31  ATTR_SUPPORTED_FEATURES,
32  ATTR_SW_VERSION,
33  ATTR_UNIT_OF_MEASUREMENT,
34  CONF_NAME,
35  CONF_TYPE,
36  LIGHT_LUX,
37  PERCENTAGE,
38  STATE_ON,
39  STATE_UNAVAILABLE,
40  STATE_UNKNOWN,
41  UnitOfTemperature,
42  __version__,
43 )
44 from homeassistant.core import (
45  CALLBACK_TYPE,
46  Context,
47  Event,
48  EventStateChangedData,
49  HassJobType,
50  HomeAssistant,
51  State,
52  callback as ha_callback,
53  split_entity_id,
54 )
55 from homeassistant.helpers.dispatcher import async_dispatcher_send
56 from homeassistant.helpers.event import async_track_state_change_event
57 from homeassistant.util.decorator import Registry
58 
59 from .const import (
60  ATTR_DISPLAY_NAME,
61  ATTR_INTEGRATION,
62  ATTR_VALUE,
63  BRIDGE_MODEL,
64  BRIDGE_SERIAL_NUMBER,
65  CHAR_BATTERY_LEVEL,
66  CHAR_CHARGING_STATE,
67  CHAR_HARDWARE_REVISION,
68  CHAR_STATUS_LOW_BATTERY,
69  CONF_FEATURE_LIST,
70  CONF_LINKED_BATTERY_CHARGING_SENSOR,
71  CONF_LINKED_BATTERY_SENSOR,
72  CONF_LOW_BATTERY_THRESHOLD,
73  DEFAULT_LOW_BATTERY_THRESHOLD,
74  EMPTY_MAC,
75  EVENT_HOMEKIT_CHANGED,
76  HK_CHARGING,
77  HK_NOT_CHARGABLE,
78  HK_NOT_CHARGING,
79  MANUFACTURER,
80  MAX_MANUFACTURER_LENGTH,
81  MAX_MODEL_LENGTH,
82  MAX_SERIAL_LENGTH,
83  MAX_VERSION_LENGTH,
84  SERV_ACCESSORY_INFO,
85  SERV_BATTERY_SERVICE,
86  SIGNAL_RELOAD_ENTITIES,
87  TYPE_FAUCET,
88  TYPE_OUTLET,
89  TYPE_SHOWER,
90  TYPE_SPRINKLER,
91  TYPE_SWITCH,
92  TYPE_VALVE,
93 )
94 from .iidmanager import AccessoryIIDStorage
95 from .util import (
96  accessory_friendly_name,
97  async_dismiss_setup_message,
98  async_show_setup_message,
99  cleanup_name_for_homekit,
100  convert_to_float,
101  format_version,
102  validate_media_player_features,
103 )
104 
105 _LOGGER = logging.getLogger(__name__)
106 SWITCH_TYPES = {
107  TYPE_FAUCET: "ValveSwitch",
108  TYPE_OUTLET: "Outlet",
109  TYPE_SHOWER: "ValveSwitch",
110  TYPE_SPRINKLER: "ValveSwitch",
111  TYPE_SWITCH: "Switch",
112  TYPE_VALVE: "ValveSwitch",
113 }
114 TYPES: Registry[str, type[HomeAccessory]] = Registry()
115 
116 RELOAD_ON_CHANGE_ATTRS = (
117  ATTR_SUPPORTED_FEATURES,
118  ATTR_DEVICE_CLASS,
119  ATTR_UNIT_OF_MEASUREMENT,
120 )
121 
122 
123 def get_accessory( # noqa: C901
124  hass: HomeAssistant, driver: HomeDriver, state: State, aid: int | None, config: dict
125 ) -> HomeAccessory | None:
126  """Take state and return an accessory object if supported."""
127  if not aid:
128  _LOGGER.warning(
129  (
130  'The entity "%s" is not supported, since it '
131  "generates an invalid aid, please change it"
132  ),
133  state.entity_id,
134  )
135  return None
136 
137  a_type = None
138  name = config.get(CONF_NAME, state.name)
139  features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
140 
141  if state.domain == "alarm_control_panel":
142  a_type = "SecuritySystem"
143 
144  elif state.domain in ("binary_sensor", "device_tracker", "person"):
145  a_type = "BinarySensor"
146 
147  elif state.domain == "climate":
148  a_type = "Thermostat"
149 
150  elif state.domain == "cover":
151  device_class = state.attributes.get(ATTR_DEVICE_CLASS)
152 
153  if device_class in (
154  CoverDeviceClass.GARAGE,
155  CoverDeviceClass.GATE,
156  ) and features & (CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE):
157  a_type = "GarageDoorOpener"
158  elif (
159  device_class == CoverDeviceClass.WINDOW
160  and features & CoverEntityFeature.SET_POSITION
161  ):
162  a_type = "Window"
163  elif (
164  device_class == CoverDeviceClass.DOOR
165  and features & CoverEntityFeature.SET_POSITION
166  ):
167  a_type = "Door"
168  elif features & CoverEntityFeature.SET_POSITION:
169  a_type = "WindowCovering"
170  elif features & (CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE):
171  a_type = "WindowCoveringBasic"
172  elif features & CoverEntityFeature.SET_TILT_POSITION:
173  # WindowCovering and WindowCoveringBasic both support tilt
174  # only WindowCovering can handle the covers that are missing
175  # CoverEntityFeature.SET_POSITION, CoverEntityFeature.OPEN,
176  # and CoverEntityFeature.CLOSE
177  a_type = "WindowCovering"
178 
179  elif state.domain == "fan":
180  a_type = "Fan"
181 
182  elif state.domain == "humidifier":
183  a_type = "HumidifierDehumidifier"
184 
185  elif state.domain == "light":
186  a_type = "Light"
187 
188  elif state.domain == "lock":
189  a_type = "Lock"
190 
191  elif state.domain == "media_player":
192  device_class = state.attributes.get(ATTR_DEVICE_CLASS)
193  feature_list = config.get(CONF_FEATURE_LIST, [])
194 
195  if device_class == MediaPlayerDeviceClass.RECEIVER:
196  a_type = "ReceiverMediaPlayer"
197  elif device_class == MediaPlayerDeviceClass.TV:
198  a_type = "TelevisionMediaPlayer"
199  elif validate_media_player_features(state, feature_list):
200  a_type = "MediaPlayer"
201 
202  elif state.domain == "sensor":
203  device_class = state.attributes.get(ATTR_DEVICE_CLASS)
204  unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
205 
206  if device_class == SensorDeviceClass.TEMPERATURE or unit in (
207  UnitOfTemperature.CELSIUS,
208  UnitOfTemperature.FAHRENHEIT,
209  ):
210  a_type = "TemperatureSensor"
211  elif device_class == SensorDeviceClass.HUMIDITY and unit == PERCENTAGE:
212  a_type = "HumiditySensor"
213  elif (
214  device_class == SensorDeviceClass.PM10
215  or SensorDeviceClass.PM10 in state.entity_id
216  ):
217  a_type = "PM10Sensor"
218  elif (
219  device_class == SensorDeviceClass.PM25
220  or SensorDeviceClass.PM25 in state.entity_id
221  ):
222  a_type = "PM25Sensor"
223  elif device_class == SensorDeviceClass.NITROGEN_DIOXIDE:
224  a_type = "NitrogenDioxideSensor"
225  elif device_class == SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS:
226  a_type = "VolatileOrganicCompoundsSensor"
227  elif (
228  device_class == SensorDeviceClass.GAS
229  or SensorDeviceClass.GAS in state.entity_id
230  ):
231  a_type = "AirQualitySensor"
232  elif device_class == SensorDeviceClass.CO:
233  a_type = "CarbonMonoxideSensor"
234  elif device_class == SensorDeviceClass.CO2 or "co2" in state.entity_id:
235  a_type = "CarbonDioxideSensor"
236  elif device_class == SensorDeviceClass.ILLUMINANCE or unit == LIGHT_LUX:
237  a_type = "LightSensor"
238 
239  elif state.domain == "switch":
240  if switch_type := config.get(CONF_TYPE):
241  a_type = SWITCH_TYPES[switch_type]
242  elif state.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.OUTLET:
243  a_type = "Outlet"
244  else:
245  a_type = "Switch"
246 
247  elif state.domain == "valve":
248  a_type = "Valve"
249 
250  elif state.domain == "vacuum":
251  a_type = "Vacuum"
252 
253  elif state.domain == "remote" and features & RemoteEntityFeature.ACTIVITY:
254  a_type = "ActivityRemote"
255 
256  elif state.domain in (
257  "automation",
258  "button",
259  "input_boolean",
260  "input_button",
261  "remote",
262  "scene",
263  "script",
264  ):
265  a_type = "Switch"
266 
267  elif state.domain in ("input_select", "select"):
268  a_type = "SelectSwitch"
269 
270  elif state.domain == "water_heater":
271  a_type = "WaterHeater"
272 
273  elif state.domain == "camera":
274  a_type = "Camera"
275 
276  if a_type is None:
277  return None
278 
279  _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type)
280  return TYPES[a_type](hass, driver, name, state.entity_id, aid, config)
281 
282 
283 class HomeAccessory(Accessory): # type: ignore[misc]
284  """Adapter class for Accessory."""
285 
286  driver: HomeDriver
287 
288  def __init__(
289  self,
290  hass: HomeAssistant,
291  driver: HomeDriver,
292  name: str,
293  entity_id: str,
294  aid: int,
295  config: dict[str, Any],
296  *args: Any,
297  category: int = CATEGORY_OTHER,
298  device_id: str | None = None,
299  **kwargs: Any,
300  ) -> None:
301  """Initialize a Accessory object."""
302  super().__init__(
303  driver=driver,
304  display_name=cleanup_name_for_homekit(name),
305  aid=aid,
306  iid_manager=HomeIIDManager(driver.iid_storage),
307  *args, # noqa: B026
308  **kwargs,
309  )
310  self._reload_on_change_attrs_reload_on_change_attrs = list(RELOAD_ON_CHANGE_ATTRS)
311  self.configconfig = config or {}
312  if device_id:
313  self.device_iddevice_id: str | None = device_id
314  serial_number = device_id
315  domain = None
316  else:
317  self.device_iddevice_id = None
318  serial_number = entity_id
319  domain = split_entity_id(entity_id)[0].replace("_", " ")
320 
321  if self.configconfig.get(ATTR_MANUFACTURER) is not None:
322  manufacturer = str(self.configconfig[ATTR_MANUFACTURER])
323  elif self.configconfig.get(ATTR_INTEGRATION) is not None:
324  manufacturer = self.configconfig[ATTR_INTEGRATION].replace("_", " ").title()
325  elif domain:
326  manufacturer = f"{MANUFACTURER} {domain}".title()
327  else:
328  manufacturer = MANUFACTURER
329  if self.configconfig.get(ATTR_MODEL) is not None:
330  model = str(self.configconfig[ATTR_MODEL])
331  elif domain:
332  model = domain.title()
333  else:
334  model = MANUFACTURER
335  sw_version = None
336  if self.configconfig.get(ATTR_SW_VERSION) is not None:
337  sw_version = format_version(self.configconfig[ATTR_SW_VERSION])
338  if sw_version is None:
339  sw_version = format_version(__version__)
340  assert sw_version is not None
341  hw_version = None
342  if self.configconfig.get(ATTR_HW_VERSION) is not None:
343  hw_version = format_version(self.configconfig[ATTR_HW_VERSION])
344 
345  self.set_info_service(
346  manufacturer=manufacturer[:MAX_MANUFACTURER_LENGTH],
347  model=model[:MAX_MODEL_LENGTH],
348  serial_number=serial_number[:MAX_SERIAL_LENGTH],
349  firmware_revision=sw_version[:MAX_VERSION_LENGTH],
350  )
351  if hw_version:
352  serv_info = self.get_service(SERV_ACCESSORY_INFO)
353  char = self.driver.loader.get_char(CHAR_HARDWARE_REVISION)
354  serv_info.add_characteristic(char)
355  serv_info.configure_char(
356  CHAR_HARDWARE_REVISION, value=hw_version[:MAX_VERSION_LENGTH]
357  )
358  char.broker = self
359  self.iid_manager.assign(char)
360 
361  self.categorycategory = category
362  self.entity_identity_id = entity_id
363  self.hasshass = hass
364  self._subscriptions: list[CALLBACK_TYPE] = []
365 
366  if device_id:
367  return
368 
369  self._char_battery_char_battery = None
370  self._char_charging_char_charging = None
371  self._char_low_battery_char_low_battery = None
372  self.linked_battery_sensorlinked_battery_sensor = self.configconfig.get(CONF_LINKED_BATTERY_SENSOR)
373  self.linked_battery_charging_sensorlinked_battery_charging_sensor = self.configconfig.get(
374  CONF_LINKED_BATTERY_CHARGING_SENSOR
375  )
376  self.low_battery_thresholdlow_battery_threshold = self.configconfig.get(
377  CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD
378  )
379 
380  """Add battery service if available"""
381  state = self.hasshass.states.get(self.entity_identity_id)
382  self._update_available_from_state_update_available_from_state(state)
383  assert state is not None
384  entity_attributes = state.attributes
385  battery_found = entity_attributes.get(ATTR_BATTERY_LEVEL)
386 
387  if self.linked_battery_sensorlinked_battery_sensor:
388  state = self.hasshass.states.get(self.linked_battery_sensorlinked_battery_sensor)
389  if state is not None:
390  battery_found = state.state
391  else:
392  _LOGGER.warning(
393  "%s: Battery sensor state missing: %s",
394  self.entity_identity_id,
395  self.linked_battery_sensorlinked_battery_sensor,
396  )
397  self.linked_battery_sensorlinked_battery_sensor = None
398 
399  if not battery_found:
400  return
401 
402  _LOGGER.debug("%s: Found battery level", self.entity_identity_id)
403 
404  if self.linked_battery_charging_sensorlinked_battery_charging_sensor:
405  state = self.hasshass.states.get(self.linked_battery_charging_sensorlinked_battery_charging_sensor)
406  if state is None:
407  self.linked_battery_charging_sensorlinked_battery_charging_sensor = None
408  _LOGGER.warning(
409  "%s: Battery charging binary_sensor state missing: %s",
410  self.entity_identity_id,
411  self.linked_battery_charging_sensorlinked_battery_charging_sensor,
412  )
413  else:
414  _LOGGER.debug("%s: Found battery charging", self.entity_identity_id)
415 
416  serv_battery = self.add_preload_service(SERV_BATTERY_SERVICE)
417  self._char_battery_char_battery = serv_battery.configure_char(CHAR_BATTERY_LEVEL, value=0)
418  self._char_charging_char_charging = serv_battery.configure_char(
419  CHAR_CHARGING_STATE, value=HK_NOT_CHARGABLE
420  )
421  self._char_low_battery_char_low_battery = serv_battery.configure_char(
422  CHAR_STATUS_LOW_BATTERY, value=0
423  )
424 
425  def _update_available_from_state(self, new_state: State | None) -> None:
426  """Update the available property based on the state."""
427  self._available_available = new_state is not None and new_state.state != STATE_UNAVAILABLE
428 
429  @property
430  def available(self) -> bool:
431  """Return if accessory is available."""
432  return self._available_available
433 
434  @ha_callback
435  @pyhap_callback # type: ignore[misc]
436  def run(self) -> None:
437  """Handle accessory driver started event."""
438  if state := self.hasshass.states.get(self.entity_identity_id):
439  self.async_update_state_callbackasync_update_state_callback(state)
440  self._update_available_from_state_update_available_from_state(state)
441  self._subscriptions.append(
443  self.hasshass,
444  [self.entity_identity_id],
445  self.async_update_event_state_callbackasync_update_event_state_callback,
446  job_type=HassJobType.Callback,
447  )
448  )
449 
450  battery_charging_state = None
451  battery_state = None
452  if self.linked_battery_sensorlinked_battery_sensor and (
453  linked_battery_sensor_state := self.hasshass.states.get(
454  self.linked_battery_sensorlinked_battery_sensor
455  )
456  ):
457  battery_state = linked_battery_sensor_state.state
458  battery_charging_state = linked_battery_sensor_state.attributes.get(
459  ATTR_BATTERY_CHARGING
460  )
461  self._subscriptions.append(
463  self.hasshass,
464  [self.linked_battery_sensorlinked_battery_sensor],
465  self.async_update_linked_battery_callbackasync_update_linked_battery_callback,
466  job_type=HassJobType.Callback,
467  )
468  )
469  elif state is not None:
470  battery_state = state.attributes.get(ATTR_BATTERY_LEVEL)
471  if self.linked_battery_charging_sensorlinked_battery_charging_sensor:
472  state = self.hasshass.states.get(self.linked_battery_charging_sensorlinked_battery_charging_sensor)
473  battery_charging_state = state and state.state == STATE_ON
474  self._subscriptions.append(
476  self.hasshass,
477  [self.linked_battery_charging_sensorlinked_battery_charging_sensor],
478  self.async_update_linked_battery_charging_callbackasync_update_linked_battery_charging_callback,
479  job_type=HassJobType.Callback,
480  )
481  )
482  elif battery_charging_state is None and state is not None:
483  battery_charging_state = state.attributes.get(ATTR_BATTERY_CHARGING)
484 
485  if battery_state is not None or battery_charging_state is not None:
486  self.async_update_batteryasync_update_battery(battery_state, battery_charging_state)
487 
488  @ha_callback
490  self, event: Event[EventStateChangedData]
491  ) -> None:
492  """Handle state change event listener callback."""
493  new_state = event.data["new_state"]
494  old_state = event.data["old_state"]
495  self._update_available_from_state_update_available_from_state(new_state)
496  if (
497  new_state
498  and old_state
499  and STATE_UNAVAILABLE not in (old_state.state, new_state.state)
500  ):
501  old_attributes = old_state.attributes
502  new_attributes = new_state.attributes
503  for attr in self._reload_on_change_attrs_reload_on_change_attrs:
504  if old_attributes.get(attr) != new_attributes.get(attr):
505  _LOGGER.debug(
506  "%s: Reloading HomeKit accessory since %s has changed from %s -> %s",
507  self.entity_identity_id,
508  attr,
509  old_attributes.get(attr),
510  new_attributes.get(attr),
511  )
512  self.async_reloadasync_reload()
513  return
514  self.async_update_state_callbackasync_update_state_callback(new_state)
515 
516  @ha_callback
517  def async_update_state_callback(self, new_state: State | None) -> None:
518  """Handle state change listener callback."""
519  _LOGGER.debug("New_state: %s", new_state)
520  # HomeKit handles unavailable state via the available property
521  # so we should not propagate it here
522  if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
523  return
524  battery_state = None
525  battery_charging_state = None
526  if (
527  not self.linked_battery_sensorlinked_battery_sensor
528  and ATTR_BATTERY_LEVEL in new_state.attributes
529  ):
530  battery_state = new_state.attributes.get(ATTR_BATTERY_LEVEL)
531  if (
532  not self.linked_battery_charging_sensorlinked_battery_charging_sensor
533  and ATTR_BATTERY_CHARGING in new_state.attributes
534  ):
535  battery_charging_state = new_state.attributes.get(ATTR_BATTERY_CHARGING)
536  if battery_state is not None or battery_charging_state is not None:
537  self.async_update_batteryasync_update_battery(battery_state, battery_charging_state)
538  self.async_update_stateasync_update_state(new_state)
539 
540  @ha_callback
542  self, event: Event[EventStateChangedData]
543  ) -> None:
544  """Handle linked battery sensor state change listener callback."""
545  if (new_state := event.data["new_state"]) is None:
546  return
547  if self.linked_battery_charging_sensorlinked_battery_charging_sensor:
548  battery_charging_state = None
549  else:
550  battery_charging_state = new_state.attributes.get(ATTR_BATTERY_CHARGING)
551  self.async_update_batteryasync_update_battery(new_state.state, battery_charging_state)
552 
553  @ha_callback
555  self, event: Event[EventStateChangedData]
556  ) -> None:
557  """Handle linked battery charging sensor state change listener callback."""
558  if (new_state := event.data["new_state"]) is None:
559  return
560  self.async_update_batteryasync_update_battery(None, new_state.state == STATE_ON)
561 
562  @ha_callback
563  def async_update_battery(self, battery_level: Any, battery_charging: Any) -> None:
564  """Update battery service if available.
565 
566  Only call this function if self._support_battery_level is True.
567  """
568  if not self._char_battery_char_battery or not self._char_low_battery_char_low_battery:
569  # Battery appeared after homekit was started
570  return
571 
572  battery_level = convert_to_float(battery_level)
573  if battery_level is not None:
574  if self._char_battery_char_battery.value != battery_level:
575  self._char_battery_char_battery.set_value(battery_level)
576  is_low_battery = 1 if battery_level < self.low_battery_thresholdlow_battery_threshold else 0
577  if self._char_low_battery_char_low_battery.value != is_low_battery:
578  self._char_low_battery_char_low_battery.set_value(is_low_battery)
579  _LOGGER.debug(
580  "%s: Updated battery level to %d", self.entity_identity_id, battery_level
581  )
582 
583  # Charging state can appear after homekit was started
584  if battery_charging is None or not self._char_charging_char_charging:
585  return
586 
587  hk_charging = HK_CHARGING if battery_charging else HK_NOT_CHARGING
588  if self._char_charging_char_charging.value != hk_charging:
589  self._char_charging_char_charging.set_value(hk_charging)
590  _LOGGER.debug(
591  "%s: Updated battery charging to %d", self.entity_identity_id, hk_charging
592  )
593 
594  @ha_callback
595  def async_update_state(self, new_state: State) -> None:
596  """Handle state change to update HomeKit value.
597 
598  Overridden by accessory types.
599  """
600  raise NotImplementedError
601 
602  @ha_callback
604  self,
605  domain: str,
606  service: str,
607  service_data: dict[str, Any] | None,
608  value: Any | None = None,
609  ) -> None:
610  """Fire event and call service for changes from HomeKit."""
611  event_data = {
612  ATTR_ENTITY_ID: self.entity_identity_id,
613  ATTR_DISPLAY_NAME: self.display_name,
614  ATTR_SERVICE: service,
615  ATTR_VALUE: value,
616  }
617  context = Context()
618 
619  self.hasshass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data, context=context)
620  self.hasshass.async_create_task(
621  self.hasshass.services.async_call(
622  domain, service, service_data, context=context
623  ),
624  eager_start=True,
625  )
626 
627  @ha_callback
628  def async_reload(self) -> None:
629  """Reload and recreate an accessory and update the c# value in the mDNS record."""
631  self.hasshass,
632  SIGNAL_RELOAD_ENTITIES.format(self.driver.entry_id),
633  (self.entity_identity_id,),
634  )
635 
636  @ha_callback
637  def async_stop(self) -> None:
638  """Cancel any subscriptions when the bridge is stopped."""
639  while self._subscriptions:
640  self._subscriptions.pop(0)()
641 
642  async def stop(self) -> None:
643  """Stop the accessory.
644 
645  This is overrides the parent class to call async_stop
646  since pyhap will call this function to stop the accessory
647  but we want to use our async_stop method since we need
648  it to be a callback to avoid races in reloading accessories.
649  """
650  self.async_stopasync_stop()
651 
652 
653 class HomeBridge(Bridge): # type: ignore[misc]
654  """Adapter class for Bridge."""
655 
656  def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None:
657  """Initialize a Bridge object."""
658  super().__init__(driver, name, iid_manager=HomeIIDManager(driver.iid_storage))
659  self.set_info_service(
660  firmware_revision=format_version(__version__),
661  manufacturer=MANUFACTURER,
662  model=BRIDGE_MODEL,
663  serial_number=BRIDGE_SERIAL_NUMBER,
664  )
665  self.hasshass = hass
666 
667  def setup_message(self) -> None:
668  """Prevent print of pyhap setup message to terminal."""
669 
670  async def async_get_snapshot(self, info: dict) -> bytes:
671  """Get snapshot from accessory if supported."""
672  if (acc := self.accessories.get(info["aid"])) is None:
673  raise ValueError("Requested snapshot for missing accessory")
674  if not hasattr(acc, "async_get_snapshot"):
675  raise ValueError(
676  "Got a request for snapshot, but the Accessory "
677  'does not define a "async_get_snapshot" method'
678  )
679  return cast(bytes, await acc.async_get_snapshot(info))
680 
681 
682 class HomeDriver(AccessoryDriver): # type: ignore[misc]
683  """Adapter class for AccessoryDriver."""
684 
685  def __init__(
686  self,
687  hass: HomeAssistant,
688  entry_id: str,
689  bridge_name: str,
690  entry_title: str,
691  iid_storage: AccessoryIIDStorage,
692  **kwargs: Any,
693  ) -> None:
694  """Initialize a AccessoryDriver object."""
695  # Always set an empty mac of pyhap will incur
696  # the cost of generating a new one for every driver
697  super().__init__(**kwargs, mac=EMPTY_MAC)
698  self.hasshass = hass
699  self.entry_identry_id = entry_id
700  self._bridge_name_bridge_name = bridge_name
701  self._entry_title_entry_title = entry_title
702  self.iid_storageiid_storage = iid_storage
703 
704  @pyhap_callback # type: ignore[misc]
705  def pair(
706  self, client_username_bytes: bytes, client_public: str, client_permissions: int
707  ) -> bool:
708  """Override super function to dismiss setup message if paired."""
709  success = super().pair(client_username_bytes, client_public, client_permissions)
710  if success:
711  async_dismiss_setup_message(self.hasshass, self.entry_identry_id)
712  return cast(bool, success)
713 
714  @pyhap_callback # type: ignore[misc]
715  def unpair(self, client_uuid: UUID) -> None:
716  """Override super function to show setup message if unpaired."""
717  super().unpair(client_uuid)
718 
719  if self.state.paired:
720  return
721 
722  async_show_setup_message(
723  self.hasshass,
724  self.entry_identry_id,
725  accessory_friendly_name(self._entry_title_entry_title, self.accessory),
726  self.state.pincode,
727  self.accessory.xhm_uri(),
728  )
729 
730 
731 class HomeIIDManager(IIDManager): # type: ignore[misc]
732  """IID Manager that remembers IIDs between restarts."""
733 
734  def __init__(self, iid_storage: AccessoryIIDStorage) -> None:
735  """Initialize a IIDManager object."""
736  super().__init__()
737  self._iid_storage_iid_storage = iid_storage
738 
739  def get_iid_for_obj(self, obj: Characteristic | Service) -> int:
740  """Get IID for object."""
741  aid = obj.broker.aid
742  if isinstance(obj, Characteristic):
743  service: Service = obj.service
744  iid = self._iid_storage_iid_storage.get_or_allocate_iid(
745  aid, service.type_id, service.unique_id, obj.type_id, obj.unique_id
746  )
747  else:
748  iid = self._iid_storage_iid_storage.get_or_allocate_iid(
749  aid, obj.type_id, obj.unique_id, None, None
750  )
751  if iid in self.objs:
752  raise RuntimeError(
753  f"Cannot assign IID {iid} to {obj} as it is already in use by:"
754  f" {self.objs[iid]}"
755  )
756  return iid
None _update_available_from_state(self, State|None new_state)
Definition: accessories.py:425
None async_call_service(self, str domain, str service, dict[str, Any]|None service_data, Any|None value=None)
Definition: accessories.py:609
None async_update_event_state_callback(self, Event[EventStateChangedData] event)
Definition: accessories.py:491
None async_update_linked_battery_charging_callback(self, Event[EventStateChangedData] event)
Definition: accessories.py:556
None async_update_linked_battery_callback(self, Event[EventStateChangedData] event)
Definition: accessories.py:543
None __init__(self, HomeAssistant hass, HomeDriver driver, str name, str entity_id, int aid, dict[str, Any] config, *Any args, int category=CATEGORY_OTHER, str|None device_id=None, **Any kwargs)
Definition: accessories.py:300
None async_update_state_callback(self, State|None new_state)
Definition: accessories.py:517
None async_update_battery(self, Any battery_level, Any battery_charging)
Definition: accessories.py:563
None __init__(self, HomeAssistant hass, HomeDriver driver, str name)
Definition: accessories.py:656
bool pair(self, bytes client_username_bytes, str client_public, int client_permissions)
Definition: accessories.py:707
None __init__(self, HomeAssistant hass, str entry_id, str bridge_name, str entry_title, AccessoryIIDStorage iid_storage, **Any kwargs)
Definition: accessories.py:693
int get_iid_for_obj(self, Characteristic|Service obj)
Definition: accessories.py:739
None __init__(self, AccessoryIIDStorage iid_storage)
Definition: accessories.py:734
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
HomeAccessory|None get_accessory(HomeAssistant hass, HomeDriver driver, State state, int|None aid, dict config)
Definition: accessories.py:125
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:314