Home Assistant Unofficial Reference 2024.12.1
iidmanager.py
Go to the documentation of this file.
1 """Manage allocation of instance 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 This module generates and stores them in a HA storage.
7 """
8 
9 from __future__ import annotations
10 
11 from uuid import UUID
12 
13 from pyhap.util import uuid_to_hap_type
14 
15 from homeassistant.core import HomeAssistant, callback
16 from homeassistant.helpers.storage import Store
17 
18 from .util import get_iid_storage_filename_for_entry_id
19 
20 IID_MANAGER_STORAGE_VERSION = 2
21 IID_MANAGER_SAVE_DELAY = 2
22 
23 ALLOCATIONS_KEY = "allocations"
24 
25 IID_MIN = 1
26 IID_MAX = 18446744073709551615
27 
28 
29 ACCESSORY_INFORMATION_SERVICE = "3E"
30 
31 
33  """Storage class for IIDManager."""
34 
36  self,
37  old_major_version: int,
38  old_minor_version: int,
39  old_data: dict,
40  ) -> dict:
41  """Migrate to the new version."""
42  if old_major_version == 1:
43  # Convert v1 to v2 format which uses a unique iid set per accessory
44  # instead of per pairing since we need the ACCESSORY_INFORMATION_SERVICE
45  # to always have iid 1 for each bridged accessory as well as the bridge
46  old_allocations: dict[str, int] = old_data.pop(ALLOCATIONS_KEY, {})
47  new_allocation: dict[str, dict[str, int]] = {}
48  old_data[ALLOCATIONS_KEY] = new_allocation
49  for allocation_key, iid in old_allocations.items():
50  aid_str, new_allocation_key = allocation_key.split("_", 1)
51  service_type, _, char_type, *_ = new_allocation_key.split("_")
52  accessory_allocation = new_allocation.setdefault(aid_str, {})
53  if service_type == ACCESSORY_INFORMATION_SERVICE and not char_type:
54  accessory_allocation[new_allocation_key] = 1
55  elif iid != 1:
56  accessory_allocation[new_allocation_key] = iid
57 
58  return old_data
59 
60  raise NotImplementedError
61 
62 
64  """Provide stable allocation of IIDs for the lifetime of an accessory.
65 
66  Will generate new ID's, ensure they are unique and store them to make sure they
67  persist over reboots.
68  """
69 
70  def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
71  """Create a new iid store."""
72  self.hasshass = hass
73  self.allocationsallocations: dict[str, dict[str, int]] = {}
74  self.allocated_iids: dict[str, list[int]] = {}
75  self.entry_identry_id = entry_id
76  self.storestore: IIDStorage | None = None
77 
78  async def async_initialize(self) -> None:
79  """Load the latest IID data."""
80  iid_store = get_iid_storage_filename_for_entry_id(self.entry_identry_id)
81  self.storestore = IIDStorage(self.hasshass, IID_MANAGER_STORAGE_VERSION, iid_store)
82 
83  if not (raw_storage := await self.storestore.async_load()):
84  # There is no data about iid allocations yet
85  return
86 
87  assert isinstance(raw_storage, dict)
88  self.allocationsallocations = raw_storage.get(ALLOCATIONS_KEY, {})
89  for aid_str, allocations in self.allocationsallocations.items():
90  self.allocated_iids[aid_str] = sorted(allocations.values())
91 
93  self,
94  aid: int,
95  service_uuid: UUID,
96  service_unique_id: str | None,
97  char_uuid: UUID | None,
98  char_unique_id: str | None,
99  ) -> int:
100  """Generate a stable iid."""
101  service_hap_type: str = uuid_to_hap_type(service_uuid)
102  char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None
103  # Allocation key must be a string since we are saving it to JSON
104  allocation_key = (
105  f'{service_hap_type}_{service_unique_id or ""}_'
106  f'{char_hap_type or ""}_{char_unique_id or ""}'
107  )
108  # AID must be a string since JSON keys cannot be int
109  aid_str = str(aid)
110  accessory_allocation = self.allocationsallocations.setdefault(aid_str, {})
111  accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, [1])
112  if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None:
113  return 1
114  if allocation_key in accessory_allocation:
115  return accessory_allocation[allocation_key]
116  if accessory_allocated_iids:
117  allocated_iid = accessory_allocated_iids[-1] + 1
118  else:
119  allocated_iid = 2
120  accessory_allocation[allocation_key] = allocated_iid
121  accessory_allocated_iids.append(allocated_iid)
122  self._async_schedule_save_async_schedule_save()
123  return allocated_iid
124 
125  @callback
126  def _async_schedule_save(self) -> None:
127  """Schedule saving the iid allocations."""
128  assert self.storestore is not None
129  self.storestore.async_delay_save(self._data_to_save_data_to_save, IID_MANAGER_SAVE_DELAY)
130 
131  async def async_save(self) -> None:
132  """Save the iid allocations."""
133  assert self.storestore is not None
134  return await self.storestore.async_save(self._data_to_save_data_to_save())
135 
136  @callback
137  def _data_to_save(self) -> dict[str, dict[str, dict[str, int]]]:
138  """Return data of entity map to store in a file."""
139  return {ALLOCATIONS_KEY: self.allocationsallocations}
None __init__(self, HomeAssistant hass, str entry_id)
Definition: iidmanager.py:70
int get_or_allocate_iid(self, int aid, UUID service_uuid, str|None service_unique_id, UUID|None char_uuid, str|None char_unique_id)
Definition: iidmanager.py:99
dict[str, dict[str, dict[str, int]]] _data_to_save(self)
Definition: iidmanager.py:137
dict _async_migrate_func(self, int old_major_version, int old_minor_version, dict old_data)
Definition: iidmanager.py:40
None async_load(HomeAssistant hass)
None async_delay_save(self, Callable[[], _T] data_func, float delay=0)
Definition: storage.py:444