Restore HomeKit Controller BLE GSN at startup (#83206)

This commit is contained in:
J. Nick Koston 2022-12-04 07:01:37 -10:00 committed by GitHub
parent de77132a5a
commit 1577f6ea50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 50 additions and 37 deletions

View file

@ -84,7 +84,7 @@ async def async_setup_entry(
entity.old_unique_id, entity.unique_id, Platform.BUTTON
)
async_add_entities(entities, True)
async_add_entities(entities)
return True
conn.add_char_factory(async_add_characteristic)

View file

@ -583,6 +583,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
accessories_state.config_num,
accessories_state.accessories.serialize(),
serialize_broadcast_key(accessories_state.broadcast_key),
accessories_state.state_num,
)
return self.async_create_entry(title=name, data=pairing_data)

View file

@ -20,7 +20,7 @@ from aiohomekit.model.services import Service
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_VIA_DEVICE, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback
from homeassistant.core import CoreState, Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.dispatcher import async_dispatcher_send
@ -116,11 +116,6 @@ class HKDevice:
self.pollable_characteristics: list[tuple[int, int]] = []
# If this is set polling is active and can be disabled by calling
# this method.
self._polling_interval_remover: CALLBACK_TYPE | None = None
self._ble_available_interval_remover: CALLBACK_TYPE | None = None
# Never allow concurrent polling of the same accessory or bridge
self._polling_lock = asyncio.Lock()
self._polling_lock_warned = False
@ -185,8 +180,8 @@ class HKDevice:
self.available = available
async_dispatcher_send(self.hass, self.signal_state_updated)
async def _async_retry_populate_ble_accessory_state(self, event: Event) -> None:
"""Try again to populate the BLE accessory state.
async def _async_populate_ble_accessory_state(self, event: Event) -> None:
"""Populate the BLE accessory state without blocking startup.
If the accessory was asleep at startup we need to retry
since we continued on to allow startup to proceed.
@ -194,6 +189,7 @@ class HKDevice:
If this fails the state may be inconsistent, but will
get corrected as soon as the accessory advertises again.
"""
self._async_start_polling()
try:
await self.pairing.async_populate_accessories_state(force_update=True)
except STARTUP_EXCEPTIONS as ex:
@ -221,20 +217,28 @@ class HKDevice:
# so we only poll those chars but that is not possible
# yet.
attempts = None if self.hass.state == CoreState.running else 1
try:
await self.pairing.async_populate_accessories_state(
force_update=True, attempts=attempts
)
except AccessoryNotFoundError:
if transport != Transport.BLE or not pairing.accessories:
# BLE devices may sleep and we can't force a connection
raise
if (
transport == Transport.BLE
and pairing.accessories
and pairing.accessories.has_aid(1)
):
# The GSN gets restored and a catch up poll will be
# triggered via disconnected events automatically
# if we are out of sync. To be sure we are in sync;
# If for some reason the BLE connection failed
# previously we force an update after startup
# is complete.
entry.async_on_unload(
self.hass.bus.async_listen(
EVENT_HOMEASSISTANT_STARTED,
self._async_retry_populate_ble_accessory_state,
self._async_populate_ble_accessory_state,
)
)
else:
await self.pairing.async_populate_accessories_state(
force_update=True, attempts=attempts
)
self._async_start_polling()
entry.async_on_unload(pairing.dispatcher_connect(self.process_new_events))
entry.async_on_unload(
@ -252,27 +256,34 @@ class HKDevice:
self.async_set_available_state(self.pairing.is_available)
# We use async_request_update to avoid multiple updates
# at the same time which would generate a spurious warning
# in the log about concurrent polling.
self._polling_interval_remover = async_track_time_interval(
self.hass, self.async_request_update, self.pairing.poll_interval
)
if transport == Transport.BLE:
# If we are using BLE, we need to periodically check of the
# BLE device is available since we won't get callbacks
# when it goes away since we HomeKit supports disconnected
# notifications and we cannot treat a disconnect as unavailability.
self._ble_available_interval_remover = async_track_time_interval(
self.hass,
self.async_update_available_state,
timedelta(seconds=BLE_AVAILABILITY_CHECK_INTERVAL),
entry.async_on_unload(
async_track_time_interval(
self.hass,
self.async_update_available_state,
timedelta(seconds=BLE_AVAILABILITY_CHECK_INTERVAL),
)
)
# BLE devices always get an RSSI sensor as well
if "sensor" not in self.platforms:
await self.async_load_platform("sensor")
@callback
def _async_start_polling(self) -> None:
"""Start polling for updates."""
# We use async_request_update to avoid multiple updates
# at the same time which would generate a spurious warning
# in the log about concurrent polling.
self.config_entry.async_on_unload(
async_track_time_interval(
self.hass, self.async_request_update, self.pairing.poll_interval
)
)
async def async_add_new_entities(self) -> None:
"""Add new entities to Home Assistant."""
await self.async_load_platforms()
@ -529,9 +540,6 @@ class HKDevice:
async def async_unload(self) -> None:
"""Stop interacting with device and prepare for removal from hass."""
if self._polling_interval_remover:
self._polling_interval_remover()
await self.pairing.shutdown()
await self.hass.config_entries.async_unload_platforms(

View file

@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit==2.3.6"],
"requirements": ["aiohomekit==2.4.0"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
"dependencies": ["bluetooth", "zeroconf"],

View file

@ -78,7 +78,7 @@ async def async_setup_entry(
entity.old_unique_id, entity.unique_id, Platform.NUMBER
)
async_add_entities(entities, True)
async_add_entities(entities)
return True
conn.add_char_factory(async_add_characteristic)

View file

@ -61,11 +61,15 @@ class EntityMapStorage:
config_num: int,
accessories: list[Any],
broadcast_key: str | None = None,
state_num: int | None = None,
) -> Pairing:
"""Create a new pairing cache."""
_LOGGER.debug("Creating or updating entity map for %s", homekit_id)
data = Pairing(
config_num=config_num, accessories=accessories, broadcast_key=broadcast_key
config_num=config_num,
accessories=accessories,
broadcast_key=broadcast_key,
state_num=state_num,
)
self.storage_data[homekit_id] = data
self._async_schedule_save()

View file

@ -174,7 +174,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==2.3.6
aiohomekit==2.4.0
# homeassistant.components.emulated_hue
# homeassistant.components.http

View file

@ -158,7 +158,7 @@ aioguardian==2022.07.0
aioharmony==0.2.9
# homeassistant.components.homekit_controller
aiohomekit==2.3.6
aiohomekit==2.4.0
# homeassistant.components.emulated_hue
# homeassistant.components.http