Skip to content

Commit

Permalink
Restore HomeKit Controller BLE GSN at startup (home-assistant#83206)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdraco authored Dec 4, 2022
1 parent de77132 commit 1577f6e
Show file tree
Hide file tree
Showing 8 changed files with 50 additions and 37 deletions.
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def async_add_characteristic(char: Characteristic) -> bool:
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)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/homekit_controller/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ async def _entry_from_accessory(self, pairing: AbstractPairing) -> FlowResult:
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)
Expand Down
70 changes: 39 additions & 31 deletions homeassistant/components/homekit_controller/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

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
Expand Down Expand Up @@ -116,11 +116,6 @@ def __init__(

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
Expand Down Expand Up @@ -185,15 +180,16 @@ def async_set_available_state(self, available: bool) -> None:
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.
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:
Expand Down Expand Up @@ -221,20 +217,28 @@ async def async_setup(self) -> None:
# 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(
Expand All @@ -252,27 +256,34 @@ async def async_setup(self) -> None:

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()
Expand Down Expand Up @@ -529,9 +540,6 @@ async def async_process_entity_map(self) -> None:

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(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/homekit_controller/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def async_add_characteristic(char: Characteristic) -> bool:
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)
Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/homekit_controller/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,15 @@ def async_create_or_update_map(
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()
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 1577f6e

Please sign in to comment.