Skip poll of HKC accessory if reachable and chars are watchable (#116200)

pull/122120/head
J. Nick Koston 2024-07-18 01:36:45 -05:00 committed by GitHub
parent 0927dd9090
commit e4ef4b81ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 96 additions and 4 deletions

View File

@ -845,6 +845,20 @@ class HKDevice:
async def async_update(self, now: datetime | None = None) -> None:
"""Poll state of all entities attached to this bridge/accessory."""
if (
len(self.entity_map.accessories) == 1
and self.available
and not (self.pollable_characteristics - 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
if not self.pollable_characteristics:
self.async_update_available_state()
_LOGGER.debug(

View File

@ -5,7 +5,7 @@ from unittest import mock
from aiohomekit.exceptions import AccessoryDisconnectedError, EncryptionError
from aiohomekit.model import CharacteristicsTypes, ServicesTypes
from aiohomekit.testing import FakePairing
from aiohomekit.testing import FakeController, FakePairing
import pytest
from homeassistant.components.homekit_controller.connection import (
@ -48,7 +48,14 @@ async def test_recover_from_failure(hass: HomeAssistant, failure_cls) -> None:
# Test that entity remains in the same state if there is a network error
next_update = dt_util.utcnow() + timedelta(seconds=60)
with mock.patch.object(FakePairing, "get_characteristics") as get_char:
with (
mock.patch.object(FakePairing, "get_characteristics") as get_char,
mock.patch.object(
FakeController,
"async_reachable",
return_value=False,
),
):
get_char.side_effect = failure_cls("Disconnected")
# Test that a poll triggers unavailable

View File

@ -1,8 +1,13 @@
"""Tests for HKDevice."""
from collections.abc import Callable
import dataclasses
from unittest import mock
from aiohomekit.controller import TransportType
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.testing import FakeController
import pytest
from homeassistant.components.homekit_controller.const import (
@ -12,11 +17,17 @@ from homeassistant.components.homekit_controller.const import (
IDENTIFIER_LEGACY_SERIAL_NUMBER,
)
from homeassistant.components.thread import async_add_dataset, dataset_store
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .common import setup_accessories_from_file, setup_platform, setup_test_accessories
from .common import (
setup_accessories_from_file,
setup_platform,
setup_test_accessories,
setup_test_component,
)
from tests.common import MockConfigEntry
@ -331,3 +342,56 @@ 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(
hass: HomeAssistant, get_next_aid: Callable[[], int]
) -> None:
"""Test that we skip polling if available and all chars are watchable accessory mode."""
def _create_accessory(accessory):
service = accessory.add_service(ServicesTypes.LIGHTBULB, name="TestDevice")
on_char = service.add_char(CharacteristicsTypes.ON)
on_char.value = 0
brightness = service.add_char(CharacteristicsTypes.BRIGHTNESS)
brightness.value = 0
return service
helper = await setup_test_component(hass, get_next_aid(), _create_accessory)
with mock.patch.object(
helper.pairing,
"get_characteristics",
wraps=helper.pairing.get_characteristics,
) as mock_get_characteristics:
# 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
# Test device goes offline
helper.pairing.available = False
with mock.patch.object(
FakeController,
"async_reachable",
return_value=False,
):
state = await helper.poll_and_get_state()
assert state.state == STATE_UNAVAILABLE
# Tries twice before declaring unavailable
assert mock_get_characteristics.call_count == 2
# 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
# 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

View File

@ -1,9 +1,11 @@
"""Basic checks for HomeKitSwitch."""
from collections.abc import Callable
from unittest import mock
from aiohomekit.model.characteristics import CharacteristicsTypes
from aiohomekit.model.services import ServicesTypes
from aiohomekit.testing import FakeController
from homeassistant.components.homekit_controller.const import KNOWN_DEVICES
from homeassistant.components.light import (
@ -372,7 +374,12 @@ async def test_light_becomes_unavailable_but_recovers(
# Test device goes offline
helper.pairing.available = False
state = await helper.poll_and_get_state()
with mock.patch.object(
FakeController,
"async_reachable",
return_value=False,
):
state = await helper.poll_and_get_state()
assert state.state == "unavailable"
# Simulate that someone switched on the device in the real world not via HA