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
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device)
self._attr_is_on = self.entity_description.get_ufp_value(self.device)
entity_description = self.entity_description
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
if self.entity_description.key == _KEY_DOOR and isinstance(self.device, Sensor):
self.entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get(
self.device.mount_type, BinarySensorDeviceClass.DOOR
if entity_description.key == _KEY_DOOR and isinstance(updated_device, Sensor):
entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get(
updated_device.mount_type, BinarySensorDeviceClass.DOOR
)
@ -615,7 +616,7 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity):
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
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
if not is_on:
self._event = None

View file

@ -183,7 +183,8 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity):
super()._async_update_device_from_protect(device)
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
)

View file

@ -151,23 +151,25 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
self._disable_stream = disable_stream
self._last_image: bytes | None = None
super().__init__(data, camera)
device = self.device
if self._secure:
self._attr_unique_id = f"{self.device.mac}_{self.channel.id}"
self._attr_name = f"{self.device.display_name} {self.channel.name}"
self._attr_unique_id = f"{device.mac}_{channel.id}"
self._attr_name = f"{device.display_name} {channel.name}"
else:
self._attr_unique_id = f"{self.device.mac}_{self.channel.id}_insecure"
self._attr_name = f"{self.device.display_name} {self.channel.name} Insecure"
self._attr_unique_id = f"{device.mac}_{channel.id}_insecure"
self._attr_name = f"{device.display_name} {channel.name} Insecure"
# only the default (first) channel is enabled by default
self._attr_entity_registry_enabled_default = is_default and secure
@callback
def _async_set_stream_source(self) -> None:
disable_stream = self._disable_stream
if not self.channel.is_rtsp_enabled:
channel = self.channel
if not channel.is_rtsp_enabled:
disable_stream = False
channel = self.channel
rtsp_url = channel.rtsps_url if self._secure else channel.rtsp_url
# _async_set_stream_source called by __init__
@ -182,27 +184,30 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device)
self.channel = self.device.channels[self.channel.id]
motion_enabled = self.device.recording_settings.enable_motion_detection
updated_device = self.device
channel = updated_device.channels[self.channel.id]
self.channel = channel
motion_enabled = updated_device.recording_settings.enable_motion_detection
self._attr_motion_detection_enabled = (
motion_enabled if motion_enabled is not None else True
)
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 = (
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
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._attr_extra_state_attributes = {
ATTR_WIDTH: self.channel.width,
ATTR_HEIGHT: self.channel.height,
ATTR_FPS: self.channel.fps,
ATTR_BITRATE: self.channel.bitrate,
ATTR_CHANNEL_ID: self.channel.id,
ATTR_WIDTH: channel.width,
ATTR_HEIGHT: channel.height,
ATTR_FPS: channel.fps,
ATTR_BITRATE: channel.bitrate,
ATTR_CHANNEL_ID: channel.id,
}
async def async_camera_image(

View file

@ -297,10 +297,12 @@ class ProtectNVREntity(ProtectDeviceEntity):
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
if self.data.last_update_success:
self.device = self.data.api.bootstrap.nvr
data = self.data
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):
@ -317,24 +319,15 @@ class EventEntityMixin(ProtectDeviceEntity):
super().__init__(*args, **kwarg)
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
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)
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
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
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.device.light_device_settings.led_level
updated_device.light_device_settings.led_level
)
async def async_turn_on(self, **kwargs: Any) -> None:

View file

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

View file

@ -98,21 +98,22 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
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 (
self.device.talkback_stream is not None
and self.device.talkback_stream.is_running
updated_device.talkback_stream is not None
and updated_device.talkback_stream.is_running
):
self._attr_state = MediaPlayerState.PLAYING
else:
self._attr_state = MediaPlayerState.IDLE
is_connected = self.data.last_update_success and (
self.device.state == StateType.CONNECTED
or (not self.device.is_adopted_by_us and self.device.can_adopt)
updated_device.state == StateType.CONNECTED
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:
"""Set volume level, range 0..1."""

View file

@ -3,7 +3,6 @@ from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
import logging
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 None
def get_is_on(self, obj: T) -> bool:
def get_is_on(self, event: Event | None) -> bool:
"""Return value if event is active."""
event = self.get_event_obj(obj)
if event is None:
return False
@ -88,17 +85,7 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
value = now > event.start
if value and event.end is not None and now > event.end:
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

View file

@ -356,15 +356,15 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity):
@callback
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
super()._async_update_device_from_protect(device)
entity_description = self.entity_description
# entities with categories are not exposed for voice
# and safe to update dynamically
if (
self.entity_description.entity_category is not None
and self.entity_description.ufp_options_fn is not None
entity_description.entity_category is not None
and entity_description.ufp_options_fn is not None
):
_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()

View file

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