Reduce unifiprotect update overhead (#96626)
This commit is contained in:
parent
cde1903e8b
commit
f2556df7db
10 changed files with 73 additions and 111 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue