Files
core/tests/components/duco/test_switch.py
T

221 lines
7.0 KiB
Python

"""Tests for the Duco switch platform."""
from dataclasses import replace
from unittest.mock import AsyncMock
from duco_connectivity import (
ActionItem,
ActionValueType,
DucoError,
DucoRateLimitError,
KnownActionName,
Node,
NodeListActionItemList,
)
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.duco.const import SCAN_INTERVAL
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import async_fire_coordinator_update, setup_platform_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
_IDENTIFY_ENTITY = "switch.living_identify"
def _identify_action(
val_type: ActionValueType = ActionValueType.BOOLEAN,
) -> ActionItem:
"""Build identify action metadata for switch tests."""
return ActionItem(
action=KnownActionName.SET_IDENTIFY,
val_type=val_type,
)
async def _async_set_identify(hass: HomeAssistant, service: str) -> None:
"""Set the identify switch state."""
await hass.services.async_call(
SWITCH_DOMAIN,
service,
{ATTR_ENTITY_ID: _IDENTIFY_ENTITY},
blocking=True,
)
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_duco_client: AsyncMock,
mock_node_actions: NodeListActionItemList,
) -> None:
"""Set up only the switch platform with identify capability."""
mock_node_actions.nodes[0].actions.append(_identify_action())
await setup_platform_integration(hass, mock_config_entry, [Platform.SWITCH])
@pytest.mark.usefixtures("init_integration")
async def test_identify_switch_entity_state(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the identify switch entity registry metadata and state."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.usefixtures("init_integration")
async def test_identify_switch_discovery_and_state(hass: HomeAssistant) -> None:
"""Test identify switches require a Boolean identify action."""
assert hass.states.is_state(_IDENTIFY_ENTITY, STATE_OFF)
assert hass.states.get("switch.office_co2_identify") is None
async def test_non_boolean_identify_action_is_ignored(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_duco_client: AsyncMock,
mock_node_actions: NodeListActionItemList,
) -> None:
"""Test identify actions with another value type are ignored."""
mock_node_actions.nodes[0].actions.append(_identify_action(ActionValueType.INTEGER))
await setup_platform_integration(hass, mock_config_entry, [Platform.SWITCH])
assert hass.states.get(_IDENTIFY_ENTITY) is None
@pytest.mark.usefixtures("init_integration")
@pytest.mark.parametrize(
("service", "identify", "expected_state"),
[
pytest.param(SERVICE_TURN_ON, True, STATE_ON, id="on"),
pytest.param(SERVICE_TURN_OFF, False, STATE_OFF, id="off"),
],
)
async def test_set_identify(
hass: HomeAssistant,
mock_duco_client: AsyncMock,
mock_nodes: list[Node],
service: str,
identify: bool,
expected_state: str,
) -> None:
"""Test identify writes use the typed helper and update state."""
box_node = mock_nodes[0]
mock_duco_client.async_get_node_info.side_effect = None
mock_duco_client.async_get_node_info.return_value = replace(
box_node,
general=replace(box_node.general, identify=int(identify)),
)
await _async_set_identify(hass, service)
mock_duco_client.async_set_node_identify.assert_awaited_once_with(1, identify)
mock_duco_client.async_get_node_info.assert_awaited_once_with(1)
assert mock_duco_client.async_get_nodes.await_count == 1
assert hass.states.is_state(_IDENTIFY_ENTITY, expected_state)
@pytest.mark.parametrize(
("exception", "match"),
[
pytest.param(
DucoError("Unexpected error"), "Failed to set node identification"
),
pytest.param(DucoRateLimitError(), "daily write limit"),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_identify_write_error(
hass: HomeAssistant,
mock_duco_client: AsyncMock,
exception: Exception,
match: str,
) -> None:
"""Test identify write failures report user-facing errors."""
mock_duco_client.async_set_node_identify.side_effect = exception
with pytest.raises(HomeAssistantError, match=match):
await _async_set_identify(hass, SERVICE_TURN_ON)
async def test_identify_switch_is_added_when_action_discovery_succeeds_later(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_duco_client: AsyncMock,
mock_node_actions: NodeListActionItemList,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test identify switches are added after action discovery recovers."""
mock_node_actions.nodes[0].actions.append(_identify_action())
mock_duco_client.async_get_node_actions.side_effect = [
DucoError("Connection refused"),
mock_node_actions,
]
await setup_platform_integration(hass, mock_config_entry, [Platform.SWITCH])
assert hass.states.get(_IDENTIFY_ENTITY) is None
await async_fire_coordinator_update(hass, freezer)
assert hass.states.get(_IDENTIFY_ENTITY) is not None
@pytest.mark.usefixtures("init_integration")
async def test_identify_switch_becomes_unavailable_when_node_is_missing(
hass: HomeAssistant,
mock_duco_client: AsyncMock,
mock_nodes: list[Node],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test an identify switch becomes unavailable when its node disappears."""
mock_duco_client.async_get_nodes.return_value = mock_nodes[1:]
await async_fire_coordinator_update(hass, freezer)
assert hass.states.is_state(_IDENTIFY_ENTITY, STATE_UNAVAILABLE)
@pytest.mark.usefixtures("init_integration")
async def test_targeted_identify_readback_keeps_coordinator_poll_schedule(
hass: HomeAssistant,
mock_duco_client: AsyncMock,
mock_nodes: list[Node],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test an identify readback does not postpone the full coordinator poll."""
box_node = mock_nodes[0]
mock_duco_client.async_get_node_info.return_value = replace(
box_node, general=replace(box_node.general, identify=1)
)
mock_duco_client.async_get_node_info.side_effect = None
freezer.tick(SCAN_INTERVAL / 2)
await _async_set_identify(hass, SERVICE_TURN_ON)
freezer.tick(SCAN_INTERVAL / 2)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert mock_duco_client.async_get_nodes.await_count == 2