diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index 61a922d17fa..95181114e79 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -25,6 +25,9 @@ AID_MANAGER_STORAGE_KEY = f"{DOMAIN}.aids" AID_MANAGER_STORAGE_VERSION = 1 AID_MANAGER_SAVE_DELAY = 2 +ALLOCATIONS_KEY = "allocations" +UNIQUE_IDS_KEY = "unique_ids" + INVALID_AIDS = (0, 1) AID_MIN = 2 @@ -46,10 +49,15 @@ def _generate_aids(unique_id: str, entity_id: str) -> int: # 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 unique_id: + # Use fnv1a_32 of the unique id as + # fnv1a_32 has less collisions than + # adler32 + yield fnv1a_32(unique_id.encode("utf-8")) + + # If there is no unique id we use + # fnv1a_32 as it is unlikely to collide + yield fnv1a_32(entity_id.encode("utf-8")) # If called again resort to random allocations. # Given the size of the range its unlikely we'll encounter duplicates @@ -86,34 +94,40 @@ class AccessoryAidStorage: # There is no data about aid allocations yet return - self.allocations = raw_storage.get("unique_ids", {}) + # Remove the UNIQUE_IDS_KEY in 0.112 and later + # The beta version used UNIQUE_IDS_KEY but + # since we now have entity ids in the dict + # we use ALLOCATIONS_KEY but check for + # UNIQUE_IDS_KEY in case the database has not + # been upgraded yet + self.allocations = raw_storage.get( + ALLOCATIONS_KEY, raw_storage.get(UNIQUE_IDS_KEY, {}) + ) 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 not entity: + return self._get_or_allocate_aid(None, 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")) + sys_unique_id = get_system_unique_id(entity) + return self._get_or_allocate_aid(sys_unique_id, entity_id) 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] + # Prefer the unique_id over the + # entitiy_id + storage_key = unique_id or entity_id + + if storage_key in self.allocations: + return self.allocations[storage_key] 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.allocations[storage_key] = aid self.allocated_aids.add(aid) self.async_schedule_save() return aid @@ -122,12 +136,12 @@ class AccessoryAidStorage: f"Unable to generate unique aid allocation for {entity_id} [{unique_id}]" ) - def delete_aid(self, unique_id: str): + def delete_aid(self, storage_key: str): """Delete an aid allocation.""" - if unique_id not in self.allocations: + if storage_key not in self.allocations: return - aid = self.allocations.pop(unique_id) + aid = self.allocations.pop(storage_key) self.allocated_aids.discard(aid) self.async_schedule_save() @@ -136,7 +150,11 @@ class AccessoryAidStorage: """Schedule saving the entity map cache.""" self.store.async_delay_save(self._data_to_save, AID_MANAGER_SAVE_DELAY) + async def async_save(self): + """Save the entity map cache.""" + return await self.store.async_save(self._data_to_save()) + @callback def _data_to_save(self): """Return data of entity map to store in a file.""" - return {"unique_ids": self.allocations} + return {ALLOCATIONS_KEY: self.allocations} diff --git a/tests/components/homekit/test_aidmanager.py b/tests/components/homekit/test_aidmanager.py index bcadac953ab..12d12082a33 100644 --- a/tests/components/homekit/test_aidmanager.py +++ b/tests/components/homekit/test_aidmanager.py @@ -1,12 +1,17 @@ """Tests for the HomeKit AID manager.""" +import os +from zlib import adler32 + from asynctest import patch import pytest from homeassistant.components.homekit.aidmanager import ( + AID_MANAGER_STORAGE_KEY, AccessoryAidStorage, get_system_unique_id, ) from homeassistant.helpers import device_registry +from homeassistant.helpers.storage import STORAGE_DIR from tests.common import MockConfigEntry, mock_device_registry, mock_registry @@ -118,3 +123,503 @@ async def test_aid_adler32_collision(hass, device_reg, entity_reg): aid = aid_storage.get_or_allocate_aid_for_entity_id(ent.entity_id) assert aid not in seen_aids seen_aids.add(aid) + + +async def test_aid_generation_no_unique_ids_handles_collision( + hass, device_reg, entity_reg +): + """Test colliding aids is stable.""" + + aid_storage = AccessoryAidStorage(hass) + await aid_storage.async_initialize() + + seen_aids = set() + collisions = [] + + for light_id in range(0, 220): + entity_id = f"light.light{light_id}" + hass.states.async_set(entity_id, "on") + expected_aid = adler32(entity_id.encode("utf-8")) + aid = aid_storage.get_or_allocate_aid_for_entity_id(entity_id) + if aid != expected_aid: + collisions.append(entity_id) + + assert aid not in seen_aids + seen_aids.add(aid) + + assert collisions == [ + "light.light201", + "light.light202", + "light.light203", + "light.light204", + "light.light205", + "light.light206", + "light.light207", + "light.light208", + "light.light209", + "light.light211", + "light.light212", + "light.light213", + "light.light214", + "light.light215", + "light.light216", + "light.light217", + "light.light218", + "light.light219", + ] + + assert aid_storage.allocations == { + "light.light0": 514851983, + "light.light1": 514917520, + "light.light10": 594609344, + "light.light100": 677446896, + "light.light101": 677512433, + "light.light102": 677577970, + "light.light103": 677643507, + "light.light104": 677709044, + "light.light105": 677774581, + "light.light106": 677840118, + "light.light107": 677905655, + "light.light108": 677971192, + "light.light109": 678036729, + "light.light11": 594674881, + "light.light110": 677577969, + "light.light111": 677643506, + "light.light112": 677709043, + "light.light113": 677774580, + "light.light114": 677840117, + "light.light115": 677905654, + "light.light116": 677971191, + "light.light117": 678036728, + "light.light118": 678102265, + "light.light119": 678167802, + "light.light12": 594740418, + "light.light120": 677709042, + "light.light121": 677774579, + "light.light122": 677840116, + "light.light123": 677905653, + "light.light124": 677971190, + "light.light125": 678036727, + "light.light126": 678102264, + "light.light127": 678167801, + "light.light128": 678233338, + "light.light129": 678298875, + "light.light13": 594805955, + "light.light130": 677840115, + "light.light131": 677905652, + "light.light132": 677971189, + "light.light133": 678036726, + "light.light134": 678102263, + "light.light135": 678167800, + "light.light136": 678233337, + "light.light137": 678298874, + "light.light138": 678364411, + "light.light139": 678429948, + "light.light14": 594871492, + "light.light140": 677971188, + "light.light141": 678036725, + "light.light142": 678102262, + "light.light143": 678167799, + "light.light144": 678233336, + "light.light145": 678298873, + "light.light146": 678364410, + "light.light147": 678429947, + "light.light148": 678495484, + "light.light149": 678561021, + "light.light15": 594937029, + "light.light150": 678102261, + "light.light151": 678167798, + "light.light152": 678233335, + "light.light153": 678298872, + "light.light154": 678364409, + "light.light155": 678429946, + "light.light156": 678495483, + "light.light157": 678561020, + "light.light158": 678626557, + "light.light159": 678692094, + "light.light16": 595002566, + "light.light160": 678233334, + "light.light161": 678298871, + "light.light162": 678364408, + "light.light163": 678429945, + "light.light164": 678495482, + "light.light165": 678561019, + "light.light166": 678626556, + "light.light167": 678692093, + "light.light168": 678757630, + "light.light169": 678823167, + "light.light17": 595068103, + "light.light170": 678364407, + "light.light171": 678429944, + "light.light172": 678495481, + "light.light173": 678561018, + "light.light174": 678626555, + "light.light175": 678692092, + "light.light176": 678757629, + "light.light177": 678823166, + "light.light178": 678888703, + "light.light179": 678954240, + "light.light18": 595133640, + "light.light180": 678495480, + "light.light181": 678561017, + "light.light182": 678626554, + "light.light183": 678692091, + "light.light184": 678757628, + "light.light185": 678823165, + "light.light186": 678888702, + "light.light187": 678954239, + "light.light188": 679019776, + "light.light189": 679085313, + "light.light19": 595199177, + "light.light190": 678626553, + "light.light191": 678692090, + "light.light192": 678757627, + "light.light193": 678823164, + "light.light194": 678888701, + "light.light195": 678954238, + "light.light196": 679019775, + "light.light197": 679085312, + "light.light198": 679150849, + "light.light199": 679216386, + "light.light2": 514983057, + "light.light20": 594740417, + "light.light200": 677643505, + "light.light201": 1682157970, + "light.light202": 1665380351, + "light.light203": 1648602732, + "light.light204": 1631825113, + "light.light205": 1615047494, + "light.light206": 1598269875, + "light.light207": 1581492256, + "light.light208": 1833156541, + "light.light209": 1816378922, + "light.light21": 594805954, + "light.light210": 677774578, + "light.light211": 1614900399, + "light.light212": 1631678018, + "light.light213": 1648455637, + "light.light214": 1531012304, + "light.light215": 1547789923, + "light.light216": 1564567542, + "light.light217": 1581345161, + "light.light218": 1732343732, + "light.light219": 1749121351, + "light.light22": 594871491, + "light.light23": 594937028, + "light.light24": 595002565, + "light.light25": 595068102, + "light.light26": 595133639, + "light.light27": 595199176, + "light.light28": 595264713, + "light.light29": 595330250, + "light.light3": 515048594, + "light.light30": 594871490, + "light.light31": 594937027, + "light.light32": 595002564, + "light.light33": 595068101, + "light.light34": 595133638, + "light.light35": 595199175, + "light.light36": 595264712, + "light.light37": 595330249, + "light.light38": 595395786, + "light.light39": 595461323, + "light.light4": 515114131, + "light.light40": 595002563, + "light.light41": 595068100, + "light.light42": 595133637, + "light.light43": 595199174, + "light.light44": 595264711, + "light.light45": 595330248, + "light.light46": 595395785, + "light.light47": 595461322, + "light.light48": 595526859, + "light.light49": 595592396, + "light.light5": 515179668, + "light.light50": 595133636, + "light.light51": 595199173, + "light.light52": 595264710, + "light.light53": 595330247, + "light.light54": 595395784, + "light.light55": 595461321, + "light.light56": 595526858, + "light.light57": 595592395, + "light.light58": 595657932, + "light.light59": 595723469, + "light.light6": 515245205, + "light.light60": 595264709, + "light.light61": 595330246, + "light.light62": 595395783, + "light.light63": 595461320, + "light.light64": 595526857, + "light.light65": 595592394, + "light.light66": 595657931, + "light.light67": 595723468, + "light.light68": 595789005, + "light.light69": 595854542, + "light.light7": 515310742, + "light.light70": 595395782, + "light.light71": 595461319, + "light.light72": 595526856, + "light.light73": 595592393, + "light.light74": 595657930, + "light.light75": 595723467, + "light.light76": 595789004, + "light.light77": 595854541, + "light.light78": 595920078, + "light.light79": 595985615, + "light.light8": 515376279, + "light.light80": 595526855, + "light.light81": 595592392, + "light.light82": 595657929, + "light.light83": 595723466, + "light.light84": 595789003, + "light.light85": 595854540, + "light.light86": 595920077, + "light.light87": 595985614, + "light.light88": 596051151, + "light.light89": 596116688, + "light.light9": 515441816, + "light.light90": 595657928, + "light.light91": 595723465, + "light.light92": 595789002, + "light.light93": 595854539, + "light.light94": 595920076, + "light.light95": 595985613, + "light.light96": 596051150, + "light.light97": 596116687, + "light.light98": 596182224, + "light.light99": 596247761, + } + + await aid_storage.async_save() + await hass.async_block_till_done() + + aid_storage = AccessoryAidStorage(hass) + await aid_storage.async_initialize() + + assert aid_storage.allocations == { + "light.light0": 514851983, + "light.light1": 514917520, + "light.light10": 594609344, + "light.light100": 677446896, + "light.light101": 677512433, + "light.light102": 677577970, + "light.light103": 677643507, + "light.light104": 677709044, + "light.light105": 677774581, + "light.light106": 677840118, + "light.light107": 677905655, + "light.light108": 677971192, + "light.light109": 678036729, + "light.light11": 594674881, + "light.light110": 677577969, + "light.light111": 677643506, + "light.light112": 677709043, + "light.light113": 677774580, + "light.light114": 677840117, + "light.light115": 677905654, + "light.light116": 677971191, + "light.light117": 678036728, + "light.light118": 678102265, + "light.light119": 678167802, + "light.light12": 594740418, + "light.light120": 677709042, + "light.light121": 677774579, + "light.light122": 677840116, + "light.light123": 677905653, + "light.light124": 677971190, + "light.light125": 678036727, + "light.light126": 678102264, + "light.light127": 678167801, + "light.light128": 678233338, + "light.light129": 678298875, + "light.light13": 594805955, + "light.light130": 677840115, + "light.light131": 677905652, + "light.light132": 677971189, + "light.light133": 678036726, + "light.light134": 678102263, + "light.light135": 678167800, + "light.light136": 678233337, + "light.light137": 678298874, + "light.light138": 678364411, + "light.light139": 678429948, + "light.light14": 594871492, + "light.light140": 677971188, + "light.light141": 678036725, + "light.light142": 678102262, + "light.light143": 678167799, + "light.light144": 678233336, + "light.light145": 678298873, + "light.light146": 678364410, + "light.light147": 678429947, + "light.light148": 678495484, + "light.light149": 678561021, + "light.light15": 594937029, + "light.light150": 678102261, + "light.light151": 678167798, + "light.light152": 678233335, + "light.light153": 678298872, + "light.light154": 678364409, + "light.light155": 678429946, + "light.light156": 678495483, + "light.light157": 678561020, + "light.light158": 678626557, + "light.light159": 678692094, + "light.light16": 595002566, + "light.light160": 678233334, + "light.light161": 678298871, + "light.light162": 678364408, + "light.light163": 678429945, + "light.light164": 678495482, + "light.light165": 678561019, + "light.light166": 678626556, + "light.light167": 678692093, + "light.light168": 678757630, + "light.light169": 678823167, + "light.light17": 595068103, + "light.light170": 678364407, + "light.light171": 678429944, + "light.light172": 678495481, + "light.light173": 678561018, + "light.light174": 678626555, + "light.light175": 678692092, + "light.light176": 678757629, + "light.light177": 678823166, + "light.light178": 678888703, + "light.light179": 678954240, + "light.light18": 595133640, + "light.light180": 678495480, + "light.light181": 678561017, + "light.light182": 678626554, + "light.light183": 678692091, + "light.light184": 678757628, + "light.light185": 678823165, + "light.light186": 678888702, + "light.light187": 678954239, + "light.light188": 679019776, + "light.light189": 679085313, + "light.light19": 595199177, + "light.light190": 678626553, + "light.light191": 678692090, + "light.light192": 678757627, + "light.light193": 678823164, + "light.light194": 678888701, + "light.light195": 678954238, + "light.light196": 679019775, + "light.light197": 679085312, + "light.light198": 679150849, + "light.light199": 679216386, + "light.light2": 514983057, + "light.light20": 594740417, + "light.light200": 677643505, + "light.light201": 1682157970, + "light.light202": 1665380351, + "light.light203": 1648602732, + "light.light204": 1631825113, + "light.light205": 1615047494, + "light.light206": 1598269875, + "light.light207": 1581492256, + "light.light208": 1833156541, + "light.light209": 1816378922, + "light.light21": 594805954, + "light.light210": 677774578, + "light.light211": 1614900399, + "light.light212": 1631678018, + "light.light213": 1648455637, + "light.light214": 1531012304, + "light.light215": 1547789923, + "light.light216": 1564567542, + "light.light217": 1581345161, + "light.light218": 1732343732, + "light.light219": 1749121351, + "light.light22": 594871491, + "light.light23": 594937028, + "light.light24": 595002565, + "light.light25": 595068102, + "light.light26": 595133639, + "light.light27": 595199176, + "light.light28": 595264713, + "light.light29": 595330250, + "light.light3": 515048594, + "light.light30": 594871490, + "light.light31": 594937027, + "light.light32": 595002564, + "light.light33": 595068101, + "light.light34": 595133638, + "light.light35": 595199175, + "light.light36": 595264712, + "light.light37": 595330249, + "light.light38": 595395786, + "light.light39": 595461323, + "light.light4": 515114131, + "light.light40": 595002563, + "light.light41": 595068100, + "light.light42": 595133637, + "light.light43": 595199174, + "light.light44": 595264711, + "light.light45": 595330248, + "light.light46": 595395785, + "light.light47": 595461322, + "light.light48": 595526859, + "light.light49": 595592396, + "light.light5": 515179668, + "light.light50": 595133636, + "light.light51": 595199173, + "light.light52": 595264710, + "light.light53": 595330247, + "light.light54": 595395784, + "light.light55": 595461321, + "light.light56": 595526858, + "light.light57": 595592395, + "light.light58": 595657932, + "light.light59": 595723469, + "light.light6": 515245205, + "light.light60": 595264709, + "light.light61": 595330246, + "light.light62": 595395783, + "light.light63": 595461320, + "light.light64": 595526857, + "light.light65": 595592394, + "light.light66": 595657931, + "light.light67": 595723468, + "light.light68": 595789005, + "light.light69": 595854542, + "light.light7": 515310742, + "light.light70": 595395782, + "light.light71": 595461319, + "light.light72": 595526856, + "light.light73": 595592393, + "light.light74": 595657930, + "light.light75": 595723467, + "light.light76": 595789004, + "light.light77": 595854541, + "light.light78": 595920078, + "light.light79": 595985615, + "light.light8": 515376279, + "light.light80": 595526855, + "light.light81": 595592392, + "light.light82": 595657929, + "light.light83": 595723466, + "light.light84": 595789003, + "light.light85": 595854540, + "light.light86": 595920077, + "light.light87": 595985614, + "light.light88": 596051151, + "light.light89": 596116688, + "light.light9": 515441816, + "light.light90": 595657928, + "light.light91": 595723465, + "light.light92": 595789002, + "light.light93": 595854539, + "light.light94": 595920076, + "light.light95": 595985613, + "light.light96": 596051150, + "light.light97": 596116687, + "light.light98": 596182224, + "light.light99": 596247761, + } + + aid_storage_path = hass.config.path(STORAGE_DIR, AID_MANAGER_STORAGE_KEY) + if await hass.async_add_executor_job(os.path.exists, aid_storage_path): + await hass.async_add_executor_job(os.unlink, aid_storage_path)