mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
UniFi events aren't reliable for device tracker usage, use last_seen instead (#64147)
This commit is contained in:
parent
5f2fd1b0e6
commit
a15bdbbc4a
@ -385,11 +385,16 @@ class UniFiController:
|
|||||||
"""Check for any devices scheduled to be marked disconnected."""
|
"""Check for any devices scheduled to be marked disconnected."""
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
|
unique_ids_to_remove = []
|
||||||
for unique_id, heartbeat_expire_time in self._heartbeat_time.items():
|
for unique_id, heartbeat_expire_time in self._heartbeat_time.items():
|
||||||
if now > heartbeat_expire_time:
|
if now > heartbeat_expire_time:
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
self.hass, f"{self.signal_heartbeat_missed}_{unique_id}"
|
self.hass, f"{self.signal_heartbeat_missed}_{unique_id}"
|
||||||
)
|
)
|
||||||
|
unique_ids_to_remove.append(unique_id)
|
||||||
|
|
||||||
|
for unique_id in unique_ids_to_remove:
|
||||||
|
del self._heartbeat_time[unique_id]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def async_config_entry_updated(hass, config_entry) -> None:
|
async def async_config_entry_updated(hass, config_entry) -> None:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Track both clients and devices using UniFi Network."""
|
"""Track both clients and devices using UniFi Network."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from aiounifi.api import SOURCE_DATA, SOURCE_EVENT
|
from aiounifi.api import SOURCE_DATA
|
||||||
from aiounifi.events import (
|
from aiounifi.events import (
|
||||||
ACCESS_POINT_UPGRADED,
|
ACCESS_POINT_UPGRADED,
|
||||||
GATEWAY_UPGRADED,
|
GATEWAY_UPGRADED,
|
||||||
@ -151,19 +151,14 @@ class UniFiClientTracker(UniFiClient, ScannerEntity):
|
|||||||
super().__init__(client, controller)
|
super().__init__(client, controller)
|
||||||
|
|
||||||
self.heartbeat_check = False
|
self.heartbeat_check = False
|
||||||
self._is_connected = False
|
|
||||||
self._controller_connection_state_changed = False
|
self._controller_connection_state_changed = False
|
||||||
self._only_listen_to_event_source = False
|
|
||||||
|
|
||||||
if client.last_seen:
|
self._last_seen = client.last_seen or 0
|
||||||
self._is_connected = (
|
self.schedule_update = self._is_connected = (
|
||||||
self.is_wired == client.is_wired
|
self.is_wired == client.is_wired
|
||||||
and dt_util.utcnow()
|
and dt_util.utcnow() - dt_util.utc_from_timestamp(float(self._last_seen))
|
||||||
- dt_util.utc_from_timestamp(float(client.last_seen))
|
< controller.option_detection_time
|
||||||
< controller.option_detection_time
|
)
|
||||||
)
|
|
||||||
|
|
||||||
self.schedule_update = self._is_connected
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Watch object when added."""
|
"""Watch object when added."""
|
||||||
@ -196,30 +191,17 @@ class UniFiClientTracker(UniFiClient, ScannerEntity):
|
|||||||
|
|
||||||
if self.controller.available:
|
if self.controller.available:
|
||||||
self.schedule_update = True
|
self.schedule_update = True
|
||||||
self._only_listen_to_event_source = False
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.controller.async_heartbeat(self.unique_id)
|
self.controller.async_heartbeat(self.unique_id)
|
||||||
|
super().async_update_callback()
|
||||||
elif self.client.last_updated == SOURCE_EVENT:
|
|
||||||
self._only_listen_to_event_source = True
|
|
||||||
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)
|
|
||||||
self.heartbeat_check = False
|
|
||||||
|
|
||||||
# Ignore extra scheduled update from wired bug
|
|
||||||
elif not self.heartbeat_check:
|
|
||||||
self.schedule_update = True
|
|
||||||
|
|
||||||
elif (
|
elif (
|
||||||
not self._only_listen_to_event_source
|
self.client.last_updated == SOURCE_DATA
|
||||||
and self.client.last_updated == SOURCE_DATA
|
and self._last_seen != self.client.last_seen
|
||||||
and self.is_wired == self.client.is_wired
|
and self.is_wired == self.client.is_wired
|
||||||
):
|
):
|
||||||
|
self._last_seen = self.client.last_seen
|
||||||
self._is_connected = True
|
self._is_connected = True
|
||||||
self.schedule_update = True
|
self.schedule_update = True
|
||||||
|
|
||||||
@ -230,7 +212,7 @@ class UniFiClientTracker(UniFiClient, ScannerEntity):
|
|||||||
)
|
)
|
||||||
self.heartbeat_check = True
|
self.heartbeat_check = True
|
||||||
|
|
||||||
super().async_update_callback()
|
super().async_update_callback()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _make_disconnected(self, *_):
|
def _make_disconnected(self, *_):
|
||||||
|
@ -3,12 +3,7 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from aiounifi.controller import (
|
from aiounifi.controller import MESSAGE_CLIENT, MESSAGE_CLIENT_REMOVED, MESSAGE_DEVICE
|
||||||
MESSAGE_CLIENT,
|
|
||||||
MESSAGE_CLIENT_REMOVED,
|
|
||||||
MESSAGE_DEVICE,
|
|
||||||
MESSAGE_EVENT,
|
|
||||||
)
|
|
||||||
from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING
|
from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
@ -70,35 +65,19 @@ async def test_tracked_wireless_clients(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
client_state = hass.states.get("device_tracker.client")
|
client_state = hass.states.get("device_tracker.client")
|
||||||
assert client_state.state == STATE_HOME
|
assert client_state.state == STATE_NOT_HOME
|
||||||
assert client_state.attributes["ip"] == "10.0.0.1"
|
assert client_state.attributes["ip"] == "10.0.0.1"
|
||||||
assert client_state.attributes["mac"] == "00:00:00:00:00:01"
|
assert client_state.attributes["mac"] == "00:00:00:00:00:01"
|
||||||
assert client_state.attributes["hostname"] == "client"
|
assert client_state.attributes["hostname"] == "client"
|
||||||
assert client_state.attributes["host_name"] == "client"
|
assert client_state.attributes["host_name"] == "client"
|
||||||
|
|
||||||
# State change signalling works with events
|
# Updated timestamp marks client as home
|
||||||
|
|
||||||
# Disconnected event
|
client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
|
||||||
|
|
||||||
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(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
"meta": {"message": MESSAGE_EVENT},
|
"meta": {"message": MESSAGE_CLIENT},
|
||||||
"data": [event],
|
"data": [client],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -114,12 +93,7 @@ async def test_tracked_wireless_clients(
|
|||||||
|
|
||||||
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
|
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
|
||||||
|
|
||||||
# To limit false positives in client tracker
|
# Same timestamp again means client is away
|
||||||
# data sources other than events are only used to update state
|
|
||||||
# until the first event has been received.
|
|
||||||
# This control will be reset if controller connection has been lost.
|
|
||||||
|
|
||||||
# New data doesn't change state
|
|
||||||
|
|
||||||
mock_unifi_websocket(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
@ -131,33 +105,6 @@ async def test_tracked_wireless_clients(
|
|||||||
|
|
||||||
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
|
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
async def test_tracked_clients(
|
async def test_tracked_clients(
|
||||||
hass, aioclient_mock, mock_unifi_websocket, mock_device_registry
|
hass, aioclient_mock, mock_unifi_websocket, mock_device_registry
|
||||||
@ -227,6 +174,7 @@ async def test_tracked_clients(
|
|||||||
|
|
||||||
# State change signalling works
|
# State change signalling works
|
||||||
|
|
||||||
|
client_1["last_seen"] += 1
|
||||||
mock_unifi_websocket(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
"meta": {"message": MESSAGE_CLIENT},
|
"meta": {"message": MESSAGE_CLIENT},
|
||||||
@ -467,91 +415,6 @@ async def test_controller_state_change(
|
|||||||
assert hass.states.get("device_tracker.device").state == STATE_HOME
|
assert hass.states.get("device_tracker.device").state == STATE_HOME
|
||||||
|
|
||||||
|
|
||||||
async def test_controller_state_change_client_to_listen_on_all_state_changes(
|
|
||||||
hass, aioclient_mock, mock_unifi_websocket, mock_device_registry
|
|
||||||
):
|
|
||||||
"""Verify entities state reflect on controller becoming unavailable."""
|
|
||||||
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, 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_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
|
|
||||||
|
|
||||||
# Controller unavailable
|
|
||||||
mock_unifi_websocket(state=STATE_DISCONNECTED)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert hass.states.get("device_tracker.client").state == STATE_UNAVAILABLE
|
|
||||||
|
|
||||||
# Controller available
|
|
||||||
mock_unifi_websocket(state=STATE_RUNNING)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
# To limit false positives in client tracker
|
|
||||||
# data sources other than events are only used to update state
|
|
||||||
# until the first event has been received.
|
|
||||||
# This control will be reset if controller connection has been lost.
|
|
||||||
|
|
||||||
# New data can change state
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def test_option_track_clients(hass, aioclient_mock, mock_device_registry):
|
async def test_option_track_clients(hass, aioclient_mock, mock_device_registry):
|
||||||
"""Test the tracking of clients can be turned off."""
|
"""Test the tracking of clients can be turned off."""
|
||||||
wireless_client = {
|
wireless_client = {
|
||||||
@ -814,6 +677,8 @@ async def test_option_ssid_filter(
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
client["last_seen"] += 1
|
||||||
|
client_on_ssid2["last_seen"] += 1
|
||||||
mock_unifi_websocket(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
"meta": {"message": MESSAGE_CLIENT},
|
"meta": {"message": MESSAGE_CLIENT},
|
||||||
@ -840,6 +705,7 @@ async def test_option_ssid_filter(
|
|||||||
|
|
||||||
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
|
assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME
|
||||||
|
|
||||||
|
client_on_ssid2["last_seen"] += 1
|
||||||
mock_unifi_websocket(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
"meta": {"message": MESSAGE_CLIENT},
|
"meta": {"message": MESSAGE_CLIENT},
|
||||||
@ -852,6 +718,7 @@ async def test_option_ssid_filter(
|
|||||||
assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_HOME
|
assert hass.states.get("device_tracker.client_on_ssid2").state == STATE_HOME
|
||||||
|
|
||||||
# Trigger update to get client marked as away
|
# Trigger update to get client marked as away
|
||||||
|
client_on_ssid2["last_seen"] += 1
|
||||||
mock_unifi_websocket(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
"meta": {"message": MESSAGE_CLIENT},
|
"meta": {"message": MESSAGE_CLIENT},
|
||||||
@ -899,6 +766,7 @@ async def test_wireless_client_go_wired_issue(
|
|||||||
assert client_state.attributes["is_wired"] is False
|
assert client_state.attributes["is_wired"] is False
|
||||||
|
|
||||||
# Trigger wired bug
|
# Trigger wired bug
|
||||||
|
client["last_seen"] += 1
|
||||||
client["is_wired"] = True
|
client["is_wired"] = True
|
||||||
mock_unifi_websocket(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
@ -925,6 +793,7 @@ async def test_wireless_client_go_wired_issue(
|
|||||||
assert client_state.attributes["is_wired"] is False
|
assert client_state.attributes["is_wired"] is False
|
||||||
|
|
||||||
# Try to mark client as connected
|
# Try to mark client as connected
|
||||||
|
client["last_seen"] += 1
|
||||||
mock_unifi_websocket(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
"meta": {"message": MESSAGE_CLIENT},
|
"meta": {"message": MESSAGE_CLIENT},
|
||||||
@ -939,6 +808,7 @@ async def test_wireless_client_go_wired_issue(
|
|||||||
assert client_state.attributes["is_wired"] is False
|
assert client_state.attributes["is_wired"] is False
|
||||||
|
|
||||||
# Make client wireless
|
# Make client wireless
|
||||||
|
client["last_seen"] += 1
|
||||||
client["is_wired"] = False
|
client["is_wired"] = False
|
||||||
mock_unifi_websocket(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
@ -1009,6 +879,7 @@ async def test_option_ignore_wired_bug(
|
|||||||
assert client_state.attributes["is_wired"] is True
|
assert client_state.attributes["is_wired"] is True
|
||||||
|
|
||||||
# Mark client as connected again
|
# Mark client as connected again
|
||||||
|
client["last_seen"] += 1
|
||||||
mock_unifi_websocket(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
"meta": {"message": MESSAGE_CLIENT},
|
"meta": {"message": MESSAGE_CLIENT},
|
||||||
@ -1023,6 +894,7 @@ async def test_option_ignore_wired_bug(
|
|||||||
assert client_state.attributes["is_wired"] is True
|
assert client_state.attributes["is_wired"] is True
|
||||||
|
|
||||||
# Make client wireless
|
# Make client wireless
|
||||||
|
client["last_seen"] += 1
|
||||||
client["is_wired"] = False
|
client["is_wired"] = False
|
||||||
mock_unifi_websocket(
|
mock_unifi_websocket(
|
||||||
data={
|
data={
|
||||||
|
Loading…
x
Reference in New Issue
Block a user