diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 8c513805641..0d21ff9ba1d 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -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( diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py index 9c6e5a6687a..a16cd052c87 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -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 diff --git a/tests/components/homekit_controller/test_connection.py b/tests/components/homekit_controller/test_connection.py index 0a77509d675..60ef0b1c547 100644 --- a/tests/components/homekit_controller/test_connection.py +++ b/tests/components/homekit_controller/test_connection.py @@ -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 diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index d0180837c11..04f4d3f5e29 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -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