Files
Imou-OpenPlatform 674dacef07 Adds the switch platform for the Imou integration (#174880)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-03 11:29:40 +02:00

263 lines
8.5 KiB
Python

"""Tests for Imou switch platform."""
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
from pyimouapi.exceptions import ImouException
from pyimouapi.ha_device import DeviceStatus, ImouHaDevice
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.imou.const import (
PARAM_HEADER_DETECT,
PARAM_MOTION_DETECT,
PARAM_STATE,
PARAM_STATUS,
)
from homeassistant.components.imou.coordinator import SCAN_INTERVAL
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from .const import DEFAULT_SWITCHES, UNKNOWN_SWITCH_KEY, create_online_device
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
async def _apply_switch_operation(
device: ImouHaDevice, switch_type: str, enable: bool
) -> None:
"""Simulate the vendor API updating switch state after a command."""
device.switches[switch_type][PARAM_STATE] = enable
SWITCH_MOCK_DEVICES = [
create_online_device(
"d1",
"Device 1",
button_keys=(),
switches=DEFAULT_SWITCHES,
),
]
@pytest.mark.parametrize("imou_mock_devices", [SWITCH_MOCK_DEVICES], indirect=True)
@pytest.mark.usefixtures("init_integration")
async def test_switch_entities_snapshot(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
) -> None:
"""Snapshot switch entities created from the mock device list."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
"imou_mock_devices",
[
[
create_online_device(
"d1",
"Device 1",
button_keys=(),
switches={
UNKNOWN_SWITCH_KEY: {PARAM_STATE: False},
PARAM_MOTION_DETECT: {PARAM_STATE: False},
},
)
]
],
indirect=True,
)
@pytest.mark.usefixtures("init_integration")
async def test_setup_ignores_unknown_switch_types(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Unknown switch keys from the API are not turned into entities."""
registry = er.async_get(hass)
entries = er.async_entries_for_config_entry(registry, mock_config_entry.entry_id)
switch_entries = [entry for entry in entries if entry.domain == SWITCH_DOMAIN]
assert len(switch_entries) == 1
assert switch_entries[0].translation_key == PARAM_MOTION_DETECT
@pytest.mark.parametrize("imou_mock_devices", [SWITCH_MOCK_DEVICES], indirect=True)
@pytest.mark.usefixtures("init_integration")
async def test_turn_on_via_service(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
init_integration: MagicMock,
) -> None:
"""Turning on a switch calls the vendor library through the coordinator."""
init_integration.async_switch_operation.side_effect = _apply_switch_operation
motion_entry = next(
entry
for entry in er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
if entry.unique_id == "d1$motion_detect"
)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: motion_entry.entity_id},
blocking=True,
)
init_integration.async_switch_operation.assert_awaited_once()
call = init_integration.async_switch_operation.await_args
assert call is not None
assert call.args[1] == PARAM_MOTION_DETECT
assert call.args[2] is True
assert hass.states.get(motion_entry.entity_id).state == "on"
@pytest.mark.parametrize("imou_mock_devices", [SWITCH_MOCK_DEVICES], indirect=True)
@pytest.mark.usefixtures("init_integration")
async def test_turn_off_via_service(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
init_integration: MagicMock,
) -> None:
"""Turning off a switch calls the vendor library through the coordinator."""
init_integration.async_switch_operation.side_effect = _apply_switch_operation
header_entry = next(
entry
for entry in er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
if entry.unique_id == "d1$header_detect"
)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: header_entry.entity_id},
blocking=True,
)
init_integration.async_switch_operation.assert_awaited_once()
call = init_integration.async_switch_operation.await_args
assert call is not None
assert call.args[1] == PARAM_HEADER_DETECT
assert call.args[2] is False
assert hass.states.get(header_entry.entity_id).state == "off"
@pytest.mark.parametrize("imou_mock_devices", [SWITCH_MOCK_DEVICES], indirect=True)
@pytest.mark.usefixtures("init_integration")
async def test_turn_on_service_propagates_api_error(
hass: HomeAssistant,
init_integration: MagicMock,
) -> None:
"""Imou API errors from async_switch_operation surface to the service call."""
init_integration.async_switch_operation.side_effect = ImouException("cloud failure")
entity_id = hass.states.async_all("switch")[0].entity_id
with pytest.raises(HomeAssistantError, match="cloud failure"):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
@pytest.mark.parametrize(
"imou_mock_devices",
[
[
create_online_device(
"d1",
"Device 1",
button_keys=(),
switches={PARAM_MOTION_DETECT: {PARAM_STATE: False}},
)
]
],
indirect=True,
)
@pytest.mark.usefixtures("init_integration")
async def test_turn_off_unavailable_offline_device_via_service(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_imou_ha_device_manager: MagicMock,
init_integration: MagicMock,
) -> None:
"""Turning off an offline device does not call the vendor library."""
motion_entry = next(
entry
for entry in er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
if entry.unique_id == "d1$motion_detect"
)
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(motion_entry.entity_id).state == STATE_UNAVAILABLE
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: motion_entry.entity_id},
blocking=True,
)
init_integration.async_switch_operation.assert_not_called()
@pytest.mark.parametrize("imou_mock_devices", [SWITCH_MOCK_DEVICES], indirect=True)
@pytest.mark.usefixtures("init_integration")
async def test_entities_removed_when_device_leaves_account(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_imou_ha_device_manager: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Switch entities are removed when the device is no longer on the account."""
motion_entry = next(
entry
for entry in er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
if entry.unique_id == "d1$motion_detect"
)
assert hass.states.get(motion_entry.entity_id).state != STATE_UNAVAILABLE
mock_imou_ha_device_manager.async_get_devices.return_value = []
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert (
er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)
== []
)
assert hass.states.get(motion_entry.entity_id) is None