1 """Support for Apple HomeKit."""
3 from __future__
import annotations
6 from collections
import defaultdict
7 from collections.abc
import Iterable
8 from copy
import deepcopy
13 from typing
import Any, cast
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
26 DOMAIN
as BINARY_SENSOR_DOMAIN,
27 BinarySensorDeviceClass,
31 async_validate_trigger_config,
39 ATTR_BATTERY_CHARGING,
51 EVENT_HOMEASSISTANT_STOP,
63 config_validation
as cv,
64 device_registry
as dr,
65 entity_registry
as er,
76 async_extract_referenced_entity_ids,
77 async_register_admin_service,
93 type_security_systems,
98 from .accessories
import HomeAccessory, HomeBridge, HomeDriver, get_accessory
99 from .aidmanager
import AccessoryAidStorage
103 BRIDGE_SERIAL_NUMBER,
107 CONF_EXCLUDE_ACCESSORY_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,
116 DEFAULT_EXCLUDE_ACCESSORY_MODE,
117 DEFAULT_HOMEKIT_MODE,
120 HOMEKIT_MODE_ACCESSORY,
124 SERVICE_HOMEKIT_RESET_ACCESSORY,
125 SERVICE_HOMEKIT_UNPAIR,
127 SIGNAL_RELOAD_ENTITIES,
129 from .iidmanager
import AccessoryIIDStorage
130 from .models
import HomeKitConfigEntry, HomeKitEntryData
131 from .type_triggers
import DeviceTriggerAccessory
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,
143 _LOGGER = logging.getLogger(__name__)
153 PORT_CLEANUP_CHECK_INTERVAL_SECS = 1
155 _HOMEKIT_CONFIG_UPDATE_TIME = (
158 _HAS_IPV6 = hasattr(socket,
"AF_INET6")
159 _DEFAULT_BIND = [
"0.0.0.0",
"::"]
if _HAS_IPV6
else [
"0.0.0.0"]
162 BATTERY_CHARGING_SENSOR = (
163 BINARY_SENSOR_DOMAIN,
164 BinarySensorDeviceClass.BATTERY_CHARGING,
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)
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)
184 BRIDGE_SCHEMA = vol.All(
187 vol.Optional(CONF_HOMEKIT_MODE, default=DEFAULT_HOMEKIT_MODE): vol.In(
190 vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All(
191 cv.string, vol.Length(min=3, max=25)
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]
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,
202 extra=vol.ALLOW_EXTRA,
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,
212 RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema(
213 {vol.Required(ATTR_ENTITY_ID): cv.entity_ids}
217 UNPAIR_SERVICE_SCHEMA = vol.All(
218 vol.Schema(cv.ENTITY_SERVICE_FIELDS),
219 cv.has_at_least_one_key(ATTR_DEVICE_ID),
224 """All active HomeKit instances."""
225 hk_data: HomeKitEntryData |
None
228 for entry
in hass.config_entries.async_entries(DOMAIN)
229 if (hk_data := getattr(entry,
"runtime_data",
None))
234 current_entries: list[ConfigEntry],
235 ) -> tuple[dict[str, ConfigEntry], dict[int, ConfigEntry]]:
236 """Return a dicts of the entries by name and port."""
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:
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
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()
257 await hass.async_add_executor_job(get_loader)
261 if DOMAIN
not in config:
264 current_entries = hass.config_entries.async_entries(DOMAIN)
269 for index, conf
in enumerate(config[DOMAIN]):
271 hass, entries_by_name, entries_by_port, conf
275 conf[CONF_ENTRY_INDEX] = index
276 hass.async_create_task(
277 hass.config_entries.flow.async_init(
279 context={
"source": SOURCE_IMPORT},
291 entries_by_name: dict[str, ConfigEntry],
292 entries_by_port: dict[int, ConfigEntry],
295 """Update a config entry with the latest yaml.
297 Returns True if a matching config entry was found
299 Returns False if there is no matching config entry
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))
312 for key
in CONFIG_OPTIONS:
314 options[key] = data[key]
317 hass.config_entries.async_update_entry(matching_entry, data=data, options=options)
322 """Set up HomeKit from a config entry."""
326 options = entry.options
328 name = conf[CONF_NAME]
329 port = conf[CONF_PORT]
330 _LOGGER.debug(
"Begin setup HomeKit for %s", name)
333 ip_address = conf.get(CONF_IP_ADDRESS, _DEFAULT_BIND)
334 advertise_ips: list[str] = conf.get(
336 )
or await network.async_get_announce_addresses(hass)
346 exclude_accessory_mode = conf.get(
347 CONF_EXCLUDE_ACCESSORY_MODE, DEFAULT_EXCLUDE_ACCESSORY_MODE
349 homekit_mode = options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE)
350 entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy()
352 devices = options.get(CONF_DEVICES, [])
360 exclude_accessory_mode,
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)
375 homekit=homekit, pairing_qr=
None, pairing_qr_secret=
None
377 entry.runtime_data = entry_data
379 async
def _async_start_homekit(hass: HomeAssistant) ->
None:
380 await homekit.async_start()
388 hass: HomeAssistant, entry: HomeKitConfigEntry
390 """Handle options update."""
391 if entry.source == SOURCE_IMPORT:
393 await hass.config_entries.async_reload(entry.entry_id)
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
402 if homekit.status == STATUS_RUNNING:
403 await homekit.async_stop()
405 logged_shutdown_wait =
False
406 for _
in range(SHUTDOWN_TIMEOUT):
407 if async_port_is_available(entry.data[CONF_PORT]):
410 if not logged_shutdown_wait:
411 _LOGGER.debug(
"Waiting for the HomeKit server to shutdown")
412 logged_shutdown_wait =
True
414 await asyncio.sleep(PORT_CLEANUP_CHECK_INTERVAL_SECS)
420 """Remove a config entry."""
421 await hass.async_add_executor_job(
422 remove_state_files_for_entry_id, hass, entry.entry_id
428 hass: HomeAssistant, entry: HomeKitConfigEntry
430 options = deepcopy(
dict(entry.options))
431 data = deepcopy(
dict(entry.data))
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]
440 hass.config_entries.async_update_entry(entry, data=data, options=options)
445 """Register events and services for HomeKit."""
446 hass.http.register_view(HomeKitPairingQRView)
448 async
def async_handle_homekit_reset_accessory(service: ServiceCall) ->
None:
449 """Handle reset accessory HomeKit service call."""
451 if homekit.status != STATUS_RUNNING:
453 "HomeKit is not running. Either it is waiting to be "
454 "started or has been stopped"
458 entity_ids = cast(list[str], service.data.get(
"entity_id"))
459 await homekit.async_reset_accessories(entity_ids)
461 hass.services.async_register(
463 SERVICE_HOMEKIT_RESET_ACCESSORY,
464 async_handle_homekit_reset_accessory,
465 schema=RESET_ACCESSORY_SERVICE_SCHEMA,
468 async
def async_handle_homekit_unpair(service: ServiceCall) ->
None:
469 """Handle unpair HomeKit service call."""
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)):
477 for ctype, cval
in dev_reg_ent.connections
478 if ctype == dr.CONNECTION_NETWORK_MAC
480 matching_instances = [
483 if homekit.driver
and dr.format_mac(homekit.driver.state.mac)
in macs
485 if not matching_instances:
487 f
"No homekit accessory found for device id: {device_id}"
489 for homekit
in matching_instances:
490 homekit.async_unpair()
492 hass.services.async_register(
494 SERVICE_HOMEKIT_UNPAIR,
495 async_handle_homekit_unpair,
496 schema=UNPAIR_SERVICE_SCHEMA,
499 async
def _handle_homekit_reload(service: ServiceCall) ->
None:
500 """Handle start HomeKit service call."""
503 if not config
or DOMAIN
not in config:
506 current_entries = hass.config_entries.async_entries(DOMAIN)
511 for conf
in config[DOMAIN]:
513 hass, entries_by_name, entries_by_port, conf
517 create_eager_task(hass.config_entries.async_reload(entry.entry_id))
518 for entry
in current_entries
521 await asyncio.gather(*reload_tasks)
527 _handle_homekit_reload,
532 """Class to handle all actions between HomeKit and Home Assistant."""
539 ip_address: str |
None,
540 entity_filter: EntityFilter,
541 exclude_accessory_mode: bool,
542 entity_config: dict[str, Any],
544 advertise_ips: list[str],
547 devices: list[str] |
None =
None,
549 """Initialize a HomeKit object."""
555 self._config: defaultdict[str, dict[str, Any]] = defaultdict(
564 self.
aid_storageaid_storage: AccessoryAidStorage |
None =
None
565 self.
iid_storageiid_storage: AccessoryIIDStorage |
None =
None
567 self.
driverdriver: HomeDriver |
None =
None
568 self.
bridgebridge: HomeBridge |
None =
None
572 def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> bool:
573 """Set up bridge and accessory driver.
575 Returns True if data was loaded from disk
577 Returns False if the persistent data was not loaded
580 persist_file = get_persist_fullpath_for_entry_id(self.
hasshass, self.
_entry_id_entry_id)
586 loop=self.
hasshass.loop,
588 port=self.
_port_port,
589 persist_file=persist_file,
591 async_zeroconf_instance=async_zeroconf_instance,
592 zeroconf_server=f
"{uuid}-hap.local.",
598 if os.path.exists(persist_file):
603 self.
driverdriver.state.mac = pyhap_util.generate_mac()
607 """Reset the accessory to load the latest configuration."""
608 _LOGGER.debug(
"Resetting accessories: %s", entity_ids)
617 """Reload the accessory to load the latest configuration."""
618 _LOGGER.debug(
"Reloading accessories: %s", entity_ids)
627 """Shutdown an accessory."""
628 assert self.
driverdriver
is not None
629 accessory.async_stop()
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)
640 self, entity_ids: Iterable[str]
642 """Reset accessories in accessory mode."""
643 assert self.
driverdriver
is not None
645 acc = cast(HomeAccessory, self.
driverdriver.accessory)
646 if acc.entity_id
not in entity_ids:
648 if not (state := self.
hasshass.states.get(acc.entity_id)):
650 "The underlying entity %s disappeared during reload", acc.entity_id
655 self.
driverdriver.accessory = new_acc
660 self, entity_ids: Iterable[str]
662 """Remove accessories by entity id."""
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:
673 removed.append(entity_id)
677 self, entity_ids: Iterable[str]
679 """Reset accessories in bridge mode."""
681 _LOGGER.debug(
"No accessories to reset in bridge mode for: %s", entity_ids)
686 assert self.
driverdriver
is not None
688 await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME)
692 self, entity_ids: Iterable[str]
694 """Reload accessories in bridge mode."""
699 self, removed: list[str]
701 """Recreate removed accessories in bridge mode."""
702 for entity_id
in removed:
703 if not (state := self.
hasshass.states.get(entity_id)):
705 "The underlying entity %s disappeared during reload", entity_id
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):
721 "Updating HomeKit accessories hash from %s -> %s", old_hash, new_hash
723 driver.async_persist()
724 driver.async_update_advertisement()
726 _LOGGER.debug(
"HomeKit accessories hash is unchanged: %s", new_hash)
730 """Try adding accessory to bridge if configured beforehand."""
731 assert self.
driverdriver
is not None
736 if state_needs_accessory_mode(state):
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 "
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()
760 self.
bridgebridge.add_accessory(acc)
764 "Failed to create a HomeKit accessory for %s", state.entity_id
769 """Check if adding another devices would reach the limit and log."""
771 assert self.
bridgebridge
is not None
772 if len(self.
bridgebridge.accessories) + 1 >= MAX_DEVICES:
775 "Cannot add %s as this would exceed the %d device limit. Consider"
776 " using the filter option"
785 self, device: dr.DeviceEntry, device_triggers: list[dict[str, Any]]
787 """Add device automation triggers to the bridge."""
792 assert self.
bridgebridge
is not None
793 aid = self.
aid_storageaid_storage.get_or_allocate_aid(device.id, device.id)
797 config: dict[str, Any] = {}
807 device_triggers=device_triggers,
809 await trigger_accessory.async_attach()
810 self.
bridgebridge.add_accessory(trigger_accessory)
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)
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):
833 if ent_reg_ent := ent_reg.async_get(entity_id):
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):
841 ent_reg_ent, dev_reg, entity_id
843 if device_id := ent_reg_ent.device_id:
844 if device_id
not in device_lookup:
845 device_lookup[device_id] = {
848 entry.device_class
or entry.original_device_class,
850 for entry
in entries.get_entries_for_device_id(device_id)
853 ent_reg_ent, device_lookup[device_id], state
856 entity_states.append(state)
861 """Load storage and start."""
862 if self.
statusstatus != STATUS_READY:
864 self.
statusstatus = STATUS_WAIT
867 SIGNAL_RELOAD_ENTITIES.format(self.
_entry_id_entry_id),
870 async_zc_instance = await zeroconf.async_get_async_instance(self.
hasshass)
871 uuid = await instance_id.async_get(self.
hasshass)
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
880 assert self.
driverdriver
is not None
885 _LOGGER.debug(
"Driver start for %s", self.
_name_name)
887 if not loaded_from_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
895 if self.
driverdriver.state.paired:
901 """Show the pairing setup message."""
902 assert self.
driverdriver
is not None
904 async_show_setup_message(
908 self.
driverdriver.state.pincode,
909 self.
driverdriver.accessory.xhm_uri(),
914 """Remove all pairings for an accessory so it can be repaired."""
915 assert self.
driverdriver
is not None
917 state = self.
driverdriver.state
918 for client_uuid
in list(state.paired_clients):
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()
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)
947 connection = (dr.CONNECTION_NETWORK_MAC, formatted_mac)
948 identifier = (DOMAIN, self.
_entry_id_entry_id, BRIDGE_SERIAL_NUMBER)
950 accessory_type = type(self.
driverdriver.accessory).__name__
951 dev_reg.async_get_or_create(
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,
966 dev_reg: dr.DeviceRegistry,
967 identifier: tuple[str, str, str],
968 connection: tuple[str, str],
970 """Purge bridges that exist from failed pairing or manual resets."""
973 for entry
in dev_reg.devices.get_devices_for_config_entry_id(self.
_entry_id_entry_id)
975 identifier
not in entry.identifiers
976 or connection
not in entry.connections
980 for device_id
in devices_to_purge:
981 dev_reg.async_remove_device(device_id)
985 self, entity_states: list[State]
986 ) -> HomeAccessory |
None:
987 """Create a single HomeKit accessory (accessory mode)."""
988 assert self.
driverdriver
is not None
990 if not entity_states:
992 "HomeKit %s cannot startup: entity not available: %s",
997 state = entity_states[0]
998 conf = self._config.
get(state.entity_id, {}).copy()
1002 "HomeKit %s cannot startup: entity not supported: %s",
1009 self, entity_states: Iterable[State]
1011 """Create a HomeKit bridge with accessories. (bridge mode)."""
1012 assert self.
driverdriver
is not None
1015 for state
in entity_states:
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):
1029 "HomeKit %s cannot add device %s because it is missing from the"
1036 valid_device_ids.append(device_id)
1037 for device_id, device_triggers
in (
1038 await device_automation.async_get_device_automations(
1040 device_automation.DeviceAutomationType.TRIGGER,
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:
1050 except vol.Invalid
as ex:
1053 "%s: cannot add unsupported trigger %s because it requires"
1054 " additional inputs which are not supported by HomeKit: %s"
1061 valid_device_triggers.append(trigger)
1065 """Create the accessories."""
1066 assert self.
driverdriver
is not None
1069 if self.
_homekit_mode_homekit_mode == HOMEKIT_MODE_ACCESSORY:
1077 self.
driverdriver.accessory = acc
1081 """Stop the accessory driver."""
1082 if self.
statusstatus != STATUS_RUNNING:
1085 self.
statusstatus = STATUS_STOPPED
1088 _LOGGER.debug(
"Driver stop for %s", self.
_name_name)
1095 ent_reg_ent: er.RegistryEntry,
1096 lookup: dict[tuple[str, str |
None], str],
1099 if (ent_reg_ent.device_class
or ent_reg_ent.original_device_class)
in (
1100 BinarySensorDeviceClass.BATTERY_CHARGING,
1101 SensorDeviceClass.BATTERY,
1105 domain = state.domain
1106 attributes = state.attributes
1107 config = self._config
1108 entity_id = state.entity_id
1110 if ATTR_BATTERY_CHARGING
not in attributes
and (
1111 battery_charging_binary_sensor_entity_id := lookup.get(
1112 BATTERY_CHARGING_SENSOR
1115 config[entity_id].setdefault(
1116 CONF_LINKED_BATTERY_CHARGING_SENSOR,
1117 battery_charging_binary_sensor_entity_id,
1120 if ATTR_BATTERY_LEVEL
not in attributes
and (
1121 battery_sensor_entity_id := lookup.get(BATTERY_SENSOR)
1123 config[entity_id].setdefault(
1124 CONF_LINKED_BATTERY_SENSOR, battery_sensor_entity_id
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
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
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
1141 if domain == HUMIDIFIER_DOMAIN
and (
1142 current_humidity_sensor_entity_id := lookup.get(HUMIDITY_SENSOR)
1144 config[entity_id].setdefault(
1145 CONF_LINKED_HUMIDITY_SENSOR, current_humidity_sensor_entity_id
1150 ent_reg_ent: er.RegistryEntry,
1151 dev_reg: dr.DeviceRegistry,
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):
1159 if ATTR_MANUFACTURER
not in ent_cfg:
1162 self.
hasshass, ent_reg_ent.platform
1164 ent_cfg[ATTR_INTEGRATION] = integration.name
1165 except IntegrationNotFound:
1166 ent_cfg[ATTR_INTEGRATION] = ent_reg_ent.platform
1169 self, device_entry: dr.DeviceEntry, config: dict[str, Any]
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
1187 """Display the homekit pairing code at a protected url."""
1189 url =
"/api/homekit/pairingqr"
1190 name =
"api:homekit:pairingqr"
1191 requires_auth =
False
1193 async
def get(self, request: web.Request) -> web.Response:
1194 """Retrieve the pairing QRCode image."""
1195 if not request.query_string:
1197 entry_id, secret = request.query_string.split(
"-")
1198 hass = request.app[KEY_HASS]
1199 entry_data: HomeKitEntryData |
None
1201 not (entry := hass.config_entries.async_get_entry(entry_id))
1202 or not (entry_data := getattr(entry,
"runtime_data",
None))
1204 or not entry_data.pairing_qr_secret
1205 or secret != entry_data.pairing_qr_secret
1208 return web.Response(
1209 body=entry_data.pairing_qr,
1210 content_type=
"image/svg+xml",
web.Response get(self, web.Request request)
_cancel_reload_dispatcher
None _async_shutdown_accessory(self, HomeAccessory accessory)
HomeAccessory|None _async_create_single_accessory(self, list[State] entity_states)
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)
HomeAccessory _async_create_bridge_accessory(self, Iterable[State] entity_states)
HomeAccessory|None async_remove_bridge_accessory(self, int aid)
None _async_configure_linked_sensors(self, er.RegistryEntry ent_reg_ent, dict[tuple[str, str|None], str] lookup, State state)
bool setup(self, AsyncZeroconf async_zeroconf_instance, str uuid)
None async_stop(self, *Any args)
list[str] _async_remove_accessories_by_entity_id(self, Iterable[str] entity_ids)
None _async_show_setup_message(self)
bool _async_create_accessories(self)
list[State] async_configure_accessories(self)
None _async_add_trigger_accessories(self)
None add_bridge_triggers_accessory(self, dr.DeviceEntry device, list[dict[str, Any]] device_triggers)
None async_start(self, *Any args)
None _async_register_bridge(self)
None _async_reload_accessories_in_accessory_mode(self, Iterable[str] entity_ids)
HomeAccessory|None add_bridge_accessory(self, State state)
None _fill_config_from_device_registry_entry(self, dr.DeviceEntry device_entry, dict[str, Any] config)
None _async_reload_accessories_in_bridge_mode(self, Iterable[str] entity_ids)
None _async_reset_accessories_in_bridge_mode(self, Iterable[str] entity_ids)
None async_reset_accessories(self, Iterable[str] entity_ids)
None _async_set_device_info_attributes(self, er.RegistryEntry ent_reg_ent, dr.DeviceRegistry dev_reg, str entity_id)
bool _would_exceed_max_devices(self, str|None name)
bool _async_update_accessories_hash(self)
None async_reload_accessories(self, Iterable[str] entity_ids)
None _async_recreate_removed_accessories_in_bridge_mode(self, list[str] removed)
None _async_purge_old_bridges(self, dr.DeviceRegistry dev_reg, tuple[str, str, str] identifier, tuple[str, str] connection)
ConfigType async_validate_trigger_config(HomeAssistant hass, ConfigType config)
web.Response get(self, web.Request request, str config_key)
HomeAccessory|None get_accessory(HomeAssistant hass, HomeDriver driver, State state, int|None aid, dict config)
bool async_unload_entry(HomeAssistant hass, HomeKitConfigEntry entry)
bool async_setup(HomeAssistant hass, ConfigType config)
tuple[dict[str, ConfigEntry], dict[int, ConfigEntry]] _async_get_imported_entries_indices(list[ConfigEntry] current_entries)
list[HomeKit] _async_all_homekit_instances(HomeAssistant hass)
None _async_import_options_from_data_if_missing(HomeAssistant hass, HomeKitConfigEntry entry)
None async_remove_entry(HomeAssistant hass, HomeKitConfigEntry entry)
list[dict[str, Any]] _has_all_unique_names_and_ports(list[dict[str, Any]] bridges)
None _async_update_listener(HomeAssistant hass, HomeKitConfigEntry entry)
bool _async_update_config_entry_from_yaml(HomeAssistant hass, dict[str, ConfigEntry] entries_by_name, dict[int, ConfigEntry] entries_by_port, ConfigType conf)
None _async_register_events_and_services(HomeAssistant hass)
bool async_setup_entry(HomeAssistant hass, HomeKitConfigEntry entry)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
ConfigType|None async_integration_yaml_config(HomeAssistant hass, str integration_name)
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))
SelectedEntities async_extract_referenced_entity_ids(HomeAssistant hass, ServiceCall service_call, bool expand_group=True)
CALLBACK_TYPE async_at_started(HomeAssistant hass, Callable[[HomeAssistant], Coroutine[Any, Any, None]|None] at_start_cb)
Integration async_get_integration(HomeAssistant hass, str domain)