diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 521ebd88bdd..a4382c59ee3 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -239,7 +239,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): | { wlan["name"] for ap in self.controller.api.devices.values() - for wlan in ap.raw.get("wlan_overrides", []) + for wlan in ap.wlan_overrides } ) ssid_filter = {ssid: ssid for ssid in sorted(list(ssids))} diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 161c862f6b4..7be0031da43 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,6 +1,9 @@ """Track devices using UniFi controllers.""" +from datetime import timedelta import logging +from aiounifi.api import SOURCE_DATA + from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER @@ -243,6 +246,9 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): self.device = device super().__init__(controller) + self._is_connected = self.device.state == 1 + self.cancel_scheduled_update = None + @property def mac(self): """Return MAC of device.""" @@ -260,20 +266,34 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): @callback def async_update_callback(self): - """Update the sensor's state.""" + """Update the devices' state.""" + + @callback + def _no_heartbeat(now): + """No heart beat by device.""" + self._is_connected = False + self.cancel_scheduled_update = None + self.async_write_ha_state() + + if self.device.last_updated == SOURCE_DATA: + self._is_connected = True + + if self.cancel_scheduled_update: + self.cancel_scheduled_update() + + self.cancel_scheduled_update = async_track_point_in_utc_time( + self.hass, + _no_heartbeat, + dt_util.utcnow() + timedelta(seconds=self.device.next_interval + 10), + ) + LOGGER.debug("Updating device %s (%s)", self.entity_id, self.device.mac) self.async_write_ha_state() @property def is_connected(self): """Return true if the device is connected to the network.""" - if self.device.state == 1 and ( - dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) - < self.controller.option_detection_time - ): - return True - - return False + return self._is_connected @property def source_type(self): @@ -333,3 +353,8 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): """Config entry options are updated, remove entity if option is disabled.""" if not self.controller.option_track_devices: await self.async_remove() + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 8c05d195316..c814971521c 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "Ubiquiti UniFi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==20"], + "requirements": ["aiounifi==21"], "codeowners": ["@kane610"], "quality_scale": "platinum" } diff --git a/requirements_all.txt b/requirements_all.txt index 45a658b72f1..4670a3243b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -215,7 +215,7 @@ aiopylgtv==0.3.3 aioswitcher==1.1.0 # homeassistant.components.unifi -aiounifi==20 +aiounifi==21 # homeassistant.components.wwlln aiowwlln==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7ac76068ed..7b83aa29a53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ aiopylgtv==0.3.3 aioswitcher==1.1.0 # homeassistant.components.unifi -aiounifi==20 +aiounifi==21 # homeassistant.components.wwlln aiowwlln==2.0.2 diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 9f37c71468d..ab23cd2222a 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -84,6 +84,7 @@ DEVICE_1 = { "mac": "00:00:00:00:01:01", "model": "US16P150", "name": "device_1", + "next_interval": 20, "overheating": True, "state": 1, "type": "usw", @@ -94,10 +95,11 @@ DEVICE_2 = { "board_rev": 3, "device_id": "mock-id", "has_fan": True, - "ip": "10.0.1.1", - "mac": "00:00:00:00:01:01", + "ip": "10.0.1.2", + "mac": "00:00:00:00:01:02", "model": "US16P150", - "name": "device_1", + "name": "device_2", + "next_interval": 20, "state": 0, "type": "usw", "version": "4.0.42.10433", @@ -206,7 +208,7 @@ async def test_tracked_wireless_clients(hass): # test wired bug -async def test_tracked_devices(hass): +async def test_tracked_clients(hass): """Test the update_items function with some clients.""" client_4_copy = copy(CLIENT_4) client_4_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) @@ -216,9 +218,9 @@ async def test_tracked_devices(hass): options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_1, CLIENT_2, CLIENT_3, CLIENT_5, client_4_copy], devices_response=[DEVICE_1, DEVICE_2], - known_wireless_clients=(CLIENT_4["mac"],), + known_wireless_clients=([CLIENT_4["mac"]]), ) - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 5 + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 6 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -242,26 +244,54 @@ async def test_tracked_devices(hass): assert client_5 is not None assert client_5.state == "not_home" - device_1 = hass.states.get("device_tracker.device_1") - assert device_1 is not None - assert device_1.state == "not_home" - # State change signalling works client_1_copy = copy(CLIENT_1) client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_copy]} controller.api.message_handler(event) - device_1_copy = copy(DEVICE_1) - device_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - event = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_1_copy]} - controller.api.message_handler(event) - await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" + +async def test_tracked_devices(hass): + """Test the update_items function with some devices.""" + controller = await setup_unifi_integration( + hass, devices_response=[DEVICE_1, DEVICE_2], + ) + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 + assert device_1.state == "home" + + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "not_home" + + # State change signalling work + device_1_copy = copy(DEVICE_1) + device_1_copy["next_interval"] = 20 + event = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_1_copy]} + controller.api.message_handler(event) + device_2_copy = copy(DEVICE_2) + device_2_copy["next_interval"] = 50 + event = {"meta": {"message": MESSAGE_DEVICE}, "data": [device_2_copy]} + controller.api.message_handler(event) + await hass.async_block_till_done() + device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == "home" + device_2 = hass.states.get("device_tracker.device_2") + assert device_2.state == "home" + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=40)) + await hass.async_block_till_done() + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == "not_home" + device_2 = hass.states.get("device_tracker.device_2") + assert device_2.state == "home" # Disabled device is unavailable device_1_copy = copy(DEVICE_1) @@ -330,7 +360,7 @@ async def test_controller_state_change(hass): assert client_1.state == "not_home" device_1 = hass.states.get("device_tracker.device_1") - assert device_1.state == "not_home" + assert device_1.state == "home" async def test_option_track_clients(hass): @@ -648,7 +678,7 @@ async def test_dont_track_clients(hass): device_1 = hass.states.get("device_tracker.device_1") assert device_1 is not None - assert device_1.state == "not_home" + assert device_1.state == "home" async def test_dont_track_devices(hass):