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

433 lines
14 KiB
Python

"""Tests for TP-Link Omada switch entities."""
import asyncio
from datetime import timedelta
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from syrupy.assertion import SnapshotAssertion
from tplink_omada_client import SwitchPortSettings
from tplink_omada_client.definitions import PoEMode
from tplink_omada_client.devices import (
OmadaGateway,
OmadaGatewayPortConfig,
OmadaGatewayPortStatus,
OmadaListDevice,
OmadaSwitch,
OmadaSwitchPortDetails,
)
from tplink_omada_client.exceptions import InvalidDevice, OmadaClientException
from homeassistant.components import switch
from homeassistant.components.tplink_omada.const import DOMAIN
from homeassistant.components.tplink_omada.coordinator import POLL_GATEWAY
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceResponse
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.util.dt import utcnow
from tests.common import MockConfigEntry, async_fire_time_changed
UPDATE_INTERVAL = timedelta(seconds=10)
POLL_INTERVAL = timedelta(seconds=POLL_GATEWAY + 10)
@pytest.mark.usefixtures("mock_omada_client")
async def test_switch_port_setup_does_not_depend_on_debounced_refresh(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_omada_site_client: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test switch port setup waits for coordinator data."""
mock_config_entry.add_to_hass(hass)
switch_setup_started = asyncio.Event()
original_get_switch = mock_omada_site_client.get_switch.return_value
original_get_switch_ports = mock_omada_site_client.get_switch_ports.return_value
sensor_port_refresh_started = asyncio.Event()
get_switch_calls = 0
get_switch_ports_calls = 0
async def get_switch(device: OmadaListDevice) -> OmadaSwitch:
nonlocal get_switch_calls
get_switch_calls += 1
if get_switch_calls == 2:
switch_setup_started.set()
await sensor_port_refresh_started.wait()
return original_get_switch
async def get_switch_ports(
network_switch: OmadaSwitch,
) -> list[OmadaSwitchPortDetails]:
nonlocal get_switch_ports_calls
get_switch_ports_calls += 1
if get_switch_ports_calls == 1:
sensor_port_refresh_started.set()
await switch_setup_started.wait()
await asyncio.sleep(0.1)
return original_get_switch_ports
mock_omada_site_client.get_switch.side_effect = get_switch
mock_omada_site_client.get_switch_ports.side_effect = get_switch_ports
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
entity_id = entity_registry.async_get_entity_id(
"switch", DOMAIN, "54-AF-97-00-00-01_000000000000000000000001_poe"
)
assert entity_id is not None
assert hass.states.get(entity_id) is not None
async def test_poe_switches(
hass: HomeAssistant,
mock_omada_site_client: MagicMock,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test PoE switch."""
poe_switch_mac = "54-AF-97-00-00-01"
await _test_poe_switch(
hass,
mock_omada_site_client,
"switch.test_poe_switch_port_1_poe",
poe_switch_mac,
1,
snapshot,
entity_registry,
)
await _test_poe_switch(
hass,
mock_omada_site_client,
"switch.test_poe_switch_port_2_renamed_port_poe",
poe_switch_mac,
2,
snapshot,
entity_registry,
)
async def test_sfp_port_has_no_poe_switch(
hass: HomeAssistant,
init_integration: MockConfigEntry,
) -> None:
"""Test PoE switch SFP ports have no PoE controls."""
entity = hass.states.get("switch.test_poe_switch_port_9_poe")
assert entity is None
entity = hass.states.get("switch.test_poe_switch_port_8_poe")
assert entity is not None
async def test_gateway_connect_ipv4_switch(
hass: HomeAssistant,
mock_omada_site_client: MagicMock,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test gateway connected switches."""
gateway_mac = "AA-BB-CC-DD-EE-FF"
entity_id = "switch.test_router_port_4_internet_connected"
entity = hass.states.get(entity_id)
assert entity == snapshot
test_gateway = await mock_omada_site_client.get_gateway(gateway_mac)
port_status = test_gateway.port_status[3]
assert port_status.port_number == 4
mock_omada_site_client.set_gateway_wan_port_connect_state = AsyncMock(
return_value=(
_get_updated_gateway_port_status(
mock_omada_site_client, test_gateway, 3, "internetState", 0
)
)
)
await call_service(hass, "turn_off", entity_id)
mock_omada_site_client.set_gateway_wan_port_connect_state.assert_called_once_with(
4, False, test_gateway, ipv6=False
)
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL)
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
assert entity and entity.state == "off"
mock_omada_site_client.set_gateway_wan_port_connect_state.reset_mock()
mock_omada_site_client.set_gateway_wan_port_connect_state.return_value = (
_get_updated_gateway_port_status(
mock_omada_site_client, test_gateway, 3, "internetState", 1
)
)
await call_service(hass, "turn_on", entity_id)
mock_omada_site_client.set_gateway_wan_port_connect_state.assert_called_once_with(
4, True, test_gateway, ipv6=False
)
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL)
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
assert entity and entity.state == "on"
async def test_gateway_port_poe_switch(
hass: HomeAssistant,
mock_omada_site_client: MagicMock,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test gateway connected switches."""
gateway_mac = "AA-BB-CC-DD-EE-FF"
entity_id = "switch.test_router_port_5_poe"
entity = hass.states.get(entity_id)
assert entity == snapshot
test_gateway = await mock_omada_site_client.get_gateway(gateway_mac)
port_config = test_gateway.port_configs[4]
assert port_config.port_number == 5
mock_omada_site_client.set_gateway_port_settings = AsyncMock(
return_value=(OmadaGatewayPortConfig(port_config.raw_data, poe_enabled=False))
)
await call_service(hass, "turn_off", entity_id)
_assert_gateway_poe_set(mock_omada_site_client, test_gateway, False)
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL)
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
assert entity and entity.state == "off"
mock_omada_site_client.set_gateway_port_settings.reset_mock()
mock_omada_site_client.set_gateway_port_settings.return_value = port_config
await call_service(hass, "turn_on", entity_id)
_assert_gateway_poe_set(mock_omada_site_client, test_gateway, True)
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL)
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
assert entity and entity.state == "on"
@pytest.mark.parametrize(
("entity_id", "service", "api_method"),
[
("switch.test_poe_switch_port_1_poe", "turn_off", "update_switch_port"),
(
"switch.test_router_port_4_internet_connected",
"turn_on",
"set_gateway_wan_port_connect_state",
),
("switch.test_router_port_5_poe", "turn_off", "set_gateway_port_settings"),
],
)
async def test_switch_action_failure_is_translated(
hass: HomeAssistant,
mock_omada_site_client: MagicMock,
init_integration: MockConfigEntry,
entity_id: str,
service: str,
api_method: str,
) -> None:
"""Test switch action failures raise a translated HomeAssistantError."""
getattr(mock_omada_site_client, api_method).side_effect = OmadaClientException(
"mock failure"
)
with pytest.raises(HomeAssistantError) as err:
await call_service(hass, service, entity_id)
assert err.value.translation_domain == DOMAIN
assert err.value.translation_key == "switch_action_failed"
async def test_gateway_wan_port_has_no_poe_switch(
hass: HomeAssistant,
init_integration: MockConfigEntry,
) -> None:
"""Test PoE switch SFP ports have no PoE controls."""
entity = hass.states.get("switch.test_router_port_1_poe")
assert entity is None
entity = hass.states.get("switch.test_router_port_9_poe")
assert entity is not None
def _assert_gateway_poe_set(mock_omada_site_client, test_gateway, poe_enabled: bool):
(
called_port,
called_settings,
called_gateway,
) = mock_omada_site_client.set_gateway_port_settings.call_args.args
mock_omada_site_client.set_gateway_port_settings.assert_called_once()
assert called_port == 5
assert called_settings.enable_poe is poe_enabled
assert called_gateway == test_gateway
async def test_gateway_api_fail_disables_switch_entities(
hass: HomeAssistant,
mock_omada_site_client: MagicMock,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test gateway connected switches."""
entity_id = "switch.test_router_port_4_internet_connected"
entity = hass.states.get(entity_id)
assert entity and entity == snapshot
assert entity.state == "on"
mock_omada_site_client.get_gateway.reset_mock()
mock_omada_site_client.get_gateway.side_effect = InvalidDevice("Expected error")
async_fire_time_changed(hass, utcnow() + POLL_INTERVAL)
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
assert entity and entity.state == "unavailable"
async def test_gateway_port_change_disables_switch_entities(
hass: HomeAssistant,
mock_omada_site_client: MagicMock,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test gateway connected switch reconfigure."""
gateway_mac = "AA-BB-CC-DD-EE-FF"
test_gateway = await mock_omada_site_client.get_gateway(gateway_mac)
entity_id = "switch.test_router_port_4_internet_connected"
entity = hass.states.get(entity_id)
assert entity and entity == snapshot
assert entity.state == "on"
mock_omada_site_client.get_gateway.reset_mock()
# Set Port 4 to LAN mode
_get_updated_gateway_port_status(mock_omada_site_client, test_gateway, 3, "mode", 1)
async_fire_time_changed(hass, utcnow() + POLL_INTERVAL)
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
assert entity and entity.state == "unavailable"
async def _test_poe_switch(
hass: HomeAssistant,
mock_omada_site_client: MagicMock,
entity_id: str,
network_switch_mac: str,
port_num: int,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
def assert_update_switch_port(
device: OmadaSwitch,
switch_port_details: OmadaSwitchPortDetails,
poe_enabled: bool,
settings: SwitchPortSettings,
) -> None:
assert device
assert device.mac == network_switch_mac
assert switch_port_details
assert switch_port_details.port == port_num
assert settings
assert settings.profile_override_enabled
assert settings.profile_overrides
assert settings.profile_overrides.enable_poe == poe_enabled
entity = hass.states.get(entity_id)
assert entity == snapshot
entry = entity_registry.async_get(entity_id)
assert entry == snapshot
mock_omada_site_client.update_switch_port = AsyncMock(
return_value=await _update_port_details(mock_omada_site_client, port_num, False)
)
await call_service(hass, "turn_off", entity_id)
mock_omada_site_client.update_switch_port.assert_called_once()
(
device,
switch_port_details,
) = mock_omada_site_client.update_switch_port.call_args.args
assert_update_switch_port(
device,
switch_port_details,
False,
**mock_omada_site_client.update_switch_port.call_args.kwargs,
)
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL)
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
assert entity and entity.state == "off"
mock_omada_site_client.update_switch_port.reset_mock()
mock_omada_site_client.update_switch_port.return_value = await _update_port_details(
mock_omada_site_client, port_num, True
)
await call_service(hass, "turn_on", entity_id)
mock_omada_site_client.update_switch_port.assert_called_once()
device, switch_port = mock_omada_site_client.update_switch_port.call_args.args
assert_update_switch_port(
device,
switch_port,
True,
**mock_omada_site_client.update_switch_port.call_args.kwargs,
)
async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL)
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
assert entity and entity.state == "on"
async def _update_port_details(
mock_omada_site_client: MagicMock,
port_num: int,
poe_enabled: bool,
) -> OmadaSwitchPortDetails:
switch_ports = await mock_omada_site_client.get_switch_ports()
port_details: OmadaSwitchPortDetails | None = None
for details in switch_ports:
if details.port == port_num:
port_details = details
break
assert port_details is not None
raw_data = port_details.raw_data.copy()
raw_data["poe"] = PoEMode.ENABLED if poe_enabled else PoEMode.DISABLED
return OmadaSwitchPortDetails(raw_data)
def _get_updated_gateway_port_status(
mock_omada_site_client: MagicMock,
gateway: OmadaGateway,
port: int,
key: str,
value: Any,
) -> OmadaGatewayPortStatus:
gateway_data = gateway.raw_data.copy()
gateway_data["portStats"][port][key] = value
mock_omada_site_client.get_gateway.reset_mock()
mock_omada_site_client.get_gateway.return_value = OmadaGateway(gateway_data)
return OmadaGatewayPortStatus(gateway_data["portStats"][port])
async def call_service(
hass: HomeAssistant, service: str, entity_id: str
) -> ServiceResponse:
"""Call any service on entity."""
return await hass.services.async_call(
switch.DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True
)