Home Assistant Unofficial Reference 2024.12.1
aidmanager.py
Go to the documentation of this file.
1 """Manage allocation of accessory ID's.
2 
3 HomeKit needs to allocate unique numbers to each accessory. These need to
4 be stable between reboots and upgrades.
5 
6 Using a hash function to generate them means collisions. It also means you
7 can't change the hash without causing breakages for HA users.
8 
9 This module generates and stores them in a HA storage.
10 """
11 
12 from __future__ import annotations
13 
14 from collections.abc import Generator
15 import random
16 
17 from fnv_hash_fast import fnv1a_32
18 
19 from homeassistant.core import HomeAssistant, callback
20 from homeassistant.helpers import entity_registry as er
21 from homeassistant.helpers.storage import Store
22 
23 from .util import get_aid_storage_filename_for_entry_id
24 
25 AID_MANAGER_STORAGE_VERSION = 1
26 AID_MANAGER_SAVE_DELAY = 2
27 
28 ALLOCATIONS_KEY = "allocations"
29 UNIQUE_IDS_KEY = "unique_ids"
30 
31 INVALID_AIDS = (0, 1)
32 
33 AID_MIN = 2
34 AID_MAX = 18446744073709551615
35 
36 
37 def get_system_unique_id(entity: er.RegistryEntry, entity_unique_id: str) -> str:
38  """Determine the system wide unique_id for an entity."""
39  return f"{entity.platform}.{entity.domain}.{entity_unique_id}"
40 
41 
42 def _generate_aids(unique_id: str | None, entity_id: str) -> Generator[int]:
43  """Generate accessory aid."""
44 
45  if unique_id:
46  # Use fnv1a_32 of the unique id as
47  # fnv1a_32 has less collisions than
48  # adler32
49  yield fnv1a_32(unique_id.encode("utf-8"))
50 
51  # If there is no unique id we use
52  # fnv1a_32 as it is unlikely to collide
53  yield fnv1a_32(entity_id.encode("utf-8"))
54 
55  # If called again resort to random allocations.
56  # Given the size of the range its unlikely we'll encounter duplicates
57  # But try a few times regardless
58  for _ in range(5):
59  yield random.randrange(AID_MIN, AID_MAX)
60 
61 
63  """Holds a map of entity ID to HomeKit ID.
64 
65  Will generate new ID's, ensure they are unique and store them to make sure they
66  persist over reboots.
67  """
68 
69  def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
70  """Create a new entity map store."""
71  self.hasshass = hass
72  self.allocationsallocations: dict[str, int] = {}
73  self.allocated_aidsallocated_aids: set[int] = set()
74  self._entry_id_entry_id = entry_id
75  self.storestore: Store | None = None
76  self._entity_registry_entity_registry = er.async_get(hass)
77 
78  async def async_initialize(self) -> None:
79  """Load the latest AID data."""
80  aidstore = get_aid_storage_filename_for_entry_id(self._entry_id_entry_id)
81  self.storestore = Store(self.hasshass, AID_MANAGER_STORAGE_VERSION, aidstore)
82 
83  if not (raw_storage := await self.storestore.async_load()):
84  # There is no data about aid allocations yet
85  return
86  assert isinstance(raw_storage, dict)
87  self.allocationsallocations = raw_storage.get(ALLOCATIONS_KEY, {})
88  self.allocated_aidsallocated_aids = set(self.allocationsallocations.values())
89 
90  def get_or_allocate_aid_for_entity_id(self, entity_id: str) -> int:
91  """Generate a stable aid for an entity id."""
92  if not (entry := self._entity_registry_entity_registry.async_get(entity_id)):
93  return self.get_or_allocate_aidget_or_allocate_aid(None, entity_id)
94 
95  sys_unique_id = get_system_unique_id(entry, entry.unique_id)
96  self._migrate_unique_id_aid_assignment_if_needed_migrate_unique_id_aid_assignment_if_needed(sys_unique_id, entry)
97  return self.get_or_allocate_aidget_or_allocate_aid(sys_unique_id, entity_id)
98 
100  self, sys_unique_id: str, entry: er.RegistryEntry
101  ) -> None:
102  """Migrate the unique id aid assignment if its changed."""
103  if sys_unique_id in self.allocationsallocations or not (
104  previous_unique_id := entry.previous_unique_id
105  ):
106  return
107  old_sys_unique_id = get_system_unique_id(entry, previous_unique_id)
108  if aid := self.allocationsallocations.pop(old_sys_unique_id, None):
109  self.allocationsallocations[sys_unique_id] = aid
110  self.async_schedule_saveasync_schedule_save()
111 
112  def get_or_allocate_aid(self, unique_id: str | None, entity_id: str) -> int:
113  """Allocate (and return) a new aid for an accessory."""
114  if unique_id and unique_id in self.allocationsallocations:
115  return self.allocationsallocations[unique_id]
116  if entity_id in self.allocationsallocations:
117  return self.allocationsallocations[entity_id]
118 
119  for aid in _generate_aids(unique_id, entity_id):
120  if aid in INVALID_AIDS:
121  continue
122  if aid not in self.allocated_aidsallocated_aids:
123  # Prefer the unique_id over the entitiy_id
124  storage_key = unique_id or entity_id
125  self.allocationsallocations[storage_key] = aid
126  self.allocated_aidsallocated_aids.add(aid)
127  self.async_schedule_saveasync_schedule_save()
128  return aid
129 
130  raise ValueError(
131  f"Unable to generate unique aid allocation for {entity_id} [{unique_id}]"
132  )
133 
134  def delete_aid(self, storage_key: str) -> None:
135  """Delete an aid allocation."""
136  if storage_key not in self.allocationsallocations:
137  return
138 
139  aid = self.allocationsallocations.pop(storage_key)
140  self.allocated_aidsallocated_aids.discard(aid)
141  self.async_schedule_saveasync_schedule_save()
142 
143  @callback
144  def async_schedule_save(self) -> None:
145  """Schedule saving the entity map cache."""
146  assert self.storestore is not None
147  self.storestore.async_delay_save(self._data_to_save_data_to_save, AID_MANAGER_SAVE_DELAY)
148 
149  async def async_save(self) -> None:
150  """Save the entity map cache."""
151  assert self.storestore is not None
152  return await self.storestore.async_save(self._data_to_save_data_to_save())
153 
154  @callback
155  def _data_to_save(self) -> dict[str, dict[str, int]]:
156  """Return data of entity map to store in a file."""
157  return {ALLOCATIONS_KEY: self.allocationsallocations}
int get_or_allocate_aid(self, str|None unique_id, str entity_id)
Definition: aidmanager.py:112
None __init__(self, HomeAssistant hass, str entry_id)
Definition: aidmanager.py:69
None _migrate_unique_id_aid_assignment_if_needed(self, str sys_unique_id, er.RegistryEntry entry)
Definition: aidmanager.py:101
bool add(self, _T matcher)
Definition: match.py:185
Generator[int] _generate_aids(str|None unique_id, str entity_id)
Definition: aidmanager.py:42
str get_system_unique_id(er.RegistryEntry entity, str entity_unique_id)
Definition: aidmanager.py:37
None async_load(HomeAssistant hass)
AreaRegistry async_get(HomeAssistant hass)
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)
Definition: storage.py:444