diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 947e4523531..b2070362d02 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging -from aiounifi.api import SOURCE_DATA +from aiounifi.api import SOURCE_DATA, SOURCE_EVENT from aiounifi.events import ( ACCESS_POINT_UPGRADED, GATEWAY_UPGRADED, @@ -156,6 +156,8 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): self._controller_connection_state_changed = False + self._only_listen_to_data_source = False + last_seen = client.last_seen or 0 self.schedule_update = self._is_connected = ( self.is_wired == client.is_wired @@ -224,6 +226,23 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): ): self._is_connected = True self.schedule_update = True + self._only_listen_to_data_source = True + + elif ( + self.client.last_updated == SOURCE_EVENT + and not self._only_listen_to_data_source + ): + + if (self.is_wired and self.client.event.event in WIRED_CONNECTION) or ( + not self.is_wired and self.client.event.event in WIRELESS_CONNECTION + ): + self._is_connected = True + self.schedule_update = False + self.controller.async_heartbeat(self.unique_id) + super().async_update_callback() + + else: + self.schedule_update = True self._async_log_debug_data("update_callback") diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 736e85d1b8c..e5d53a6d882 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -3,7 +3,12 @@ from datetime import timedelta from unittest.mock import patch -from aiounifi.controller import MESSAGE_CLIENT, MESSAGE_CLIENT_REMOVED, MESSAGE_DEVICE +from aiounifi.controller import ( + MESSAGE_CLIENT, + MESSAGE_CLIENT_REMOVED, + MESSAGE_DEVICE, + MESSAGE_EVENT, +) from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING from homeassistant import config_entries @@ -169,6 +174,140 @@ async def test_tracked_clients( assert hass.states.get("device_tracker.client_1").state == STATE_HOME +async def test_tracked_wireless_clients_event_source( + hass, aioclient_mock, mock_unifi_websocket, mock_device_registry +): + """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] + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + 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 + + 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"]}] with SSID "{client["essid"]}" on "channel 44(na)"', + "_id": "5ea331fa30c49e00f90ddc1a", + } + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_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"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])', + "_id": "5ea32ff730c49e00f90dca1a", + } + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_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 + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=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 + + # To limit false positives in client tracker + # data sources are prioritized when available + # once real data is received events will be ignored. + + # New data + + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_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"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])', + "_id": "5ea32ff730c49e00f90dca1a", + } + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_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 + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() + + assert hass.states.get("device_tracker.client").state == STATE_HOME + + async def test_tracked_devices( hass, aioclient_mock, mock_unifi_websocket, mock_device_registry ):