mirror of
https://github.com/home-assistant/core.git
synced 2026-07-01 03:36:05 +01:00
365 lines
12 KiB
Python
365 lines
12 KiB
Python
"""Tests for the Imou init."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
from pyimouapi.exceptions import ImouException
|
|
from pyimouapi.ha_device import DeviceStatus, ImouHaDevice
|
|
import pytest
|
|
|
|
from homeassistant.components.imou.button import PARAM_MUTE, PARAM_PTZ_UP
|
|
from homeassistant.components.imou.const import DOMAIN, PARAM_STATE, PARAM_STATUS
|
|
from homeassistant.components.imou.coordinator import SCAN_INTERVAL
|
|
from homeassistant.config_entries import ConfigEntryState
|
|
from homeassistant.const import STATE_UNAVAILABLE
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
|
|
from .const import DEFAULT_MOCK_DEVICES, create_offline_device, create_online_device
|
|
|
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
|
|
|
EXPECTED_TRANSLATION_KEYS = {
|
|
"mute": PARAM_MUTE,
|
|
"camera_sd": "camera_sd",
|
|
"camera_hd": "camera_hd",
|
|
}
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_imou_openapi_client", "mock_imou_ha_device_manager")
|
|
async def test_setup_and_unload_entry(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
init_integration: MagicMock,
|
|
) -> None:
|
|
"""Test loading and unloading the config entry."""
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_imou_openapi_client", "mock_imou_ha_device_manager")
|
|
async def test_setup_entry_failed_on_refresh(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_imou_ha_device_manager: AsyncMock,
|
|
) -> None:
|
|
"""Device fetch failure during coordinator setup surfaces as setup retry."""
|
|
mock_imou_ha_device_manager.async_get_devices.side_effect = RuntimeError(
|
|
"Setup failed"
|
|
)
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_device_registry_identifiers(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Device registry uses channel-aware identifiers from the default mock devices."""
|
|
registry = dr.async_get(hass)
|
|
devices = dr.async_entries_for_config_entry(registry, mock_config_entry.entry_id)
|
|
assert len(devices) == 1
|
|
assert (DOMAIN, "d1") in devices[0].identifiers
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"imou_mock_devices",
|
|
[
|
|
[
|
|
create_online_device(
|
|
"dev-1",
|
|
"Cam",
|
|
channel_id="ch9",
|
|
button_keys=(PARAM_MUTE,),
|
|
),
|
|
create_online_device(
|
|
"dev-1",
|
|
"Cam",
|
|
channel_id="ch10",
|
|
button_keys=(PARAM_MUTE,),
|
|
),
|
|
]
|
|
],
|
|
indirect=True,
|
|
)
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_multiple_channels_create_separate_devices(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
device_registry: dr.DeviceRegistry,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Each channel gets its own device and button entities in the registries."""
|
|
devices = dr.async_entries_for_config_entry(
|
|
device_registry, mock_config_entry.entry_id
|
|
)
|
|
device_ids_by_key = {
|
|
next(iter(device.identifiers))[1]: device.id for device in devices
|
|
}
|
|
assert set(device_ids_by_key) == {"dev-1_ch9", "dev-1_ch10"}
|
|
|
|
entries = er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
assert len(entries) == 6
|
|
assert {entry.unique_id for entry in entries} == {
|
|
"dev-1_ch9$mute",
|
|
"dev-1_ch10$mute",
|
|
"dev-1_ch9$camera_sd",
|
|
"dev-1_ch9$camera_hd",
|
|
"dev-1_ch10$camera_sd",
|
|
"dev-1_ch10$camera_hd",
|
|
}
|
|
for entry in entries:
|
|
device_key, entity_type = entry.unique_id.split("$", 1)
|
|
assert entry.device_id == device_ids_by_key[device_key]
|
|
assert entry.translation_key == EXPECTED_TRANSLATION_KEYS[entity_type]
|
|
state = hass.states.get(entry.entity_id)
|
|
assert state is not None
|
|
assert state.state != STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.parametrize("imou_mock_devices", [[]], indirect=True)
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_coordinator_adds_entities_after_initial_empty_device_list(
|
|
hass: HomeAssistant,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_imou_ha_device_manager: MagicMock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Devices added after an empty first refresh still get entities via callbacks."""
|
|
assert (
|
|
len(
|
|
er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
)
|
|
== 0
|
|
)
|
|
|
|
mock_imou_ha_device_manager.async_get_devices.return_value = DEFAULT_MOCK_DEVICES
|
|
freezer.tick(SCAN_INTERVAL)
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
entries = er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
assert len(entries) == 3
|
|
assert {entry.unique_id for entry in entries} == {
|
|
"d1$mute",
|
|
"d1$ptz_up",
|
|
"d1$restart_device",
|
|
}
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_coordinator_adds_entities_for_new_device(
|
|
hass: HomeAssistant,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_imou_ha_device_manager: MagicMock,
|
|
device_registry: dr.DeviceRegistry,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""A device added to the Imou account is discovered on the next coordinator refresh."""
|
|
assert (
|
|
len(
|
|
er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
)
|
|
== 3
|
|
)
|
|
|
|
mock_imou_ha_device_manager.async_get_devices.return_value = [
|
|
*DEFAULT_MOCK_DEVICES,
|
|
create_online_device("d2", "Device 2", button_keys=(PARAM_PTZ_UP,)),
|
|
]
|
|
freezer.tick(SCAN_INTERVAL)
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
entries = er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
assert len(entries) == 4
|
|
assert "d2$ptz_up" in {entry.unique_id for entry in entries}
|
|
ptz_entry = next(entry for entry in entries if entry.unique_id == "d2$ptz_up")
|
|
assert hass.states.get(ptz_entry.entity_id).state != STATE_UNAVAILABLE
|
|
|
|
devices = dr.async_entries_for_config_entry(
|
|
device_registry, mock_config_entry.entry_id
|
|
)
|
|
assert len(devices) == 2
|
|
device_keys = {next(iter(device.identifiers))[1] for device in devices}
|
|
assert device_keys == {"d1", "d2"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"imou_mock_devices",
|
|
[
|
|
[
|
|
create_online_device("d1", "Device 1", button_keys=(PARAM_MUTE,)),
|
|
create_online_device("d2", "Device 2", button_keys=(PARAM_PTZ_UP,)),
|
|
]
|
|
],
|
|
indirect=True,
|
|
)
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_coordinator_removes_device_updates_registries(
|
|
hass: HomeAssistant,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_imou_ha_device_manager: MagicMock,
|
|
device_registry: dr.DeviceRegistry,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""A removed device is dropped from the device and entity registries."""
|
|
assert (
|
|
len(
|
|
dr.async_entries_for_config_entry(
|
|
device_registry, mock_config_entry.entry_id
|
|
)
|
|
)
|
|
== 2
|
|
)
|
|
entries_before = er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
assert {entry.unique_id for entry in entries_before} == {
|
|
"d1$mute",
|
|
"d2$ptz_up",
|
|
}
|
|
|
|
mock_imou_ha_device_manager.async_get_devices.return_value = DEFAULT_MOCK_DEVICES
|
|
freezer.tick(SCAN_INTERVAL)
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
devices = dr.async_entries_for_config_entry(
|
|
device_registry, mock_config_entry.entry_id
|
|
)
|
|
assert len(devices) == 1
|
|
assert (DOMAIN, "d1") in devices[0].identifiers
|
|
|
|
entries_after = er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
assert {entry.unique_id for entry in entries_after} == {"d1$mute"}
|
|
mute_entry = next(entry for entry in entries_after if entry.unique_id == "d1$mute")
|
|
assert hass.states.get(mute_entry.entity_id).state != STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"imou_mock_devices",
|
|
[
|
|
[
|
|
create_online_device(
|
|
"d1",
|
|
"Device 1",
|
|
button_keys=(PARAM_MUTE,),
|
|
)
|
|
]
|
|
],
|
|
indirect=True,
|
|
)
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_offline_device_marked_unavailable_after_refresh(
|
|
hass: HomeAssistant,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_imou_ha_device_manager: MagicMock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""An offline device reported on refresh marks button entities unavailable."""
|
|
mute_entry = next(
|
|
entry
|
|
for entry in er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
if entry.unique_id == "d1$mute"
|
|
)
|
|
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}
|
|
|
|
mock_imou_ha_device_manager.async_update_device_status.side_effect = (
|
|
set_device_offline
|
|
)
|
|
freezer.tick(SCAN_INTERVAL)
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
assert hass.states.get(mute_entry.entity_id).state == STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_coordinator_update_fails_when_all_devices_fail(
|
|
hass: HomeAssistant,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_imou_ha_device_manager: MagicMock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""When every device status update fails, the coordinator update fails."""
|
|
mute_entry = next(
|
|
entry
|
|
for entry in er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
if entry.unique_id == "d1$mute"
|
|
)
|
|
assert hass.states.get(mute_entry.entity_id).state != STATE_UNAVAILABLE
|
|
|
|
mock_imou_ha_device_manager.async_update_device_status.side_effect = ImouException(
|
|
"cloud failure"
|
|
)
|
|
freezer.tick(SCAN_INTERVAL)
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
assert mock_config_entry.runtime_data.last_update_success is False
|
|
assert hass.states.get(mute_entry.entity_id).state == STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"imou_mock_devices",
|
|
[
|
|
[
|
|
create_offline_device(
|
|
"d1",
|
|
"Device 1",
|
|
button_keys=(PARAM_MUTE,),
|
|
)
|
|
]
|
|
],
|
|
indirect=True,
|
|
)
|
|
@pytest.mark.usefixtures("init_integration")
|
|
async def test_offline_device_unavailable_at_setup(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""An offline device marks button entities unavailable via the state machine."""
|
|
mute_entry = next(
|
|
entry
|
|
for entry in er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
if entry.unique_id == "d1$mute"
|
|
)
|
|
assert hass.states.get(mute_entry.entity_id).state == STATE_UNAVAILABLE
|