Store HomeKit generated accessory id against unique_id where possible (#33109)
* HomeKit: Store generated aid against unique_id where possible * Fix conflict * Fix max accessories check * homekit counts the bridge as an accessory * Add coverage for aidmanager * prepare for merge Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
3259ba6116
commit
a80ce60e75
8 changed files with 317 additions and 40 deletions
142
homeassistant/components/homekit/aidmanager.py
Normal file
142
homeassistant/components/homekit/aidmanager.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
"""
|
||||
Manage allocation of accessory ID's.
|
||||
|
||||
HomeKit needs to allocate unique numbers to each accessory. These need to
|
||||
be stable between reboots and upgrades.
|
||||
|
||||
Using a hash function to generate them means collisions. It also means you
|
||||
can't change the hash without causing breakages for HA users.
|
||||
|
||||
This module generates and stores them in a HA storage.
|
||||
"""
|
||||
import logging
|
||||
import random
|
||||
from zlib import adler32
|
||||
|
||||
from fnvhash import fnv1a_32
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
AID_MANAGER_STORAGE_KEY = f"{DOMAIN}.aids"
|
||||
AID_MANAGER_STORAGE_VERSION = 1
|
||||
AID_MANAGER_SAVE_DELAY = 2
|
||||
|
||||
INVALID_AIDS = (0, 1)
|
||||
|
||||
AID_MIN = 2
|
||||
AID_MAX = 18446744073709551615
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_system_unique_id(entity: RegistryEntry):
|
||||
"""Determine the system wide unique_id for an entity."""
|
||||
return f"{entity.platform}.{entity.domain}.{entity.unique_id}"
|
||||
|
||||
|
||||
def _generate_aids(unique_id: str, entity_id: str) -> int:
|
||||
"""Generate accessory aid."""
|
||||
|
||||
# Backward compatibility: Previously HA used to *only* do adler32 on the entity id.
|
||||
# Not stable if entity ID changes
|
||||
# Not robust against collisions
|
||||
yield adler32(entity_id.encode("utf-8"))
|
||||
|
||||
# Use fnv1a_32 of the unique id as
|
||||
# fnv1a_32 has less collisions than
|
||||
# adler32
|
||||
yield fnv1a_32(unique_id.encode("utf-8"))
|
||||
|
||||
# If called again resort to random allocations.
|
||||
# Given the size of the range its unlikely we'll encounter duplicates
|
||||
# But try a few times regardless
|
||||
for _ in range(5):
|
||||
yield random.randrange(AID_MIN, AID_MAX)
|
||||
|
||||
|
||||
class AccessoryAidStorage:
|
||||
"""
|
||||
Holds a map of entity ID to HomeKit ID.
|
||||
|
||||
Will generate new ID's, ensure they are unique and store them to make sure they
|
||||
persist over reboots.
|
||||
"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant):
|
||||
"""Create a new entity map store."""
|
||||
self.hass = hass
|
||||
self.store = Store(hass, AID_MANAGER_STORAGE_VERSION, AID_MANAGER_STORAGE_KEY)
|
||||
self.allocations = {}
|
||||
self.allocated_aids = set()
|
||||
|
||||
self._entity_registry = None
|
||||
|
||||
async def async_initialize(self):
|
||||
"""Load the latest AID data."""
|
||||
self._entity_registry = (
|
||||
await self.hass.helpers.entity_registry.async_get_registry()
|
||||
)
|
||||
|
||||
raw_storage = await self.store.async_load()
|
||||
if not raw_storage:
|
||||
# There is no data about aid allocations yet
|
||||
return
|
||||
|
||||
self.allocations = raw_storage.get("unique_ids", {})
|
||||
self.allocated_aids = set(self.allocations.values())
|
||||
|
||||
def get_or_allocate_aid_for_entity_id(self, entity_id: str):
|
||||
"""Generate a stable aid for an entity id."""
|
||||
entity = self._entity_registry.async_get(entity_id)
|
||||
|
||||
if entity:
|
||||
return self._get_or_allocate_aid(
|
||||
get_system_unique_id(entity), entity.entity_id
|
||||
)
|
||||
|
||||
_LOGGER.warning(
|
||||
"Entity '%s' does not have a stable unique identifier so aid allocation will be unstable and may cause collisions",
|
||||
entity_id,
|
||||
)
|
||||
return adler32(entity_id.encode("utf-8"))
|
||||
|
||||
def _get_or_allocate_aid(self, unique_id: str, entity_id: str):
|
||||
"""Allocate (and return) a new aid for an accessory."""
|
||||
if unique_id in self.allocations:
|
||||
return self.allocations[unique_id]
|
||||
|
||||
for aid in _generate_aids(unique_id, entity_id):
|
||||
if aid in INVALID_AIDS:
|
||||
continue
|
||||
if aid not in self.allocated_aids:
|
||||
self.allocations[unique_id] = aid
|
||||
self.allocated_aids.add(aid)
|
||||
self.async_schedule_save()
|
||||
return aid
|
||||
|
||||
raise ValueError(
|
||||
f"Unable to generate unique aid allocation for {entity_id} [{unique_id}]"
|
||||
)
|
||||
|
||||
def delete_aid(self, unique_id: str):
|
||||
"""Delete an aid allocation."""
|
||||
if unique_id not in self.allocations:
|
||||
return
|
||||
|
||||
aid = self.allocations.pop(unique_id)
|
||||
self.allocated_aids.discard(aid)
|
||||
self.async_schedule_save()
|
||||
|
||||
@callback
|
||||
def async_schedule_save(self):
|
||||
"""Schedule saving the entity map cache."""
|
||||
self.store.async_delay_save(self._data_to_save, AID_MANAGER_SAVE_DELAY)
|
||||
|
||||
@callback
|
||||
def _data_to_save(self):
|
||||
"""Return data of entity map to store in a file."""
|
||||
return {"unique_ids": self.allocations}
|
Loading…
Add table
Add a link
Reference in a new issue