Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Apple HomeKit."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections import defaultdict
7 from collections.abc import Iterable
8 from copy import deepcopy
9 import ipaddress
10 import logging
11 import os
12 import socket
13 from typing import Any, cast
14 
15 from aiohttp import web
16 from pyhap import util as pyhap_util
17 from pyhap.characteristic import Characteristic
18 from pyhap.const import STANDALONE_AID
19 from pyhap.loader import get_loader
20 from pyhap.service import Service
21 import voluptuous as vol
22 from zeroconf.asyncio import AsyncZeroconf
23 
24 from homeassistant.components import device_automation, network, zeroconf
26  DOMAIN as BINARY_SENSOR_DOMAIN,
27  BinarySensorDeviceClass,
28 )
29 from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
31  async_validate_trigger_config,
32 )
33 from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventDeviceClass
34 from homeassistant.components.http import KEY_HASS, HomeAssistantView
35 from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN
36 from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
37 from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
38 from homeassistant.const import (
39  ATTR_BATTERY_CHARGING,
40  ATTR_BATTERY_LEVEL,
41  ATTR_DEVICE_ID,
42  ATTR_ENTITY_ID,
43  ATTR_HW_VERSION,
44  ATTR_MANUFACTURER,
45  ATTR_MODEL,
46  ATTR_SW_VERSION,
47  CONF_DEVICES,
48  CONF_IP_ADDRESS,
49  CONF_NAME,
50  CONF_PORT,
51  EVENT_HOMEASSISTANT_STOP,
52  SERVICE_RELOAD,
53 )
54 from homeassistant.core import (
55  CALLBACK_TYPE,
56  HomeAssistant,
57  ServiceCall,
58  State,
59  callback,
60 )
61 from homeassistant.exceptions import HomeAssistantError, Unauthorized
62 from homeassistant.helpers import (
63  config_validation as cv,
64  device_registry as dr,
65  entity_registry as er,
66  instance_id,
67 )
68 from homeassistant.helpers.dispatcher import async_dispatcher_connect
70  BASE_FILTER_SCHEMA,
71  FILTER_SCHEMA,
72  EntityFilter,
73 )
74 from homeassistant.helpers.reload import async_integration_yaml_config
76  async_extract_referenced_entity_ids,
77  async_register_admin_service,
78 )
79 from homeassistant.helpers.start import async_at_started
80 from homeassistant.helpers.typing import ConfigType
81 from homeassistant.loader import IntegrationNotFound, async_get_integration
82 from homeassistant.util.async_ import create_eager_task
83 
84 from . import ( # noqa: F401
85  type_cameras,
86  type_covers,
87  type_fans,
88  type_humidifiers,
89  type_lights,
90  type_locks,
91  type_media_players,
92  type_remotes,
93  type_security_systems,
94  type_sensors,
95  type_switches,
96  type_thermostats,
97 )
98 from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory
99 from .aidmanager import AccessoryAidStorage
100 from .const import (
101  ATTR_INTEGRATION,
102  BRIDGE_NAME,
103  BRIDGE_SERIAL_NUMBER,
104  CONF_ADVERTISE_IP,
105  CONF_ENTITY_CONFIG,
106  CONF_ENTRY_INDEX,
107  CONF_EXCLUDE_ACCESSORY_MODE,
108  CONF_FILTER,
109  CONF_HOMEKIT_MODE,
110  CONF_LINKED_BATTERY_CHARGING_SENSOR,
111  CONF_LINKED_BATTERY_SENSOR,
112  CONF_LINKED_DOORBELL_SENSOR,
113  CONF_LINKED_HUMIDITY_SENSOR,
114  CONF_LINKED_MOTION_SENSOR,
115  CONFIG_OPTIONS,
116  DEFAULT_EXCLUDE_ACCESSORY_MODE,
117  DEFAULT_HOMEKIT_MODE,
118  DEFAULT_PORT,
119  DOMAIN,
120  HOMEKIT_MODE_ACCESSORY,
121  HOMEKIT_MODES,
122  MANUFACTURER,
123  PERSIST_LOCK_DATA,
124  SERVICE_HOMEKIT_RESET_ACCESSORY,
125  SERVICE_HOMEKIT_UNPAIR,
126  SHUTDOWN_TIMEOUT,
127  SIGNAL_RELOAD_ENTITIES,
128 )
129 from .iidmanager import AccessoryIIDStorage
130 from .models import HomeKitConfigEntry, HomeKitEntryData
131 from .type_triggers import DeviceTriggerAccessory
132 from .util import (
133  accessory_friendly_name,
134  async_dismiss_setup_message,
135  async_port_is_available,
136  async_show_setup_message,
137  get_persist_fullpath_for_entry_id,
138  remove_state_files_for_entry_id,
139  state_needs_accessory_mode,
140  validate_entity_config,
141 )
142 
143 _LOGGER = logging.getLogger(__name__)
144 
145 MAX_DEVICES = 150 # includes the bridge
146 
147 # #### Driver Status ####
148 STATUS_READY = 0
149 STATUS_RUNNING = 1
150 STATUS_STOPPED = 2
151 STATUS_WAIT = 3
152 
153 PORT_CLEANUP_CHECK_INTERVAL_SECS = 1
154 
155 _HOMEKIT_CONFIG_UPDATE_TIME = (
156  10 # number of seconds to wait for homekit to see the c# change
157 )
158 _HAS_IPV6 = hasattr(socket, "AF_INET6")
159 _DEFAULT_BIND = ["0.0.0.0", "::"] if _HAS_IPV6 else ["0.0.0.0"]
160 
161 
162 BATTERY_CHARGING_SENSOR = (
163  BINARY_SENSOR_DOMAIN,
164  BinarySensorDeviceClass.BATTERY_CHARGING,
165 )
166 BATTERY_SENSOR = (SENSOR_DOMAIN, SensorDeviceClass.BATTERY)
167 MOTION_EVENT_SENSOR = (EVENT_DOMAIN, EventDeviceClass.MOTION)
168 MOTION_SENSOR = (BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass.MOTION)
169 DOORBELL_EVENT_SENSOR = (EVENT_DOMAIN, EventDeviceClass.DOORBELL)
170 HUMIDITY_SENSOR = (SENSOR_DOMAIN, SensorDeviceClass.HUMIDITY)
171 
172 
174  bridges: list[dict[str, Any]],
175 ) -> list[dict[str, Any]]:
176  """Validate that each homekit bridge configured has a unique name."""
177  names = [bridge[CONF_NAME] for bridge in bridges]
178  ports = [bridge[CONF_PORT] for bridge in bridges]
179  vol.Schema(vol.Unique())(names)
180  vol.Schema(vol.Unique())(ports)
181  return bridges
182 
183 
184 BRIDGE_SCHEMA = vol.All(
185  vol.Schema(
186  {
187  vol.Optional(CONF_HOMEKIT_MODE, default=DEFAULT_HOMEKIT_MODE): vol.In(
188  HOMEKIT_MODES
189  ),
190  vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All(
191  cv.string, vol.Length(min=3, max=25)
192  ),
193  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
194  vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string),
195  vol.Optional(CONF_ADVERTISE_IP): vol.All(
196  cv.ensure_list, [ipaddress.ip_address], [cv.string]
197  ),
198  vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA,
199  vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
200  vol.Optional(CONF_DEVICES): cv.ensure_list,
201  },
202  extra=vol.ALLOW_EXTRA,
203  ),
204 )
205 
206 CONFIG_SCHEMA = vol.Schema(
207  {DOMAIN: vol.All(cv.ensure_list, [BRIDGE_SCHEMA], _has_all_unique_names_and_ports)},
208  extra=vol.ALLOW_EXTRA,
209 )
210 
211 
212 RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema(
213  {vol.Required(ATTR_ENTITY_ID): cv.entity_ids}
214 )
215 
216 
217 UNPAIR_SERVICE_SCHEMA = vol.All(
218  vol.Schema(cv.ENTITY_SERVICE_FIELDS),
219  cv.has_at_least_one_key(ATTR_DEVICE_ID),
220 )
221 
222 
223 def _async_all_homekit_instances(hass: HomeAssistant) -> list[HomeKit]:
224  """All active HomeKit instances."""
225  hk_data: HomeKitEntryData | None
226  return [
227  hk_data.homekit
228  for entry in hass.config_entries.async_entries(DOMAIN)
229  if (hk_data := getattr(entry, "runtime_data", None))
230  ]
231 
232 
234  current_entries: list[ConfigEntry],
235 ) -> tuple[dict[str, ConfigEntry], dict[int, ConfigEntry]]:
236  """Return a dicts of the entries by name and port."""
237 
238  # For backwards compat, its possible the first bridge is using the default
239  # name.
240  entries_by_name: dict[str, ConfigEntry] = {}
241  entries_by_port: dict[int, ConfigEntry] = {}
242  for entry in current_entries:
243  if entry.source != SOURCE_IMPORT:
244  continue
245  entries_by_name[entry.data.get(CONF_NAME, BRIDGE_NAME)] = entry
246  entries_by_port[entry.data.get(CONF_PORT, DEFAULT_PORT)] = entry
247  return entries_by_name, entries_by_port
248 
249 
250 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
251  """Set up the HomeKit from yaml."""
252  hass.data[PERSIST_LOCK_DATA] = asyncio.Lock()
253 
254  # Initialize the loader before loading entries to ensure
255  # there is no race where multiple entries try to load it
256  # at the same time.
257  await hass.async_add_executor_job(get_loader)
258 
260 
261  if DOMAIN not in config:
262  return True
263 
264  current_entries = hass.config_entries.async_entries(DOMAIN)
265  entries_by_name, entries_by_port = _async_get_imported_entries_indices(
266  current_entries
267  )
268 
269  for index, conf in enumerate(config[DOMAIN]):
271  hass, entries_by_name, entries_by_port, conf
272  ):
273  continue
274 
275  conf[CONF_ENTRY_INDEX] = index
276  hass.async_create_task(
277  hass.config_entries.flow.async_init(
278  DOMAIN,
279  context={"source": SOURCE_IMPORT},
280  data=conf,
281  ),
282  eager_start=True,
283  )
284 
285  return True
286 
287 
288 @callback
290  hass: HomeAssistant,
291  entries_by_name: dict[str, ConfigEntry],
292  entries_by_port: dict[int, ConfigEntry],
293  conf: ConfigType,
294 ) -> bool:
295  """Update a config entry with the latest yaml.
296 
297  Returns True if a matching config entry was found
298 
299  Returns False if there is no matching config entry
300  """
301  if not (
302  matching_entry := entries_by_name.get(conf.get(CONF_NAME, BRIDGE_NAME))
303  or entries_by_port.get(conf.get(CONF_PORT, DEFAULT_PORT))
304  ):
305  return False
306 
307  # If they alter the yaml config we import the changes
308  # since there currently is no practical way to support
309  # all the options in the UI at this time.
310  data = conf.copy()
311  options = {}
312  for key in CONFIG_OPTIONS:
313  if key in data:
314  options[key] = data[key]
315  del data[key]
316 
317  hass.config_entries.async_update_entry(matching_entry, data=data, options=options)
318  return True
319 
320 
321 async def async_setup_entry(hass: HomeAssistant, entry: HomeKitConfigEntry) -> bool:
322  """Set up HomeKit from a config entry."""
324 
325  conf = entry.data
326  options = entry.options
327 
328  name = conf[CONF_NAME]
329  port = conf[CONF_PORT]
330  _LOGGER.debug("Begin setup HomeKit for %s", name)
331 
332  # ip_address and advertise_ip are yaml only
333  ip_address = conf.get(CONF_IP_ADDRESS, _DEFAULT_BIND)
334  advertise_ips: list[str] = conf.get(
335  CONF_ADVERTISE_IP
336  ) or await network.async_get_announce_addresses(hass)
337 
338  # exclude_accessory_mode is only used for config flow
339  # to indicate that the config entry was setup after
340  # we started creating config entries for entities that
341  # to run in accessory mode and that we should never include
342  # these entities on the bridge. For backwards compatibility
343  # with users who have not migrated yet we do not do exclude
344  # these entities by default as we cannot migrate automatically
345  # since it requires a re-pairing.
346  exclude_accessory_mode = conf.get(
347  CONF_EXCLUDE_ACCESSORY_MODE, DEFAULT_EXCLUDE_ACCESSORY_MODE
348  )
349  homekit_mode = options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
350  entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy()
351  entity_filter = FILTER_SCHEMA(options.get(CONF_FILTER, {}))
352  devices = options.get(CONF_DEVICES, [])
353 
354  homekit = HomeKit(
355  hass,
356  name,
357  port,
358  ip_address,
359  entity_filter,
360  exclude_accessory_mode,
361  entity_config,
362  homekit_mode,
363  advertise_ips,
364  entry.entry_id,
365  entry.title,
366  devices=devices,
367  )
368 
369  entry.async_on_unload(entry.add_update_listener(_async_update_listener))
370  entry.async_on_unload(
371  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, homekit.async_stop)
372  )
373 
374  entry_data = HomeKitEntryData(
375  homekit=homekit, pairing_qr=None, pairing_qr_secret=None
376  )
377  entry.runtime_data = entry_data
378 
379  async def _async_start_homekit(hass: HomeAssistant) -> None:
380  await homekit.async_start()
381 
382  entry.async_on_unload(async_at_started(hass, _async_start_homekit))
383 
384  return True
385 
386 
388  hass: HomeAssistant, entry: HomeKitConfigEntry
389 ) -> None:
390  """Handle options update."""
391  if entry.source == SOURCE_IMPORT:
392  return
393  await hass.config_entries.async_reload(entry.entry_id)
394 
395 
396 async def async_unload_entry(hass: HomeAssistant, entry: HomeKitConfigEntry) -> bool:
397  """Unload a config entry."""
398  async_dismiss_setup_message(hass, entry.entry_id)
399  entry_data = entry.runtime_data
400  homekit = entry_data.homekit
401 
402  if homekit.status == STATUS_RUNNING:
403  await homekit.async_stop()
404 
405  logged_shutdown_wait = False
406  for _ in range(SHUTDOWN_TIMEOUT):
407  if async_port_is_available(entry.data[CONF_PORT]):
408  break
409 
410  if not logged_shutdown_wait:
411  _LOGGER.debug("Waiting for the HomeKit server to shutdown")
412  logged_shutdown_wait = True
413 
414  await asyncio.sleep(PORT_CLEANUP_CHECK_INTERVAL_SECS)
415 
416  return True
417 
418 
419 async def async_remove_entry(hass: HomeAssistant, entry: HomeKitConfigEntry) -> None:
420  """Remove a config entry."""
421  await hass.async_add_executor_job(
422  remove_state_files_for_entry_id, hass, entry.entry_id
423  )
424 
425 
426 @callback
428  hass: HomeAssistant, entry: HomeKitConfigEntry
429 ) -> None:
430  options = deepcopy(dict(entry.options))
431  data = deepcopy(dict(entry.data))
432  modified = False
433  for importable_option in CONFIG_OPTIONS:
434  if importable_option not in entry.options and importable_option in entry.data:
435  options[importable_option] = entry.data[importable_option]
436  del data[importable_option]
437  modified = True
438 
439  if modified:
440  hass.config_entries.async_update_entry(entry, data=data, options=options)
441 
442 
443 @callback
444 def _async_register_events_and_services(hass: HomeAssistant) -> None:
445  """Register events and services for HomeKit."""
446  hass.http.register_view(HomeKitPairingQRView)
447 
448  async def async_handle_homekit_reset_accessory(service: ServiceCall) -> None:
449  """Handle reset accessory HomeKit service call."""
450  for homekit in _async_all_homekit_instances(hass):
451  if homekit.status != STATUS_RUNNING:
452  _LOGGER.warning(
453  "HomeKit is not running. Either it is waiting to be "
454  "started or has been stopped"
455  )
456  continue
457 
458  entity_ids = cast(list[str], service.data.get("entity_id"))
459  await homekit.async_reset_accessories(entity_ids)
460 
461  hass.services.async_register(
462  DOMAIN,
463  SERVICE_HOMEKIT_RESET_ACCESSORY,
464  async_handle_homekit_reset_accessory,
465  schema=RESET_ACCESSORY_SERVICE_SCHEMA,
466  )
467 
468  async def async_handle_homekit_unpair(service: ServiceCall) -> None:
469  """Handle unpair HomeKit service call."""
470  referenced = async_extract_referenced_entity_ids(hass, service)
471  dev_reg = dr.async_get(hass)
472  for device_id in referenced.referenced_devices:
473  if not (dev_reg_ent := dev_reg.async_get(device_id)):
474  raise HomeAssistantError(f"No device found for device id: {device_id}")
475  macs = [
476  cval
477  for ctype, cval in dev_reg_ent.connections
478  if ctype == dr.CONNECTION_NETWORK_MAC
479  ]
480  matching_instances = [
481  homekit
482  for homekit in _async_all_homekit_instances(hass)
483  if homekit.driver and dr.format_mac(homekit.driver.state.mac) in macs
484  ]
485  if not matching_instances:
486  raise HomeAssistantError(
487  f"No homekit accessory found for device id: {device_id}"
488  )
489  for homekit in matching_instances:
490  homekit.async_unpair()
491 
492  hass.services.async_register(
493  DOMAIN,
494  SERVICE_HOMEKIT_UNPAIR,
495  async_handle_homekit_unpair,
496  schema=UNPAIR_SERVICE_SCHEMA,
497  )
498 
499  async def _handle_homekit_reload(service: ServiceCall) -> None:
500  """Handle start HomeKit service call."""
501  config = await async_integration_yaml_config(hass, DOMAIN)
502 
503  if not config or DOMAIN not in config:
504  return
505 
506  current_entries = hass.config_entries.async_entries(DOMAIN)
507  entries_by_name, entries_by_port = _async_get_imported_entries_indices(
508  current_entries
509  )
510 
511  for conf in config[DOMAIN]:
513  hass, entries_by_name, entries_by_port, conf
514  )
515 
516  reload_tasks = [
517  create_eager_task(hass.config_entries.async_reload(entry.entry_id))
518  for entry in current_entries
519  ]
520 
521  await asyncio.gather(*reload_tasks)
522 
524  hass,
525  DOMAIN,
526  SERVICE_RELOAD,
527  _handle_homekit_reload,
528  )
529 
530 
531 class HomeKit:
532  """Class to handle all actions between HomeKit and Home Assistant."""
533 
534  def __init__(
535  self,
536  hass: HomeAssistant,
537  name: str,
538  port: int,
539  ip_address: str | None,
540  entity_filter: EntityFilter,
541  exclude_accessory_mode: bool,
542  entity_config: dict[str, Any],
543  homekit_mode: str,
544  advertise_ips: list[str],
545  entry_id: str,
546  entry_title: str,
547  devices: list[str] | None = None,
548  ) -> None:
549  """Initialize a HomeKit object."""
550  self.hasshass = hass
551  self._name_name = name
552  self._port_port = port
553  self._ip_address_ip_address = ip_address
554  self._filter_filter = entity_filter
555  self._config: defaultdict[str, dict[str, Any]] = defaultdict(
556  dict, entity_config
557  )
558  self._exclude_accessory_mode_exclude_accessory_mode = exclude_accessory_mode
559  self._advertise_ips_advertise_ips = advertise_ips
560  self._entry_id_entry_id = entry_id
561  self._entry_title_entry_title = entry_title
562  self._homekit_mode_homekit_mode = homekit_mode
563  self._devices_devices = devices or []
564  self.aid_storageaid_storage: AccessoryAidStorage | None = None
565  self.iid_storageiid_storage: AccessoryIIDStorage | None = None
566  self.statusstatus = STATUS_READY
567  self.driverdriver: HomeDriver | None = None
568  self.bridgebridge: HomeBridge | None = None
569  self._reset_lock_reset_lock = asyncio.Lock()
570  self._cancel_reload_dispatcher_cancel_reload_dispatcher: CALLBACK_TYPE | None = None
571 
572  def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> bool:
573  """Set up bridge and accessory driver.
574 
575  Returns True if data was loaded from disk
576 
577  Returns False if the persistent data was not loaded
578  """
579  assert self.iid_storageiid_storage is not None
580  persist_file = get_persist_fullpath_for_entry_id(self.hasshass, self._entry_id_entry_id)
581  self.driverdriver = HomeDriver(
582  self.hasshass,
583  self._entry_id_entry_id,
584  self._name_name,
585  self._entry_title_entry_title,
586  loop=self.hasshass.loop,
587  address=self._ip_address_ip_address,
588  port=self._port_port,
589  persist_file=persist_file,
590  advertised_address=self._advertise_ips_advertise_ips,
591  async_zeroconf_instance=async_zeroconf_instance,
592  zeroconf_server=f"{uuid}-hap.local.",
593  loader=get_loader(),
594  iid_storage=self.iid_storageiid_storage,
595  )
596  # If we do not load the mac address will be wrong
597  # as pyhap uses a random one until state is restored
598  if os.path.exists(persist_file):
599  self.driverdriver.load()
600  return True
601 
602  # If there is no persist file, we need to generate a mac
603  self.driverdriver.state.mac = pyhap_util.generate_mac()
604  return False
605 
606  async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None:
607  """Reset the accessory to load the latest configuration."""
608  _LOGGER.debug("Resetting accessories: %s", entity_ids)
609  async with self._reset_lock_reset_lock:
610  if not self.bridgebridge:
611  # For accessory mode reset and reload are the same
612  await self._async_reload_accessories_in_accessory_mode_async_reload_accessories_in_accessory_mode(entity_ids)
613  return
614  await self._async_reset_accessories_in_bridge_mode_async_reset_accessories_in_bridge_mode(entity_ids)
615 
616  async def async_reload_accessories(self, entity_ids: Iterable[str]) -> None:
617  """Reload the accessory to load the latest configuration."""
618  _LOGGER.debug("Reloading accessories: %s", entity_ids)
619  async with self._reset_lock_reset_lock:
620  if not self.bridgebridge:
621  await self._async_reload_accessories_in_accessory_mode_async_reload_accessories_in_accessory_mode(entity_ids)
622  return
623  await self._async_reload_accessories_in_bridge_mode_async_reload_accessories_in_bridge_mode(entity_ids)
624 
625  @callback
626  def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
627  """Shutdown an accessory."""
628  assert self.driverdriver is not None
629  accessory.async_stop()
630  # Deallocate the IIDs for the accessory
631  iid_manager = accessory.iid_manager
632  services: list[Service] = accessory.services
633  for service in services:
634  iid_manager.remove_obj(service)
635  characteristics: list[Characteristic] = service.characteristics
636  for char in characteristics:
637  iid_manager.remove_obj(char)
638 
640  self, entity_ids: Iterable[str]
641  ) -> None:
642  """Reset accessories in accessory mode."""
643  assert self.driverdriver is not None
644 
645  acc = cast(HomeAccessory, self.driverdriver.accessory)
646  if acc.entity_id not in entity_ids:
647  return
648  if not (state := self.hasshass.states.get(acc.entity_id)):
649  _LOGGER.warning(
650  "The underlying entity %s disappeared during reload", acc.entity_id
651  )
652  return
653  self._async_shutdown_accessory_async_shutdown_accessory(acc)
654  if new_acc := self._async_create_single_accessory_async_create_single_accessory([state]):
655  self.driverdriver.accessory = new_acc
656  new_acc.run()
657  self._async_update_accessories_hash_async_update_accessories_hash()
658 
660  self, entity_ids: Iterable[str]
661  ) -> list[str]:
662  """Remove accessories by entity id."""
663  assert self.aid_storageaid_storage is not None
664  assert self.bridgebridge is not None
665  removed: list[str] = []
666  acc: HomeAccessory | None
667  for entity_id in entity_ids:
668  aid = self.aid_storageaid_storage.get_or_allocate_aid_for_entity_id(entity_id)
669  if aid not in self.bridgebridge.accessories:
670  continue
671  if acc := self.async_remove_bridge_accessoryasync_remove_bridge_accessory(aid):
672  self._async_shutdown_accessory_async_shutdown_accessory(acc)
673  removed.append(entity_id)
674  return removed
675 
677  self, entity_ids: Iterable[str]
678  ) -> None:
679  """Reset accessories in bridge mode."""
680  if not (removed := self._async_remove_accessories_by_entity_id_async_remove_accessories_by_entity_id(entity_ids)):
681  _LOGGER.debug("No accessories to reset in bridge mode for: %s", entity_ids)
682  return
683  # With a reset, we need to remove the accessories,
684  # and force config change so iCloud deletes them from
685  # the database.
686  assert self.driverdriver is not None
687  self._async_update_accessories_hash_async_update_accessories_hash()
688  await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME)
689  await self._async_recreate_removed_accessories_in_bridge_mode_async_recreate_removed_accessories_in_bridge_mode(removed)
690 
692  self, entity_ids: Iterable[str]
693  ) -> None:
694  """Reload accessories in bridge mode."""
695  removed = self._async_remove_accessories_by_entity_id_async_remove_accessories_by_entity_id(entity_ids)
696  await self._async_recreate_removed_accessories_in_bridge_mode_async_recreate_removed_accessories_in_bridge_mode(removed)
697 
699  self, removed: list[str]
700  ) -> None:
701  """Recreate removed accessories in bridge mode."""
702  for entity_id in removed:
703  if not (state := self.hasshass.states.get(entity_id)):
704  _LOGGER.warning(
705  "The underlying entity %s disappeared during reload", entity_id
706  )
707  continue
708  if acc := self.add_bridge_accessoryadd_bridge_accessory(state):
709  acc.run()
710  self._async_update_accessories_hash_async_update_accessories_hash()
711 
712  @callback
714  """Update the accessories hash."""
715  assert self.driverdriver is not None
716  driver = self.driverdriver
717  old_hash = driver.state.accessories_hash
718  new_hash = driver.accessories_hash
719  if driver.state.set_accessories_hash(new_hash):
720  _LOGGER.debug(
721  "Updating HomeKit accessories hash from %s -> %s", old_hash, new_hash
722  )
723  driver.async_persist()
724  driver.async_update_advertisement()
725  return True
726  _LOGGER.debug("HomeKit accessories hash is unchanged: %s", new_hash)
727  return False
728 
729  def add_bridge_accessory(self, state: State) -> HomeAccessory | None:
730  """Try adding accessory to bridge if configured beforehand."""
731  assert self.driverdriver is not None
732 
733  if self._would_exceed_max_devices_would_exceed_max_devices(state.entity_id):
734  return None
735 
736  if state_needs_accessory_mode(state):
737  if self._exclude_accessory_mode_exclude_accessory_mode:
738  return None
739  _LOGGER.warning(
740  (
741  "The bridge %s has entity %s. For best performance, "
742  "and to prevent unexpected unavailability, create and "
743  "pair a separate HomeKit instance in accessory mode for "
744  "this entity"
745  ),
746  self._name_name,
747  state.entity_id,
748  )
749 
750  assert self.aid_storageaid_storage is not None
751  assert self.bridgebridge is not None
752  aid = self.aid_storageaid_storage.get_or_allocate_aid_for_entity_id(state.entity_id)
753  conf = self._config.get(state.entity_id, {}).copy()
754  # If an accessory cannot be created or added due to an exception
755  # of any kind (usually in pyhap) it should not prevent
756  # the rest of the accessories from being created
757  try:
758  acc = get_accessory(self.hasshass, self.driverdriver, state, aid, conf)
759  if acc is not None:
760  self.bridgebridge.add_accessory(acc)
761  return acc
762  except Exception:
763  _LOGGER.exception(
764  "Failed to create a HomeKit accessory for %s", state.entity_id
765  )
766  return None
767 
768  def _would_exceed_max_devices(self, name: str | None) -> bool:
769  """Check if adding another devices would reach the limit and log."""
770  # The bridge itself counts as an accessory
771  assert self.bridgebridge is not None
772  if len(self.bridgebridge.accessories) + 1 >= MAX_DEVICES:
773  _LOGGER.warning(
774  (
775  "Cannot add %s as this would exceed the %d device limit. Consider"
776  " using the filter option"
777  ),
778  name,
779  MAX_DEVICES,
780  )
781  return True
782  return False
783 
785  self, device: dr.DeviceEntry, device_triggers: list[dict[str, Any]]
786  ) -> None:
787  """Add device automation triggers to the bridge."""
788  if self._would_exceed_max_devices_would_exceed_max_devices(device.name):
789  return
790 
791  assert self.aid_storageaid_storage is not None
792  assert self.bridgebridge is not None
793  aid = self.aid_storageaid_storage.get_or_allocate_aid(device.id, device.id)
794  # If an accessory cannot be created or added due to an exception
795  # of any kind (usually in pyhap) it should not prevent
796  # the rest of the accessories from being created
797  config: dict[str, Any] = {}
798  self._fill_config_from_device_registry_entry_fill_config_from_device_registry_entry(device, config)
799  trigger_accessory = DeviceTriggerAccessory(
800  self.hasshass,
801  self.driverdriver,
802  device.name,
803  None,
804  aid,
805  config,
806  device_id=device.id,
807  device_triggers=device_triggers,
808  )
809  await trigger_accessory.async_attach()
810  self.bridgebridge.add_accessory(trigger_accessory)
811 
812  @callback
813  def async_remove_bridge_accessory(self, aid: int) -> HomeAccessory | None:
814  """Try adding accessory to bridge if configured beforehand."""
815  assert self.bridgebridge is not None
816  if acc := self.bridgebridge.accessories.pop(aid, None):
817  return cast(HomeAccessory, acc)
818  return None
819 
820  async def async_configure_accessories(self) -> list[State]:
821  """Configure accessories for the included states."""
822  dev_reg = dr.async_get(self.hasshass)
823  ent_reg = er.async_get(self.hasshass)
824  device_lookup: dict[str, dict[tuple[str, str | None], str]] = {}
825  entity_states: list[State] = []
826  entity_filter = self._filter_filter.get_filter()
827  entries = ent_reg.entities
828  for state in self.hasshass.states.async_all():
829  entity_id = state.entity_id
830  if not entity_filter(entity_id):
831  continue
832 
833  if ent_reg_ent := ent_reg.async_get(entity_id):
834  if (
835  ent_reg_ent.entity_category is not None
836  or ent_reg_ent.hidden_by is not None
837  ) and not self._filter_filter.explicitly_included(entity_id):
838  continue
839 
840  await self._async_set_device_info_attributes_async_set_device_info_attributes(
841  ent_reg_ent, dev_reg, entity_id
842  )
843  if device_id := ent_reg_ent.device_id:
844  if device_id not in device_lookup:
845  device_lookup[device_id] = {
846  (
847  entry.domain,
848  entry.device_class or entry.original_device_class,
849  ): entry.entity_id
850  for entry in entries.get_entries_for_device_id(device_id)
851  }
852  self._async_configure_linked_sensors_async_configure_linked_sensors(
853  ent_reg_ent, device_lookup[device_id], state
854  )
855 
856  entity_states.append(state)
857 
858  return entity_states
859 
860  async def async_start(self, *args: Any) -> None:
861  """Load storage and start."""
862  if self.statusstatus != STATUS_READY:
863  return
864  self.statusstatus = STATUS_WAIT
865  self._cancel_reload_dispatcher_cancel_reload_dispatcher = async_dispatcher_connect(
866  self.hasshass,
867  SIGNAL_RELOAD_ENTITIES.format(self._entry_id_entry_id),
868  self.async_reload_accessoriesasync_reload_accessories,
869  )
870  async_zc_instance = await zeroconf.async_get_async_instance(self.hasshass)
871  uuid = await instance_id.async_get(self.hasshass)
872  self.aid_storageaid_storage = AccessoryAidStorage(self.hasshass, self._entry_id_entry_id)
873  self.iid_storageiid_storage = AccessoryIIDStorage(self.hasshass, self._entry_id_entry_id)
874  # Avoid gather here since it will be I/O bound anyways
875  await self.aid_storageaid_storage.async_initialize()
876  await self.iid_storageiid_storage.async_initialize()
877  loaded_from_disk = await self.hasshass.async_add_executor_job(
878  self.setupsetup, async_zc_instance, uuid
879  )
880  assert self.driverdriver is not None
881 
882  if not await self._async_create_accessories_async_create_accessories():
883  return
884  self._async_register_bridge_async_register_bridge()
885  _LOGGER.debug("Driver start for %s", self._name_name)
886  await self.driverdriver.async_start()
887  if not loaded_from_disk:
888  # If the state was not loaded from disk, it means this is the
889  # first time the bridge is ever starting up. In this case, we
890  # need to make sure its persisted to disk.
891  async with self.hasshass.data[PERSIST_LOCK_DATA]:
892  await self.hasshass.async_add_executor_job(self.driverdriver.persist)
893  self.statusstatus = STATUS_RUNNING
894 
895  if self.driverdriver.state.paired:
896  return
897  self._async_show_setup_message_async_show_setup_message()
898 
899  @callback
900  def _async_show_setup_message(self) -> None:
901  """Show the pairing setup message."""
902  assert self.driverdriver is not None
903 
904  async_show_setup_message(
905  self.hasshass,
906  self._entry_id_entry_id,
907  accessory_friendly_name(self._entry_title_entry_title, self.driverdriver.accessory),
908  self.driverdriver.state.pincode,
909  self.driverdriver.accessory.xhm_uri(),
910  )
911 
912  @callback
913  def async_unpair(self) -> None:
914  """Remove all pairings for an accessory so it can be repaired."""
915  assert self.driverdriver is not None
916 
917  state = self.driverdriver.state
918  for client_uuid in list(state.paired_clients):
919  # We need to check again since removing a single client
920  # can result in removing all the clients that the client
921  # granted access to if it was an admin, otherwise
922  # remove_paired_client can generate a KeyError
923  if client_uuid in state.paired_clients:
924  state.remove_paired_client(client_uuid)
925  self.driverdriver.async_persist()
926  self.driverdriver.async_update_advertisement()
927  self._async_show_setup_message_async_show_setup_message()
928 
929  @callback
930  def _async_register_bridge(self) -> None:
931  """Register the bridge as a device so homekit_controller and exclude it from discovery."""
932  assert self.driverdriver is not None
933  dev_reg = dr.async_get(self.hasshass)
934  formatted_mac = dr.format_mac(self.driverdriver.state.mac)
935  # Connections and identifiers are both used here.
936  #
937  # connections exists so homekit_controller can know the
938  # virtual mac address of the bridge and know to not offer
939  # it via discovery.
940  #
941  # identifiers is used as well since the virtual mac may change
942  # because it will not survive manual pairing resets (deleting state file)
943  # which we have trained users to do over the past few years
944  # because this was the way you had to fix homekit when pairing
945  # failed.
946  #
947  connection = (dr.CONNECTION_NETWORK_MAC, formatted_mac)
948  identifier = (DOMAIN, self._entry_id_entry_id, BRIDGE_SERIAL_NUMBER)
949  self._async_purge_old_bridges_async_purge_old_bridges(dev_reg, identifier, connection)
950  accessory_type = type(self.driverdriver.accessory).__name__
951  dev_reg.async_get_or_create(
952  config_entry_id=self._entry_id_entry_id,
953  identifiers={
954  identifier # type: ignore[arg-type]
955  }, # this needs to be migrated as a 2 item tuple at some point
956  connections={connection},
957  manufacturer=MANUFACTURER,
958  name=accessory_friendly_name(self._entry_title_entry_title, self.driverdriver.accessory),
959  model=accessory_type,
960  entry_type=dr.DeviceEntryType.SERVICE,
961  )
962 
963  @callback
965  self,
966  dev_reg: dr.DeviceRegistry,
967  identifier: tuple[str, str, str],
968  connection: tuple[str, str],
969  ) -> None:
970  """Purge bridges that exist from failed pairing or manual resets."""
971  devices_to_purge = [
972  entry.id
973  for entry in dev_reg.devices.get_devices_for_config_entry_id(self._entry_id_entry_id)
974  if (
975  identifier not in entry.identifiers # type: ignore[comparison-overlap]
976  or connection not in entry.connections
977  )
978  ]
979 
980  for device_id in devices_to_purge:
981  dev_reg.async_remove_device(device_id)
982 
983  @callback
985  self, entity_states: list[State]
986  ) -> HomeAccessory | None:
987  """Create a single HomeKit accessory (accessory mode)."""
988  assert self.driverdriver is not None
989 
990  if not entity_states:
991  _LOGGER.error(
992  "HomeKit %s cannot startup: entity not available: %s",
993  self._name_name,
994  self._filter_filter.config,
995  )
996  return None
997  state = entity_states[0]
998  conf = self._config.get(state.entity_id, {}).copy()
999  acc = get_accessory(self.hasshass, self.driverdriver, state, STANDALONE_AID, conf)
1000  if acc is None:
1001  _LOGGER.error(
1002  "HomeKit %s cannot startup: entity not supported: %s",
1003  self._name_name,
1004  self._filter_filter.config,
1005  )
1006  return acc
1007 
1009  self, entity_states: Iterable[State]
1010  ) -> HomeAccessory:
1011  """Create a HomeKit bridge with accessories. (bridge mode)."""
1012  assert self.driverdriver is not None
1013 
1014  self.bridgebridge = HomeBridge(self.hasshass, self.driverdriver, self._name_name)
1015  for state in entity_states:
1016  self.add_bridge_accessoryadd_bridge_accessory(state)
1017  if self._devices_devices:
1018  await self._async_add_trigger_accessories_async_add_trigger_accessories()
1019  return self.bridgebridge
1020 
1021  async def _async_add_trigger_accessories(self) -> None:
1022  """Add devices with triggers to the bridge."""
1023  dev_reg = dr.async_get(self.hasshass)
1024  valid_device_ids = []
1025  for device_id in self._devices_devices:
1026  if not dev_reg.async_get(device_id):
1027  _LOGGER.warning(
1028  (
1029  "HomeKit %s cannot add device %s because it is missing from the"
1030  " device registry"
1031  ),
1032  self._name_name,
1033  device_id,
1034  )
1035  else:
1036  valid_device_ids.append(device_id)
1037  for device_id, device_triggers in (
1038  await device_automation.async_get_device_automations(
1039  self.hasshass,
1040  device_automation.DeviceAutomationType.TRIGGER,
1041  valid_device_ids,
1042  )
1043  ).items():
1044  device = dev_reg.async_get(device_id)
1045  assert device is not None
1046  valid_device_triggers: list[dict[str, Any]] = []
1047  for trigger in device_triggers:
1048  try:
1049  await async_validate_trigger_config(self.hasshass, trigger)
1050  except vol.Invalid as ex:
1051  _LOGGER.debug(
1052  (
1053  "%s: cannot add unsupported trigger %s because it requires"
1054  " additional inputs which are not supported by HomeKit: %s"
1055  ),
1056  self._name_name,
1057  trigger,
1058  ex,
1059  )
1060  continue
1061  valid_device_triggers.append(trigger)
1062  await self.add_bridge_triggers_accessoryadd_bridge_triggers_accessory(device, valid_device_triggers)
1063 
1064  async def _async_create_accessories(self) -> bool:
1065  """Create the accessories."""
1066  assert self.driverdriver is not None
1067 
1068  entity_states = await self.async_configure_accessoriesasync_configure_accessories()
1069  if self._homekit_mode_homekit_mode == HOMEKIT_MODE_ACCESSORY:
1070  acc = self._async_create_single_accessory_async_create_single_accessory(entity_states)
1071  else:
1072  acc = await self._async_create_bridge_accessory_async_create_bridge_accessory(entity_states)
1073 
1074  if acc is None:
1075  return False
1076  # No need to load/persist as we do it in setup
1077  self.driverdriver.accessory = acc
1078  return True
1079 
1080  async def async_stop(self, *args: Any) -> None:
1081  """Stop the accessory driver."""
1082  if self.statusstatus != STATUS_RUNNING:
1083  return
1084  async with self._reset_lock_reset_lock:
1085  self.statusstatus = STATUS_STOPPED
1086  assert self._cancel_reload_dispatcher_cancel_reload_dispatcher is not None
1087  self._cancel_reload_dispatcher_cancel_reload_dispatcher()
1088  _LOGGER.debug("Driver stop for %s", self._name_name)
1089  if self.driverdriver:
1090  await self.driverdriver.async_stop()
1091 
1092  @callback
1094  self,
1095  ent_reg_ent: er.RegistryEntry,
1096  lookup: dict[tuple[str, str | None], str],
1097  state: State,
1098  ) -> None:
1099  if (ent_reg_ent.device_class or ent_reg_ent.original_device_class) in (
1100  BinarySensorDeviceClass.BATTERY_CHARGING,
1101  SensorDeviceClass.BATTERY,
1102  ):
1103  return
1104 
1105  domain = state.domain
1106  attributes = state.attributes
1107  config = self._config
1108  entity_id = state.entity_id
1109 
1110  if ATTR_BATTERY_CHARGING not in attributes and (
1111  battery_charging_binary_sensor_entity_id := lookup.get(
1112  BATTERY_CHARGING_SENSOR
1113  )
1114  ):
1115  config[entity_id].setdefault(
1116  CONF_LINKED_BATTERY_CHARGING_SENSOR,
1117  battery_charging_binary_sensor_entity_id,
1118  )
1119 
1120  if ATTR_BATTERY_LEVEL not in attributes and (
1121  battery_sensor_entity_id := lookup.get(BATTERY_SENSOR)
1122  ):
1123  config[entity_id].setdefault(
1124  CONF_LINKED_BATTERY_SENSOR, battery_sensor_entity_id
1125  )
1126 
1127  if domain == CAMERA_DOMAIN:
1128  if motion_event_entity_id := lookup.get(MOTION_EVENT_SENSOR):
1129  config[entity_id].setdefault(
1130  CONF_LINKED_MOTION_SENSOR, motion_event_entity_id
1131  )
1132  elif motion_binary_sensor_entity_id := lookup.get(MOTION_SENSOR):
1133  config[entity_id].setdefault(
1134  CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id
1135  )
1136  if doorbell_event_entity_id := lookup.get(DOORBELL_EVENT_SENSOR):
1137  config[entity_id].setdefault(
1138  CONF_LINKED_DOORBELL_SENSOR, doorbell_event_entity_id
1139  )
1140 
1141  if domain == HUMIDIFIER_DOMAIN and (
1142  current_humidity_sensor_entity_id := lookup.get(HUMIDITY_SENSOR)
1143  ):
1144  config[entity_id].setdefault(
1145  CONF_LINKED_HUMIDITY_SENSOR, current_humidity_sensor_entity_id
1146  )
1147 
1149  self,
1150  ent_reg_ent: er.RegistryEntry,
1151  dev_reg: dr.DeviceRegistry,
1152  entity_id: str,
1153  ) -> None:
1154  """Set attributes that will be used for homekit device info."""
1155  ent_cfg = self._config[entity_id]
1156  if ent_reg_ent.device_id:
1157  if dev_reg_ent := dev_reg.async_get(ent_reg_ent.device_id):
1158  self._fill_config_from_device_registry_entry_fill_config_from_device_registry_entry(dev_reg_ent, ent_cfg)
1159  if ATTR_MANUFACTURER not in ent_cfg:
1160  try:
1161  integration = await async_get_integration(
1162  self.hasshass, ent_reg_ent.platform
1163  )
1164  ent_cfg[ATTR_INTEGRATION] = integration.name
1165  except IntegrationNotFound:
1166  ent_cfg[ATTR_INTEGRATION] = ent_reg_ent.platform
1167 
1169  self, device_entry: dr.DeviceEntry, config: dict[str, Any]
1170  ) -> None:
1171  """Populate a config dict from the registry."""
1172  if device_entry.manufacturer:
1173  config[ATTR_MANUFACTURER] = device_entry.manufacturer
1174  if device_entry.model:
1175  config[ATTR_MODEL] = device_entry.model
1176  if device_entry.sw_version:
1177  config[ATTR_SW_VERSION] = device_entry.sw_version
1178  if device_entry.hw_version:
1179  config[ATTR_HW_VERSION] = device_entry.hw_version
1180  if device_entry.config_entries:
1181  first_entry = list(device_entry.config_entries)[0]
1182  if entry := self.hasshass.config_entries.async_get_entry(first_entry):
1183  config[ATTR_INTEGRATION] = entry.domain
1184 
1185 
1186 class HomeKitPairingQRView(HomeAssistantView):
1187  """Display the homekit pairing code at a protected url."""
1188 
1189  url = "/api/homekit/pairingqr"
1190  name = "api:homekit:pairingqr"
1191  requires_auth = False
1192 
1193  async def get(self, request: web.Request) -> web.Response:
1194  """Retrieve the pairing QRCode image."""
1195  if not request.query_string:
1196  raise Unauthorized
1197  entry_id, secret = request.query_string.split("-")
1198  hass = request.app[KEY_HASS]
1199  entry_data: HomeKitEntryData | None
1200  if (
1201  not (entry := hass.config_entries.async_get_entry(entry_id))
1202  or not (entry_data := getattr(entry, "runtime_data", None))
1203  or not secret
1204  or not entry_data.pairing_qr_secret
1205  or secret != entry_data.pairing_qr_secret
1206  ):
1207  raise Unauthorized
1208  return web.Response(
1209  body=entry_data.pairing_qr,
1210  content_type="image/svg+xml",
1211  )
web.Response get(self, web.Request request)
Definition: __init__.py:1193
None _async_shutdown_accessory(self, HomeAccessory accessory)
Definition: __init__.py:626
HomeAccessory|None _async_create_single_accessory(self, list[State] entity_states)
Definition: __init__.py:986
None __init__(self, HomeAssistant hass, str name, int port, str|None ip_address, EntityFilter entity_filter, bool exclude_accessory_mode, dict[str, Any] entity_config, str homekit_mode, list[str] advertise_ips, str entry_id, str entry_title, list[str]|None devices=None)
Definition: __init__.py:548
HomeAccessory _async_create_bridge_accessory(self, Iterable[State] entity_states)
Definition: __init__.py:1010
HomeAccessory|None async_remove_bridge_accessory(self, int aid)
Definition: __init__.py:813
None _async_configure_linked_sensors(self, er.RegistryEntry ent_reg_ent, dict[tuple[str, str|None], str] lookup, State state)
Definition: __init__.py:1098
bool setup(self, AsyncZeroconf async_zeroconf_instance, str uuid)
Definition: __init__.py:572
list[str] _async_remove_accessories_by_entity_id(self, Iterable[str] entity_ids)
Definition: __init__.py:661
list[State] async_configure_accessories(self)
Definition: __init__.py:820
None add_bridge_triggers_accessory(self, dr.DeviceEntry device, list[dict[str, Any]] device_triggers)
Definition: __init__.py:786
None async_start(self, *Any args)
Definition: __init__.py:860
None _async_reload_accessories_in_accessory_mode(self, Iterable[str] entity_ids)
Definition: __init__.py:641
HomeAccessory|None add_bridge_accessory(self, State state)
Definition: __init__.py:729
None _fill_config_from_device_registry_entry(self, dr.DeviceEntry device_entry, dict[str, Any] config)
Definition: __init__.py:1170
None _async_reload_accessories_in_bridge_mode(self, Iterable[str] entity_ids)
Definition: __init__.py:693
None _async_reset_accessories_in_bridge_mode(self, Iterable[str] entity_ids)
Definition: __init__.py:678
None async_reset_accessories(self, Iterable[str] entity_ids)
Definition: __init__.py:606
None _async_set_device_info_attributes(self, er.RegistryEntry ent_reg_ent, dr.DeviceRegistry dev_reg, str entity_id)
Definition: __init__.py:1153
bool _would_exceed_max_devices(self, str|None name)
Definition: __init__.py:768
None async_reload_accessories(self, Iterable[str] entity_ids)
Definition: __init__.py:616
None _async_recreate_removed_accessories_in_bridge_mode(self, list[str] removed)
Definition: __init__.py:700
None _async_purge_old_bridges(self, dr.DeviceRegistry dev_reg, tuple[str, str, str] identifier, tuple[str, str] connection)
Definition: __init__.py:969
ConfigType async_validate_trigger_config(HomeAssistant hass, ConfigType config)
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
bool async_unload_entry(HomeAssistant hass, HomeKitConfigEntry entry)
Definition: __init__.py:396
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:250
tuple[dict[str, ConfigEntry], dict[int, ConfigEntry]] _async_get_imported_entries_indices(list[ConfigEntry] current_entries)
Definition: __init__.py:235
list[HomeKit] _async_all_homekit_instances(HomeAssistant hass)
Definition: __init__.py:223
None _async_import_options_from_data_if_missing(HomeAssistant hass, HomeKitConfigEntry entry)
Definition: __init__.py:429
None async_remove_entry(HomeAssistant hass, HomeKitConfigEntry entry)
Definition: __init__.py:419
list[dict[str, Any]] _has_all_unique_names_and_ports(list[dict[str, Any]] bridges)
Definition: __init__.py:175
None _async_update_listener(HomeAssistant hass, HomeKitConfigEntry entry)
Definition: __init__.py:389
bool _async_update_config_entry_from_yaml(HomeAssistant hass, dict[str, ConfigEntry] entries_by_name, dict[int, ConfigEntry] entries_by_port, ConfigType conf)
Definition: __init__.py:294
None _async_register_events_and_services(HomeAssistant hass)
Definition: __init__.py:444
bool async_setup_entry(HomeAssistant hass, HomeKitConfigEntry entry)
Definition: __init__.py:321
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
ConfigType|None async_integration_yaml_config(HomeAssistant hass, str integration_name)
Definition: reload.py:142
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
SelectedEntities async_extract_referenced_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
Definition: service.py:507
CALLBACK_TYPE async_at_started(HomeAssistant hass, Callable[[HomeAssistant], Coroutine[Any, Any, None]|None] at_start_cb)
Definition: start.py:80
Integration async_get_integration(HomeAssistant hass, str domain)
Definition: loader.py:1354