core/tests/components/unifi/test_device_tracker.py

1197 lines
39 KiB
Python

"""The tests for the UniFi Network device tracker platform."""
from collections.abc import Callable
from datetime import timedelta
from typing import Any
from aiounifi.models.message import MessageKey
from freezegun.api import FrozenDateTimeFactory, freeze_time
import pytest
from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN
from homeassistant.components.unifi.const import (
CONF_BLOCK_CLIENT,
CONF_CLIENT_SOURCE,
CONF_DETECTION_TIME,
CONF_IGNORE_WIRED_BUG,
CONF_SSID_FILTER,
CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
CONF_TRACK_WIRED_CLIENTS,
DEFAULT_DETECTION_TIME,
DOMAIN as UNIFI_DOMAIN,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed
@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(
hass: HomeAssistant,
mock_websocket_message,
config_entry_setup: ConfigEntry,
client_payload: list[dict[str, Any]],
) -> None:
"""Verify tracking of wireless clients."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
# Updated timestamp marks client as home
client = client_payload[0]
client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
mock_websocket_message(message=MessageKey.CLIENT, data=client)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_HOME
# Change time to mark client as away
new_time = dt_util.utcnow() + timedelta(
seconds=config_entry_setup.options.get(
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
)
)
with freeze_time(new_time):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
# Same timestamp doesn't explicitly mark client as away
mock_websocket_message(message=MessageKey.CLIENT, data=client)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_HOME
@pytest.mark.parametrize(
"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("mock_device_registry")
async def test_tracked_clients(
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,
freezer: FrozenDateTimeFactory,
mock_websocket_message,
config_entry_setup: ConfigEntry,
client_payload: list[dict[str, Any]],
) -> None:
"""Verify tracking of wireless clients based on event source."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
# State change signalling works with events
# Connected event
client = client_payload[0]
event = {
"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
event = {
"user": client["mac"],
"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
freezer.tick(
timedelta(
seconds=(
config_entry_setup.options.get(
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
)
+ 1
)
)
)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
# To limit false positives in client tracker
# data sources are prioritized when available
# once real data is received events will be ignored.
# New data
client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
mock_websocket_message(message=MessageKey.CLIENT, data=client)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_HOME
# Disconnection event will be ignored
event = {
"user": client["mac"],
"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
freezer.tick(
timedelta(
seconds=(
config_entry_setup.options.get(
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
)
+ 1
)
)
)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
@pytest.mark.parametrize(
"device_payload",
[
[
{
"board_rev": 3,
"device_id": "mock-id",
"has_fan": True,
"fan_level": 0,
"ip": "10.0.1.1",
"last_seen": 1562600145,
"mac": "00:00:00:00:01:01",
"model": "US16P150",
"name": "Device 1",
"next_interval": 20,
"overheating": True,
"state": 1,
"type": "usw",
"upgradable": True,
"version": "4.0.42.10433",
},
{
"board_rev": 3,
"device_id": "mock-id",
"has_fan": True,
"ip": "10.0.1.2",
"mac": "00:00:00:00:01:02",
"model": "US16P150",
"name": "Device 2",
"next_interval": 20,
"state": 0,
"type": "usw",
"version": "4.0.42.10433",
},
]
],
)
@pytest.mark.usefixtures("config_entry_setup")
@pytest.mark.usefixtures("mock_device_registry")
async def test_tracked_devices(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_websocket_message,
device_payload: list[dict[str, Any]],
) -> None:
"""Test the update_items function with some devices."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
assert hass.states.get("device_tracker.device_1").state == STATE_HOME
assert hass.states.get("device_tracker.device_2").state == STATE_NOT_HOME
# State change signalling work
device_1 = device_payload[0]
device_1["next_interval"] = 20
device_2 = device_payload[1]
device_2["state"] = 1
device_2["next_interval"] = 50
mock_websocket_message(message=MessageKey.DEVICE, data=[device_1, device_2])
await hass.async_block_till_done()
assert hass.states.get("device_tracker.device_1").state == STATE_HOME
assert hass.states.get("device_tracker.device_2").state == STATE_HOME
# Change of time can mark device not_home outside of expected reporting interval
new_time = dt_util.utcnow() + timedelta(seconds=90)
freezer.move_to(new_time)
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.device_1").state == STATE_NOT_HOME
assert hass.states.get("device_tracker.device_2").state == STATE_HOME
# Disabled device is unavailable
device_1["disabled"] = True
mock_websocket_message(message=MessageKey.DEVICE, data=device_1)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.device_1").state == STATE_UNAVAILABLE
assert hass.states.get("device_tracker.device_2").state == STATE_HOME
@pytest.mark.parametrize(
"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("mock_device_registry")
async def test_remove_clients(
hass: HomeAssistant, mock_websocket_message, client_payload: list[dict[str, Any]]
) -> None:
"""Test the remove_items function with some clients."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
assert hass.states.get("device_tracker.client_1")
assert hass.states.get("device_tracker.client_2")
# Remove client
mock_websocket_message(message=MessageKey.CLIENT_REMOVED, data=client_payload[0])
await hass.async_block_till_done()
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
assert not hass.states.get("device_tracker.client_1")
assert hass.states.get("device_tracker.client_2")
@pytest.mark.parametrize(
"client_payload",
[
[
{
"essid": "ssid",
"hostname": "client",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
}
]
],
)
@pytest.mark.parametrize(
"device_payload",
[
[
{
"board_rev": 3,
"device_id": "mock-id",
"has_fan": True,
"fan_level": 0,
"ip": "10.0.1.1",
"last_seen": 1562600145,
"mac": "00:00:00:00:01:01",
"model": "US16P150",
"name": "Device",
"next_interval": 20,
"overheating": True,
"state": 1,
"type": "usw",
"upgradable": True,
"version": "4.0.42.10433",
}
]
],
)
@pytest.mark.usefixtures("config_entry_setup")
@pytest.mark.usefixtures("mock_device_registry")
async def test_hub_state_change(hass: HomeAssistant, mock_websocket_state) -> None:
"""Verify entities state reflect on hub connection becoming unavailable."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
assert hass.states.get("device_tracker.device").state == STATE_HOME
# Controller unavailable
await mock_websocket_state.disconnect()
assert hass.states.get("device_tracker.client").state == STATE_UNAVAILABLE
assert hass.states.get("device_tracker.device").state == STATE_UNAVAILABLE
# Controller available
await mock_websocket_state.reconnect()
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
assert hass.states.get("device_tracker.device").state == STATE_HOME
@pytest.mark.parametrize(
"client_payload",
[
[
{
"essid": "ssid",
"hostname": "wireless_client",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
},
{
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
"name": "Wired Client",
},
]
],
)
@pytest.mark.parametrize(
"device_payload",
[
[
{
"board_rev": 3,
"device_id": "mock-id",
"has_fan": True,
"fan_level": 0,
"ip": "10.0.1.1",
"last_seen": 1562600145,
"mac": "00:00:00:00:01:01",
"model": "US16P150",
"name": "Device",
"next_interval": 20,
"overheating": True,
"state": 1,
"type": "usw",
"upgradable": True,
"version": "4.0.42.10433",
}
]
],
)
@pytest.mark.usefixtures("mock_device_registry")
async def test_option_track_clients(
hass: HomeAssistant, config_entry_setup: ConfigEntry
) -> None:
"""Test the tracking of clients can be turned off."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
assert hass.states.get("device_tracker.wireless_client")
assert hass.states.get("device_tracker.wired_client")
assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_TRACK_CLIENTS: False}
)
await hass.async_block_till_done()
assert not hass.states.get("device_tracker.wireless_client")
assert not hass.states.get("device_tracker.wired_client")
assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_TRACK_CLIENTS: True}
)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.wireless_client")
assert hass.states.get("device_tracker.wired_client")
assert hass.states.get("device_tracker.device")
@pytest.mark.parametrize(
"client_payload",
[
[
{
"essid": "ssid",
"hostname": "wireless_client",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
},
{
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
"name": "Wired Client",
},
]
],
)
@pytest.mark.parametrize(
"device_payload",
[
[
{
"board_rev": 3,
"device_id": "mock-id",
"has_fan": True,
"fan_level": 0,
"ip": "10.0.1.1",
"last_seen": 1562600145,
"mac": "00:00:00:00:01:01",
"model": "US16P150",
"name": "Device",
"next_interval": 20,
"overheating": True,
"state": 1,
"type": "usw",
"upgradable": True,
"version": "4.0.42.10433",
}
]
],
)
@pytest.mark.usefixtures("mock_device_registry")
async def test_option_track_wired_clients(
hass: HomeAssistant, config_entry_setup: ConfigEntry
) -> None:
"""Test the tracking of wired clients can be turned off."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
assert hass.states.get("device_tracker.wireless_client")
assert hass.states.get("device_tracker.wired_client")
assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: False}
)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.wireless_client")
assert not hass.states.get("device_tracker.wired_client")
assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: True}
)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.wireless_client")
assert hass.states.get("device_tracker.wired_client")
assert hass.states.get("device_tracker.device")
@pytest.mark.parametrize(
"client_payload",
[
[
{
"hostname": "client",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
}
]
],
)
@pytest.mark.parametrize(
"device_payload",
[
[
{
"board_rev": 3,
"device_id": "mock-id",
"last_seen": 1562600145,
"ip": "10.0.1.1",
"mac": "00:00:00:00:01:01",
"model": "US16P150",
"name": "Device",
"next_interval": 20,
"overheating": True,
"state": 1,
"type": "usw",
"upgradable": True,
"version": "4.0.42.10433",
}
]
],
)
@pytest.mark.usefixtures("mock_device_registry")
async def test_option_track_devices(
hass: HomeAssistant, config_entry_setup: ConfigEntry
) -> None:
"""Test the tracking of devices can be turned off."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
assert hass.states.get("device_tracker.client")
assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_TRACK_DEVICES: False}
)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client")
assert not hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_TRACK_DEVICES: True}
)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client")
assert hass.states.get("device_tracker.device")
@pytest.mark.usefixtures("mock_device_registry")
async def test_option_ssid_filter(
hass: HomeAssistant,
mock_websocket_message,
config_entry_factory: Callable[[], ConfigEntry],
client_payload: list[dict[str, Any]],
) -> None:
"""Test the SSID filter works.
Client will travel from a supported SSID to an unsupported ssid.
Client on SSID2 will be removed on change of options.
"""
client_payload += [
{
"essid": "ssid",
"hostname": "client",
"is_wired": False,
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
"mac": "00:00:00:00:00:01",
},
{
"essid": "ssid2",
"hostname": "client_on_ssid2",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
},
]
config_entry = await config_entry_factory()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
assert hass.states.get("device_tracker.client").state == STATE_HOME
assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME
# Setting SSID filter will remove clients outside of filter
hass.config_entries.async_update_entry(
config_entry, options={CONF_SSID_FILTER: ["ssid"]}
)
await hass.async_block_till_done()
# Not affected by SSID filter
assert hass.states.get("device_tracker.client").state == STATE_HOME
# Removed due to SSID filter
assert not hass.states.get("device_tracker.client_on_ssid2")
# Roams to SSID outside of filter
client = client_payload[0]
client["essid"] = "other_ssid"
mock_websocket_message(message=MessageKey.CLIENT, data=client)
# Data update while SSID filter is in effect shouldn't create the client
client_on_ssid2 = client_payload[1]
client_on_ssid2["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
mock_websocket_message(message=MessageKey.CLIENT, data=client_on_ssid2)
await hass.async_block_till_done()
new_time = dt_util.utcnow() + timedelta(
seconds=(
config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1
)
)
with freeze_time(new_time):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
# SSID filter marks client as away
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
# SSID still outside of filter
assert not hass.states.get("device_tracker.client_on_ssid2")
# Remove SSID filter
hass.config_entries.async_update_entry(config_entry, options={CONF_SSID_FILTER: []})
await hass.async_block_till_done()
client["last_seen"] += 1
client_on_ssid2["last_seen"] += 1
mock_websocket_message(message=MessageKey.CLIENT, data=[client, client_on_ssid2])
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_HOME
assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_HOME
# Time pass to mark client as away
new_time += timedelta(
seconds=(
config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + 1
)
)
with freeze_time(new_time):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
client_on_ssid2["last_seen"] += 1
mock_websocket_message(message=MessageKey.CLIENT, data=client_on_ssid2)
await hass.async_block_till_done()
# Client won't go away until after next update
assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_HOME
# Trigger update to get client marked as away
client_on_ssid2["last_seen"] += 1
mock_websocket_message(message=MessageKey.CLIENT, data=client_on_ssid2)
await hass.async_block_till_done()
new_time += timedelta(
seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME))
)
with freeze_time(new_time):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME
@pytest.mark.usefixtures("mock_device_registry")
async def test_wireless_client_go_wired_issue(
hass: HomeAssistant,
mock_websocket_message,
config_entry_factory: Callable[[], ConfigEntry],
client_payload: list[dict[str, Any]],
) -> None:
"""Test the solution to catch wireless device go wired UniFi issue.
UniFi Network has a known issue that when a wireless device goes away it sometimes gets marked as wired.
"""
client_payload.append(
{
"essid": "ssid",
"hostname": "client",
"ip": "10.0.0.1",
"is_wired": False,
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
"mac": "00:00:00:00:00:01",
}
)
config_entry = await config_entry_factory()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
# Client is wireless
client_state = hass.states.get("device_tracker.client")
assert client_state.state == STATE_HOME
# Trigger wired bug
client = client_payload[0]
client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
client["is_wired"] = True
mock_websocket_message(message=MessageKey.CLIENT, data=client)
await hass.async_block_till_done()
# Wired bug fix keeps client marked as wireless
client_state = hass.states.get("device_tracker.client")
assert client_state.state == STATE_HOME
# Pass time
new_time = dt_util.utcnow() + timedelta(
seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME))
)
with freeze_time(new_time):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
# Marked as home according to the timer
client_state = hass.states.get("device_tracker.client")
assert client_state.state == STATE_NOT_HOME
# Try to mark client as connected
client["last_seen"] += 1
mock_websocket_message(message=MessageKey.CLIENT, data=client)
await hass.async_block_till_done()
# Make sure it don't go online again until wired bug disappears
client_state = hass.states.get("device_tracker.client")
assert client_state.state == STATE_NOT_HOME
# Make client wireless
client["last_seen"] += 1
client["is_wired"] = False
mock_websocket_message(message=MessageKey.CLIENT, data=client)
await hass.async_block_till_done()
# Client is no longer affected by wired bug and can be marked online
client_state = hass.states.get("device_tracker.client")
assert client_state.state == STATE_HOME
@pytest.mark.parametrize("config_entry_options", [{CONF_IGNORE_WIRED_BUG: True}])
@pytest.mark.usefixtures("mock_device_registry")
async def test_option_ignore_wired_bug(
hass: HomeAssistant,
mock_websocket_message,
config_entry_factory: Callable[[], ConfigEntry],
client_payload: list[dict[str, Any]],
) -> None:
"""Test option to ignore wired bug."""
client_payload.append(
{
"ap_mac": "00:00:00:00:02:01",
"essid": "ssid",
"hostname": "client",
"ip": "10.0.0.1",
"is_wired": False,
"last_seen": dt_util.as_timestamp(dt_util.utcnow()),
"mac": "00:00:00:00:00:01",
}
)
config_entry = await config_entry_factory()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
# Client is wireless
client_state = hass.states.get("device_tracker.client")
assert client_state.state == STATE_HOME
# Trigger wired bug
client = client_payload[0]
client["is_wired"] = True
mock_websocket_message(message=MessageKey.CLIENT, data=client)
await hass.async_block_till_done()
# Wired bug in effect
client_state = hass.states.get("device_tracker.client")
assert client_state.state == STATE_HOME
# pass time
new_time = dt_util.utcnow() + timedelta(
seconds=config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)
)
with freeze_time(new_time):
async_fire_time_changed(hass, new_time)
await hass.async_block_till_done()
# Timer marks client as away
client_state = hass.states.get("device_tracker.client")
assert client_state.state == STATE_NOT_HOME
# Mark client as connected again
client["last_seen"] += 1
mock_websocket_message(message=MessageKey.CLIENT, data=client)
await hass.async_block_till_done()
# Ignoring wired bug allows client to go home again even while affected
client_state = hass.states.get("device_tracker.client")
assert client_state.state == STATE_HOME
# Make client wireless
client["last_seen"] += 1
client["is_wired"] = False
mock_websocket_message(message=MessageKey.CLIENT, data=client)
await hass.async_block_till_done()
# Client is wireless and still connected
client_state = hass.states.get("device_tracker.client")
assert client_state.state == STATE_HOME
@pytest.mark.parametrize(
"config_entry_options", [{CONF_BLOCK_CLIENT: ["00:00:00:00:00:02"]}]
)
@pytest.mark.parametrize(
"client_payload",
[
[
{
"hostname": "client",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
}
]
],
)
@pytest.mark.parametrize(
"clients_all_payload",
[
[
{
"hostname": "restored",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
},
{ # Not previously seen by integration, will not be restored
"hostname": "not_restored",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:03",
},
]
],
)
@pytest.mark.usefixtures("mock_device_registry")
async def test_restoring_client(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
config_entry: ConfigEntry,
config_entry_factory: Callable[[], ConfigEntry],
client_payload: list[dict[str, Any]],
clients_all_payload: list[dict[str, Any]],
) -> None:
"""Verify clients are restored from clients_all if they ever was registered to entity registry."""
entity_registry.async_get_or_create( # Make sure unique ID converts to site_id-mac
TRACKER_DOMAIN,
UNIFI_DOMAIN,
f'{clients_all_payload[0]["mac"]}-site_id',
suggested_object_id=clients_all_payload[0]["hostname"],
config_entry=config_entry,
)
entity_registry.async_get_or_create( # Unique ID already follow format site_id-mac
TRACKER_DOMAIN,
UNIFI_DOMAIN,
f'site_id-{client_payload[0]["mac"]}',
suggested_object_id=client_payload[0]["hostname"],
config_entry=config_entry,
)
await config_entry_factory()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
assert hass.states.get("device_tracker.client")
assert hass.states.get("device_tracker.restored")
assert not hass.states.get("device_tracker.not_restored")
@pytest.mark.parametrize("config_entry_options", [{CONF_TRACK_CLIENTS: False}])
@pytest.mark.parametrize(
"client_payload",
[
[
{
"essid": "ssid",
"hostname": "Wireless client",
"ip": "10.0.0.1",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
},
{
"hostname": "Wired client",
"ip": "10.0.0.2",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
},
]
],
)
@pytest.mark.parametrize(
"device_payload",
[
[
{
"board_rev": 3,
"device_id": "mock-id",
"has_fan": True,
"fan_level": 0,
"ip": "10.0.1.1",
"last_seen": 1562600145,
"mac": "00:00:00:00:01:01",
"model": "US16P150",
"name": "Device",
"next_interval": 20,
"overheating": True,
"state": 1,
"type": "usw",
"upgradable": True,
"version": "4.0.42.10433",
},
]
],
)
@pytest.mark.usefixtures("mock_device_registry")
async def test_dont_track_clients(
hass: HomeAssistant, config_entry_setup: ConfigEntry
) -> None:
"""Test don't track clients config works."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
assert not hass.states.get("device_tracker.wireless_client")
assert not hass.states.get("device_tracker.wired_client")
assert hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_TRACK_CLIENTS: True}
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3
assert hass.states.get("device_tracker.wireless_client")
assert hass.states.get("device_tracker.wired_client")
assert hass.states.get("device_tracker.device")
@pytest.mark.parametrize("config_entry_options", [{CONF_TRACK_DEVICES: False}])
@pytest.mark.parametrize(
"client_payload",
[
[
{
"hostname": "client",
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
},
]
],
)
@pytest.mark.parametrize(
"device_payload",
[
[
{
"board_rev": 3,
"device_id": "mock-id",
"has_fan": True,
"fan_level": 0,
"ip": "10.0.1.1",
"last_seen": 1562600145,
"mac": "00:00:00:00:01:01",
"model": "US16P150",
"name": "Device",
"next_interval": 20,
"overheating": True,
"state": 1,
"type": "usw",
"upgradable": True,
"version": "4.0.42.10433",
},
]
],
)
@pytest.mark.usefixtures("mock_device_registry")
async def test_dont_track_devices(
hass: HomeAssistant, config_entry_setup: ConfigEntry
) -> None:
"""Test don't track devices config works."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
assert hass.states.get("device_tracker.client")
assert not hass.states.get("device_tracker.device")
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_TRACK_DEVICES: True}
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
assert hass.states.get("device_tracker.client")
assert hass.states.get("device_tracker.device")
@pytest.mark.parametrize("config_entry_options", [{CONF_TRACK_WIRED_CLIENTS: False}])
@pytest.mark.parametrize(
"client_payload",
[
[
{
"essid": "ssid",
"hostname": "Wireless Client",
"is_wired": False,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:01",
},
{
"is_wired": True,
"last_seen": 1562600145,
"mac": "00:00:00:00:00:02",
"name": "Wired Client",
},
]
],
)
@pytest.mark.usefixtures("mock_device_registry")
async def test_dont_track_wired_clients(
hass: HomeAssistant, config_entry_setup: ConfigEntry
) -> None:
"""Test don't track wired clients config works."""
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1
assert hass.states.get("device_tracker.wireless_client")
assert not hass.states.get("device_tracker.wired_client")
hass.config_entries.async_update_entry(
config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: True}
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2
assert hass.states.get("device_tracker.wireless_client")
assert hass.states.get("device_tracker.wired_client")