Refactor HomeKit to allow supported features/device class to change (#101719)
This commit is contained in:
parent
f166e1cc1a
commit
7b4b8e7516
18 changed files with 662 additions and 452 deletions
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue