mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 17:31:15 -04:00
Use batch device status updates in Imou coordinator (#182471)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
[
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user