From 6d600423cf7564cb67c8a8c0f5519d6ebab9f0a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Jul 2024 20:18:53 -0500 Subject: [PATCH 1/2] Cache unifi device_tracker properties that never change --- homeassistant/components/unifi/device_tracker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index a1014bfd184..800730f3b49 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -5,6 +5,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping from dataclasses import dataclass from datetime import timedelta +from functools import cached_property import logging from typing import Any @@ -261,17 +262,17 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): """Return the primary ip address of the device.""" return self.entity_description.ip_address_fn(self.hub.api, self._obj_id) - @property + @cached_property def mac_address(self) -> str: """Return the mac address of the device.""" return self._obj_id - @property + @cached_property def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" return SourceType.ROUTER - @property + @cached_property def unique_id(self) -> str: """Return a unique ID.""" return self._attr_unique_id From cf7d63b5c58154a3063fc83011246ba6f98888a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 26 Jul 2024 20:37:38 -0500 Subject: [PATCH 2/2] Small speedups to unifi - Use a set for event_is_on to avoid linear search - Avoid many duplicate property lookups --- homeassistant/components/unifi/button.py | 2 +- .../components/unifi/device_tracker.py | 35 ++++++++++--------- homeassistant/components/unifi/entity.py | 13 +++---- homeassistant/components/unifi/image.py | 4 +-- homeassistant/components/unifi/sensor.py | 2 +- homeassistant/components/unifi/switch.py | 13 +++---- homeassistant/components/unifi/update.py | 6 ++-- 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/unifi/button.py b/homeassistant/components/unifi/button.py index 716d3734953..c53f8be147f 100644 --- a/homeassistant/components/unifi/button.py +++ b/homeassistant/components/unifi/button.py @@ -149,7 +149,7 @@ class UnifiButtonEntity(UnifiEntity[HandlerT, ApiItemT], ButtonEntity): async def async_press(self) -> None: """Press the button.""" - await self.entity_description.control_fn(self.hub.api, self._obj_id) + await self.entity_description.control_fn(self.api, self._obj_id) @callback def async_update_state(self, event: ItemEvent, obj_id: str) -> None: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index a1014bfd184..90926cc295a 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -152,7 +152,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiTrackerEntityDescription, ...] = ( allowed_fn=async_client_allowed_fn, api_handler_fn=lambda api: api.clients, device_info_fn=lambda api, obj_id: None, - event_is_on=(WIRED_CONNECTION + WIRELESS_CONNECTION), + event_is_on=set(WIRED_CONNECTION + WIRELESS_CONNECTION), event_to_subscribe=( WIRED_CONNECTION + WIRED_DISCONNECTION @@ -225,7 +225,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): entity_description: UnifiTrackerEntityDescription - _event_is_on: tuple[EventKey, ...] + _event_is_on: set[EventKey] _ignore_events: bool _is_connected: bool @@ -236,7 +236,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): Initiate is_connected. """ description = self.entity_description - self._event_is_on = description.event_is_on or () + self._event_is_on = description.event_is_on or set() self._ignore_events = False self._is_connected = description.is_connected_fn(self.hub, self._obj_id) if self.is_connected: @@ -254,12 +254,12 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): @property def hostname(self) -> str | None: """Return hostname of the device.""" - return self.entity_description.hostname_fn(self.hub.api, self._obj_id) + return self.entity_description.hostname_fn(self.api, self._obj_id) @property def ip_address(self) -> str | None: """Return the primary ip address of the device.""" - return self.entity_description.ip_address_fn(self.hub.api, self._obj_id) + return self.entity_description.ip_address_fn(self.api, self._obj_id) @property def mac_address(self) -> str: @@ -292,42 +292,45 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): Schedule new heartbeat check if connected. """ description = self.entity_description + hub = self.hub - if event == ItemEvent.CHANGED: + if event is ItemEvent.CHANGED: # Prioritize normal data updates over events self._ignore_events = True - elif event == ItemEvent.ADDED and not self.available: + elif event is ItemEvent.ADDED and not self.available: # From unifi.entity.async_signal_reachable_callback # Controller connection state has changed and entity is unavailable # Cancel heartbeat - self.hub.remove_heartbeat(self.unique_id) + hub.remove_heartbeat(self.unique_id) return - if is_connected := description.is_connected_fn(self.hub, self._obj_id): + obj_id = self._obj_id + if is_connected := description.is_connected_fn(hub, obj_id): self._is_connected = is_connected self.hub.update_heartbeat( self.unique_id, - dt_util.utcnow() - + description.heartbeat_timedelta_fn(self.hub, self._obj_id), + dt_util.utcnow() + description.heartbeat_timedelta_fn(hub, obj_id), ) @callback def async_event_callback(self, event: Event) -> None: """Event subscription callback.""" - if event.mac != self._obj_id or self._ignore_events: + obj_id = self._obj_id + if event.mac != obj_id or self._ignore_events: return + hub = self.hub if event.key in self._event_is_on: - self.hub.remove_heartbeat(self.unique_id) + hub.remove_heartbeat(self.unique_id) self._is_connected = True self.async_write_ha_state() return - self.hub.update_heartbeat( + hub.update_heartbeat( self.unique_id, dt_util.utcnow() - + self.entity_description.heartbeat_timedelta_fn(self.hub, self._obj_id), + + self.entity_description.heartbeat_timedelta_fn(hub, obj_id), ) async def async_added_to_hass(self) -> None: @@ -352,7 +355,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity): if self.entity_description.key != "Client device scanner": return None - client = self.entity_description.object_fn(self.hub.api, self._obj_id) + client = self.entity_description.object_fn(self.api, self._obj_id) raw = client.raw attributes_to_check = CLIENT_STATIC_ATTRIBUTES diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index e162b32ba42..1f9d5b304bc 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -120,7 +120,7 @@ class UnifiEntityDescription(EntityDescription, Generic[HandlerT, ApiItemT]): # Optional constants has_entity_name = True # Part of EntityDescription """Has entity name defaults to true.""" - event_is_on: tuple[EventKey, ...] | None = None + event_is_on: set[EventKey] | None = None """Which UniFi events should be used to consider state 'on'.""" event_to_subscribe: tuple[EventKey, ...] | None = None """Which UniFi events to listen on.""" @@ -143,6 +143,7 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): """Set up UniFi switch entity.""" self._obj_id = obj_id self.hub = hub + self.api = hub.api self.entity_description = description hub.entity_loader.known_objects.add((description.key, obj_id)) @@ -154,14 +155,14 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): self._attr_should_poll = description.should_poll self._attr_unique_id = description.unique_id_fn(hub, obj_id) - obj = description.object_fn(self.hub.api, obj_id) + obj = description.object_fn(self.api, obj_id) self._attr_name = description.name_fn(obj) self.async_initiate_state() async def async_added_to_hass(self) -> None: """Register callbacks.""" description = self.entity_description - handler = description.api_handler_fn(self.hub.api) + handler = description.api_handler_fn(self.api) @callback def unregister_object() -> None: @@ -201,7 +202,7 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): # Subscribe to events if defined if description.event_to_subscribe is not None: self.async_on_remove( - self.hub.api.events.subscribe( + self.api.events.subscribe( self.async_event_callback, description.event_to_subscribe, ) @@ -210,8 +211,8 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]): @callback def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: """Update the entity state.""" - if event == ItemEvent.DELETED and obj_id == self._obj_id: - self.hass.async_create_task(self.remove_item({self._obj_id})) + if event is ItemEvent.DELETED and obj_id == self._obj_id: + self.hass.async_create_task(self.remove_item({obj_id})) return description = self.entity_description diff --git a/homeassistant/components/unifi/image.py b/homeassistant/components/unifi/image.py index bbc20e2b06b..426f2ce2884 100644 --- a/homeassistant/components/unifi/image.py +++ b/homeassistant/components/unifi/image.py @@ -97,7 +97,7 @@ class UnifiImageEntity(UnifiEntity[HandlerT, ApiItemT], ImageEntity): """Return bytes of image.""" if self.current_image is None: description = self.entity_description - obj = description.object_fn(self.hub.api, self._obj_id) + obj = description.object_fn(self.api, self._obj_id) self.current_image = description.image_fn(self.hub, obj) return self.current_image @@ -105,7 +105,7 @@ class UnifiImageEntity(UnifiEntity[HandlerT, ApiItemT], ImageEntity): def async_update_state(self, event: ItemEvent, obj_id: str) -> None: """Update entity state.""" description = self.entity_description - obj = description.object_fn(self.hub.api, self._obj_id) + obj = description.object_fn(self.api, self._obj_id) if (value := description.value_fn(obj)) != self.previous_value: self.previous_value = value self.current_image = None diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 071230a9652..bb974864f60 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -490,7 +490,7 @@ class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity): Update native_value. """ description = self.entity_description - obj = description.object_fn(self.hub.api, self._obj_id) + obj = description.object_fn(self.api, self._obj_id) # Update the value only if value is considered to have changed relative to its previous state if description.value_changed_fn( self.native_value, (value := description.value_fn(self.hub, obj)) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index be475803f7e..ef30abb9349 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -11,7 +11,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any +from typing import TYPE_CHECKING, Any import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -189,7 +189,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = ( api_handler_fn=lambda api: api.clients, control_fn=async_block_client_control_fn, device_info_fn=async_client_device_info_fn, - event_is_on=CLIENT_UNBLOCKED, + event_is_on=set(CLIENT_UNBLOCKED), event_to_subscribe=CLIENT_BLOCKED + CLIENT_UNBLOCKED, is_on_fn=lambda hub, client: not client.blocked, object_fn=lambda api, obj_id: api.clients[obj_id], @@ -342,7 +342,7 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity): return description = self.entity_description - obj = description.object_fn(self.hub.api, self._obj_id) + obj = description.object_fn(self.api, self._obj_id) if (is_on := description.is_on_fn(self.hub, obj)) != self.is_on: self._attr_is_on = is_on @@ -353,8 +353,9 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity): return description = self.entity_description - assert isinstance(description.event_to_subscribe, tuple) - assert isinstance(description.event_is_on, tuple) + if TYPE_CHECKING: + assert description.event_to_subscribe is not None + assert description.event_is_on is not None if event.key in description.event_to_subscribe: self._attr_is_on = event.key in description.event_is_on @@ -367,7 +368,7 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity): if self.entity_description.custom_subscribe is not None: self.async_on_remove( - self.entity_description.custom_subscribe(self.hub.api)( + self.entity_description.custom_subscribe(self.api)( self.async_signalling_callback, ItemEvent.CHANGED ), ) diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index b3cfc6f1c66..65202045a05 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -96,7 +96,7 @@ class UnifiDeviceUpdateEntity(UnifiEntity[_HandlerT, _DataT], UpdateEntity): self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" - await self.entity_description.control_fn(self.hub.api, self._obj_id) + await self.entity_description.control_fn(self.api, self._obj_id) @callback def async_update_state(self, event: ItemEvent, obj_id: str) -> None: @@ -106,7 +106,7 @@ class UnifiDeviceUpdateEntity(UnifiEntity[_HandlerT, _DataT], UpdateEntity): """ description = self.entity_description - obj = description.object_fn(self.hub.api, self._obj_id) - self._attr_in_progress = description.state_fn(self.hub.api, obj) + obj = description.object_fn(self.api, self._obj_id) + self._attr_in_progress = description.state_fn(self.api, obj) self._attr_installed_version = obj.version self._attr_latest_version = obj.upgrade_to_firmware or obj.version