Reduce unifiprotect update overhead (#96626)

This commit is contained in:
J. Nick Koston 2023-07-16 06:24:27 -10:00 committed by GitHub
parent cde1903e8b
commit f2556df7db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 73 additions and 111 deletions

View File

@ -556,12 +556,13 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
entity_description = self.entity_description
self._attr_is_on = self.entity_description.get_ufp_value(self.device) updated_device = self.device
self._attr_is_on = entity_description.get_ufp_value(updated_device)
# UP Sense can be any of the 3 contact sensor device classes # UP Sense can be any of the 3 contact sensor device classes
if self.entity_description.key == _KEY_DOOR and isinstance(self.device, Sensor): if entity_description.key == _KEY_DOOR and isinstance(updated_device, Sensor):
self.entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get( entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get(
self.device.mount_type, BinarySensorDeviceClass.DOOR updated_device.mount_type, BinarySensorDeviceClass.DOOR
) )
@ -615,7 +616,7 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity):
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
is_on = self.entity_description.get_is_on(device) is_on = self.entity_description.get_is_on(self._event)
self._attr_is_on: bool | None = is_on self._attr_is_on: bool | None = is_on
if not is_on: if not is_on:
self._event = None self._event = None

View File

@ -183,7 +183,8 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity):
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
if self.entity_description.key == KEY_ADOPT: if self.entity_description.key == KEY_ADOPT:
self._attr_available = self.device.can_adopt and self.device.can_create( device = self.device
self._attr_available = device.can_adopt and device.can_create(
self.data.api.bootstrap.auth_user self.data.api.bootstrap.auth_user
) )

View File

@ -151,23 +151,25 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
self._disable_stream = disable_stream self._disable_stream = disable_stream
self._last_image: bytes | None = None self._last_image: bytes | None = None
super().__init__(data, camera) super().__init__(data, camera)
device = self.device
if self._secure: if self._secure:
self._attr_unique_id = f"{self.device.mac}_{self.channel.id}" self._attr_unique_id = f"{device.mac}_{channel.id}"
self._attr_name = f"{self.device.display_name} {self.channel.name}" self._attr_name = f"{device.display_name} {channel.name}"
else: else:
self._attr_unique_id = f"{self.device.mac}_{self.channel.id}_insecure" self._attr_unique_id = f"{device.mac}_{channel.id}_insecure"
self._attr_name = f"{self.device.display_name} {self.channel.name} Insecure" self._attr_name = f"{device.display_name} {channel.name} Insecure"
# only the default (first) channel is enabled by default # only the default (first) channel is enabled by default
self._attr_entity_registry_enabled_default = is_default and secure self._attr_entity_registry_enabled_default = is_default and secure
@callback @callback
def _async_set_stream_source(self) -> None: def _async_set_stream_source(self) -> None:
disable_stream = self._disable_stream disable_stream = self._disable_stream
if not self.channel.is_rtsp_enabled: channel = self.channel
if not channel.is_rtsp_enabled:
disable_stream = False disable_stream = False
channel = self.channel
rtsp_url = channel.rtsps_url if self._secure else channel.rtsp_url rtsp_url = channel.rtsps_url if self._secure else channel.rtsp_url
# _async_set_stream_source called by __init__ # _async_set_stream_source called by __init__
@ -182,27 +184,30 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
self.channel = self.device.channels[self.channel.id] updated_device = self.device
motion_enabled = self.device.recording_settings.enable_motion_detection channel = updated_device.channels[self.channel.id]
self.channel = channel
motion_enabled = updated_device.recording_settings.enable_motion_detection
self._attr_motion_detection_enabled = ( self._attr_motion_detection_enabled = (
motion_enabled if motion_enabled is not None else True motion_enabled if motion_enabled is not None else True
) )
self._attr_is_recording = ( self._attr_is_recording = (
self.device.state == StateType.CONNECTED and self.device.is_recording updated_device.state == StateType.CONNECTED and updated_device.is_recording
) )
is_connected = ( is_connected = (
self.data.last_update_success and self.device.state == StateType.CONNECTED self.data.last_update_success
and updated_device.state == StateType.CONNECTED
) )
# some cameras have detachable lens that could cause the camera to be offline # some cameras have detachable lens that could cause the camera to be offline
self._attr_available = is_connected and self.device.is_video_ready self._attr_available = is_connected and updated_device.is_video_ready
self._async_set_stream_source() self._async_set_stream_source()
self._attr_extra_state_attributes = { self._attr_extra_state_attributes = {
ATTR_WIDTH: self.channel.width, ATTR_WIDTH: channel.width,
ATTR_HEIGHT: self.channel.height, ATTR_HEIGHT: channel.height,
ATTR_FPS: self.channel.fps, ATTR_FPS: channel.fps,
ATTR_BITRATE: self.channel.bitrate, ATTR_BITRATE: channel.bitrate,
ATTR_CHANNEL_ID: self.channel.id, ATTR_CHANNEL_ID: channel.id,
} }
async def async_camera_image( async def async_camera_image(

View File

@ -297,10 +297,12 @@ class ProtectNVREntity(ProtectDeviceEntity):
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
if self.data.last_update_success: data = self.data
self.device = self.data.api.bootstrap.nvr last_update_success = data.last_update_success
if last_update_success:
self.device = data.api.bootstrap.nvr
self._attr_available = self.data.last_update_success self._attr_available = last_update_success
class EventEntityMixin(ProtectDeviceEntity): class EventEntityMixin(ProtectDeviceEntity):
@ -317,24 +319,15 @@ class EventEntityMixin(ProtectDeviceEntity):
super().__init__(*args, **kwarg) super().__init__(*args, **kwarg)
self._event: Event | None = None self._event: Event | None = None
@callback
def _async_event_extra_attrs(self) -> dict[str, Any]:
attrs: dict[str, Any] = {}
if self._event is None:
return attrs
attrs[ATTR_EVENT_ID] = self._event.id
attrs[ATTR_EVENT_SCORE] = self._event.score
return attrs
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
event = self.entity_description.get_event_obj(device)
if event is not None:
self._attr_extra_state_attributes = {
ATTR_EVENT_ID: event.id,
ATTR_EVENT_SCORE: event.score,
}
else:
self._attr_extra_state_attributes = {}
self._event = event
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
self._event = self.entity_description.get_event_obj(device)
attrs = self.extra_state_attributes or {}
self._attr_extra_state_attributes = {
**attrs,
**self._async_event_extra_attrs(),
}

View File

@ -73,9 +73,10 @@ class ProtectLight(ProtectDeviceEntity, LightEntity):
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
self._attr_is_on = self.device.is_light_on updated_device = self.device
self._attr_is_on = updated_device.is_light_on
self._attr_brightness = unifi_brightness_to_hass( self._attr_brightness = unifi_brightness_to_hass(
self.device.light_device_settings.led_level updated_device.light_device_settings.led_level
) )
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:

View File

@ -73,18 +73,19 @@ class ProtectLock(ProtectDeviceEntity, LockEntity):
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
lock_status = self.device.lock_status
self._attr_is_locked = False self._attr_is_locked = False
self._attr_is_locking = False self._attr_is_locking = False
self._attr_is_unlocking = False self._attr_is_unlocking = False
self._attr_is_jammed = False self._attr_is_jammed = False
if self.device.lock_status == LockStatusType.CLOSED: if lock_status == LockStatusType.CLOSED:
self._attr_is_locked = True self._attr_is_locked = True
elif self.device.lock_status == LockStatusType.CLOSING: elif lock_status == LockStatusType.CLOSING:
self._attr_is_locking = True self._attr_is_locking = True
elif self.device.lock_status == LockStatusType.OPENING: elif lock_status == LockStatusType.OPENING:
self._attr_is_unlocking = True self._attr_is_unlocking = True
elif self.device.lock_status in ( elif lock_status in (
LockStatusType.FAILED_WHILE_CLOSING, LockStatusType.FAILED_WHILE_CLOSING,
LockStatusType.FAILED_WHILE_OPENING, LockStatusType.FAILED_WHILE_OPENING,
LockStatusType.JAMMED_WHILE_CLOSING, LockStatusType.JAMMED_WHILE_CLOSING,
@ -92,7 +93,7 @@ class ProtectLock(ProtectDeviceEntity, LockEntity):
): ):
self._attr_is_jammed = True self._attr_is_jammed = True
# lock is not fully initialized yet # lock is not fully initialized yet
elif self.device.lock_status != LockStatusType.OPEN: elif lock_status != LockStatusType.OPEN:
self._attr_available = False self._attr_available = False
async def async_unlock(self, **kwargs: Any) -> None: async def async_unlock(self, **kwargs: Any) -> None:

View File

@ -98,21 +98,22 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
self._attr_volume_level = float(self.device.speaker_settings.volume / 100) updated_device = self.device
self._attr_volume_level = float(updated_device.speaker_settings.volume / 100)
if ( if (
self.device.talkback_stream is not None updated_device.talkback_stream is not None
and self.device.talkback_stream.is_running and updated_device.talkback_stream.is_running
): ):
self._attr_state = MediaPlayerState.PLAYING self._attr_state = MediaPlayerState.PLAYING
else: else:
self._attr_state = MediaPlayerState.IDLE self._attr_state = MediaPlayerState.IDLE
is_connected = self.data.last_update_success and ( is_connected = self.data.last_update_success and (
self.device.state == StateType.CONNECTED updated_device.state == StateType.CONNECTED
or (not self.device.is_adopted_by_us and self.device.can_adopt) or (not updated_device.is_adopted_by_us and updated_device.can_adopt)
) )
self._attr_available = is_connected and self.device.feature_flags.has_speaker self._attr_available = is_connected and updated_device.feature_flags.has_speaker
async def async_set_volume_level(self, volume: float) -> None: async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""

View File

@ -3,7 +3,6 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta
from enum import Enum from enum import Enum
import logging import logging
from typing import Any, Generic, TypeVar, cast from typing import Any, Generic, TypeVar, cast
@ -77,10 +76,8 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
return cast(Event, get_nested_attr(obj, self.ufp_event_obj)) return cast(Event, get_nested_attr(obj, self.ufp_event_obj))
return None return None
def get_is_on(self, obj: T) -> bool: def get_is_on(self, event: Event | None) -> bool:
"""Return value if event is active.""" """Return value if event is active."""
event = self.get_event_obj(obj)
if event is None: if event is None:
return False return False
@ -88,17 +85,7 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
value = now > event.start value = now > event.start
if value and event.end is not None and now > event.end: if value and event.end is not None and now > event.end:
value = False value = False
# only log if the recent ended recently
if event.end + timedelta(seconds=10) < now:
_LOGGER.debug(
"%s (%s): end ended at %s",
self.name,
obj.mac,
event.end.isoformat(),
)
if value:
_LOGGER.debug("%s (%s): value is on", self.name, obj.mac)
return value return value

View File

@ -356,15 +356,15 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity):
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
entity_description = self.entity_description
# entities with categories are not exposed for voice # entities with categories are not exposed for voice
# and safe to update dynamically # and safe to update dynamically
if ( if (
self.entity_description.entity_category is not None entity_description.entity_category is not None
and self.entity_description.ufp_options_fn is not None and entity_description.ufp_options_fn is not None
): ):
_LOGGER.debug( _LOGGER.debug(
"Updating dynamic select options for %s", self.entity_description.name "Updating dynamic select options for %s", entity_description.name
) )
self._async_set_options() self._async_set_options()

View File

@ -710,15 +710,6 @@ class ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity):
entity_description: ProtectSensorEntityDescription entity_description: ProtectSensorEntityDescription
def __init__(
self,
data: ProtectData,
device: ProtectAdoptableDeviceModel,
description: ProtectSensorEntityDescription,
) -> None:
"""Initialize an UniFi Protect sensor."""
super().__init__(data, device, description)
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
@ -730,15 +721,6 @@ class ProtectNVRSensor(ProtectNVREntity, SensorEntity):
entity_description: ProtectSensorEntityDescription entity_description: ProtectSensorEntityDescription
def __init__(
self,
data: ProtectData,
device: NVR,
description: ProtectSensorEntityDescription,
) -> None:
"""Initialize an UniFi Protect sensor."""
super().__init__(data, device, description)
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device) super()._async_update_device_from_protect(device)
@ -750,32 +732,22 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity):
entity_description: ProtectSensorEventEntityDescription entity_description: ProtectSensorEventEntityDescription
def __init__(
self,
data: ProtectData,
device: ProtectAdoptableDeviceModel,
description: ProtectSensorEventEntityDescription,
) -> None:
"""Initialize an UniFi Protect sensor."""
super().__init__(data, device, description)
@callback @callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
# do not call ProtectDeviceSensor method since we want event to get value here # do not call ProtectDeviceSensor method since we want event to get value here
EventEntityMixin._async_update_device_from_protect(self, device) EventEntityMixin._async_update_device_from_protect(self, device)
is_on = self.entity_description.get_is_on(device) event = self._event
entity_description = self.entity_description
is_on = entity_description.get_is_on(event)
is_license_plate = ( is_license_plate = (
self.entity_description.ufp_event_obj == "last_license_plate_detect_event" entity_description.ufp_event_obj == "last_license_plate_detect_event"
) )
if ( if (
not is_on not is_on
or self._event is None or event is None
or ( or (
is_license_plate is_license_plate
and ( and (event.metadata is None or event.metadata.license_plate is None)
self._event.metadata is None
or self._event.metadata.license_plate is None
)
) )
): ):
self._attr_native_value = OBJECT_TYPE_NONE self._attr_native_value = OBJECT_TYPE_NONE
@ -785,6 +757,6 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity):
if is_license_plate: if is_license_plate:
# type verified above # type verified above
self._attr_native_value = self._event.metadata.license_plate.name # type: ignore[union-attr] self._attr_native_value = event.metadata.license_plate.name # type: ignore[union-attr]
else: else:
self._attr_native_value = self._event.smart_detect_types[0].value self._attr_native_value = event.smart_detect_types[0].value