core/homeassistant/components/homekit/iidmanager.py

140 lines
5.1 KiB
Python

"""Manage allocation of instance ID's.
HomeKit needs to allocate unique numbers to each accessory. These need to
be stable between reboots and upgrades.
This module generates and stores them in a HA storage.
"""
from __future__ import annotations
from uuid import UUID
from pyhap.util import uuid_to_hap_type
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.storage import Store
from .util import get_iid_storage_filename_for_entry_id
IID_MANAGER_STORAGE_VERSION = 2
IID_MANAGER_SAVE_DELAY = 2
ALLOCATIONS_KEY = "allocations"
IID_MIN = 1
IID_MAX = 18446744073709551615
ACCESSORY_INFORMATION_SERVICE = "3E"
class IIDStorage(Store):
"""Storage class for IIDManager."""
async def _async_migrate_func(
self,
old_major_version: int,
old_minor_version: int,
old_data: dict,
) -> dict:
"""Migrate to the new version."""
if old_major_version == 1:
# Convert v1 to v2 format which uses a unique iid set per accessory
# instead of per pairing since we need the ACCESSORY_INFORMATION_SERVICE
# to always have iid 1 for each bridged accessory as well as the bridge
old_allocations: dict[str, int] = old_data.pop(ALLOCATIONS_KEY, {})
new_allocation: dict[str, dict[str, int]] = {}
old_data[ALLOCATIONS_KEY] = new_allocation
for allocation_key, iid in old_allocations.items():
aid_str, new_allocation_key = allocation_key.split("_", 1)
service_type, _, char_type, *_ = new_allocation_key.split("_")
accessory_allocation = new_allocation.setdefault(aid_str, {})
if service_type == ACCESSORY_INFORMATION_SERVICE and not char_type:
accessory_allocation[new_allocation_key] = 1
elif iid != 1:
accessory_allocation[new_allocation_key] = iid
return old_data
raise NotImplementedError
class AccessoryIIDStorage:
"""Provide stable allocation of IIDs for the lifetime of an accessory.
Will generate new ID's, ensure they are unique and store them to make sure they
persist over reboots.
"""
def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
"""Create a new iid store."""
self.hass = hass
self.allocations: dict[str, dict[str, int]] = {}
self.allocated_iids: dict[str, list[int]] = {}
self.entry_id = entry_id
self.store: IIDStorage | None = None
async def async_initialize(self) -> None:
"""Load the latest IID data."""
iid_store = get_iid_storage_filename_for_entry_id(self.entry_id)
self.store = IIDStorage(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
if not (raw_storage := await self.store.async_load()):
# There is no data about iid allocations yet
return
assert isinstance(raw_storage, dict)
self.allocations = raw_storage.get(ALLOCATIONS_KEY, {})
for aid_str, allocations in self.allocations.items():
self.allocated_iids[aid_str] = sorted(allocations.values())
def get_or_allocate_iid(
self,
aid: int,
service_uuid: UUID,
service_unique_id: str | None,
char_uuid: UUID | None,
char_unique_id: str | None,
) -> int:
"""Generate a stable iid."""
service_hap_type: str = uuid_to_hap_type(service_uuid)
char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None
# Allocation key must be a string since we are saving it to JSON
allocation_key = (
f'{service_hap_type}_{service_unique_id or ""}_'
f'{char_hap_type or ""}_{char_unique_id or ""}'
)
# AID must be a string since JSON keys cannot be int
aid_str = str(aid)
accessory_allocation = self.allocations.setdefault(aid_str, {})
accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, [1])
if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None:
return 1
if allocation_key in accessory_allocation:
return accessory_allocation[allocation_key]
if accessory_allocated_iids:
allocated_iid = accessory_allocated_iids[-1] + 1
else:
allocated_iid = 2
accessory_allocation[allocation_key] = allocated_iid
accessory_allocated_iids.append(allocated_iid)
self._async_schedule_save()
return allocated_iid
@callback
def _async_schedule_save(self) -> None:
"""Schedule saving the iid allocations."""
assert self.store is not None
self.store.async_delay_save(self._data_to_save, IID_MANAGER_SAVE_DELAY)
async def async_save(self) -> None:
"""Save the iid allocations."""
assert self.store is not None
return await self.store.async_save(self._data_to_save())
@callback
def _data_to_save(self) -> dict[str, dict[str, dict[str, int]]]:
"""Return data of entity map to store in a file."""
return {ALLOCATIONS_KEY: self.allocations}