Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
J. Nick Koston
12fa24b756
Merge branch 'unifi_no_change' into unifi_combi 2024-07-26 20:39:26 -05:00
J. Nick Koston
cf7d63b5c5
Small speedups to unifi
- Use a set for event_is_on to avoid linear search
- Avoid many duplicate property lookups
2024-07-26 20:37:38 -05:00
J. Nick Koston
6d600423cf
Cache unifi device_tracker properties that never change 2024-07-26 20:18:53 -05:00
7 changed files with 44 additions and 38 deletions

View file

@ -149,7 +149,7 @@ class UnifiButtonEntity(UnifiEntity[HandlerT, ApiItemT], ButtonEntity):
async def async_press(self) -> None: async def async_press(self) -> None:
"""Press the button.""" """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 @callback
def async_update_state(self, event: ItemEvent, obj_id: str) -> None: def async_update_state(self, event: ItemEvent, obj_id: str) -> None:

View file

@ -5,6 +5,7 @@ from __future__ import annotations
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from functools import cached_property
import logging import logging
from typing import Any from typing import Any
@ -152,7 +153,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiTrackerEntityDescription, ...] = (
allowed_fn=async_client_allowed_fn, allowed_fn=async_client_allowed_fn,
api_handler_fn=lambda api: api.clients, api_handler_fn=lambda api: api.clients,
device_info_fn=lambda api, obj_id: None, 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=( event_to_subscribe=(
WIRED_CONNECTION WIRED_CONNECTION
+ WIRED_DISCONNECTION + WIRED_DISCONNECTION
@ -225,7 +226,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity):
entity_description: UnifiTrackerEntityDescription entity_description: UnifiTrackerEntityDescription
_event_is_on: tuple[EventKey, ...] _event_is_on: set[EventKey]
_ignore_events: bool _ignore_events: bool
_is_connected: bool _is_connected: bool
@ -236,7 +237,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity):
Initiate is_connected. Initiate is_connected.
""" """
description = self.entity_description 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._ignore_events = False
self._is_connected = description.is_connected_fn(self.hub, self._obj_id) self._is_connected = description.is_connected_fn(self.hub, self._obj_id)
if self.is_connected: if self.is_connected:
@ -254,24 +255,24 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity):
@property @property
def hostname(self) -> str | None: def hostname(self) -> str | None:
"""Return hostname of the device.""" """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 @property
def ip_address(self) -> str | None: def ip_address(self) -> str | None:
"""Return the primary ip address of the device.""" """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 @cached_property
def mac_address(self) -> str: def mac_address(self) -> str:
"""Return the mac address of the device.""" """Return the mac address of the device."""
return self._obj_id return self._obj_id
@property @cached_property
def source_type(self) -> SourceType: def source_type(self) -> SourceType:
"""Return the source type, eg gps or router, of the device.""" """Return the source type, eg gps or router, of the device."""
return SourceType.ROUTER return SourceType.ROUTER
@property @cached_property
def unique_id(self) -> str: def unique_id(self) -> str:
"""Return a unique ID.""" """Return a unique ID."""
return self._attr_unique_id return self._attr_unique_id
@ -292,42 +293,45 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity):
Schedule new heartbeat check if connected. Schedule new heartbeat check if connected.
""" """
description = self.entity_description description = self.entity_description
hub = self.hub
if event == ItemEvent.CHANGED: if event is ItemEvent.CHANGED:
# Prioritize normal data updates over events # Prioritize normal data updates over events
self._ignore_events = True 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 # From unifi.entity.async_signal_reachable_callback
# Controller connection state has changed and entity is unavailable # Controller connection state has changed and entity is unavailable
# Cancel heartbeat # Cancel heartbeat
self.hub.remove_heartbeat(self.unique_id) hub.remove_heartbeat(self.unique_id)
return 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._is_connected = is_connected
self.hub.update_heartbeat( self.hub.update_heartbeat(
self.unique_id, self.unique_id,
dt_util.utcnow() dt_util.utcnow() + description.heartbeat_timedelta_fn(hub, obj_id),
+ description.heartbeat_timedelta_fn(self.hub, self._obj_id),
) )
@callback @callback
def async_event_callback(self, event: Event) -> None: def async_event_callback(self, event: Event) -> None:
"""Event subscription callback.""" """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 return
hub = self.hub
if event.key in self._event_is_on: 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._is_connected = True
self.async_write_ha_state() self.async_write_ha_state()
return return
self.hub.update_heartbeat( hub.update_heartbeat(
self.unique_id, self.unique_id,
dt_util.utcnow() 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: async def async_added_to_hass(self) -> None:
@ -352,7 +356,7 @@ class UnifiScannerEntity(UnifiEntity[HandlerT, ApiItemT], ScannerEntity):
if self.entity_description.key != "Client device scanner": if self.entity_description.key != "Client device scanner":
return None 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 raw = client.raw
attributes_to_check = CLIENT_STATIC_ATTRIBUTES attributes_to_check = CLIENT_STATIC_ATTRIBUTES

View file

@ -120,7 +120,7 @@ class UnifiEntityDescription(EntityDescription, Generic[HandlerT, ApiItemT]):
# Optional constants # Optional constants
has_entity_name = True # Part of EntityDescription has_entity_name = True # Part of EntityDescription
"""Has entity name defaults to true.""" """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'.""" """Which UniFi events should be used to consider state 'on'."""
event_to_subscribe: tuple[EventKey, ...] | None = None event_to_subscribe: tuple[EventKey, ...] | None = None
"""Which UniFi events to listen on.""" """Which UniFi events to listen on."""
@ -143,6 +143,7 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]):
"""Set up UniFi switch entity.""" """Set up UniFi switch entity."""
self._obj_id = obj_id self._obj_id = obj_id
self.hub = hub self.hub = hub
self.api = hub.api
self.entity_description = description self.entity_description = description
hub.entity_loader.known_objects.add((description.key, obj_id)) 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_should_poll = description.should_poll
self._attr_unique_id = description.unique_id_fn(hub, obj_id) 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._attr_name = description.name_fn(obj)
self.async_initiate_state() self.async_initiate_state()
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register callbacks.""" """Register callbacks."""
description = self.entity_description description = self.entity_description
handler = description.api_handler_fn(self.hub.api) handler = description.api_handler_fn(self.api)
@callback @callback
def unregister_object() -> None: def unregister_object() -> None:
@ -201,7 +202,7 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]):
# Subscribe to events if defined # Subscribe to events if defined
if description.event_to_subscribe is not None: if description.event_to_subscribe is not None:
self.async_on_remove( self.async_on_remove(
self.hub.api.events.subscribe( self.api.events.subscribe(
self.async_event_callback, self.async_event_callback,
description.event_to_subscribe, description.event_to_subscribe,
) )
@ -210,8 +211,8 @@ class UnifiEntity(Entity, Generic[HandlerT, ApiItemT]):
@callback @callback
def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None:
"""Update the entity state.""" """Update the entity state."""
if event == ItemEvent.DELETED and obj_id == self._obj_id: if event is ItemEvent.DELETED and obj_id == self._obj_id:
self.hass.async_create_task(self.remove_item({self._obj_id})) self.hass.async_create_task(self.remove_item({obj_id}))
return return
description = self.entity_description description = self.entity_description

View file

@ -97,7 +97,7 @@ class UnifiImageEntity(UnifiEntity[HandlerT, ApiItemT], ImageEntity):
"""Return bytes of image.""" """Return bytes of image."""
if self.current_image is None: if self.current_image is None:
description = self.entity_description 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) self.current_image = description.image_fn(self.hub, obj)
return self.current_image 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: def async_update_state(self, event: ItemEvent, obj_id: str) -> None:
"""Update entity state.""" """Update entity state."""
description = self.entity_description 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: if (value := description.value_fn(obj)) != self.previous_value:
self.previous_value = value self.previous_value = value
self.current_image = None self.current_image = None

View file

@ -490,7 +490,7 @@ class UnifiSensorEntity(UnifiEntity[HandlerT, ApiItemT], SensorEntity):
Update native_value. Update native_value.
""" """
description = self.entity_description 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 # Update the value only if value is considered to have changed relative to its previous state
if description.value_changed_fn( if description.value_changed_fn(
self.native_value, (value := description.value_fn(self.hub, obj)) self.native_value, (value := description.value_fn(self.hub, obj))

View file

@ -11,7 +11,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import TYPE_CHECKING, Any
import aiounifi import aiounifi
from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.api_handlers import ItemEvent
@ -189,7 +189,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = (
api_handler_fn=lambda api: api.clients, api_handler_fn=lambda api: api.clients,
control_fn=async_block_client_control_fn, control_fn=async_block_client_control_fn,
device_info_fn=async_client_device_info_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, event_to_subscribe=CLIENT_BLOCKED + CLIENT_UNBLOCKED,
is_on_fn=lambda hub, client: not client.blocked, is_on_fn=lambda hub, client: not client.blocked,
object_fn=lambda api, obj_id: api.clients[obj_id], object_fn=lambda api, obj_id: api.clients[obj_id],
@ -342,7 +342,7 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity):
return return
description = self.entity_description 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: if (is_on := description.is_on_fn(self.hub, obj)) != self.is_on:
self._attr_is_on = is_on self._attr_is_on = is_on
@ -353,8 +353,9 @@ class UnifiSwitchEntity(UnifiEntity[HandlerT, ApiItemT], SwitchEntity):
return return
description = self.entity_description description = self.entity_description
assert isinstance(description.event_to_subscribe, tuple) if TYPE_CHECKING:
assert isinstance(description.event_is_on, tuple) assert description.event_to_subscribe is not None
assert description.event_is_on is not None
if event.key in description.event_to_subscribe: if event.key in description.event_to_subscribe:
self._attr_is_on = event.key in description.event_is_on 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: if self.entity_description.custom_subscribe is not None:
self.async_on_remove( 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 self.async_signalling_callback, ItemEvent.CHANGED
), ),
) )

View file

@ -96,7 +96,7 @@ class UnifiDeviceUpdateEntity(UnifiEntity[_HandlerT, _DataT], UpdateEntity):
self, version: str | None, backup: bool, **kwargs: Any self, version: str | None, backup: bool, **kwargs: Any
) -> None: ) -> None:
"""Install an update.""" """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 @callback
def async_update_state(self, event: ItemEvent, obj_id: str) -> None: 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 description = self.entity_description
obj = description.object_fn(self.hub.api, self._obj_id) obj = description.object_fn(self.api, self._obj_id)
self._attr_in_progress = description.state_fn(self.hub.api, obj) self._attr_in_progress = description.state_fn(self.api, obj)
self._attr_installed_version = obj.version self._attr_installed_version = obj.version
self._attr_latest_version = obj.upgrade_to_firmware or obj.version self._attr_latest_version = obj.upgrade_to_firmware or obj.version