mirror of
https://github.com/home-assistant/core.git
synced 2025-07-31 19:25:12 +02:00
Restore accessory state into pairing using new HKC methods (#75276)
This commit is contained in:
@@ -5,6 +5,7 @@ import asyncio
|
||||
from collections.abc import Callable
|
||||
import datetime
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from aiohomekit import Controller
|
||||
@@ -17,8 +18,9 @@ from aiohomekit.model import Accessories, Accessory
|
||||
from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
|
||||
from aiohomekit.model.services import Service
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_VIA_DEVICE
|
||||
from homeassistant.core import CALLBACK_TYPE, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
@@ -62,7 +64,12 @@ def valid_serial_number(serial: str) -> bool:
|
||||
class HKDevice:
|
||||
"""HomeKit device."""
|
||||
|
||||
def __init__(self, hass, config_entry, pairing_data) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
pairing_data: MappingProxyType[str, Any],
|
||||
) -> None:
|
||||
"""Initialise a generic HomeKit device."""
|
||||
|
||||
self.hass = hass
|
||||
@@ -78,11 +85,6 @@ class HKDevice:
|
||||
self.pairing_data["AccessoryPairingID"], self.pairing_data
|
||||
)
|
||||
|
||||
self.accessories: list[Any] | None = None
|
||||
self.config_num = 0
|
||||
|
||||
self.entity_map = Accessories()
|
||||
|
||||
# A list of callbacks that turn HK accessories into entities
|
||||
self.accessory_factories: list[AddAccessoryCb] = []
|
||||
|
||||
@@ -132,6 +134,17 @@ class HKDevice:
|
||||
self.watchable_characteristics: list[tuple[int, int]] = []
|
||||
|
||||
self.pairing.dispatcher_connect(self.process_new_events)
|
||||
self.pairing.dispatcher_connect_config_changed(self.process_config_changed)
|
||||
|
||||
@property
|
||||
def entity_map(self) -> Accessories:
|
||||
"""Return the accessories from the pairing."""
|
||||
return self.pairing.accessories_state.accessories
|
||||
|
||||
@property
|
||||
def config_num(self) -> int:
|
||||
"""Return the config num from the pairing."""
|
||||
return self.pairing.accessories_state.config_num
|
||||
|
||||
def add_pollable_characteristics(
|
||||
self, characteristics: list[tuple[int, int]]
|
||||
@@ -169,13 +182,13 @@ class HKDevice:
|
||||
self.available = available
|
||||
async_dispatcher_send(self.hass, self.signal_state_updated)
|
||||
|
||||
async def async_ensure_available(self) -> bool:
|
||||
async def async_ensure_available(self) -> None:
|
||||
"""Verify the accessory is available after processing the entity map."""
|
||||
if self.available:
|
||||
return True
|
||||
return
|
||||
if self.watchable_characteristics and self.pollable_characteristics:
|
||||
# We already tried, no need to try again
|
||||
return False
|
||||
return
|
||||
# We there are no watchable and not pollable characteristics,
|
||||
# we need to force a connection to the device to verify its alive.
|
||||
#
|
||||
@@ -185,34 +198,42 @@ class HKDevice:
|
||||
primary = self.entity_map.accessories[0]
|
||||
aid = primary.aid
|
||||
iid = primary.accessory_information[CharacteristicsTypes.SERIAL_NUMBER].iid
|
||||
try:
|
||||
await self.pairing.get_characteristics([(aid, iid)])
|
||||
except (AccessoryDisconnectedError, EncryptionError, AccessoryNotFoundError):
|
||||
return False
|
||||
await self.pairing.get_characteristics([(aid, iid)])
|
||||
self.async_set_available_state(True)
|
||||
return True
|
||||
|
||||
async def async_setup(self) -> bool:
|
||||
async def async_setup(self) -> None:
|
||||
"""Prepare to use a paired HomeKit device in Home Assistant."""
|
||||
entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP]
|
||||
if cache := entity_storage.get_map(self.unique_id):
|
||||
self.accessories = cache["accessories"]
|
||||
self.config_num = cache["config_num"]
|
||||
self.entity_map = Accessories.from_list(self.accessories)
|
||||
elif not await self.async_refresh_entity_map(self.config_num):
|
||||
return False
|
||||
self.pairing.restore_accessories_state(
|
||||
cache["accessories"], cache["config_num"]
|
||||
)
|
||||
|
||||
# We need to force an update here to make sure we have
|
||||
# the latest values since the async_update we do in
|
||||
# async_process_entity_map will no values to poll yet
|
||||
# since entities are added via dispatching and then
|
||||
# they add the chars they are concerned about in
|
||||
# async_added_to_hass which is too late.
|
||||
#
|
||||
# Ideally we would know which entities we are about to add
|
||||
# so we only poll those chars but that is not possible
|
||||
# yet.
|
||||
await self.pairing.async_populate_accessories_state(force_update=True)
|
||||
|
||||
await self.async_process_entity_map()
|
||||
|
||||
if not await self.async_ensure_available():
|
||||
return False
|
||||
await self.async_ensure_available()
|
||||
|
||||
if not cache:
|
||||
# If its missing from the cache, make sure we save it
|
||||
self.async_save_entity_map()
|
||||
# If everything is up to date, we can create the entities
|
||||
# since we know the data is not stale.
|
||||
await self.async_add_new_entities()
|
||||
self._polling_interval_remover = async_track_time_interval(
|
||||
self.hass, self.async_update, DEFAULT_SCAN_INTERVAL
|
||||
)
|
||||
return True
|
||||
|
||||
async def async_add_new_entities(self) -> None:
|
||||
"""Add new entities to Home Assistant."""
|
||||
@@ -390,9 +411,6 @@ class HKDevice:
|
||||
# Ensure the Pairing object has access to the latest version of the entity map. This
|
||||
# is especially important for BLE, as the Pairing instance relies on the entity map
|
||||
# to map aid/iid to GATT characteristics. So push it to there as well.
|
||||
|
||||
self.pairing.pairing_data["accessories"] = self.accessories # type: ignore[attr-defined]
|
||||
|
||||
self.async_detect_workarounds()
|
||||
|
||||
# Migrate to new device ids
|
||||
@@ -403,13 +421,6 @@ class HKDevice:
|
||||
# Load any triggers for this config entry
|
||||
await async_setup_triggers_for_entry(self.hass, self.config_entry)
|
||||
|
||||
if self.watchable_characteristics:
|
||||
await self.pairing.subscribe(self.watchable_characteristics)
|
||||
if not self.pairing.is_connected:
|
||||
return
|
||||
|
||||
await self.async_update()
|
||||
|
||||
async def async_unload(self) -> None:
|
||||
"""Stop interacting with device and prepare for removal from hass."""
|
||||
if self._polling_interval_remover:
|
||||
@@ -421,34 +432,31 @@ class HKDevice:
|
||||
self.config_entry, self.platforms
|
||||
)
|
||||
|
||||
async def async_refresh_entity_map_and_entities(self, config_num: int) -> None:
|
||||
"""Refresh the entity map and entities for this pairing."""
|
||||
await self.async_refresh_entity_map(config_num)
|
||||
def async_notify_config_changed(self, config_num: int) -> None:
|
||||
"""Notify the pairing of a config change."""
|
||||
self.pairing.notify_config_changed(config_num)
|
||||
|
||||
def process_config_changed(self, config_num: int) -> None:
|
||||
"""Handle a config change notification from the pairing."""
|
||||
self.hass.async_create_task(self.async_update_new_accessories_state())
|
||||
|
||||
async def async_update_new_accessories_state(self) -> None:
|
||||
"""Process a change in the pairings accessories state."""
|
||||
self.async_save_entity_map()
|
||||
await self.async_process_entity_map()
|
||||
if self.watchable_characteristics:
|
||||
await self.pairing.subscribe(self.watchable_characteristics)
|
||||
await self.async_update()
|
||||
await self.async_add_new_entities()
|
||||
|
||||
async def async_refresh_entity_map(self, config_num: int) -> bool:
|
||||
"""Handle setup of a HomeKit accessory."""
|
||||
try:
|
||||
self.accessories = await self.pairing.list_accessories_and_characteristics()
|
||||
except AccessoryDisconnectedError:
|
||||
# If we fail to refresh this data then we will naturally retry
|
||||
# later when Bonjour spots c# is still not up to date.
|
||||
return False
|
||||
|
||||
assert self.accessories is not None
|
||||
|
||||
self.entity_map = Accessories.from_list(self.accessories)
|
||||
|
||||
@callback
|
||||
def async_save_entity_map(self) -> None:
|
||||
"""Save the entity map."""
|
||||
entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP]
|
||||
|
||||
entity_storage.async_create_or_update_map(
|
||||
self.unique_id, config_num, self.accessories
|
||||
self.unique_id, self.config_num, self.entity_map.serialize()
|
||||
)
|
||||
|
||||
self.config_num = config_num
|
||||
return True
|
||||
|
||||
def add_accessory_factory(self, add_entities_cb) -> None:
|
||||
"""Add a callback to run when discovering new entities for accessories."""
|
||||
self.accessory_factories.append(add_entities_cb)
|
||||
|
Reference in New Issue
Block a user