From 1577f6ea500919a0f141bf8efc5ddb9e388b0124 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 4 Dec 2022 07:01:37 -1000 Subject: [PATCH] Restore HomeKit Controller BLE GSN at startup (#83206) --- .../components/homekit_controller/button.py | 2 +- .../homekit_controller/config_flow.py | 1 + .../homekit_controller/connection.py | 70 +++++++++++-------- .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/number.py | 2 +- .../components/homekit_controller/storage.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 50 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index 4ce2b425a5e..11e935df455 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -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) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 10d3ed0cfa8..1cfedb05847 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -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) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index dbcd8b28fc2..d230fe64517 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -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( diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 4ac8578eff5..c5047f9d215 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -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"], diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index a20ba83e80a..5d72516bc06 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -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) diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index a5afb07620a..de4f23ad8da 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -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() diff --git a/requirements_all.txt b/requirements_all.txt index 548af1f905e..3f5b912671a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3e57929b70..cda9f8cc031 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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