Refactor HomeKit to allow supported features/device class to change (#101719)

This commit is contained in:
J. Nick Koston 2023-10-10 06:20:25 -10:00 committed by GitHub
parent f166e1cc1a
commit 7b4b8e7516
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 662 additions and 452 deletions

View file

@ -47,7 +47,14 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD,
)
from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State, callback
from homeassistant.core import (
CALLBACK_TYPE,
CoreState,
HomeAssistant,
ServiceCall,
State,
callback,
)
from homeassistant.exceptions import HomeAssistantError, Unauthorized
from homeassistant.helpers import (
config_validation as cv,
@ -55,6 +62,7 @@ from homeassistant.helpers import (
entity_registry as er,
instance_id,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entityfilter import (
BASE_FILTER_SCHEMA,
FILTER_SCHEMA,
@ -534,6 +542,7 @@ class HomeKit:
self.driver: HomeDriver | None = None
self.bridge: HomeBridge | None = None
self._reset_lock = asyncio.Lock()
self._cancel_reload_dispatcher: CALLBACK_TYPE | None = None
def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None:
"""Set up bridge and accessory driver."""
@ -563,16 +572,28 @@ class HomeKit:
async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None:
"""Reset the accessory to load the latest configuration."""
_LOGGER.debug("Resetting accessories: %s", entity_ids)
async with self._reset_lock:
if not self.bridge:
await self.async_reset_accessories_in_accessory_mode(entity_ids)
# For accessory mode reset and reload are the same
await self._async_reload_accessories_in_accessory_mode(entity_ids)
return
await self.async_reset_accessories_in_bridge_mode(entity_ids)
await self._async_reset_accessories_in_bridge_mode(entity_ids)
async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
async def async_reload_accessories(self, entity_ids: Iterable[str]) -> None:
"""Reload the accessory to load the latest configuration."""
_LOGGER.debug("Reloading accessories: %s", entity_ids)
async with self._reset_lock:
if not self.bridge:
await self._async_reload_accessories_in_accessory_mode(entity_ids)
return
await self._async_reload_accessories_in_bridge_mode(entity_ids)
@callback
def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
"""Shutdown an accessory."""
assert self.driver is not None
await accessory.stop()
accessory.async_stop()
# Deallocate the IIDs for the accessory
iid_manager = accessory.iid_manager
services: list[Service] = accessory.services
@ -582,7 +603,7 @@ class HomeKit:
for char in characteristics:
iid_manager.remove_obj(char)
async def async_reset_accessories_in_accessory_mode(
async def _async_reload_accessories_in_accessory_mode(
self, entity_ids: Iterable[str]
) -> None:
"""Reset accessories in accessory mode."""
@ -593,63 +614,88 @@ class HomeKit:
return
if not (state := self.hass.states.get(acc.entity_id)):
_LOGGER.warning(
"The underlying entity %s disappeared during reset", acc.entity_id
"The underlying entity %s disappeared during reload", acc.entity_id
)
return
await self._async_shutdown_accessory(acc)
self._async_shutdown_accessory(acc)
if new_acc := self._async_create_single_accessory([state]):
self.driver.accessory = new_acc
self.hass.async_create_task(
new_acc.run(), f"HomeKit Bridge Accessory: {new_acc.entity_id}"
)
await self.async_config_changed()
# Run must be awaited here since it may change
# the accessories hash
await new_acc.run()
self._async_update_accessories_hash()
async def async_reset_accessories_in_bridge_mode(
def _async_remove_accessories_by_entity_id(
self, entity_ids: Iterable[str]
) -> None:
"""Reset accessories in bridge mode."""
) -> list[str]:
"""Remove accessories by entity id."""
assert self.aid_storage is not None
assert self.bridge is not None
assert self.driver is not None
new = []
removed: list[str] = []
acc: HomeAccessory | None
for entity_id in entity_ids:
aid = self.aid_storage.get_or_allocate_aid_for_entity_id(entity_id)
if aid not in self.bridge.accessories:
continue
_LOGGER.info(
"HomeKit Bridge %s will reset accessory with linked entity_id %s",
self._name,
entity_id,
)
acc = await self.async_remove_bridge_accessory(aid)
if acc:
await self._async_shutdown_accessory(acc)
if acc and (state := self.hass.states.get(acc.entity_id)):
new.append(state)
else:
_LOGGER.warning(
"The underlying entity %s disappeared during reset", entity_id
)
if acc := self.async_remove_bridge_accessory(aid):
self._async_shutdown_accessory(acc)
removed.append(entity_id)
return removed
if not new:
# No matched accessories, probably on another bridge
async def _async_reset_accessories_in_bridge_mode(
self, entity_ids: Iterable[str]
) -> None:
"""Reset accessories in bridge mode."""
if not (removed := self._async_remove_accessories_by_entity_id(entity_ids)):
_LOGGER.debug("No accessories to reset in bridge mode for: %s", entity_ids)
return
await self.async_config_changed()
await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME)
for state in new:
if acc := self.add_bridge_accessory(state):
self.hass.async_create_task(
acc.run(), f"HomeKit Bridge Accessory: {acc.entity_id}"
)
await self.async_config_changed()
async def async_config_changed(self) -> None:
"""Call config changed which writes out the new config to disk."""
# With a reset, we need to remove the accessories,
# and force config change so iCloud deletes them from
# the database.
assert self.driver is not None
await self.hass.async_add_executor_job(self.driver.config_changed)
self._async_update_accessories_hash()
await asyncio.sleep(_HOMEKIT_CONFIG_UPDATE_TIME)
await self._async_recreate_removed_accessories_in_bridge_mode(removed)
async def _async_reload_accessories_in_bridge_mode(
self, entity_ids: Iterable[str]
) -> None:
"""Reload accessories in bridge mode."""
removed = self._async_remove_accessories_by_entity_id(entity_ids)
await self._async_recreate_removed_accessories_in_bridge_mode(removed)
async def _async_recreate_removed_accessories_in_bridge_mode(
self, removed: list[str]
) -> None:
"""Recreate removed accessories in bridge mode."""
for entity_id in removed:
if not (state := self.hass.states.get(entity_id)):
_LOGGER.warning(
"The underlying entity %s disappeared during reload", entity_id
)
continue
if acc := self.add_bridge_accessory(state):
# Run must be awaited here since it may change
# the accessories hash
await acc.run()
self._async_update_accessories_hash()
@callback
def _async_update_accessories_hash(self) -> bool:
"""Update the accessories hash."""
assert self.driver is not None
driver = self.driver
old_hash = driver.state.accessories_hash
new_hash = driver.accessories_hash
if driver.state.set_accessories_hash(new_hash):
_LOGGER.debug(
"Updating HomeKit accessories hash from %s -> %s", old_hash, new_hash
)
driver.async_persist()
driver.async_update_advertisement()
return True
_LOGGER.debug("HomeKit accessories hash is unchanged: %s", new_hash)
return False
def add_bridge_accessory(self, state: State) -> HomeAccessory | None:
"""Try adding accessory to bridge if configured beforehand."""
@ -734,7 +780,8 @@ class HomeKit:
)
)
async def async_remove_bridge_accessory(self, aid: int) -> HomeAccessory | None:
@callback
def async_remove_bridge_accessory(self, aid: int) -> HomeAccessory | None:
"""Try adding accessory to bridge if configured beforehand."""
assert self.bridge is not None
if acc := self.bridge.accessories.pop(aid, None):
@ -782,6 +829,11 @@ class HomeKit:
if self.status != STATUS_READY:
return
self.status = STATUS_WAIT
self._cancel_reload_dispatcher = async_dispatcher_connect(
self.hass,
f"homekit_reload_entities_{self._entry_id}",
self.async_reload_accessories,
)
async_zc_instance = await zeroconf.async_get_async_instance(self.hass)
uuid = await instance_id.async_get(self.hass)
self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id)
@ -989,10 +1041,13 @@ class HomeKit:
"""Stop the accessory driver."""
if self.status != STATUS_RUNNING:
return
self.status = STATUS_STOPPED
_LOGGER.debug("Driver stop for %s", self._name)
if self.driver:
await self.driver.async_stop()
async with self._reset_lock:
self.status = STATUS_STOPPED
assert self._cancel_reload_dispatcher is not None
self._cancel_reload_dispatcher()
_LOGGER.debug("Driver stop for %s", self._name)
if self.driver:
await self.driver.async_stop()
@callback
def _async_configure_linked_sensors(