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:
parent
bd37ce6e9a
commit
f09063d706
10 changed files with 638 additions and 70 deletions
|
@ -19,7 +19,12 @@ from .config_flow import CONF_SITE, create_omada_client
|
|||
from .const import DOMAIN
|
||||
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:
|
||||
|
@ -50,10 +55,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
gateway_coordinator = await controller.get_gateway_coordinator()
|
||||
if gateway_coordinator:
|
||||
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
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -1,58 +1,15 @@
|
|||
"""Controller for sharing Omada API coordinators between platforms."""
|
||||
|
||||
from tplink_omada_client import OmadaSiteClient
|
||||
from tplink_omada_client.devices import (
|
||||
OmadaGateway,
|
||||
OmadaSwitch,
|
||||
OmadaSwitchPortDetails,
|
||||
)
|
||||
from tplink_omada_client.devices import OmadaSwitch
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import OmadaCoordinator
|
||||
|
||||
POLL_SWITCH_PORT = 300
|
||||
POLL_GATEWAY = 300
|
||||
|
||||
|
||||
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}
|
||||
from .coordinator import (
|
||||
OmadaClientsCoordinator,
|
||||
OmadaGatewayCoordinator,
|
||||
OmadaSwitchPortCoordinator,
|
||||
)
|
||||
|
||||
|
||||
class OmadaSiteController:
|
||||
|
@ -60,6 +17,7 @@ class OmadaSiteController:
|
|||
|
||||
_gateway_coordinator: OmadaGatewayCoordinator | None = None
|
||||
_initialized_gateway_coordinator = False
|
||||
_clients_coordinator: OmadaClientsCoordinator | None = None
|
||||
|
||||
def __init__(self, hass: HomeAssistant, omada_client: OmadaSiteClient) -> None:
|
||||
"""Create the controller."""
|
||||
|
@ -98,3 +56,12 @@ class OmadaSiteController:
|
|||
)
|
||||
|
||||
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
|
||||
|
|
|
@ -4,7 +4,9 @@ import asyncio
|
|||
from datetime import timedelta
|
||||
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 homeassistant.core import HomeAssistant
|
||||
|
@ -12,6 +14,10 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
POLL_SWITCH_PORT = 300
|
||||
POLL_GATEWAY = 300
|
||||
POLL_CLIENTS = 300
|
||||
|
||||
|
||||
class OmadaCoordinator[_T](DataUpdateCoordinator[dict[str, _T]]):
|
||||
"""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]:
|
||||
"""Poll the current data from the controller."""
|
||||
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)
|
||||
}
|
||||
|
|
107
homeassistant/components/tplink_omada/device_tracker.py
Normal file
107
homeassistant/components/tplink_omada/device_tracker.py
Normal 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}"
|
|
@ -1,9 +1,16 @@
|
|||
"""Test fixtures for TP-Link Omada integration."""
|
||||
|
||||
from collections.abc import AsyncIterable
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from tplink_omada_client.clients import (
|
||||
OmadaConnectedClient,
|
||||
OmadaNetworkClient,
|
||||
OmadaWiredClient,
|
||||
OmadaWirelessClient,
|
||||
)
|
||||
from tplink_omada_client.devices import (
|
||||
OmadaGateway,
|
||||
OmadaListDevice,
|
||||
|
@ -49,29 +56,82 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
|||
@pytest.fixture
|
||||
def mock_omada_site_client() -> Generator[AsyncMock]:
|
||||
"""Mock Omada site client."""
|
||||
site_client = AsyncMock()
|
||||
site_client = MagicMock()
|
||||
|
||||
gateway_data = json.loads(load_fixture("gateway-TL-ER7212PC.json", DOMAIN))
|
||||
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 = 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 = [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(
|
||||
load_fixture("switch-ports-TL-SG3210XHP-M2.json", DOMAIN)
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
def mock_omada_client(mock_omada_site_client: AsyncMock) -> Generator[MagicMock]:
|
||||
"""Mock Omada client."""
|
||||
|
@ -85,13 +145,39 @@ def mock_omada_client(mock_omada_site_client: AsyncMock) -> Generator[MagicMock]
|
|||
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
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_omada_client: MagicMock,
|
||||
) -> MockConfigEntry:
|
||||
"""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)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
|
120
tests/components/tplink_omada/fixtures/connected-clients.json
Normal file
120
tests/components/tplink_omada/fixtures/connected-clients.json
Normal 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": []
|
||||
}
|
||||
]
|
67
tests/components/tplink_omada/fixtures/known-clients.json
Normal file
67
tests/components/tplink_omada/fixtures/known-clients.json
Normal 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
|
||||
}
|
||||
]
|
|
@ -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',
|
||||
})
|
||||
# ---
|
117
tests/components/tplink_omada/test_device_tracker.py
Normal file
117
tests/components/tplink_omada/test_device_tracker.py
Normal 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
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from tplink_omada_client import SwitchPortOverrides
|
||||
|
@ -17,7 +17,7 @@ from tplink_omada_client.devices import (
|
|||
from tplink_omada_client.exceptions import InvalidDevice
|
||||
|
||||
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.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
@ -34,6 +34,7 @@ async def test_poe_switches(
|
|||
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"
|
||||
|
@ -44,6 +45,7 @@ async def test_poe_switches(
|
|||
poe_switch_mac,
|
||||
1,
|
||||
snapshot,
|
||||
entity_registry,
|
||||
)
|
||||
|
||||
await _test_poe_switch(
|
||||
|
@ -53,6 +55,7 @@ async def test_poe_switches(
|
|||
poe_switch_mac,
|
||||
2,
|
||||
snapshot,
|
||||
entity_registry,
|
||||
)
|
||||
|
||||
|
||||
|
@ -84,10 +87,11 @@ async def test_gateway_connect_ipv4_switch(
|
|||
port_status = test_gateway.port_status[3]
|
||||
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.return_value = (
|
||||
_get_updated_gateway_port_status(
|
||||
mock_omada_site_client, test_gateway, 3, "internetState", 0
|
||||
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)
|
||||
|
@ -136,8 +140,8 @@ async def test_gateway_port_poe_switch(
|
|||
port_config = test_gateway.port_configs[4]
|
||||
assert port_config.port_number == 5
|
||||
|
||||
mock_omada_site_client.set_gateway_port_settings.return_value = (
|
||||
OmadaGatewayPortConfig(port_config.raw_data, poe_enabled=False)
|
||||
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)
|
||||
|
@ -239,9 +243,8 @@ async def _test_poe_switch(
|
|||
network_switch_mac: str,
|
||||
port_num: int,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
def assert_update_switch_port(
|
||||
device: OmadaSwitch,
|
||||
switch_port_details: OmadaSwitchPortDetails,
|
||||
|
@ -260,9 +263,8 @@ async def _test_poe_switch(
|
|||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry == snapshot
|
||||
|
||||
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, False
|
||||
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)
|
||||
|
|
Loading…
Add table
Reference in a new issue