Use batch device status updates in Imou coordinator (#182471)

This commit is contained in:
Imou-OpenPlatform
2026-09-19 13:44:12 +02:00
committed by GitHub
parent cbc435157b
commit 2966e25d9a
8 changed files with 64 additions and 57 deletions
+9 -28
View File
@@ -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:
+1
View File
@@ -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
+5 -4
View File
@@ -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)
+5 -4
View File
@@ -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)
+26 -6
View File
@@ -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",
[
+5 -4
View File
@@ -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)
+8 -7
View File
@@ -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)
+5 -4
View File
@@ -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)