diff --git a/homeassistant/components/iaqualink/coordinator.py b/homeassistant/components/iaqualink/coordinator.py index 8f50d30e949c2..ade0dec0b2f4f 100644 --- a/homeassistant/components/iaqualink/coordinator.py +++ b/homeassistant/components/iaqualink/coordinator.py @@ -6,6 +6,7 @@ import httpx from iaqualink.exception import ( AqualinkServiceException, + AqualinkServiceThrottledException, AqualinkServiceUnauthorizedException, ) @@ -44,6 +45,12 @@ async def _async_update_data(self) -> None: await self.system.update() except AqualinkServiceUnauthorizedException as err: raise ConfigEntryAuthFailed("Invalid credentials for iAquaLink") from err + except AqualinkServiceThrottledException: + _LOGGER.warning( + "Rate limited by iAquaLink system %s, will retry later", + self.system.serial, + ) + return except (AqualinkServiceException, httpx.HTTPError) as err: raise UpdateFailed( f"Unable to update iAquaLink system {self.system.serial}: {err}" diff --git a/tests/components/iaqualink/test_init.py b/tests/components/iaqualink/test_init.py index 57d205c9caaf8..fe4058fa5e558 100644 --- a/tests/components/iaqualink/test_init.py +++ b/tests/components/iaqualink/test_init.py @@ -6,6 +6,7 @@ from iaqualink.client import AqualinkClient from iaqualink.exception import ( AqualinkServiceException, + AqualinkServiceThrottledException, AqualinkServiceUnauthorizedException, ) from iaqualink.systems.iaqua.device import ( @@ -16,6 +17,7 @@ IaquaThermostat, ) from iaqualink.systems.iaqua.system import IaquaSystem +import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN @@ -46,7 +48,9 @@ async def _advance_coordinator_time( hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: """Advance time to trigger coordinator update interval.""" - freezer.tick(delta=UPDATE_INTERVAL_BY_SYSTEM_TYPE["iaqua"]) + update_interval = UPDATE_INTERVAL_BY_SYSTEM_TYPE["iaqua"] + + freezer.tick(delta=update_interval) async_fire_time_changed(hass, dt_util.utcnow()) await hass.async_block_till_done(wait_background_tasks=True) @@ -104,6 +108,57 @@ async def fail_update() -> None: assert state.state == STATE_UNAVAILABLE +async def test_system_rate_limited_keeps_entities_available( + hass: HomeAssistant, + config_entry: MockConfigEntry, + client: AqualinkClient, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test a rate-limited update keeps entities at their last known state.""" + config_entry.add_to_hass(hass) + + system = get_aqualink_system(client, cls=IaquaSystem) + system.online = True + system.update = AsyncMock() + systems = {system.serial: system} + light = get_aqualink_device( + system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"} + ) + devices = {light.name: light} + system.get_devices = AsyncMock(return_value=devices) + + with ( + patch( + "homeassistant.components.iaqualink.AqualinkClient.login", + return_value=None, + ), + patch( + "homeassistant.components.iaqualink.AqualinkClient.get_systems", + return_value=systems, + ), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN) + assert len(entity_ids) == 1 + entity_id = entity_ids[0] + + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + + system.update = AsyncMock(side_effect=AqualinkServiceThrottledException) + + await _advance_coordinator_time(hass, freezer) + + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_ON + assert "Rate limited by iAquaLink" in caplog.text + + async def test_light_service_calls_update_entity_state( hass: HomeAssistant, config_entry: MockConfigEntry,