Ensure HomeKit connection is kept alive for devices that timeout too quickly (#123601)

This commit is contained in:
J. Nick Koston 2024-08-12 07:54:57 -05:00 committed by GitHub
parent f6e82ae0ba
commit b20623447e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 36 additions and 15 deletions

View File

@ -845,21 +845,41 @@ class HKDevice:
async def async_update(self, now: datetime | None = None) -> None:
"""Poll state of all entities attached to this bridge/accessory."""
to_poll = self.pollable_characteristics
accessories = self.entity_map.accessories
if (
len(self.entity_map.accessories) == 1
len(accessories) == 1
and self.available
and not (self.pollable_characteristics - self.watchable_characteristics)
and not (to_poll - self.watchable_characteristics)
and self.pairing.is_available
and await self.pairing.controller.async_reachable(
self.unique_id, timeout=5.0
)
):
# If its a single accessory and all chars are watchable,
# we don't need to poll.
_LOGGER.debug("Accessory is reachable, skip polling: %s", self.unique_id)
return
# only poll the firmware version to keep the connection alive
# https://github.com/home-assistant/core/issues/123412
#
# Firmware revision is used here since iOS does this to keep camera
# connections alive, and the goal is to not regress
# https://github.com/home-assistant/core/issues/116143
# by polling characteristics that are not normally polled frequently
# and may not be tested by the device vendor.
#
_LOGGER.debug(
"Accessory is reachable, limiting poll to firmware version: %s",
self.unique_id,
)
first_accessory = accessories[0]
accessory_info = first_accessory.services.first(
service_type=ServicesTypes.ACCESSORY_INFORMATION
)
assert accessory_info is not None
firmware_iid = accessory_info[CharacteristicsTypes.FIRMWARE_REVISION].iid
to_poll = {(first_accessory.aid, firmware_iid)}
if not self.pollable_characteristics:
if not to_poll:
self.async_update_available_state()
_LOGGER.debug(
"HomeKit connection not polling any characteristics: %s", self.unique_id
@ -892,9 +912,7 @@ class HKDevice:
_LOGGER.debug("Starting HomeKit device update: %s", self.unique_id)
try:
new_values_dict = await self.get_characteristics(
self.pollable_characteristics
)
new_values_dict = await self.get_characteristics(to_poll)
except AccessoryNotFoundError:
# Not only did the connection fail, but also the accessory is not
# visible on the network.

View File

@ -344,10 +344,10 @@ async def test_thread_provision_migration_failed(hass: HomeAssistant) -> None:
assert config_entry.data["Connection"] == "BLE"
async def test_skip_polling_all_watchable_accessory_mode(
async def test_poll_firmware_version_only_all_watchable_accessory_mode(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we skip polling if available and all chars are watchable accessory mode."""
"""Test that we only poll firmware if available and all chars are watchable accessory mode."""
def _create_accessory(accessory):
service = accessory.add_service(ServicesTypes.LIGHTBULB, name="TestDevice")
@ -370,7 +370,10 @@ async def test_skip_polling_all_watchable_accessory_mode(
# Initial state is that the light is off
state = await helper.poll_and_get_state()
assert state.state == STATE_OFF
assert mock_get_characteristics.call_count == 0
assert mock_get_characteristics.call_count == 2
# Verify only firmware version is polled
assert mock_get_characteristics.call_args_list[0][0][0] == {(1, 7)}
assert mock_get_characteristics.call_args_list[1][0][0] == {(1, 7)}
# Test device goes offline
helper.pairing.available = False
@ -382,16 +385,16 @@ async def test_skip_polling_all_watchable_accessory_mode(
state = await helper.poll_and_get_state()
assert state.state == STATE_UNAVAILABLE
# Tries twice before declaring unavailable
assert mock_get_characteristics.call_count == 2
assert mock_get_characteristics.call_count == 4
# Test device comes back online
helper.pairing.available = True
state = await helper.poll_and_get_state()
assert state.state == STATE_OFF
assert mock_get_characteristics.call_count == 3
assert mock_get_characteristics.call_count == 6
# Next poll should not happen because its a single
# accessory, available, and all chars are watchable
state = await helper.poll_and_get_state()
assert state.state == STATE_OFF
assert mock_get_characteristics.call_count == 3
assert mock_get_characteristics.call_count == 8