8000 Fix homekit bridge iid allocations by bdraco · Pull Request #81613 · home-assistant/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Fix homekit bridge iid allocations #81613

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions homeassistant/components/homekit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
from typing import Any, cast

from aiohttp import web
from pyhap.characteristic import Characteristic
from pyhap.const import STANDALONE_AID
from pyhap.loader import get_loader
from pyhap.service import Service
import voluptuous as vol
from zeroconf.asyncio import AsyncZeroconf

Expand Down Expand Up @@ -74,13 +76,7 @@
type_switches,
type_thermostats,
)
from .accessories import (
HomeAccessory,
HomeBridge,
HomeDriver,
HomeIIDManager,
get_accessory,
)
from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory
from .aidmanager import AccessoryAidStorage
from .const import (
ATTR_INTEGRATION,
Expand Down Expand Up @@ -548,7 +544,7 @@ def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None:
async_zeroconf_instance=async_zeroconf_instance,
zeroconf_server=f"{uuid}-hap.local.",
loader=get_loader(),
iid_manager=HomeIIDManager(self.iid_storage),
iid_storage=self.iid_storage,
)

# If we do not load the mac address will be wrong
Expand All @@ -568,11 +564,13 @@ async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
assert self.driver is not None
await accessory.stop()
# Deallocate the IIDs for the accessory
iid_manager = self.driver.iid_manager
for service in accessory.services:
iid_manager.remove_iid(iid_manager.remove_obj(service))
for char in service.characteristics:
iid_manager.remove_iid(iid_manager.remove_obj(char))
iid_manager = accessory.iid_manager
services: list[Service] = accessory.services
for service in services:
iid_manager.remove_obj(service)
characteristics: list[Characteristic] = service.characteristics
for char in characteristics:
iid_manager.remove_obj(char)

async def async_reset_accessories_in_accessory_mode(
self, entity_ids: Iterable[str]
Expand Down
8 changes: 4 additions & 4 deletions homeassistant/components/homekit/accessories.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def __init__(
driver=driver,
display_name=cleanup_name_for_homekit(name),
aid=aid,
iid_manager=driver.iid_manager,
iid_manager=HomeIIDManager(driver.iid_storage),
*args,
**kwargs,
)
Expand Down Expand Up @@ -574,7 +574,7 @@ class HomeBridge(Bridge): # type: ignore[misc]

def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None:
"""Initialize a Bridge object."""
super().__init__(driver, name, iid_manager=driver.iid_manager)
super().__init__(driver, name, iid_manager=HomeIIDManager(driver.iid_storage))
self.set_info_service(
firmware_revision=format_version(__version__),
manufacturer=MANUFACTURER,
Expand D 8000 own Expand Up @@ -607,7 +607,7 @@ def __init__(
entry_id: str,
bridge_name: str,
entry_title: str,
iid_manager: HomeIIDManager,
iid_storage: AccessoryIIDStorage,
**kwargs: Any,
) -> None:
"""Initialize a AccessoryDriver object."""
Expand All @@ -616,7 +616,7 @@ def __init__(
self._entry_id = entry_id
self._bridge_name = bridge_name
self._entry_title = entry_title
self.iid_manager = iid_manager
self.iid_storage = iid_storage

@pyhap_callback # type: ignore[misc]
def pair(
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/homekit/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ async def async_ 8000 get_config_entry_diagnostics(
"options": dict(entry.options),
},
}
if homekit.iid_storage:
data["iid_storage"] = homekit.iid_storage.allocations
if not homekit.driver: # not started yet or startup failed
return data
driver: AccessoryDriver = homekit.driver
Expand Down
72 changes: 58 additions & 14 deletions homeassistant/components/homekit/iidmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from .util import get_iid_storage_filename_for_entry_id

IID_MANAGER_STORAGE_VERSION = 1
IID_MANAGER_STORAGE_VERSION = 2
IID_MANAGER_SAVE_DELAY = 2

ALLOCATIONS_KEY = "allocations"
Expand All @@ -26,6 +26,40 @@
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,
):
"""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.
Expand All @@ -37,23 +71,24 @@ class AccessoryIIDStorage:
def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
"""Create a new iid store."""
self.hass = hass
self.allocations: dict[str, int] = {}
self.allocated_iids: list[int] = []
self.allocations: dict[str, dict[str, int]] = {}
self.allocated_iids: dict[str, list[int]] = {}
self.entry_id = entry_id
self.store: Store | None = None
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 = Store(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
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, {})
self.allocated_iids = sorted(self.allocations.values())
for aid_str, allocations in self.allocations.items():
self.allocated_iids[aid_str] = sorted(allocations.values())

def get_or_allocate_iid(
self,
Expand All @@ -68,16 +103,25 @@ def get_or_allocate_iid(
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'{aid}_{service_hap_type}_{service_unique_id or ""}_'
f'{service_hap_type}_{service_unique_id or ""}_'
f'{char_hap_type or ""}_{char_unique_id or ""}'
)
if allocation_key in self.allocations:
return self.allocations[allocation_key]
next_iid = self.allocated_iids[-1] + 1 if self.allocated_iids else 1
self.allocations[allocation_key] = next_iid
self.allocated_iids.append(next_iid)
# 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, [])
if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None:
allocated_iid = 1
elif allocation_key in accessory_allocation:
return accessory_allocation[allocation_key]
elif 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 next_iid
return allocated_iid

@callback
def _async_schedule_save(self) -> None:
Expand All @@ -91,6 +135,6 @@ async def async_save(self) -> None:
return await self.store.async_save(self._data_to_save())

@callback
def _data_to_save(self) -> dict[str, dict[str, int]]:
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}
8 changes: 4 additions & 4 deletions tests/components/homekit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest

from homeassistant.components.device_tracker.legacy import YAML_DEVICES
from homeassistant.components.homekit.accessories import HomeDriver, HomeIIDManager
from homeassistant.components.homekit.accessories import HomeDriver
from homeassistant.components.homekit.const import BRIDGE_NAME, EVENT_HOMEKIT_CHANGED
from homeassistant.components.homekit.iidmanager import AccessoryIIDStorage

Expand Down Expand Up @@ -39,7 +39,7 @@ def run_driver(hass, loop, iid_storage):
entry_id="",
entry_title="mock entry",
bridge_name=BRIDGE_NAME,
iid_manager=HomeIIDManager(iid_storage),
iid_storage=iid_storage,
address="127.0.0.1",
loop=loop,
)
Expand All @@ -63,7 +63,7 @@ def hk_driver(hass, loop, iid_storage):
entry_id="",
entry_title="mock entry",
bridge_name=BRIDGE_NAME,
iid_manager=HomeIIDManager(iid_storage),
iid_storage=iid_storage,
address="127.0.0.1",
loop=loop,
)
Expand Down Expand Up @@ -91,7 +91,7 @@ def mock_hap(hass, loop, iid_storage, mock_zeroconf):
entry_id="",
entry_title="mock entry",
bridge_name=BRIDGE_NAME,
iid_manager=HomeIIDManager(iid_storage),
iid_storage=iid_storage,
address="127.0.0.1",
loop=loop,
)
Expand Down
Loading
0