From 2966e25d9acb0dc13d9653d414e1c10b704c9677 Mon Sep 17 00:00:00 2001 From: Imou-OpenPlatform Date: Sat, 19 Sep 2026 19:44:12 +0800 Subject: [PATCH] Use batch device status updates in Imou coordinator (#182471) --- homeassistant/components/imou/coordinator.py | 37 +++++--------------- tests/components/imou/conftest.py | 1 + tests/components/imou/test_binary_sensor.py | 9 ++--- tests/components/imou/test_button.py | 9 ++--- tests/components/imou/test_init.py | 32 +++++++++++++---- tests/components/imou/test_select.py | 9 ++--- tests/components/imou/test_sensor.py | 15 ++++---- tests/components/imou/test_switch.py | 9 ++--- 8 files changed, 64 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/imou/coordinator.py b/homeassistant/components/imou/coordinator.py index 883cb89c9f3c..179b15db1bc1 100644 --- a/homeassistant/components/imou/coordinator.py +++ b/homeassistant/components/imou/coordinator.py @@ -102,41 +102,22 @@ class ImouDataUpdateCoordinator(DataUpdateCoordinator[None]): try: async with asyncio.timeout(UPDATE_TIMEOUT): - results = await asyncio.gather( - *( - self._device_manager.async_update_device_status(device) - for device in devices - ), - return_exceptions=True, - ) + await self._device_manager.async_update_devices_status(devices) except TimeoutError as err: raise UpdateFailed(f"Timeout while fetching data: {err}") from err - - failures: list[Exception] = [] - for device, result in zip(devices, results, strict=True): - if isinstance(result, BaseException) and not isinstance(result, Exception): - # Propagate CancelledError and other BaseExceptions instead of - # swallowing them as a regular device failure. - raise result - if not isinstance(result, Exception): - continue - device_key = imou_device_identifier(device) - _LOGGER.warning( - "Error updating status for Imou device %s: %s", - device_key, - result, - ) - failures.append(result) - if failures and len(failures) == len(devices): - raise UpdateFailed( - f"Error updating Imou devices: {failures[0]}" - ) from failures[0] + except InvalidAppIdOrSecretException as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="invalid_auth", + ) from err + except ImouException as err: + raise UpdateFailed(f"Error updating Imou devices: {err}") from err def _async_add_remove_devices(self, fresh_by_key: dict[str, ImouHaDevice]) -> None: """Add new devices, remove devices no longer in the account. This only tracks which devices exist on the account; per-device state - is updated in place by `async_update_device_status`, so devices that + is updated in place by `async_update_devices_status`, so devices that remain on the account keep their existing object and are not replaced. """ if not self._devices_initialized: diff --git a/tests/components/imou/conftest.py b/tests/components/imou/conftest.py index eff6c0c90da6..9ae5fd1be9ff 100644 --- a/tests/components/imou/conftest.py +++ b/tests/components/imou/conftest.py @@ -66,6 +66,7 @@ def mock_imou_ha_device_manager( with patch(PATCH_IMOU_HA_DEVICE_MANAGER, autospec=True) as mock_manager: device_manager = mock_manager.return_value device_manager.async_get_devices.return_value = imou_mock_devices + device_manager.async_update_devices_status.return_value = set() yield device_manager diff --git a/tests/components/imou/test_binary_sensor.py b/tests/components/imou/test_binary_sensor.py index 09485961615a..41f22c6fabac 100644 --- a/tests/components/imou/test_binary_sensor.py +++ b/tests/components/imou/test_binary_sensor.py @@ -158,11 +158,12 @@ async def test_binary_sensor_unavailable_when_device_offline( ) -> None: """Binary sensors become unavailable when the device is offline.""" - async def set_device_offline(device: ImouHaDevice) -> None: - device._sensors[PARAM_STATUS] = {PARAM_STATE: DeviceStatus.OFFLINE.value} + async def set_devices_offline(devices: list[ImouHaDevice]) -> None: + for device in devices: + device._sensors[PARAM_STATUS] = {PARAM_STATE: DeviceStatus.OFFLINE.value} - mock_imou_ha_device_manager.async_update_device_status.side_effect = ( - set_device_offline + mock_imou_ha_device_manager.async_update_devices_status.side_effect = ( + set_devices_offline ) freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) diff --git a/tests/components/imou/test_button.py b/tests/components/imou/test_button.py index f1586d917654..3a44f76a3753 100644 --- a/tests/components/imou/test_button.py +++ b/tests/components/imou/test_button.py @@ -198,11 +198,12 @@ async def test_press_unavailable_offline_device_via_service( if entry.unique_id == "d1$mute" ) - async def set_device_offline(device: ImouHaDevice) -> None: - device._sensors[PARAM_STATUS] = {PARAM_STATE: DeviceStatus.OFFLINE.value} + async def set_devices_offline(devices: list[ImouHaDevice]) -> None: + for device in devices: + device._sensors[PARAM_STATUS] = {PARAM_STATE: DeviceStatus.OFFLINE.value} - mock_imou_ha_device_manager.async_update_device_status.side_effect = ( - set_device_offline + mock_imou_ha_device_manager.async_update_devices_status.side_effect = ( + set_devices_offline ) freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) diff --git a/tests/components/imou/test_init.py b/tests/components/imou/test_init.py index 3d1f1282338f..4ad12e77fd6c 100644 --- a/tests/components/imou/test_init.py +++ b/tests/components/imou/test_init.py @@ -11,7 +11,7 @@ import pytest from homeassistant.components.imou.button import PARAM_MUTE, PARAM_PTZ_UP from homeassistant.components.imou.const import DOMAIN from homeassistant.components.imou.coordinator import SCAN_INTERVAL -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -312,11 +312,12 @@ async def test_offline_device_marked_unavailable_after_refresh( ) assert hass.states.get(mute_entry.entity_id).state != STATE_UNAVAILABLE - async def set_device_offline(device: ImouHaDevice) -> None: - device._sensors[PARAM_STATUS] = {PARAM_STATE: DeviceStatus.OFFLINE.value} + async def set_devices_offline(devices: list[ImouHaDevice]) -> None: + for device in devices: + device._sensors[PARAM_STATUS] = {PARAM_STATE: DeviceStatus.OFFLINE.value} - mock_imou_ha_device_manager.async_update_device_status.side_effect = ( - set_device_offline + mock_imou_ha_device_manager.async_update_devices_status.side_effect = ( + set_devices_offline ) freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) @@ -343,7 +344,7 @@ async def test_coordinator_update_fails_when_all_devices_fail( ) assert hass.states.get(mute_entry.entity_id).state != STATE_UNAVAILABLE - mock_imou_ha_device_manager.async_update_device_status.side_effect = ImouException( + mock_imou_ha_device_manager.async_update_devices_status.side_effect = ImouException( "cloud failure" ) freezer.tick(SCAN_INTERVAL) @@ -354,6 +355,25 @@ async def test_coordinator_update_fails_when_all_devices_fail( assert hass.states.get(mute_entry.entity_id).state == STATE_UNAVAILABLE +@pytest.mark.usefixtures("init_integration") +async def test_coordinator_status_refresh_invalid_auth( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, + mock_imou_ha_device_manager: MagicMock, +) -> None: + """Invalid credentials during status refresh start reauthentication.""" + mock_imou_ha_device_manager.async_update_devices_status.side_effect = ( + InvalidAppIdOrSecretException("bad credentials") + ) + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done(wait_background_tasks=True) + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert any(mock_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) + + @pytest.mark.parametrize( "imou_mock_devices", [ diff --git a/tests/components/imou/test_select.py b/tests/components/imou/test_select.py index 922f5d1008cc..d1a321be3042 100644 --- a/tests/components/imou/test_select.py +++ b/tests/components/imou/test_select.py @@ -250,11 +250,12 @@ async def test_select_option_unavailable_offline_device( if entry.unique_id == "d1$night_vision_mode" ) - async def set_device_offline(device: ImouHaDevice) -> None: - device._sensors[PARAM_STATUS] = {PARAM_STATE: DeviceStatus.OFFLINE.value} + async def set_devices_offline(devices: list[ImouHaDevice]) -> None: + for device in devices: + device._sensors[PARAM_STATUS] = {PARAM_STATE: DeviceStatus.OFFLINE.value} - mock_imou_ha_device_manager.async_update_device_status.side_effect = ( - set_device_offline + mock_imou_ha_device_manager.async_update_devices_status.side_effect = ( + set_devices_offline ) freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) diff --git a/tests/components/imou/test_sensor.py b/tests/components/imou/test_sensor.py index 01934ea3d45f..a60c53898139 100644 --- a/tests/components/imou/test_sensor.py +++ b/tests/components/imou/test_sensor.py @@ -96,14 +96,15 @@ async def test_sensor_availability_when_device_offline( ) -> None: """Status stays available offline; other sensors become unavailable.""" - async def set_device_offline(device: ImouHaDevice) -> None: - device._sensors[PARAM_STATUS] = { - PARAM_STATE: DeviceStatus.OFFLINE.value, - PARAM_STATE_VARIANT: STATE_VARIANT_ENUM, - } + async def set_devices_offline(devices: list[ImouHaDevice]) -> None: + for device in devices: + device._sensors[PARAM_STATUS] = { + PARAM_STATE: DeviceStatus.OFFLINE.value, + PARAM_STATE_VARIANT: STATE_VARIANT_ENUM, + } - mock_imou_ha_device_manager.async_update_device_status.side_effect = ( - set_device_offline + mock_imou_ha_device_manager.async_update_devices_status.side_effect = ( + set_devices_offline ) freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) diff --git a/tests/components/imou/test_switch.py b/tests/components/imou/test_switch.py index 04f2a7f4a9d2..2c5b88c89beb 100644 --- a/tests/components/imou/test_switch.py +++ b/tests/components/imou/test_switch.py @@ -243,11 +243,12 @@ async def test_turn_off_unavailable_offline_device_via_service( if entry.unique_id == "d1$motion_detect" ) - async def set_device_offline(device: ImouHaDevice) -> None: - device._sensors[PARAM_STATUS] = {PARAM_STATE: DeviceStatus.OFFLINE.value} + async def set_devices_offline(devices: list[ImouHaDevice]) -> None: + for device in devices: + device._sensors[PARAM_STATUS] = {PARAM_STATE: DeviceStatus.OFFLINE.value} - mock_imou_ha_device_manager.async_update_device_status.side_effect = ( - set_device_offline + mock_imou_ha_device_manager.async_update_devices_status.side_effect = ( + set_devices_offline ) freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass)