Improve UniFi device tracker client tests (#119982)

This commit is contained in:
Robert Svensson 2024-06-21 12:20:13 +02:00 committed by GitHub
parent 2157d0c05e
commit 6ddc872655
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -5,6 +5,7 @@ from datetime import timedelta
from types import MappingProxyType from types import MappingProxyType
from typing import Any from typing import Any
from aiounifi.models.event import EventKey
from aiounifi.models.message import MessageKey from aiounifi.models.message import MessageKey
from freezegun.api import FrozenDateTimeFactory, freeze_time from freezegun.api import FrozenDateTimeFactory, freeze_time
import pytest import pytest
@ -47,6 +48,24 @@ WIRELESS_CLIENT_1 = {
"mac": "00:00:00:00:00:01", "mac": "00:00:00:00:00:01",
} }
WIRED_BUG_CLIENT = {
"essid": "ssid",
"hostname": "wd_bug_client",
"ip": "10.0.0.3",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:03",
}
UNSEEN_CLIENT = {
"essid": "ssid",
"hostname": "unseen_client",
"ip": "10.0.0.4",
"is_wired": True,
"last_seen": None,
"mac": "00:00:00:00:00:04",
}
SWITCH_1 = { SWITCH_1 = {
"board_rev": 3, "board_rev": 3,
"device_id": "mock-id-1", "device_id": "mock-id-1",
@ -67,292 +86,131 @@ SWITCH_1 = {
@pytest.mark.parametrize( @pytest.mark.parametrize(
"client_payload", "client_payload", [[WIRELESS_CLIENT_1, WIRED_BUG_CLIENT, UNSEEN_CLIENT]]
[
[
{
"ap_mac": "00:00:00:00:02:01",
"essid": "ssid",
"hostname": "client",
"ip": "10.0.0.1",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
}
]
],
) )
@pytest.mark.parametrize("known_wireless_clients", [[WIRED_BUG_CLIENT["mac"]]])
@pytest.mark.usefixtures("mock_device_registry") @pytest.mark.usefixtures("mock_device_registry")
async def test_tracked_wireless_clients( async def test_client_state_update(
hass: HomeAssistant, hass: HomeAssistant,
mock_websocket_message, mock_websocket_message,
config_entry_setup: ConfigEntry, config_entry_factory: Callable[[], ConfigEntry],
client_payload: list[dict[str, Any]], client_payload: list[dict[str, Any]],
) -> None: ) -> None:
"""Verify tracking of wireless clients.""" """Verify tracking of wireless clients."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # A normal client with current timestamp should have STATE_HOME, this is wired bug
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME client_payload[1] |= {"last_seen": dt_util.as_timestamp(dt_util.utcnow())}
await config_entry_factory()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME
assert (
hass.states.get("device_tracker.ws_client_1").attributes["host_name"]
== "ws_client_1"
)
# Wireless client with wired bug, if bug active on restart mark device away
assert hass.states.get("device_tracker.wd_bug_client").state == STATE_NOT_HOME
# A client that has never been seen should be marked away.
assert hass.states.get("device_tracker.unseen_client").state == STATE_NOT_HOME
# Updated timestamp marks client as home # Updated timestamp marks client as home
client = client_payload[0] ws_client_1 = client_payload[0] | {
client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) "last_seen": dt_util.as_timestamp(dt_util.utcnow())
mock_websocket_message(message=MessageKey.CLIENT, data=client) }
mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_HOME assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME
# Change time to mark client as away # Change time to mark client as away
new_time = dt_util.utcnow() + timedelta( new_time = dt_util.utcnow() + timedelta(seconds=DEFAULT_DETECTION_TIME)
seconds=config_entry_setup.options.get(
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
)
)
with freeze_time(new_time): with freeze_time(new_time):
async_fire_time_changed(hass, new_time) async_fire_time_changed(hass, new_time)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME
# Same timestamp doesn't explicitly mark client as away # Same timestamp doesn't explicitly mark client as away
mock_websocket_message(message=MessageKey.CLIENT, data=client) mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_HOME assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME
@pytest.mark.parametrize( @pytest.mark.parametrize("client_payload", [[WIRELESS_CLIENT_1]])
"config_entry_options",
[{CONF_SSID_FILTER: ["ssid"], CONF_CLIENT_SOURCE: ["00:00:00:00:00:06"]}],
)
@pytest.mark.parametrize(
"client_payload",
[
[
{
"ap_mac": "00:00:00:00:02:01",
"essid": "ssid",
"hostname": "client_1",
"ip": "10.0.0.1",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
},
{
"ip": "10.0.0.2",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
"name": "Client 2",
},
{
"essid": "ssid2",
"hostname": "client_3",
"ip": "10.0.0.3",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:03",
},
{
"essid": "ssid",
"hostname": "client_4",
"ip": "10.0.0.4",
"is_wired": True,
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
"mac": "00:00:00:00:00:04",
},
{
"essid": "ssid",
"hostname": "client_5",
"ip": "10.0.0.5",
"is_wired": True,
"last_seen": None,
"mac": "00:00:00:00:00:05",
},
{
"hostname": "client_6",
"ip": "10.0.0.6",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:06",
},
]
],
)
@pytest.mark.parametrize("known_wireless_clients", [["00:00:00:00:00:04"]])
@pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("config_entry_setup")
@pytest.mark.usefixtures("mock_device_registry") @pytest.mark.usefixtures("mock_device_registry")
async def test_tracked_clients( async def test_client_state_from_event_source(
hass: HomeAssistant, mock_websocket_message, client_payload: list[dict[str, Any]]
) -> None:
"""Test the update_items function with some clients."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 5
assert hass.states.get("device_tracker.client_1").state == STATE_NOT_HOME
assert hass.states.get("device_tracker.client_2").state == STATE_NOT_HOME
assert (
hass.states.get("device_tracker.client_5").attributes["host_name"] == "client_5"
)
assert hass.states.get("device_tracker.client_6").state == STATE_NOT_HOME
# Client on SSID not in SSID filter
assert not hass.states.get("device_tracker.client_3")
# Wireless client with wired bug, if bug active on restart mark device away
assert hass.states.get("device_tracker.client_4").state == STATE_NOT_HOME
# A client that has never been seen should be marked away.
assert hass.states.get("device_tracker.client_5").state == STATE_NOT_HOME
# State change signalling works
client_1 = client_payload[0]
client_1["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
mock_websocket_message(message=MessageKey.CLIENT, data=client_1)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client_1").state == STATE_HOME
@pytest.mark.parametrize(
"client_payload",
[
[
{
"ap_mac": "00:00:00:00:02:01",
"essid": "ssid",
"hostname": "client",
"ip": "10.0.0.1",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
}
]
],
)
@pytest.mark.usefixtures("mock_device_registry")
async def test_tracked_wireless_clients_event_source(
hass: HomeAssistant, hass: HomeAssistant,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,
mock_websocket_message, mock_websocket_message,
config_entry_setup: ConfigEntry,
client_payload: list[dict[str, Any]], client_payload: list[dict[str, Any]],
) -> None: ) -> None:
"""Verify tracking of wireless clients based on event source.""" """Verify update state of client based on event source."""
async def mock_event(client: dict[str, Any], event_key: EventKey) -> dict[str, Any]:
"""Create and send event based on client payload."""
event = {
"user": client["mac"],
"ssid": client["essid"],
"hostname": client["hostname"],
"ap": client["ap_mac"],
"duration": 467,
"bytes": 459039,
"key": event_key,
"subsystem": "wlan",
"site_id": "name",
"time": 1587752927000,
"datetime": "2020-04-24T18:28:47Z",
"_id": "5ea32ff730c49e00f90dca1a",
}
mock_websocket_message(message=MessageKey.EVENT, data=event)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME
# State change signalling works with events # State change signalling works with events
# Connected event # Connected event
client = client_payload[0] await mock_event(client_payload[0], EventKey.WIRELESS_CLIENT_CONNECTED)
event = { assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME
"user": client["mac"],
"ssid": client["essid"],
"ap": client["ap_mac"],
"radio": "na",
"channel": "44",
"hostname": client["hostname"],
"key": "EVT_WU_Connected",
"subsystem": "wlan",
"site_id": "name",
"time": 1587753456179,
"datetime": "2020-04-24T18:37:36Z",
"msg": (
f'User{[client["mac"]]} has connected to AP[{client["ap_mac"]}] '
f'with SSID "{client["essid"]}" on "channel 44(na)"'
),
"_id": "5ea331fa30c49e00f90ddc1a",
}
mock_websocket_message(message=MessageKey.EVENT, data=event)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_HOME
# Disconnected event # Disconnected event
event = { await mock_event(client_payload[0], EventKey.WIRELESS_CLIENT_DISCONNECTED)
"user": client["mac"], assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME
"ssid": client["essid"],
"hostname": client["hostname"],
"ap": client["ap_mac"],
"duration": 467,
"bytes": 459039,
"key": "EVT_WU_Disconnected",
"subsystem": "wlan",
"site_id": "name",
"time": 1587752927000,
"datetime": "2020-04-24T18:28:47Z",
"msg": (
f'User{[client["mac"]]} disconnected from "{client["essid"]}" '
f'(7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])'
),
"_id": "5ea32ff730c49e00f90dca1a",
}
mock_websocket_message(message=MessageKey.EVENT, data=event)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_HOME
# Change time to mark client as away # Change time to mark client as away
freezer.tick( freezer.tick(timedelta(seconds=(DEFAULT_DETECTION_TIME + 1)))
timedelta(
seconds=(
config_entry_setup.options.get(
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
)
+ 1
)
)
)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME
# To limit false positives in client tracker # To limit false positives in client tracker
# data sources are prioritized when available # data sources are prioritized when available
# once real data is received events will be ignored. # once real data is received events will be ignored.
# New data # New data
client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) ws_client_1 = client_payload[0] | {
mock_websocket_message(message=MessageKey.CLIENT, data=client) "last_seen": dt_util.as_timestamp(dt_util.utcnow())
}
mock_websocket_message(message=MessageKey.CLIENT, data=ws_client_1)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_HOME assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME
# Disconnection event will be ignored # Disconnection event will be ignored
event = { await mock_event(client_payload[0], EventKey.WIRELESS_CLIENT_DISCONNECTED)
"user": client["mac"], assert hass.states.get("device_tracker.ws_client_1").state == STATE_HOME
"ssid": client["essid"],
"hostname": client["hostname"],
"ap": client["ap_mac"],
"duration": 467,
"bytes": 459039,
"key": "EVT_WU_Disconnected",
"subsystem": "wlan",
"site_id": "name",
"time": 1587752927000,
"datetime": "2020-04-24T18:28:47Z",
"msg": (
f'User{[client["mac"]]} disconnected from "{client["essid"]}" '
f'(7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])'
),
"_id": "5ea32ff730c49e00f90dca1a",
}
mock_websocket_message(message=MessageKey.EVENT, data=event)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_HOME
# Change time to mark client as away # Change time to mark client as away
freezer.tick( freezer.tick(timedelta(seconds=(DEFAULT_DETECTION_TIME + 1)))
timedelta(
seconds=(
config_entry_setup.options.get(
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
)
+ 1
)
)
)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME assert hass.states.get("device_tracker.ws_client_1").state == STATE_NOT_HOME
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -435,26 +293,7 @@ async def test_tracked_devices(
assert hass.states.get("device_tracker.device_2").state == STATE_HOME assert hass.states.get("device_tracker.device_2").state == STATE_HOME
@pytest.mark.parametrize( @pytest.mark.parametrize("client_payload", [[WIRELESS_CLIENT_1, WIRED_CLIENT_1]])
"client_payload",
[
[
{
"essid": "ssid",
"hostname": "client_1",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
},
{
"hostname": "client_2",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
},
]
],
)
@pytest.mark.usefixtures("config_entry_setup") @pytest.mark.usefixtures("config_entry_setup")
@pytest.mark.usefixtures("mock_device_registry") @pytest.mark.usefixtures("mock_device_registry")
async def test_remove_clients( async def test_remove_clients(
@ -462,17 +301,16 @@ async def test_remove_clients(
) -> None: ) -> None:
"""Test the remove_items function with some clients.""" """Test the remove_items function with some clients."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
assert hass.states.get("device_tracker.client_1") assert hass.states.get("device_tracker.ws_client_1")
assert hass.states.get("device_tracker.client_2") assert hass.states.get("device_tracker.wd_client_1")
# Remove client # Remove client
mock_websocket_message(message=MessageKey.CLIENT_REMOVED, data=client_payload[0]) mock_websocket_message(message=MessageKey.CLIENT_REMOVED, data=client_payload[0])
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
assert not hass.states.get("device_tracker.client_1") assert not hass.states.get("device_tracker.ws_client_1")
assert hass.states.get("device_tracker.client_2") assert hass.states.get("device_tracker.wd_client_1")
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -793,21 +631,9 @@ async def test_option_ignore_wired_bug(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config_entry_options", [{CONF_BLOCK_CLIENT: ["00:00:00:00:00:02"]}] "config_entry_options", [{CONF_BLOCK_CLIENT: ["00:00:00:00:00:03"]}]
)
@pytest.mark.parametrize(
"client_payload",
[
[
{
"hostname": "client",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
}
]
],
) )
@pytest.mark.parametrize("client_payload", [[WIRED_CLIENT_1]])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"clients_all_payload", "clients_all_payload",
[ [
@ -816,13 +642,13 @@ async def test_option_ignore_wired_bug(
"hostname": "restored", "hostname": "restored",
"is_wired": True, "is_wired": True,
"last_seen": 1562600145, "last_seen": 1562600145,
"mac": "00:00:00:00:00:02", "mac": "00:00:00:00:00:03",
}, },
{ # Not previously seen by integration, will not be restored { # Not previously seen by integration, will not be restored
"hostname": "not_restored", "hostname": "not_restored",
"is_wired": True, "is_wired": True,
"last_seen": 1562600145, "last_seen": 1562600145,
"mac": "00:00:00:00:00:03", "mac": "00:00:00:00:00:04",
}, },
] ]
], ],
@ -855,7 +681,7 @@ async def test_restoring_client(
await config_entry_factory() await config_entry_factory()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
assert hass.states.get("device_tracker.client") assert hass.states.get("device_tracker.wd_client_1")
assert hass.states.get("device_tracker.restored") assert hass.states.get("device_tracker.restored")
assert not hass.states.get("device_tracker.not_restored") assert not hass.states.get("device_tracker.not_restored")