diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 2ea772b5173..5fdeb1889fe 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -12,6 +12,7 @@ from unittest.mock import patch from aiounifi.models.message import MessageKey import pytest +from homeassistant.components.unifi import STORAGE_KEY, STORAGE_VERSION from homeassistant.components.unifi.const import CONF_SITE_ID, DOMAIN as UNIFI_DOMAIN from homeassistant.components.unifi.hub.websocket import RETRY_TIMER from homeassistant.config_entries import ConfigEntry @@ -111,6 +112,27 @@ def config_entry_options_fixture() -> MappingProxyType[str, Any]: return {} +# Known wireless clients + + +@pytest.fixture(name="known_wireless_clients") +def known_wireless_clients_fixture() -> list[str]: + """Known previously observed wireless clients.""" + return [] + + +@pytest.fixture(autouse=True) +def mock_wireless_client_storage(hass_storage, known_wireless_clients: list[str]): + """Mock the known wireless storage.""" + data: dict[str, list[str]] = ( + {"wireless_clients": known_wireless_clients} if known_wireless_clients else {} + ) + hass_storage[STORAGE_KEY] = {"version": STORAGE_VERSION, "data": data} + + +# UniFi request mocks + + @pytest.fixture(name="mock_requests") def request_fixture( aioclient_mock: AiohttpClientMocker, diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 4037d976430..1bc4c4ff632 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,9 +1,12 @@ """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 ( @@ -18,51 +21,44 @@ from homeassistant.components.unifi.const import ( 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 .test_hub import ENTRY_CONFIG, setup_unifi_integration - -from tests.common import MockConfigEntry, async_fire_time_changed -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def test_no_entities( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test the update_clients function when no clients are found.""" - await setup_unifi_integration(hass, aioclient_mock) - - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 0 +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, - aioclient_mock: AiohttpClientMocker, - mock_device_registry, mock_unifi_websocket, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Verify tracking of wireless clients.""" - 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", - } - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=[client] - ) - 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_unifi_websocket(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() @@ -70,9 +66,10 @@ async def test_tracked_wireless_clients( 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.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + seconds=config_entry_setup.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) @@ -81,76 +78,77 @@ async def test_tracked_wireless_clients( assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME # Same timestamp doesn't explicitly mark client as away - mock_unifi_websocket(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, - aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, - mock_device_registry, + hass: HomeAssistant, mock_unifi_websocket, client_payload: list[dict[str, Any]] ) -> None: """Test the update_items function with some clients.""" - client_1 = { - "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", - } - client_2 = { - "ip": "10.0.0.2", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Client 2", - } - client_3 = { - "essid": "ssid2", - "hostname": "client_3", - "ip": "10.0.0.3", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:03", - } - client_4 = { - "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", - } - client_5 = { - "essid": "ssid", - "hostname": "client_5", - "ip": "10.0.0.5", - "is_wired": True, - "last_seen": None, - "mac": "00:00:00:00:00:05", - } - client_6 = { - "hostname": "client_6", - "ip": "10.0.0.6", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:06", - } - - await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_SSID_FILTER: ["ssid"], CONF_CLIENT_SOURCE: [client_6["mac"]]}, - clients_response=[client_1, client_2, client_3, client_4, client_5, client_6], - known_wireless_clients=(client_4["mac"],), - ) - 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 @@ -170,6 +168,7 @@ async def test_tracked_clients( # State change signalling works + client_1 = client_payload[0] client_1["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) mock_unifi_websocket(message=MessageKey.CLIENT, data=client_1) await hass.async_block_till_done() @@ -177,34 +176,38 @@ async def test_tracked_clients( 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, - aioclient_mock: AiohttpClientMocker, freezer: FrozenDateTimeFactory, mock_unifi_websocket, - mock_device_registry, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Verify tracking of wireless clients based on event source.""" - 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", - } - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=[client] - ) - 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"], @@ -217,7 +220,10 @@ async def test_tracked_wireless_clients_event_source( "site_id": "name", "time": 1587753456179, "datetime": "2020-04-24T18:37:36Z", - "msg": f'User{[client["mac"]]} has connected to AP[{client["ap_mac"]}] with SSID "{client["essid"]}" on "channel 44(na)"', + "msg": ( + f'User{[client["mac"]]} has connected to AP[{client["ap_mac"]}] ' + f'with SSID "{client["essid"]}" on "channel 44(na)"' + ), "_id": "5ea331fa30c49e00f90ddc1a", } mock_unifi_websocket(message=MessageKey.EVENT, data=event) @@ -225,7 +231,6 @@ async def test_tracked_wireless_clients_event_source( assert hass.states.get("device_tracker.client").state == STATE_HOME # Disconnected event - event = { "user": client["mac"], "ssid": client["essid"], @@ -238,7 +243,10 @@ async def test_tracked_wireless_clients_event_source( "site_id": "name", "time": 1587752927000, "datetime": "2020-04-24T18:28:47Z", - "msg": f'User{[client["mac"]]} disconnected from "{client["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])', + "msg": ( + f'User{[client["mac"]]} disconnected from "{client["essid"]}" ' + f'(7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])' + ), "_id": "5ea32ff730c49e00f90dca1a", } mock_unifi_websocket(message=MessageKey.EVENT, data=event) @@ -249,7 +257,9 @@ async def test_tracked_wireless_clients_event_source( freezer.tick( timedelta( seconds=( - config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + config_entry_setup.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) + 1 ) ) @@ -264,14 +274,12 @@ async def test_tracked_wireless_clients_event_source( # once real data is received events will be ignored. # New data - client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) mock_unifi_websocket(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"], @@ -284,7 +292,10 @@ async def test_tracked_wireless_clients_event_source( "site_id": "name", "time": 1587752927000, "datetime": "2020-04-24T18:28:47Z", - "msg": f'User{[client["mac"]]} disconnected from "{client["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])', + "msg": ( + f'User{[client["mac"]]} disconnected from "{client["essid"]}" ' + f'(7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])' + ), "_id": "5ea32ff730c49e00f90dca1a", } mock_unifi_websocket(message=MessageKey.EVENT, data=event) @@ -295,7 +306,9 @@ async def test_tracked_wireless_clients_event_source( freezer.tick( timedelta( seconds=( - config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + config_entry_setup.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) + 1 ) ) @@ -306,57 +319,60 @@ async def test_tracked_wireless_clients_event_source( 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, - aioclient_mock: AiohttpClientMocker, freezer: FrozenDateTimeFactory, mock_unifi_websocket, - mock_device_registry, + device_payload: list[dict[str, Any]], ) -> None: """Test the update_items function with some devices.""" - device_1 = { - "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", - } - device_2 = { - "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", - } - await setup_unifi_integration( - hass, - aioclient_mock, - devices_response=[device_1, device_2], - ) - 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_unifi_websocket(message=MessageKey.DEVICE, data=[device_1, device_2]) @@ -366,7 +382,6 @@ async def test_tracked_devices( 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) @@ -376,7 +391,6 @@ async def test_tracked_devices( assert hass.states.get("device_tracker.device_2").state == STATE_HOME # Disabled device is unavailable - device_1["disabled"] = True mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() @@ -385,37 +399,38 @@ async def test_tracked_devices( 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, - aioclient_mock: AiohttpClientMocker, - mock_unifi_websocket, - mock_device_registry, + hass: HomeAssistant, mock_unifi_websocket, client_payload: list[dict[str, Any]] ) -> None: """Test the remove_items function with some clients.""" - client_1 = { - "essid": "ssid", - "hostname": "client_1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - client_2 = { - "hostname": "client_2", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - } - await setup_unifi_integration( - hass, aioclient_mock, clients_response=[client_1, client_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.client_2") # Remove client - - mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=client_1) + mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=client_payload[0]) await hass.async_block_till_done() await hass.async_block_till_done() @@ -424,45 +439,48 @@ async def test_remove_clients( assert hass.states.get("device_tracker.client_2") -async def test_hub_state_change( - hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, - websocket_mock, - mock_device_registry, -) -> None: +@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, websocket_mock) -> None: """Verify entities state reflect on hub connection becoming unavailable.""" - client = { - "essid": "ssid", - "hostname": "client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - device = { - "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", - } - - await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=[client], - devices_response=[device], - ) - 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 @@ -478,47 +496,55 @@ async def test_hub_state_change( 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, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test the tracking of clients can be turned off.""" - wireless_client = { - "essid": "ssid", - "hostname": "wireless_client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - wired_client = { - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Wired Client", - } - device = { - "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", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=[wireless_client, wired_client], - devices_response=[device], - ) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 3 assert hass.states.get("device_tracker.wireless_client") @@ -526,8 +552,7 @@ async def test_option_track_clients( assert hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_CLIENTS: False}, + config_entry_setup, options={CONF_TRACK_CLIENTS: False} ) await hass.async_block_till_done() @@ -536,8 +561,7 @@ async def test_option_track_clients( assert hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_CLIENTS: True}, + config_entry_setup, options={CONF_TRACK_CLIENTS: True} ) await hass.async_block_till_done() @@ -546,56 +570,62 @@ async def test_option_track_clients( 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, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test the tracking of wired clients can be turned off.""" - wireless_client = { - "essid": "ssid", - "hostname": "wireless_client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - wired_client = { - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Wired Client", - } - device = { - "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", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=[wireless_client, wired_client], - devices_response=[device], - ) - 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, - options={CONF_TRACK_WIRED_CLIENTS: False}, + config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: False} ) await hass.async_block_till_done() @@ -604,8 +634,7 @@ async def test_option_track_wired_clients( assert hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_WIRED_CLIENTS: True}, + config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: True} ) await hass.async_block_till_done() @@ -614,46 +643,52 @@ async def test_option_track_wired_clients( 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, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test the tracking of devices can be turned off.""" - client = { - "hostname": "client", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - device = { - "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", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=[client], - devices_response=[device], - ) - 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, - options={CONF_TRACK_DEVICES: False}, + config_entry_setup, options={CONF_TRACK_DEVICES: False} ) await hass.async_block_till_done() @@ -661,8 +696,7 @@ async def test_option_track_devices( assert not hass.states.get("device_tracker.device") hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_DEVICES: True}, + config_entry_setup, options={CONF_TRACK_DEVICES: True} ) await hass.async_block_till_done() @@ -670,44 +704,46 @@ async def test_option_track_devices( assert hass.states.get("device_tracker.device") +@pytest.mark.parametrize( + "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", + }, + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_option_ssid_filter( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, - mock_device_registry, + config_entry_setup: 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 = { - "essid": "ssid", - "hostname": "client", - "is_wired": False, - "last_seen": dt_util.as_timestamp(dt_util.utcnow()), - "mac": "00:00:00:00:00:01", - } - client_on_ssid2 = { - "essid": "ssid2", - "hostname": "client_on_ssid2", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - } - - config_entry = await setup_unifi_integration( - hass, aioclient_mock, clients_response=[client, client_on_ssid2] - ) - 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"]}, + config_entry_setup, options={CONF_SSID_FILTER: ["ssid"]} ) await hass.async_block_till_done() @@ -718,17 +754,20 @@ async def test_option_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_unifi_websocket(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_unifi_websocket(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 + config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + + 1 ) ) with freeze_time(new_time): @@ -743,8 +782,7 @@ async def test_option_ssid_filter( # Remove SSID filter hass.config_entries.async_update_entry( - config_entry, - options={CONF_SSID_FILTER: []}, + config_entry_setup, options={CONF_SSID_FILTER: []} ) await hass.async_block_till_done() @@ -757,10 +795,10 @@ async def test_option_ssid_filter( 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 + config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + + 1 ) ) with freeze_time(new_time): @@ -782,7 +820,9 @@ async def test_option_ssid_filter( await hass.async_block_till_done() new_time += timedelta( - seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)) + seconds=( + config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + ) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) @@ -791,29 +831,32 @@ async def test_option_ssid_filter( assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_NOT_HOME +@pytest.mark.parametrize( + "client_payload", + [ + [ + { + "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", + } + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_wireless_client_go_wired_issue( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, - mock_device_registry, + config_entry_setup: 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 = { - "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 setup_unifi_integration( - hass, aioclient_mock, clients_response=[client] - ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless @@ -821,6 +864,7 @@ async def test_wireless_client_go_wired_issue( 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_unifi_websocket(message=MessageKey.CLIENT, data=client) @@ -832,7 +876,9 @@ async def test_wireless_client_go_wired_issue( # Pass time new_time = dt_util.utcnow() + timedelta( - seconds=(config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME)) + seconds=( + config_entry_setup.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + ) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) @@ -862,29 +908,31 @@ async def test_wireless_client_go_wired_issue( assert client_state.state == STATE_HOME +@pytest.mark.parametrize("config_entry_options", [{CONF_IGNORE_WIRED_BUG: True}]) +@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": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + } + ] + ], +) +@pytest.mark.usefixtures("mock_device_registry") async def test_option_ignore_wired_bug( hass: HomeAssistant, - aioclient_mock: AiohttpClientMocker, mock_unifi_websocket, - mock_device_registry, + config_entry_setup: ConfigEntry, + client_payload: list[dict[str, Any]], ) -> None: """Test option to ignore wired bug.""" - client = { - "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 setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_IGNORE_WIRED_BUG: True}, - clients_response=[client], - ) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 # Client is wireless @@ -892,6 +940,7 @@ async def test_option_ignore_wired_bug( assert client_state.state == STATE_HOME # Trigger wired bug + client = client_payload[0] client["is_wired"] = True mock_unifi_websocket(message=MessageKey.CLIENT, data=client) await hass.async_block_till_done() @@ -902,7 +951,9 @@ async def test_option_ignore_wired_bug( # pass time new_time = dt_util.utcnow() + timedelta( - seconds=config_entry.options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + seconds=config_entry_setup.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) ) with freeze_time(new_time): async_fire_time_changed(hass, new_time) @@ -932,62 +983,67 @@ async def test_option_ignore_wired_bug( 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, - aioclient_mock: AiohttpClientMocker, - mock_device_registry, + 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.""" - client = { - "hostname": "client", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - restored = { - "hostname": "restored", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - } - not_restored = { - "hostname": "not_restored", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:03", - } - - config_entry = MockConfigEntry( - domain=UNIFI_DOMAIN, - data=ENTRY_CONFIG, - source="test", - options={}, - entry_id="1", - ) - - entity_registry.async_get_or_create( # Unique ID updated + entity_registry.async_get_or_create( # Make sure unique ID converts to site_id-mac TRACKER_DOMAIN, UNIFI_DOMAIN, - f'{restored["mac"]}-site_id', - suggested_object_id=restored["hostname"], + 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 updated + entity_registry.async_get_or_create( # Unique ID already follow format site_id-mac TRACKER_DOMAIN, UNIFI_DOMAIN, - f'site_id-{client["mac"]}', - suggested_object_id=client["hostname"], + f'site_id-{client_payload[0]["mac"]}', + suggested_object_id=client_payload[0]["hostname"], config_entry=config_entry, ) - await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_BLOCK_CLIENT: [restored["mac"]]}, - clients_response=[client], - clients_all_response=[restored, not_restored], - ) + await config_entry_factory() assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 assert hass.states.get("device_tracker.client") @@ -995,59 +1051,65 @@ async def test_restoring_client( 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, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test don't track clients config works.""" - wireless_client = { - "essid": "ssid", - "hostname": "Wireless client", - "ip": "10.0.0.1", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - wired_client = { - "hostname": "Wired client", - "ip": "10.0.0.2", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - } - device = { - "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", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_TRACK_CLIENTS: False}, - clients_response=[wireless_client, wired_client], - devices_response=[device], - ) - 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, - options={CONF_TRACK_CLIENTS: True}, + config_entry_setup, options={CONF_TRACK_CLIENTS: True} ) await hass.async_block_till_done() @@ -1057,49 +1119,55 @@ async def test_dont_track_clients( 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, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test don't track devices config works.""" - client = { - "hostname": "client", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - device = { - "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", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_TRACK_DEVICES: False}, - clients_response=[client], - devices_response=[device], - ) - 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, - options={CONF_TRACK_DEVICES: True}, + config_entry_setup, options={CONF_TRACK_DEVICES: True} ) await hass.async_block_till_done() @@ -1108,38 +1176,38 @@ async def test_dont_track_devices( 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, aioclient_mock: AiohttpClientMocker, mock_device_registry + hass: HomeAssistant, config_entry_setup: ConfigEntry ) -> None: """Test don't track wired clients config works.""" - wireless_client = { - "essid": "ssid", - "hostname": "Wireless Client", - "is_wired": False, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - } - wired_client = { - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:02", - "name": "Wired Client", - } - - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_TRACK_WIRED_CLIENTS: False}, - clients_response=[wireless_client, wired_client], - ) - 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, - options={CONF_TRACK_WIRED_CLIENTS: True}, + config_entry_setup, options={CONF_TRACK_WIRED_CLIENTS: True} ) await hass.async_block_till_done()