Add device trackers to tplink_omada (#115601)

* Add device trackers to tplink_omada

* tplink_omada - Remove trackers and options flow

* Addressed code review feedback

* Run linter

* Use entity registry fixture
This commit is contained in:
MarkGodwin 2024-06-17 06:36:35 +01:00 committed by GitHub
parent bd37ce6e9a
commit f09063d706
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 638 additions and 70 deletions

View File

@ -19,7 +19,12 @@ from .config_flow import CONF_SITE, create_omada_client
from .const import DOMAIN from .const import DOMAIN
from .controller import OmadaSiteController from .controller import OmadaSiteController
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SWITCH, Platform.UPDATE] PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.DEVICE_TRACKER,
Platform.SWITCH,
Platform.UPDATE,
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -50,10 +55,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
gateway_coordinator = await controller.get_gateway_coordinator() gateway_coordinator = await controller.get_gateway_coordinator()
if gateway_coordinator: if gateway_coordinator:
await gateway_coordinator.async_config_entry_first_refresh() await gateway_coordinator.async_config_entry_first_refresh()
await controller.get_clients_coordinator().async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = controller hass.data[DOMAIN][entry.entry_id] = controller
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True

View File

@ -1,58 +1,15 @@
"""Controller for sharing Omada API coordinators between platforms.""" """Controller for sharing Omada API coordinators between platforms."""
from tplink_omada_client import OmadaSiteClient from tplink_omada_client import OmadaSiteClient
from tplink_omada_client.devices import ( from tplink_omada_client.devices import OmadaSwitch
OmadaGateway,
OmadaSwitch,
OmadaSwitchPortDetails,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .coordinator import OmadaCoordinator from .coordinator import (
OmadaClientsCoordinator,
POLL_SWITCH_PORT = 300 OmadaGatewayCoordinator,
POLL_GATEWAY = 300 OmadaSwitchPortCoordinator,
)
class OmadaSwitchPortCoordinator(OmadaCoordinator[OmadaSwitchPortDetails]): # pylint: disable=hass-enforce-coordinator-module
"""Coordinator for getting details about ports on a switch."""
def __init__(
self,
hass: HomeAssistant,
omada_client: OmadaSiteClient,
network_switch: OmadaSwitch,
) -> None:
"""Initialize my coordinator."""
super().__init__(
hass, omada_client, f"{network_switch.name} Ports", POLL_SWITCH_PORT
)
self._network_switch = network_switch
async def poll_update(self) -> dict[str, OmadaSwitchPortDetails]:
"""Poll a switch's current state."""
ports = await self.omada_client.get_switch_ports(self._network_switch)
return {p.port_id: p for p in ports}
class OmadaGatewayCoordinator(OmadaCoordinator[OmadaGateway]): # pylint: disable=hass-enforce-coordinator-module
"""Coordinator for getting details about the site's gateway."""
def __init__(
self,
hass: HomeAssistant,
omada_client: OmadaSiteClient,
mac: str,
) -> None:
"""Initialize my coordinator."""
super().__init__(hass, omada_client, "Gateway", POLL_GATEWAY)
self.mac = mac
async def poll_update(self) -> dict[str, OmadaGateway]:
"""Poll a the gateway's current state."""
gateway = await self.omada_client.get_gateway(self.mac)
return {self.mac: gateway}
class OmadaSiteController: class OmadaSiteController:
@ -60,6 +17,7 @@ class OmadaSiteController:
_gateway_coordinator: OmadaGatewayCoordinator | None = None _gateway_coordinator: OmadaGatewayCoordinator | None = None
_initialized_gateway_coordinator = False _initialized_gateway_coordinator = False
_clients_coordinator: OmadaClientsCoordinator | None = None
def __init__(self, hass: HomeAssistant, omada_client: OmadaSiteClient) -> None: def __init__(self, hass: HomeAssistant, omada_client: OmadaSiteClient) -> None:
"""Create the controller.""" """Create the controller."""
@ -98,3 +56,12 @@ class OmadaSiteController:
) )
return self._gateway_coordinator return self._gateway_coordinator
def get_clients_coordinator(self) -> OmadaClientsCoordinator:
"""Get coordinator for site's clients."""
if not self._clients_coordinator:
self._clients_coordinator = OmadaClientsCoordinator(
self._hass, self._omada_client
)
return self._clients_coordinator

View File

@ -4,7 +4,9 @@ import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
from tplink_omada_client import OmadaSiteClient from tplink_omada_client import OmadaSiteClient, OmadaSwitchPortDetails
from tplink_omada_client.clients import OmadaWirelessClient
from tplink_omada_client.devices import OmadaGateway, OmadaSwitch
from tplink_omada_client.exceptions import OmadaClientException from tplink_omada_client.exceptions import OmadaClientException
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -12,6 +14,10 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
POLL_SWITCH_PORT = 300
POLL_GATEWAY = 300
POLL_CLIENTS = 300
class OmadaCoordinator[_T](DataUpdateCoordinator[dict[str, _T]]): class OmadaCoordinator[_T](DataUpdateCoordinator[dict[str, _T]]):
"""Coordinator for synchronizing bulk Omada data.""" """Coordinator for synchronizing bulk Omada data."""
@ -43,3 +49,59 @@ class OmadaCoordinator[_T](DataUpdateCoordinator[dict[str, _T]]):
async def poll_update(self) -> dict[str, _T]: async def poll_update(self) -> dict[str, _T]:
"""Poll the current data from the controller.""" """Poll the current data from the controller."""
raise NotImplementedError("Update method not implemented") raise NotImplementedError("Update method not implemented")
class OmadaSwitchPortCoordinator(OmadaCoordinator[OmadaSwitchPortDetails]):
"""Coordinator for getting details about ports on a switch."""
def __init__(
self,
hass: HomeAssistant,
omada_client: OmadaSiteClient,
network_switch: OmadaSwitch,
) -> None:
"""Initialize my coordinator."""
super().__init__(
hass, omada_client, f"{network_switch.name} Ports", POLL_SWITCH_PORT
)
self._network_switch = network_switch
async def poll_update(self) -> dict[str, OmadaSwitchPortDetails]:
"""Poll a switch's current state."""
ports = await self.omada_client.get_switch_ports(self._network_switch)
return {p.port_id: p for p in ports}
class OmadaGatewayCoordinator(OmadaCoordinator[OmadaGateway]):
"""Coordinator for getting details about the site's gateway."""
def __init__(
self,
hass: HomeAssistant,
omada_client: OmadaSiteClient,
mac: str,
) -> None:
"""Initialize my coordinator."""
super().__init__(hass, omada_client, "Gateway", POLL_GATEWAY)
self.mac = mac
async def poll_update(self) -> dict[str, OmadaGateway]:
"""Poll a the gateway's current state."""
gateway = await self.omada_client.get_gateway(self.mac)
return {self.mac: gateway}
class OmadaClientsCoordinator(OmadaCoordinator[OmadaWirelessClient]):
"""Coordinator for getting details about the site's connected clients."""
def __init__(self, hass: HomeAssistant, omada_client: OmadaSiteClient) -> None:
"""Initialize my coordinator."""
super().__init__(hass, omada_client, "ClientsList", POLL_CLIENTS)
async def poll_update(self) -> dict[str, OmadaWirelessClient]:
"""Poll the site's current active wi-fi clients."""
return {
c.mac: c
async for c in self.omada_client.get_connected_clients()
if isinstance(c, OmadaWirelessClient)
}

View File

@ -0,0 +1,107 @@
"""Connected Wi-Fi device scanners for TP-Link Omada access points."""
import logging
from tplink_omada_client.clients import OmadaWirelessClient
from homeassistant.components.device_tracker import ScannerEntity, SourceType
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .config_flow import CONF_SITE
from .const import DOMAIN
from .controller import OmadaClientsCoordinator, OmadaSiteController
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up device trackers and scanners."""
controller: OmadaSiteController = hass.data[DOMAIN][config_entry.entry_id]
clients_coordinator = controller.get_clients_coordinator()
site_id = config_entry.data[CONF_SITE]
# Add all known WiFi devices as potentially tracked devices. They will only be
# tracked if the user enables the entity.
async_add_entities(
[
OmadaClientScannerEntity(
site_id, client.mac, client.name, clients_coordinator
)
async for client in controller.omada_client.get_known_clients()
if isinstance(client, OmadaWirelessClient)
]
)
class OmadaClientScannerEntity(
CoordinatorEntity[OmadaClientsCoordinator], ScannerEntity
):
"""Entity for a client connected to the Omada network."""
_client_details: OmadaWirelessClient | None = None
def __init__(
self,
site_id: str,
client_id: str,
display_name: str,
coordinator: OmadaClientsCoordinator,
) -> None:
"""Initialize the scanner."""
super().__init__(coordinator)
self._site_id = site_id
self._client_id = client_id
self._attr_name = display_name
@property
def source_type(self) -> SourceType:
"""Return the source type of the device."""
return SourceType.ROUTER
def _do_update(self) -> None:
self._client_details = self.coordinator.data.get(self._client_id)
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._do_update()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._do_update()
self.async_write_ha_state()
@property
def ip_address(self) -> str | None:
"""Return the primary ip address of the device."""
return self._client_details.ip if self._client_details else None
@property
def mac_address(self) -> str | None:
"""Return the mac address of the device."""
return self._client_id
@property
def hostname(self) -> str | None:
"""Return hostname of the device."""
return self._client_details.host_name if self._client_details else None
@property
def is_connected(self) -> bool:
"""Return true if the device is connected to the network."""
return self._client_details.is_active if self._client_details else False
@property
def unique_id(self) -> str | None:
"""Return the unique id of the device."""
return f"scanner_{self._site_id}_{self._client_id}"

View File

@ -1,9 +1,16 @@
"""Test fixtures for TP-Link Omada integration.""" """Test fixtures for TP-Link Omada integration."""
from collections.abc import AsyncIterable
import json import json
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
from tplink_omada_client.clients import (
OmadaConnectedClient,
OmadaNetworkClient,
OmadaWiredClient,
OmadaWirelessClient,
)
from tplink_omada_client.devices import ( from tplink_omada_client.devices import (
OmadaGateway, OmadaGateway,
OmadaListDevice, OmadaListDevice,
@ -49,29 +56,82 @@ def mock_setup_entry() -> Generator[AsyncMock]:
@pytest.fixture @pytest.fixture
def mock_omada_site_client() -> Generator[AsyncMock]: def mock_omada_site_client() -> Generator[AsyncMock]:
"""Mock Omada site client.""" """Mock Omada site client."""
site_client = AsyncMock() site_client = MagicMock()
gateway_data = json.loads(load_fixture("gateway-TL-ER7212PC.json", DOMAIN)) gateway_data = json.loads(load_fixture("gateway-TL-ER7212PC.json", DOMAIN))
gateway = OmadaGateway(gateway_data) gateway = OmadaGateway(gateway_data)
site_client.get_gateway.return_value = gateway site_client.get_gateway = AsyncMock(return_value=gateway)
switch1_data = json.loads(load_fixture("switch-TL-SG3210XHP-M2.json", DOMAIN)) switch1_data = json.loads(load_fixture("switch-TL-SG3210XHP-M2.json", DOMAIN))
switch1 = OmadaSwitch(switch1_data) switch1 = OmadaSwitch(switch1_data)
site_client.get_switches.return_value = [switch1] site_client.get_switches = AsyncMock(return_value=[switch1])
devices_data = json.loads(load_fixture("devices.json", DOMAIN)) devices_data = json.loads(load_fixture("devices.json", DOMAIN))
devices = [OmadaListDevice(d) for d in devices_data] devices = [OmadaListDevice(d) for d in devices_data]
site_client.get_devices.return_value = devices site_client.get_devices = AsyncMock(return_value=devices)
switch1_ports_data = json.loads( switch1_ports_data = json.loads(
load_fixture("switch-ports-TL-SG3210XHP-M2.json", DOMAIN) load_fixture("switch-ports-TL-SG3210XHP-M2.json", DOMAIN)
) )
switch1_ports = [OmadaSwitchPortDetails(p) for p in switch1_ports_data] switch1_ports = [OmadaSwitchPortDetails(p) for p in switch1_ports_data]
site_client.get_switch_ports.return_value = switch1_ports site_client.get_switch_ports = AsyncMock(return_value=switch1_ports)
async def async_empty() -> AsyncIterable:
for c in []:
yield c
site_client.get_known_clients.return_value = async_empty()
site_client.get_connected_clients.return_value = async_empty()
return site_client
@pytest.fixture
def mock_omada_clients_only_site_client() -> Generator[AsyncMock]:
"""Mock Omada site client containing only client connection data."""
site_client = MagicMock()
site_client.get_switches = AsyncMock(return_value=[])
site_client.get_devices = AsyncMock(return_value=[])
site_client.get_switch_ports = AsyncMock(return_value=[])
site_client.get_client = AsyncMock(side_effect=_get_mock_client)
site_client.get_known_clients.side_effect = _get_mock_known_clients
site_client.get_connected_clients.side_effect = _get_mock_connected_clients
return site_client return site_client
async def _get_mock_known_clients() -> AsyncIterable[OmadaNetworkClient]:
"""Mock known clients of the Omada network."""
known_clients_data = json.loads(load_fixture("known-clients.json", DOMAIN))
for c in known_clients_data:
if c["wireless"]:
yield OmadaWirelessClient(c)
else:
yield OmadaWiredClient(c)
async def _get_mock_connected_clients() -> AsyncIterable[OmadaConnectedClient]:
"""Mock connected clients of the Omada network."""
connected_clients_data = json.loads(load_fixture("connected-clients.json", DOMAIN))
for c in connected_clients_data:
if c["wireless"]:
yield OmadaWirelessClient(c)
else:
yield OmadaWiredClient(c)
def _get_mock_client(mac: str) -> OmadaNetworkClient:
"""Mock an Omada client."""
connected_clients_data = json.loads(load_fixture("connected-clients.json", DOMAIN))
for c in connected_clients_data:
if c["mac"] == mac:
if c["wireless"]:
return OmadaWirelessClient(c)
return OmadaWiredClient(c)
@pytest.fixture @pytest.fixture
def mock_omada_client(mock_omada_site_client: AsyncMock) -> Generator[MagicMock]: def mock_omada_client(mock_omada_site_client: AsyncMock) -> Generator[MagicMock]:
"""Mock Omada client.""" """Mock Omada client."""
@ -85,13 +145,39 @@ def mock_omada_client(mock_omada_site_client: AsyncMock) -> Generator[MagicMock]
yield client yield client
@pytest.fixture
def mock_omada_clients_only_client(
mock_omada_clients_only_site_client: AsyncMock,
) -> Generator[MagicMock]:
"""Mock Omada client."""
with patch(
"homeassistant.components.tplink_omada.create_omada_client",
autospec=True,
) as client_mock:
client = client_mock.return_value
client.get_site_client.return_value = mock_omada_clients_only_site_client
yield client
@pytest.fixture @pytest.fixture
async def init_integration( async def init_integration(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_omada_client: MagicMock, mock_omada_client: MagicMock,
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Set up the TP-Link Omada integration for testing.""" """Set up the TP-Link Omada integration for testing."""
mock_config_entry = MockConfigEntry(
title="Test Omada Controller",
domain=DOMAIN,
data={
CONF_HOST: "127.0.0.1",
CONF_PASSWORD: "mocked-password",
CONF_USERNAME: "mocked-user",
CONF_VERIFY_SSL: False,
CONF_SITE: "Default",
},
unique_id="12345",
)
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.config_entries.async_setup(mock_config_entry.entry_id)

View File

@ -0,0 +1,120 @@
[
{
"mac": "16-32-50-ED-FB-15",
"name": "16-32-50-ED-FB-15",
"deviceType": "unknown",
"ip": "192.168.1.177",
"connectType": 1,
"connectDevType": "ap",
"connectedToWirelessRouter": false,
"wireless": true,
"ssid": "OFFICE_SSID",
"signalLevel": 62,
"healthScore": -1,
"signalRank": 4,
"wifiMode": 4,
"apName": "Office",
"apMac": "E8-48-B8-7E-C7-1A",
"radioId": 0,
"channel": 1,
"rxRate": 65000,
"txRate": 72000,
"powerSave": false,
"rssi": -65,
"snr": 30,
"stackableSwitch": false,
"vid": 0,
"activity": 96,
"trafficDown": 25412800785,
"trafficUp": 1636427981,
"uptime": 621441,
"lastSeen": 1713109713169,
"authStatus": 0,
"guest": false,
"active": true,
"manager": false,
"downPacket": 30179275,
"upPacket": 14288106,
"support5g2": false,
"multiLink": []
},
{
"mac": "2E-DC-E1-C4-37-D3",
"name": "Apple",
"deviceType": "unknown",
"ip": "192.168.1.192",
"connectType": 1,
"connectDevType": "ap",
"connectedToWirelessRouter": false,
"wireless": true,
"ssid": "ROAMING_SSID",
"signalLevel": 67,
"healthScore": -1,
"signalRank": 4,
"wifiMode": 5,
"apName": "Spare Room",
"apMac": "C0-C9-E3-4B-AF-0E",
"radioId": 1,
"channel": 44,
"rxRate": 7000,
"txRate": 390000,
"powerSave": false,
"rssi": -63,
"snr": 32,
"stackableSwitch": false,
"vid": 0,
"activity": 0,
"trafficDown": 3327229,
"trafficUp": 746841,
"uptime": 2091,
"lastSeen": 1713109728764,
"authStatus": 0,
"guest": false,
"active": true,
"manager": false,
"downPacket": 5128,
"upPacket": 3611,
"support5g2": false,
"multiLink": []
},
{
"mac": "2C-71-FF-ED-34-83",
"name": "Banana",
"hostName": "testhost",
"deviceType": "unknown",
"ip": "192.168.1.102",
"connectType": 1,
"connectDevType": "ap",
"connectedToWirelessRouter": false,
"wireless": true,
"ssid": "ROAMING_SSID",
"signalLevel": 57,
"healthScore": -1,
"signalRank": 3,
"wifiMode": 5,
"apName": "Living Room",
"apMac": "C0-C9-E3-4B-A7-FE",
"radioId": 1,
"channel": 36,
"rxRate": 6000,
"txRate": 390000,
"powerSave": false,
"rssi": -67,
"snr": 28,
"stackableSwitch": false,
"vid": 0,
"activity": 39,
"trafficDown": 407300090,
"trafficUp": 94910187,
"uptime": 621461,
"lastSeen": 1713109729576,
"authStatus": 0,
"guest": false,
"active": true,
"manager": false,
"downPacket": 477858,
"upPacket": 501956,
"support5g2": false,
"multiLink": []
}
]

View File

@ -0,0 +1,67 @@
[
{
"name": "16-32-50-ED-FB-15",
"mac": "16-32-50-ED-FB-15",
"wireless": true,
"guest": false,
"download": 259310931013,
"upload": 43957031162,
"duration": 6832173,
"lastSeen": 1712488285622,
"block": false,
"manager": false,
"lockToAp": false
},
{
"name": "Banana",
"mac": "2C-71-FF-ED-34-83",
"wireless": true,
"guest": false,
"download": 22093851790,
"upload": 6961197401,
"duration": 16192898,
"lastSeen": 1712488285767,
"block": false,
"manager": false,
"lockToAp": false
},
{
"name": "Pear",
"mac": "2C-D2-6B-BA-9C-94",
"wireless": true,
"guest": false,
"download": 0,
"upload": 0,
"duration": 23,
"lastSeen": 1713083620997,
"block": false,
"manager": false,
"lockToAp": false
},
{
"name": "Apple",
"mac": "2E-DC-E1-C4-37-D3",
"wireless": true,
"guest": false,
"download": 1366833567,
"upload": 30126947,
"duration": 60255,
"lastSeen": 1713107649827,
"block": false,
"manager": false,
"lockToAp": false
},
{
"name": "32-39-24-B1-67-23",
"mac": "32-39-24-B1-67-23",
"wireless": false,
"guest": false,
"download": 1621140542,
"upload": 433306522,
"duration": 60571,
"lastSeen": 1713107438528,
"block": false,
"manager": false,
"lockToAp": false
}
]

View File

@ -0,0 +1,33 @@
# serializer version: 1
# name: test_device_scanner_created
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Banana',
'host_name': 'testhost',
'ip': '192.168.1.102',
'mac': '2C-71-FF-ED-34-83',
'source_type': <SourceType.ROUTER: 'router'>,
}),
'context': <ANY>,
'entity_id': 'device_tracker.banana',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'home',
})
# ---
# name: test_device_scanner_update_to_away_nulls_properties
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Banana',
'mac': '2C-71-FF-ED-34-83',
'source_type': <SourceType.ROUTER: 'router'>,
}),
'context': <ANY>,
'entity_id': 'device_tracker.banana',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'not_home',
})
# ---

View File

@ -0,0 +1,117 @@
"""Tests for TP-Link Omada device tracker entities."""
from collections.abc import AsyncIterable
from datetime import timedelta
from unittest.mock import MagicMock
import pytest
from syrupy.assertion import SnapshotAssertion
from tplink_omada_client.clients import OmadaConnectedClient
from homeassistant.components.tplink_omada.const import DOMAIN
from homeassistant.components.tplink_omada.coordinator import POLL_CLIENTS
from homeassistant.core import HomeAssistant
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_CLIENTS + 10)
MOCK_ENTRY_DATA = {
"host": "https://fake.omada.host",
"verify_ssl": True,
"site": "SiteId",
"username": "test-username",
"password": "test-password",
}
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_omada_clients_only_client: MagicMock,
) -> MockConfigEntry:
"""Set up the TP-Link Omada integration for testing."""
mock_config_entry = MockConfigEntry(
title="Test Omada Controller",
domain=DOMAIN,
data=dict(MOCK_ENTRY_DATA),
unique_id="12345",
)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry
async def test_device_scanner_created(
hass: HomeAssistant,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test gateway connected switches."""
entity_id = "device_tracker.banana"
updated_entity = entity_registry.async_update_entity(entity_id, disabled_by=None)
assert not updated_entity.disabled
async_fire_time_changed(hass, utcnow() + POLL_INTERVAL)
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
assert entity is not None
assert entity == snapshot
async def test_device_scanner_update_to_away_nulls_properties(
hass: HomeAssistant,
mock_omada_clients_only_site_client: MagicMock,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test gateway connected switches."""
entity_id = "device_tracker.banana"
updated_entity = entity_registry.async_update_entity(entity_id, disabled_by=None)
assert not updated_entity.disabled
async_fire_time_changed(hass, utcnow() + POLL_INTERVAL)
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
await _setup_client_disconnect(
mock_omada_clients_only_site_client, "2C-71-FF-ED-34-83"
)
async_fire_time_changed(hass, utcnow() + (POLL_INTERVAL * 2))
await hass.async_block_till_done()
entity = hass.states.get(entity_id)
assert entity is not None
assert entity == snapshot
mock_omada_clients_only_site_client.get_connected_clients.assert_called_once()
async def _setup_client_disconnect(
mock_omada_site_client: MagicMock,
client_mac: str,
):
original_clients = [
c
async for c in mock_omada_site_client.get_connected_clients()
if c.mac != client_mac
]
async def get_filtered_clients() -> AsyncIterable[OmadaConnectedClient]:
for c in original_clients:
yield c
mock_omada_site_client.get_connected_clients.reset_mock()
mock_omada_site_client.get_connected_clients.side_effect = get_filtered_clients

View File

@ -2,7 +2,7 @@
from datetime import timedelta from datetime import timedelta
from typing import Any from typing import Any
from unittest.mock import MagicMock from unittest.mock import AsyncMock, MagicMock
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from tplink_omada_client import SwitchPortOverrides from tplink_omada_client import SwitchPortOverrides
@ -17,7 +17,7 @@ from tplink_omada_client.devices import (
from tplink_omada_client.exceptions import InvalidDevice from tplink_omada_client.exceptions import InvalidDevice
from homeassistant.components import switch from homeassistant.components import switch
from homeassistant.components.tplink_omada.controller import POLL_GATEWAY from homeassistant.components.tplink_omada.coordinator import POLL_GATEWAY
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -34,6 +34,7 @@ async def test_poe_switches(
mock_omada_site_client: MagicMock, mock_omada_site_client: MagicMock,
init_integration: MockConfigEntry, init_integration: MockConfigEntry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test PoE switch.""" """Test PoE switch."""
poe_switch_mac = "54-AF-97-00-00-01" poe_switch_mac = "54-AF-97-00-00-01"
@ -44,6 +45,7 @@ async def test_poe_switches(
poe_switch_mac, poe_switch_mac,
1, 1,
snapshot, snapshot,
entity_registry,
) )
await _test_poe_switch( await _test_poe_switch(
@ -53,6 +55,7 @@ async def test_poe_switches(
poe_switch_mac, poe_switch_mac,
2, 2,
snapshot, snapshot,
entity_registry,
) )
@ -84,10 +87,11 @@ async def test_gateway_connect_ipv4_switch(
port_status = test_gateway.port_status[3] port_status = test_gateway.port_status[3]
assert port_status.port_number == 4 assert port_status.port_number == 4
mock_omada_site_client.set_gateway_wan_port_connect_state.reset_mock() mock_omada_site_client.set_gateway_wan_port_connect_state = AsyncMock(
mock_omada_site_client.set_gateway_wan_port_connect_state.return_value = ( return_value=(
_get_updated_gateway_port_status( _get_updated_gateway_port_status(
mock_omada_site_client, test_gateway, 3, "internetState", 0 mock_omada_site_client, test_gateway, 3, "internetState", 0
)
) )
) )
await call_service(hass, "turn_off", entity_id) await call_service(hass, "turn_off", entity_id)
@ -136,8 +140,8 @@ async def test_gateway_port_poe_switch(
port_config = test_gateway.port_configs[4] port_config = test_gateway.port_configs[4]
assert port_config.port_number == 5 assert port_config.port_number == 5
mock_omada_site_client.set_gateway_port_settings.return_value = ( mock_omada_site_client.set_gateway_port_settings = AsyncMock(
OmadaGatewayPortConfig(port_config.raw_data, poe_enabled=False) return_value=(OmadaGatewayPortConfig(port_config.raw_data, poe_enabled=False))
) )
await call_service(hass, "turn_off", entity_id) await call_service(hass, "turn_off", entity_id)
_assert_gateway_poe_set(mock_omada_site_client, test_gateway, False) _assert_gateway_poe_set(mock_omada_site_client, test_gateway, False)
@ -239,9 +243,8 @@ async def _test_poe_switch(
network_switch_mac: str, network_switch_mac: str,
port_num: int, port_num: int,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None: ) -> None:
entity_registry = er.async_get(hass)
def assert_update_switch_port( def assert_update_switch_port(
device: OmadaSwitch, device: OmadaSwitch,
switch_port_details: OmadaSwitchPortDetails, switch_port_details: OmadaSwitchPortDetails,
@ -260,9 +263,8 @@ async def _test_poe_switch(
entry = entity_registry.async_get(entity_id) entry = entity_registry.async_get(entity_id)
assert entry == snapshot assert entry == snapshot
mock_omada_site_client.update_switch_port.reset_mock() mock_omada_site_client.update_switch_port = AsyncMock(
mock_omada_site_client.update_switch_port.return_value = await _update_port_details( return_value=await _update_port_details(mock_omada_site_client, port_num, False)
mock_omada_site_client, port_num, False
) )
await call_service(hass, "turn_off", entity_id) await call_service(hass, "turn_off", entity_id)