Refactor EntityDescriptions for UniFi Protect (#63716)
This commit is contained in:
parent
b658c053ec
commit
d8ba90fb8a
13 changed files with 465 additions and 507 deletions
|
@ -4,7 +4,6 @@ from __future__ import annotations
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pyunifiprotect.data import NVR, Camera, Event, Light, Sensor
|
from pyunifiprotect.data import NVR, Camera, Event, Light, Sensor
|
||||||
|
|
||||||
|
@ -39,26 +38,21 @@ class ProtectBinaryEntityDescription(
|
||||||
):
|
):
|
||||||
"""Describes UniFi Protect Binary Sensor entity."""
|
"""Describes UniFi Protect Binary Sensor entity."""
|
||||||
|
|
||||||
|
ufp_last_trip_value: str | None = None
|
||||||
_KEY_DOORBELL = "doorbell"
|
|
||||||
_KEY_MOTION = "motion"
|
|
||||||
_KEY_DOOR = "door"
|
|
||||||
_KEY_DARK = "dark"
|
|
||||||
_KEY_BATTERY_LOW = "battery_low"
|
|
||||||
_KEY_DISK_HEALTH = "disk_health"
|
|
||||||
|
|
||||||
|
|
||||||
CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key=_KEY_DOORBELL,
|
key="doorbell",
|
||||||
name="Doorbell",
|
name="Doorbell",
|
||||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||||
icon="mdi:doorbell-video",
|
icon="mdi:doorbell-video",
|
||||||
ufp_required_field="feature_flags.has_chime",
|
ufp_required_field="feature_flags.has_chime",
|
||||||
ufp_value="is_ringing",
|
ufp_value="is_ringing",
|
||||||
|
ufp_last_trip_value="last_ring",
|
||||||
),
|
),
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key=_KEY_DARK,
|
key="dark",
|
||||||
name="Is Dark",
|
name="Is Dark",
|
||||||
icon="mdi:brightness-6",
|
icon="mdi:brightness-6",
|
||||||
ufp_value="is_dark",
|
ufp_value="is_dark",
|
||||||
|
@ -67,54 +61,58 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
|
|
||||||
LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key=_KEY_DARK,
|
key="dark",
|
||||||
name="Is Dark",
|
name="Is Dark",
|
||||||
icon="mdi:brightness-6",
|
icon="mdi:brightness-6",
|
||||||
ufp_value="is_dark",
|
ufp_value="is_dark",
|
||||||
),
|
),
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key=_KEY_MOTION,
|
key="motion",
|
||||||
name="Motion Detected",
|
name="Motion Detected",
|
||||||
device_class=BinarySensorDeviceClass.MOTION,
|
device_class=BinarySensorDeviceClass.MOTION,
|
||||||
ufp_value="is_pir_motion_detected",
|
ufp_value="is_pir_motion_detected",
|
||||||
|
ufp_last_trip_value="last_motion",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key=_KEY_DOOR,
|
key="door",
|
||||||
name="Door",
|
name="Door",
|
||||||
device_class=BinarySensorDeviceClass.DOOR,
|
device_class=BinarySensorDeviceClass.DOOR,
|
||||||
ufp_value="is_opened",
|
ufp_value="is_opened",
|
||||||
|
ufp_last_trip_value="open_status_changed_at",
|
||||||
),
|
),
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key=_KEY_BATTERY_LOW,
|
key="battery_low",
|
||||||
name="Battery low",
|
name="Battery low",
|
||||||
device_class=BinarySensorDeviceClass.BATTERY,
|
device_class=BinarySensorDeviceClass.BATTERY,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
ufp_value="battery_status.is_low",
|
ufp_value="battery_status.is_low",
|
||||||
),
|
),
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key=_KEY_MOTION,
|
key="motion",
|
||||||
name="Motion Detected",
|
name="Motion Detected",
|
||||||
device_class=BinarySensorDeviceClass.MOTION,
|
device_class=BinarySensorDeviceClass.MOTION,
|
||||||
ufp_value="is_motion_detected",
|
ufp_value="is_motion_detected",
|
||||||
|
ufp_last_trip_value="motion_detected_at",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
MOTION_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
MOTION_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key=_KEY_MOTION,
|
key="motion",
|
||||||
name="Motion",
|
name="Motion",
|
||||||
device_class=BinarySensorDeviceClass.MOTION,
|
device_class=BinarySensorDeviceClass.MOTION,
|
||||||
ufp_value="is_motion_detected",
|
ufp_value="is_motion_detected",
|
||||||
|
ufp_last_trip_value="last_motion",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
DISK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
DISK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key=_KEY_DISK_HEALTH,
|
key="disk_health",
|
||||||
name="Disk {index} Health",
|
name="Disk {index} Health",
|
||||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
@ -181,65 +179,30 @@ def _async_nvr_entities(
|
||||||
class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
|
class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
|
||||||
"""A UniFi Protect Device Binary Sensor."""
|
"""A UniFi Protect Device Binary Sensor."""
|
||||||
|
|
||||||
def __init__(
|
device: Camera | Light | Sensor
|
||||||
self,
|
entity_description: ProtectBinaryEntityDescription
|
||||||
data: ProtectData,
|
|
||||||
description: ProtectBinaryEntityDescription,
|
|
||||||
device: Camera | Light | Sensor | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the Binary Sensor."""
|
|
||||||
|
|
||||||
if device and not hasattr(self, "device"):
|
|
||||||
self.device: Camera | Light | Sensor = device
|
|
||||||
self.entity_description: ProtectBinaryEntityDescription = description
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _async_update_extra_attrs_from_protect(self) -> dict[str, Any]:
|
|
||||||
attrs: dict[str, Any] = {}
|
|
||||||
key = self.entity_description.key
|
|
||||||
|
|
||||||
if key == _KEY_DARK:
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
if isinstance(self.device, Camera):
|
|
||||||
if key == _KEY_DOORBELL:
|
|
||||||
attrs[ATTR_LAST_TRIP_TIME] = self.device.last_ring
|
|
||||||
elif key == _KEY_MOTION:
|
|
||||||
attrs[ATTR_LAST_TRIP_TIME] = self.device.last_motion
|
|
||||||
elif isinstance(self.device, Sensor):
|
|
||||||
if key in (_KEY_MOTION, _KEY_DOOR):
|
|
||||||
if key == _KEY_MOTION:
|
|
||||||
last_trip = self.device.motion_detected_at
|
|
||||||
else:
|
|
||||||
last_trip = self.device.open_status_changed_at
|
|
||||||
|
|
||||||
attrs[ATTR_LAST_TRIP_TIME] = last_trip
|
|
||||||
elif isinstance(self.device, Light):
|
|
||||||
if key == _KEY_MOTION:
|
|
||||||
attrs[ATTR_LAST_TRIP_TIME] = self.device.last_motion
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self) -> None:
|
def _async_update_device_from_protect(self) -> None:
|
||||||
super()._async_update_device_from_protect()
|
super()._async_update_device_from_protect()
|
||||||
|
|
||||||
assert self.entity_description.ufp_value is not None
|
self._attr_is_on = self.entity_description.get_ufp_value(self.device)
|
||||||
|
if self.entity_description.ufp_last_trip_value is not None:
|
||||||
self._attr_is_on = get_nested_attr(
|
last_trip = get_nested_attr(
|
||||||
self.device, self.entity_description.ufp_value
|
self.device, self.entity_description.ufp_last_trip_value
|
||||||
)
|
)
|
||||||
attrs = self.extra_state_attributes or {}
|
attrs = self.extra_state_attributes or {}
|
||||||
self._attr_extra_state_attributes = {
|
self._attr_extra_state_attributes = {
|
||||||
**attrs,
|
**attrs,
|
||||||
**self._async_update_extra_attrs_from_protect(),
|
ATTR_LAST_TRIP_TIME: last_trip,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
|
class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
|
||||||
"""A UniFi Protect NVR Disk Binary Sensor."""
|
"""A UniFi Protect NVR Disk Binary Sensor."""
|
||||||
|
|
||||||
|
entity_description: ProtectBinaryEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: ProtectData,
|
data: ProtectData,
|
||||||
|
@ -252,8 +215,7 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
|
||||||
description.key = f"{description.key}_{index}"
|
description.key = f"{description.key}_{index}"
|
||||||
description.name = (description.name or "{index}").format(index=index)
|
description.name = (description.name or "{index}").format(index=index)
|
||||||
self._index = index
|
self._index = index
|
||||||
self.entity_description: ProtectBinaryEntityDescription = description
|
super().__init__(data, device, description)
|
||||||
super().__init__(data, device)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self) -> None:
|
def _async_update_device_from_protect(self) -> None:
|
||||||
|
@ -271,15 +233,7 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
|
||||||
class ProtectEventBinarySensor(EventThumbnailMixin, ProtectDeviceBinarySensor):
|
class ProtectEventBinarySensor(EventThumbnailMixin, ProtectDeviceBinarySensor):
|
||||||
"""A UniFi Protect Device Binary Sensor with access tokens."""
|
"""A UniFi Protect Device Binary Sensor with access tokens."""
|
||||||
|
|
||||||
def __init__(
|
device: Camera
|
||||||
self,
|
|
||||||
data: ProtectData,
|
|
||||||
device: Camera,
|
|
||||||
description: ProtectBinaryEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Init a binary sensor that uses access tokens."""
|
|
||||||
self.device: Camera = device
|
|
||||||
super().__init__(data, description=description)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_get_event(self) -> Event | None:
|
def _async_get_event(self) -> Event | None:
|
||||||
|
|
|
@ -88,6 +88,8 @@ async def async_setup_entry(
|
||||||
class ProtectCamera(ProtectDeviceEntity, Camera):
|
class ProtectCamera(ProtectDeviceEntity, Camera):
|
||||||
"""A Ubiquiti UniFi Protect Camera."""
|
"""A Ubiquiti UniFi Protect Camera."""
|
||||||
|
|
||||||
|
device: UFPCamera
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: ProtectData,
|
data: ProtectData,
|
||||||
|
@ -98,12 +100,11 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
|
||||||
disable_stream: bool,
|
disable_stream: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an UniFi camera."""
|
"""Initialize an UniFi camera."""
|
||||||
self.device: UFPCamera = camera
|
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self._secure = secure
|
self._secure = secure
|
||||||
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)
|
super().__init__(data, camera)
|
||||||
|
|
||||||
if self._secure:
|
if self._secure:
|
||||||
self._attr_unique_id = f"{self.device.id}_{self.channel.id}"
|
self._attr_unique_id = f"{self.device.id}_{self.channel.id}"
|
||||||
|
|
|
@ -109,30 +109,26 @@ def async_all_device_entities(
|
||||||
class ProtectDeviceEntity(Entity):
|
class ProtectDeviceEntity(Entity):
|
||||||
"""Base class for UniFi protect entities."""
|
"""Base class for UniFi protect entities."""
|
||||||
|
|
||||||
|
device: ProtectAdoptableDeviceModel
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: ProtectData,
|
data: ProtectData,
|
||||||
device: ProtectAdoptableDeviceModel | None = None,
|
device: ProtectAdoptableDeviceModel,
|
||||||
description: EntityDescription | None = None,
|
description: EntityDescription | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.data: ProtectData = data
|
self.data: ProtectData = data
|
||||||
|
self.device = device
|
||||||
if device and not hasattr(self, "device"):
|
|
||||||
self.device: ProtectAdoptableDeviceModel = device
|
|
||||||
|
|
||||||
if description and not hasattr(self, "entity_description"):
|
|
||||||
self.entity_description = description
|
|
||||||
elif hasattr(self, "entity_description"):
|
|
||||||
description = self.entity_description
|
|
||||||
|
|
||||||
if description is None:
|
if description is None:
|
||||||
self._attr_unique_id = f"{self.device.id}"
|
self._attr_unique_id = f"{self.device.id}"
|
||||||
self._attr_name = f"{self.device.name}"
|
self._attr_name = f"{self.device.name}"
|
||||||
else:
|
else:
|
||||||
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{self.device.id}_{description.key}"
|
self._attr_unique_id = f"{self.device.id}_{description.key}"
|
||||||
name = description.name or ""
|
name = description.name or ""
|
||||||
self._attr_name = f"{self.device.name} {name.title()}"
|
self._attr_name = f"{self.device.name} {name.title()}"
|
||||||
|
@ -191,6 +187,9 @@ class ProtectDeviceEntity(Entity):
|
||||||
class ProtectNVREntity(ProtectDeviceEntity):
|
class ProtectNVREntity(ProtectDeviceEntity):
|
||||||
"""Base class for unifi protect entities."""
|
"""Base class for unifi protect entities."""
|
||||||
|
|
||||||
|
# separate subclass on purpose
|
||||||
|
device: NVR # type: ignore[assignment]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
entry: ProtectData,
|
entry: ProtectData,
|
||||||
|
@ -198,9 +197,7 @@ class ProtectNVREntity(ProtectDeviceEntity):
|
||||||
description: EntityDescription | None = None,
|
description: EntityDescription | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
# ProtectNVREntity is intentionally a separate base class
|
super().__init__(entry, device, description) # type: ignore[arg-type]
|
||||||
self.device: NVR = device # type: ignore
|
|
||||||
super().__init__(entry, description=description)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_set_device_info(self) -> None:
|
def _async_set_device_info(self) -> None:
|
||||||
|
@ -222,13 +219,12 @@ class ProtectNVREntity(ProtectDeviceEntity):
|
||||||
self._attr_available = self.data.last_update_success
|
self._attr_available = self.data.last_update_success
|
||||||
|
|
||||||
|
|
||||||
class AccessTokenMixin(Entity):
|
class AccessTokenMixin(ProtectDeviceEntity):
|
||||||
"""Adds access_token attribute and provides access tokens for use for anonymous views."""
|
"""Adds access_token attribute and provides access tokens for use for anonymous views."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def access_tokens(self) -> deque[str]:
|
def access_tokens(self) -> deque[str]:
|
||||||
"""Get valid access_tokens for current entity."""
|
"""Get valid access_tokens for current entity."""
|
||||||
assert isinstance(self, ProtectDeviceEntity)
|
|
||||||
return self.data.async_get_or_create_access_tokens(self.entity_id)
|
return self.data.async_get_or_create_access_tokens(self.entity_id)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -247,7 +243,6 @@ class AccessTokenMixin(Entity):
|
||||||
@callback
|
@callback
|
||||||
def async_cleanup_tokens(self) -> None:
|
def async_cleanup_tokens(self) -> None:
|
||||||
"""Clean up any remaining tokens on removal."""
|
"""Clean up any remaining tokens on removal."""
|
||||||
assert isinstance(self, ProtectDeviceEntity)
|
|
||||||
if self.entity_id in self.data.access_tokens:
|
if self.entity_id in self.data.access_tokens:
|
||||||
del self.data.access_tokens[self.entity_id]
|
del self.data.access_tokens[self.entity_id]
|
||||||
|
|
||||||
|
@ -307,8 +302,7 @@ class EventThumbnailMixin(AccessTokenMixin):
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self) -> None:
|
def _async_update_device_from_protect(self) -> None:
|
||||||
assert isinstance(self, ProtectDeviceEntity)
|
super()._async_update_device_from_protect()
|
||||||
super()._async_update_device_from_protect() # type: ignore
|
|
||||||
self._event = self._async_get_event()
|
self._event = self._async_get_event()
|
||||||
|
|
||||||
attrs = self.extra_state_attributes or {}
|
attrs = self.extra_state_attributes or {}
|
||||||
|
|
|
@ -56,18 +56,11 @@ def hass_to_unifi_brightness(value: int) -> int:
|
||||||
class ProtectLight(ProtectDeviceEntity, LightEntity):
|
class ProtectLight(ProtectDeviceEntity, LightEntity):
|
||||||
"""A Ubiquiti UniFi Protect Light Entity."""
|
"""A Ubiquiti UniFi Protect Light Entity."""
|
||||||
|
|
||||||
|
device: Light
|
||||||
|
|
||||||
_attr_icon = "mdi:spotlight-beam"
|
_attr_icon = "mdi:spotlight-beam"
|
||||||
_attr_supported_features = SUPPORT_BRIGHTNESS
|
_attr_supported_features = SUPPORT_BRIGHTNESS
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
data: ProtectData,
|
|
||||||
device: Light,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize an UniFi light."""
|
|
||||||
self.device: Light = device
|
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self) -> None:
|
def _async_update_device_from_protect(self) -> None:
|
||||||
super()._async_update_device_from_protect()
|
super()._async_update_device_from_protect()
|
||||||
|
|
|
@ -55,18 +55,22 @@ async def async_setup_entry(
|
||||||
class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
|
class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
|
||||||
"""A Ubiquiti UniFi Protect Speaker."""
|
"""A Ubiquiti UniFi Protect Speaker."""
|
||||||
|
|
||||||
|
device: Camera
|
||||||
|
entity_description: MediaPlayerEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: ProtectData,
|
data: ProtectData,
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an UniFi speaker."""
|
"""Initialize an UniFi speaker."""
|
||||||
|
super().__init__(
|
||||||
self.device: Camera = camera
|
data,
|
||||||
self.entity_description = MediaPlayerEntityDescription(
|
camera,
|
||||||
key="speaker", device_class=MediaPlayerDeviceClass.SPEAKER
|
MediaPlayerEntityDescription(
|
||||||
|
key="speaker", device_class=MediaPlayerDeviceClass.SPEAKER
|
||||||
|
),
|
||||||
)
|
)
|
||||||
super().__init__(data)
|
|
||||||
|
|
||||||
self._attr_name = f"{self.device.name} Speaker"
|
self._attr_name = f"{self.device.name} Speaker"
|
||||||
self._attr_supported_features = (
|
self._attr_supported_features = (
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
"""The unifiprotect integration models."""
|
"""The unifiprotect integration models."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pyunifiprotect.data import NVR, ProtectAdoptableDeviceModel
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
|
||||||
|
from .utils import get_nested_attr
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -10,3 +21,35 @@ class ProtectRequiredKeysMixin:
|
||||||
|
|
||||||
ufp_required_field: str | None = None
|
ufp_required_field: str | None = None
|
||||||
ufp_value: str | None = None
|
ufp_value: str | None = None
|
||||||
|
ufp_value_fn: Callable[[ProtectAdoptableDeviceModel | NVR], Any] | None = None
|
||||||
|
|
||||||
|
def get_ufp_value(self, obj: ProtectAdoptableDeviceModel | NVR) -> Any:
|
||||||
|
"""Return value from UniFi Protect device."""
|
||||||
|
if self.ufp_value is not None:
|
||||||
|
return get_nested_attr(obj, self.ufp_value)
|
||||||
|
if self.ufp_value_fn is not None:
|
||||||
|
return self.ufp_value_fn(obj)
|
||||||
|
|
||||||
|
# reminder for future that one is required
|
||||||
|
raise RuntimeError( # pragma: no cover
|
||||||
|
"`ufp_value` or `ufp_value_fn` is required"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProtectSetableKeysMixin(ProtectRequiredKeysMixin):
|
||||||
|
"""Mixin to for settable values."""
|
||||||
|
|
||||||
|
ufp_set_method: str | None = None
|
||||||
|
ufp_set_method_fn: Callable[
|
||||||
|
[ProtectAdoptableDeviceModel, Any], Coroutine[Any, Any, None]
|
||||||
|
] | None = None
|
||||||
|
|
||||||
|
async def ufp_set(self, obj: ProtectAdoptableDeviceModel, value: Any) -> None:
|
||||||
|
"""Set value for UniFi Protect device."""
|
||||||
|
assert isinstance(self, EntityDescription)
|
||||||
|
_LOGGER.debug("Setting %s to %s for %s", self.name, value, obj.name)
|
||||||
|
if self.ufp_set_method is not None:
|
||||||
|
await getattr(obj, self.ufp_set_method)(value)
|
||||||
|
elif self.ufp_set_method_fn is not None:
|
||||||
|
await self.ufp_set_method_fn(obj, value)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
from typing import Any
|
||||||
|
|
||||||
from pyunifiprotect.data.devices import Camera, Light
|
from pyunifiprotect.data.devices import Camera, Light
|
||||||
|
|
||||||
|
@ -16,17 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .data import ProtectData
|
from .data import ProtectData
|
||||||
from .entity import ProtectDeviceEntity, async_all_device_entities
|
from .entity import ProtectDeviceEntity, async_all_device_entities
|
||||||
from .models import ProtectRequiredKeysMixin
|
from .models import ProtectSetableKeysMixin
|
||||||
from .utils import get_nested_attr
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_KEY_WDR = "wdr_value"
|
|
||||||
_KEY_MIC_LEVEL = "mic_level"
|
|
||||||
_KEY_ZOOM_POS = "zoom_position"
|
|
||||||
_KEY_SENSITIVITY = "sensitivity"
|
|
||||||
_KEY_DURATION = "duration"
|
|
||||||
_KEY_CHIME = "chime_duration"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -36,19 +26,28 @@ class NumberKeysMixin:
|
||||||
ufp_max: int
|
ufp_max: int
|
||||||
ufp_min: int
|
ufp_min: int
|
||||||
ufp_step: int
|
ufp_step: int
|
||||||
ufp_set_function: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ProtectNumberEntityDescription(
|
class ProtectNumberEntityDescription(
|
||||||
ProtectRequiredKeysMixin, NumberEntityDescription, NumberKeysMixin
|
ProtectSetableKeysMixin, NumberEntityDescription, NumberKeysMixin
|
||||||
):
|
):
|
||||||
"""Describes UniFi Protect Number entity."""
|
"""Describes UniFi Protect Number entity."""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_pir_duration(obj: Any) -> int:
|
||||||
|
assert isinstance(obj, Light)
|
||||||
|
return int(obj.light_device_settings.pir_duration.total_seconds())
|
||||||
|
|
||||||
|
|
||||||
|
async def _set_pir_duration(obj: Any, value: float) -> None:
|
||||||
|
assert isinstance(obj, Light)
|
||||||
|
await obj.set_duration(timedelta(seconds=value))
|
||||||
|
|
||||||
|
|
||||||
CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
||||||
ProtectNumberEntityDescription(
|
ProtectNumberEntityDescription(
|
||||||
key=_KEY_WDR,
|
key="wdr_value",
|
||||||
name="Wide Dynamic Range",
|
name="Wide Dynamic Range",
|
||||||
icon="mdi:state-machine",
|
icon="mdi:state-machine",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
@ -57,10 +56,10 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
||||||
ufp_step=1,
|
ufp_step=1,
|
||||||
ufp_required_field="feature_flags.has_wdr",
|
ufp_required_field="feature_flags.has_wdr",
|
||||||
ufp_value="isp_settings.wdr",
|
ufp_value="isp_settings.wdr",
|
||||||
ufp_set_function="set_wdr_level",
|
ufp_set_method="set_wdr_level",
|
||||||
),
|
),
|
||||||
ProtectNumberEntityDescription(
|
ProtectNumberEntityDescription(
|
||||||
key=_KEY_MIC_LEVEL,
|
key="mic_level",
|
||||||
name="Microphone Level",
|
name="Microphone Level",
|
||||||
icon="mdi:microphone",
|
icon="mdi:microphone",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
@ -69,10 +68,10 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
||||||
ufp_step=1,
|
ufp_step=1,
|
||||||
ufp_required_field="feature_flags.has_mic",
|
ufp_required_field="feature_flags.has_mic",
|
||||||
ufp_value="mic_volume",
|
ufp_value="mic_volume",
|
||||||
ufp_set_function="set_mic_volume",
|
ufp_set_method="set_mic_volume",
|
||||||
),
|
),
|
||||||
ProtectNumberEntityDescription(
|
ProtectNumberEntityDescription(
|
||||||
key=_KEY_ZOOM_POS,
|
key="zoom_position",
|
||||||
name="Zoom Level",
|
name="Zoom Level",
|
||||||
icon="mdi:magnify-plus-outline",
|
icon="mdi:magnify-plus-outline",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
@ -81,10 +80,10 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
||||||
ufp_step=1,
|
ufp_step=1,
|
||||||
ufp_required_field="feature_flags.can_optical_zoom",
|
ufp_required_field="feature_flags.can_optical_zoom",
|
||||||
ufp_value="isp_settings.zoom_position",
|
ufp_value="isp_settings.zoom_position",
|
||||||
ufp_set_function="set_camera_zoom",
|
ufp_set_method="set_camera_zoom",
|
||||||
),
|
),
|
||||||
ProtectNumberEntityDescription(
|
ProtectNumberEntityDescription(
|
||||||
key=_KEY_CHIME,
|
key="duration",
|
||||||
name="Chime Duration",
|
name="Chime Duration",
|
||||||
icon="mdi:camera-timer",
|
icon="mdi:camera-timer",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
@ -93,13 +92,13 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
||||||
ufp_step=100,
|
ufp_step=100,
|
||||||
ufp_required_field="feature_flags.has_chime",
|
ufp_required_field="feature_flags.has_chime",
|
||||||
ufp_value="chime_duration",
|
ufp_value="chime_duration",
|
||||||
ufp_set_function="set_chime_duration",
|
ufp_set_method="set_chime_duration",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
||||||
ProtectNumberEntityDescription(
|
ProtectNumberEntityDescription(
|
||||||
key=_KEY_SENSITIVITY,
|
key="sensitivity",
|
||||||
name="Motion Sensitivity",
|
name="Motion Sensitivity",
|
||||||
icon="mdi:walk",
|
icon="mdi:walk",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
@ -108,10 +107,10 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
||||||
ufp_step=1,
|
ufp_step=1,
|
||||||
ufp_required_field=None,
|
ufp_required_field=None,
|
||||||
ufp_value="light_device_settings.pir_sensitivity",
|
ufp_value="light_device_settings.pir_sensitivity",
|
||||||
ufp_set_function="set_sensitivity",
|
ufp_set_method="set_sensitivity",
|
||||||
),
|
),
|
||||||
ProtectNumberEntityDescription(
|
ProtectNumberEntityDescription(
|
||||||
key=_KEY_DURATION,
|
key="duration",
|
||||||
name="Auto-shutoff Duration",
|
name="Auto-shutoff Duration",
|
||||||
icon="mdi:camera-timer",
|
icon="mdi:camera-timer",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
@ -119,8 +118,8 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
|
||||||
ufp_max=900,
|
ufp_max=900,
|
||||||
ufp_step=15,
|
ufp_step=15,
|
||||||
ufp_required_field=None,
|
ufp_required_field=None,
|
||||||
ufp_value="light_device_settings.pir_duration",
|
ufp_value_fn=_get_pir_duration,
|
||||||
ufp_set_function="set_duration",
|
ufp_set_method_fn=_set_pir_duration,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -145,6 +144,9 @@ async def async_setup_entry(
|
||||||
class ProtectNumbers(ProtectDeviceEntity, NumberEntity):
|
class ProtectNumbers(ProtectDeviceEntity, NumberEntity):
|
||||||
"""A UniFi Protect Number Entity."""
|
"""A UniFi Protect Number Entity."""
|
||||||
|
|
||||||
|
device: Camera | Light
|
||||||
|
entity_description: ProtectNumberEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: ProtectData,
|
data: ProtectData,
|
||||||
|
@ -152,9 +154,7 @@ class ProtectNumbers(ProtectDeviceEntity, NumberEntity):
|
||||||
description: ProtectNumberEntityDescription,
|
description: ProtectNumberEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Number Entities."""
|
"""Initialize the Number Entities."""
|
||||||
self.device: Camera | Light = device
|
super().__init__(data, device, description)
|
||||||
self.entity_description: ProtectNumberEntityDescription = description
|
|
||||||
super().__init__(data)
|
|
||||||
self._attr_max_value = self.entity_description.ufp_max
|
self._attr_max_value = self.entity_description.ufp_max
|
||||||
self._attr_min_value = self.entity_description.ufp_min
|
self._attr_min_value = self.entity_description.ufp_min
|
||||||
self._attr_step = self.entity_description.ufp_step
|
self._attr_step = self.entity_description.ufp_step
|
||||||
|
@ -162,30 +162,8 @@ class ProtectNumbers(ProtectDeviceEntity, NumberEntity):
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self) -> None:
|
def _async_update_device_from_protect(self) -> None:
|
||||||
super()._async_update_device_from_protect()
|
super()._async_update_device_from_protect()
|
||||||
|
self._attr_value = self.entity_description.get_ufp_value(self.device)
|
||||||
assert self.entity_description.ufp_value is not None
|
|
||||||
|
|
||||||
value: float | timedelta = get_nested_attr(
|
|
||||||
self.device, self.entity_description.ufp_value
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(value, timedelta):
|
|
||||||
self._attr_value = int(value.total_seconds())
|
|
||||||
else:
|
|
||||||
self._attr_value = value
|
|
||||||
|
|
||||||
async def async_set_value(self, value: float) -> None:
|
async def async_set_value(self, value: float) -> None:
|
||||||
"""Set new value."""
|
"""Set new value."""
|
||||||
function = self.entity_description.ufp_set_function
|
await self.entity_description.ufp_set(self.device, value)
|
||||||
_LOGGER.debug(
|
|
||||||
"Calling %s to set %s for %s",
|
|
||||||
function,
|
|
||||||
value,
|
|
||||||
self.device.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
set_value: float | timedelta = value
|
|
||||||
if self.entity_description.key == _KEY_DURATION:
|
|
||||||
set_value = timedelta(seconds=value)
|
|
||||||
|
|
||||||
await getattr(self.device, function)(set_value)
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
"""This component provides select entities for UniFi Protect."""
|
"""This component provides select entities for UniFi Protect."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from pyunifiprotect.api import ProtectApiClient
|
||||||
from pyunifiprotect.data import (
|
from pyunifiprotect.data import (
|
||||||
Camera,
|
Camera,
|
||||||
DoorbellMessageType,
|
DoorbellMessageType,
|
||||||
|
@ -14,11 +16,9 @@ from pyunifiprotect.data import (
|
||||||
Light,
|
Light,
|
||||||
LightModeEnableType,
|
LightModeEnableType,
|
||||||
LightModeType,
|
LightModeType,
|
||||||
Liveview,
|
|
||||||
RecordingMode,
|
RecordingMode,
|
||||||
Viewer,
|
Viewer,
|
||||||
)
|
)
|
||||||
from pyunifiprotect.data.devices import LCDMessage
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
|
@ -33,17 +33,10 @@ from homeassistant.util.dt import utcnow
|
||||||
from .const import ATTR_DURATION, ATTR_MESSAGE, DOMAIN, TYPE_EMPTY_VALUE
|
from .const import ATTR_DURATION, ATTR_MESSAGE, DOMAIN, TYPE_EMPTY_VALUE
|
||||||
from .data import ProtectData
|
from .data import ProtectData
|
||||||
from .entity import ProtectDeviceEntity, async_all_device_entities
|
from .entity import ProtectDeviceEntity, async_all_device_entities
|
||||||
from .models import ProtectRequiredKeysMixin
|
from .models import ProtectSetableKeysMixin
|
||||||
from .utils import get_nested_attr
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
_KEY_IR = "infrared"
|
|
||||||
_KEY_REC_MODE = "recording_mode"
|
|
||||||
_KEY_VIEWER = "viewer"
|
|
||||||
_KEY_LIGHT_MOTION = "light_motion"
|
_KEY_LIGHT_MOTION = "light_motion"
|
||||||
_KEY_DOORBELL_TEXT = "doorbell_text"
|
|
||||||
_KEY_PAIRED_CAMERA = "paired_camera"
|
|
||||||
|
|
||||||
INFRARED_MODES = [
|
INFRARED_MODES = [
|
||||||
{"id": IRLEDMode.AUTO.value, "name": "Auto"},
|
{"id": IRLEDMode.AUTO.value, "name": "Auto"},
|
||||||
|
@ -93,27 +86,112 @@ SET_DOORBELL_LCD_MESSAGE_SCHEMA = vol.Schema(
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ProtectSelectEntityDescription(ProtectRequiredKeysMixin, SelectEntityDescription):
|
class ProtectSelectEntityDescription(ProtectSetableKeysMixin, SelectEntityDescription):
|
||||||
"""Describes UniFi Protect Select entity."""
|
"""Describes UniFi Protect Select entity."""
|
||||||
|
|
||||||
ufp_options: list[dict[str, Any]] | None = None
|
ufp_options: list[dict[str, Any]] | None = None
|
||||||
|
ufp_options_callable: Callable[
|
||||||
|
[ProtectApiClient], list[dict[str, Any]]
|
||||||
|
] | None = None
|
||||||
ufp_enum_type: type[Enum] | None = None
|
ufp_enum_type: type[Enum] | None = None
|
||||||
ufp_set_function: str | None = None
|
ufp_set_method: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_viewer_options(api: ProtectApiClient) -> list[dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{"id": item.id, "name": item.name} for item in api.bootstrap.liveviews.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]:
|
||||||
|
default_message = api.bootstrap.nvr.doorbell_settings.default_message_text
|
||||||
|
messages = api.bootstrap.nvr.doorbell_settings.all_messages
|
||||||
|
built_messages = ({"id": item.type.value, "name": item.text} for item in messages)
|
||||||
|
|
||||||
|
return [
|
||||||
|
{"id": "", "name": f"Default Message ({default_message})"},
|
||||||
|
*built_messages,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]:
|
||||||
|
options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}]
|
||||||
|
for camera in api.bootstrap.cameras.values():
|
||||||
|
options.append({"id": camera.id, "name": camera.name})
|
||||||
|
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def _get_viewer_current(obj: Any) -> str:
|
||||||
|
assert isinstance(obj, Viewer)
|
||||||
|
return obj.liveview_id
|
||||||
|
|
||||||
|
|
||||||
|
def _get_light_motion_current(obj: Any) -> str:
|
||||||
|
assert isinstance(obj, Light)
|
||||||
|
# a bit of extra to allow On Motion Always/Dark
|
||||||
|
if (
|
||||||
|
obj.light_mode_settings.mode == LightModeType.MOTION
|
||||||
|
and obj.light_mode_settings.enable_at == LightModeEnableType.DARK
|
||||||
|
):
|
||||||
|
return f"{LightModeType.MOTION.value}Dark"
|
||||||
|
return obj.light_mode_settings.mode.value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_doorbell_current(obj: Any) -> str | None:
|
||||||
|
assert isinstance(obj, Camera)
|
||||||
|
if obj.lcd_message is None:
|
||||||
|
return None
|
||||||
|
return obj.lcd_message.text
|
||||||
|
|
||||||
|
|
||||||
|
async def _set_light_mode(obj: Any, mode: str) -> None:
|
||||||
|
assert isinstance(obj, Light)
|
||||||
|
lightmode, timing = LIGHT_MODE_TO_SETTINGS[mode]
|
||||||
|
await obj.set_light_settings(
|
||||||
|
LightModeType(lightmode),
|
||||||
|
enable_at=None if timing is None else LightModeEnableType(timing),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _set_paired_camera(obj: Any, camera_id: str) -> None:
|
||||||
|
assert isinstance(obj, Light)
|
||||||
|
if camera_id == TYPE_EMPTY_VALUE:
|
||||||
|
camera: Camera | None = None
|
||||||
|
else:
|
||||||
|
camera = obj.api.bootstrap.cameras.get(camera_id)
|
||||||
|
await obj.set_paired_camera(camera)
|
||||||
|
|
||||||
|
|
||||||
|
async def _set_doorbell_message(obj: Any, message: str) -> None:
|
||||||
|
assert isinstance(obj, Camera)
|
||||||
|
if message.startswith(DoorbellMessageType.CUSTOM_MESSAGE.value):
|
||||||
|
await obj.set_lcd_text(DoorbellMessageType.CUSTOM_MESSAGE, text=message)
|
||||||
|
elif message == TYPE_EMPTY_VALUE:
|
||||||
|
await obj.set_lcd_text(None)
|
||||||
|
else:
|
||||||
|
await obj.set_lcd_text(DoorbellMessageType(message))
|
||||||
|
|
||||||
|
|
||||||
|
async def _set_liveview(obj: Any, liveview_id: str) -> None:
|
||||||
|
assert isinstance(obj, Viewer)
|
||||||
|
liveview = obj.api.bootstrap.liveviews[liveview_id]
|
||||||
|
await obj.set_liveview(liveview)
|
||||||
|
|
||||||
|
|
||||||
CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
||||||
ProtectSelectEntityDescription(
|
ProtectSelectEntityDescription(
|
||||||
key=_KEY_REC_MODE,
|
key="recording_mode",
|
||||||
name="Recording Mode",
|
name="Recording Mode",
|
||||||
icon="mdi:video-outline",
|
icon="mdi:video-outline",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_options=DEVICE_RECORDING_MODES,
|
ufp_options=DEVICE_RECORDING_MODES,
|
||||||
ufp_enum_type=RecordingMode,
|
ufp_enum_type=RecordingMode,
|
||||||
ufp_value="recording_settings.mode",
|
ufp_value="recording_settings.mode",
|
||||||
ufp_set_function="set_recording_mode",
|
ufp_set_method="set_recording_mode",
|
||||||
),
|
),
|
||||||
ProtectSelectEntityDescription(
|
ProtectSelectEntityDescription(
|
||||||
key=_KEY_IR,
|
key="infrared",
|
||||||
name="Infrared Mode",
|
name="Infrared Mode",
|
||||||
icon="mdi:circle-opacity",
|
icon="mdi:circle-opacity",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
@ -121,16 +199,18 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
||||||
ufp_options=INFRARED_MODES,
|
ufp_options=INFRARED_MODES,
|
||||||
ufp_enum_type=IRLEDMode,
|
ufp_enum_type=IRLEDMode,
|
||||||
ufp_value="isp_settings.ir_led_mode",
|
ufp_value="isp_settings.ir_led_mode",
|
||||||
ufp_set_function="set_ir_led_model",
|
ufp_set_method="set_ir_led_model",
|
||||||
),
|
),
|
||||||
ProtectSelectEntityDescription(
|
ProtectSelectEntityDescription(
|
||||||
key=_KEY_DOORBELL_TEXT,
|
key="doorbell_text",
|
||||||
name="Doorbell Text",
|
name="Doorbell Text",
|
||||||
icon="mdi:card-text",
|
icon="mdi:card-text",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
device_class=DEVICE_CLASS_LCD_MESSAGE,
|
device_class=DEVICE_CLASS_LCD_MESSAGE,
|
||||||
ufp_required_field="feature_flags.has_lcd_screen",
|
ufp_required_field="feature_flags.has_lcd_screen",
|
||||||
ufp_value="lcd_message",
|
ufp_value_fn=_get_doorbell_current,
|
||||||
|
ufp_options_callable=_get_doorbell_options,
|
||||||
|
ufp_set_method_fn=_set_doorbell_message,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -141,25 +221,29 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
||||||
icon="mdi:spotlight",
|
icon="mdi:spotlight",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_options=MOTION_MODE_TO_LIGHT_MODE,
|
ufp_options=MOTION_MODE_TO_LIGHT_MODE,
|
||||||
ufp_value="light_mode_settings.mode",
|
ufp_value_fn=_get_light_motion_current,
|
||||||
|
ufp_set_method_fn=_set_light_mode,
|
||||||
),
|
),
|
||||||
ProtectSelectEntityDescription(
|
ProtectSelectEntityDescription(
|
||||||
key=_KEY_PAIRED_CAMERA,
|
key="paired_camera",
|
||||||
name="Paired Camera",
|
name="Paired Camera",
|
||||||
icon="mdi:cctv",
|
icon="mdi:cctv",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_value="camera_id",
|
ufp_value="camera_id",
|
||||||
|
ufp_options_callable=_get_paired_camera_options,
|
||||||
|
ufp_set_method_fn=_set_paired_camera,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
|
||||||
ProtectSelectEntityDescription(
|
ProtectSelectEntityDescription(
|
||||||
key=_KEY_VIEWER,
|
key="viewer",
|
||||||
name="Liveview",
|
name="Liveview",
|
||||||
icon="mdi:view-dashboard",
|
icon="mdi:view-dashboard",
|
||||||
entity_category=None,
|
entity_category=None,
|
||||||
ufp_value="liveview",
|
ufp_options_callable=_get_viewer_options,
|
||||||
ufp_set_function="set_liveview",
|
ufp_value_fn=_get_viewer_current,
|
||||||
|
ufp_set_method_fn=_set_liveview,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -191,6 +275,9 @@ async def async_setup_entry(
|
||||||
class ProtectSelects(ProtectDeviceEntity, SelectEntity):
|
class ProtectSelects(ProtectDeviceEntity, SelectEntity):
|
||||||
"""A UniFi Protect Select Entity."""
|
"""A UniFi Protect Select Entity."""
|
||||||
|
|
||||||
|
device: Camera | Light | Viewer
|
||||||
|
entity_description: ProtectSelectEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: ProtectData,
|
data: ProtectData,
|
||||||
|
@ -198,66 +285,33 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity):
|
||||||
description: ProtectSelectEntityDescription,
|
description: ProtectSelectEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the unifi protect select entity."""
|
"""Initialize the unifi protect select entity."""
|
||||||
assert description.ufp_value is not None
|
super().__init__(data, device, description)
|
||||||
|
|
||||||
self.device: Camera | Light | Viewer = device
|
|
||||||
self.entity_description: ProtectSelectEntityDescription = description
|
|
||||||
super().__init__(data)
|
|
||||||
self._attr_name = f"{self.device.name} {self.entity_description.name}"
|
self._attr_name = f"{self.device.name} {self.entity_description.name}"
|
||||||
|
self._async_set_options()
|
||||||
options = description.ufp_options
|
|
||||||
if options is not None:
|
|
||||||
self._attr_options = [item["name"] for item in options]
|
|
||||||
self._hass_to_unifi_options: dict[str, Any] = {
|
|
||||||
item["name"]: item["id"] for item in options
|
|
||||||
}
|
|
||||||
self._unifi_to_hass_options: dict[Any, str] = {
|
|
||||||
item["id"]: item["name"] for item in options
|
|
||||||
}
|
|
||||||
self._async_set_dynamic_options()
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self) -> None:
|
def _async_update_device_from_protect(self) -> None:
|
||||||
super()._async_update_device_from_protect()
|
super()._async_update_device_from_protect()
|
||||||
|
|
||||||
# entities with categories are not exposed for voice and safe to update dynamically
|
# entities with categories are not exposed for voice and safe to update dynamically
|
||||||
if self.entity_description.entity_category is not None:
|
if (
|
||||||
|
self.entity_description.entity_category is not None
|
||||||
|
and self.entity_description.ufp_options_callable is not None
|
||||||
|
):
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Updating dynamic select options for %s", self.entity_description.name
|
"Updating dynamic select options for %s", self.entity_description.name
|
||||||
)
|
)
|
||||||
self._async_set_dynamic_options()
|
self._async_set_options()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_set_dynamic_options(self) -> None:
|
def _async_set_options(self) -> None:
|
||||||
"""Options that do not actually update dynamically.
|
"""Set options attributes from UniFi Protect device."""
|
||||||
|
|
||||||
This is due to possible downstream platforms dependencies on these options.
|
|
||||||
"""
|
|
||||||
if self.entity_description.ufp_options is not None:
|
if self.entity_description.ufp_options is not None:
|
||||||
return
|
options = self.entity_description.ufp_options
|
||||||
|
else:
|
||||||
if self.entity_description.key == _KEY_VIEWER:
|
assert self.entity_description.ufp_options_callable is not None
|
||||||
options = [
|
options = self.entity_description.ufp_options_callable(self.data.api)
|
||||||
{"id": item.id, "name": item.name}
|
|
||||||
for item in self.data.api.bootstrap.liveviews.values()
|
|
||||||
]
|
|
||||||
elif self.entity_description.key == _KEY_DOORBELL_TEXT:
|
|
||||||
default_message = (
|
|
||||||
self.data.api.bootstrap.nvr.doorbell_settings.default_message_text
|
|
||||||
)
|
|
||||||
messages = self.data.api.bootstrap.nvr.doorbell_settings.all_messages
|
|
||||||
built_messages = (
|
|
||||||
{"id": item.type.value, "name": item.text} for item in messages
|
|
||||||
)
|
|
||||||
|
|
||||||
options = [
|
|
||||||
{"id": "", "name": f"Default Message ({default_message})"},
|
|
||||||
*built_messages,
|
|
||||||
]
|
|
||||||
elif self.entity_description.key == _KEY_PAIRED_CAMERA:
|
|
||||||
options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}]
|
|
||||||
for camera in self.data.api.bootstrap.cameras.values():
|
|
||||||
options.append({"id": camera.id, "name": camera.name})
|
|
||||||
|
|
||||||
self._attr_options = [item["name"] for item in options]
|
self._attr_options = [item["name"] for item in options]
|
||||||
self._hass_to_unifi_options = {item["name"]: item["id"] for item in options}
|
self._hass_to_unifi_options = {item["name"]: item["id"] for item in options}
|
||||||
|
@ -267,79 +321,29 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity):
|
||||||
def current_option(self) -> str:
|
def current_option(self) -> str:
|
||||||
"""Return the current selected option."""
|
"""Return the current selected option."""
|
||||||
|
|
||||||
assert self.entity_description.ufp_value is not None
|
unifi_value = self.entity_description.get_ufp_value(self.device)
|
||||||
unifi_value = get_nested_attr(self.device, self.entity_description.ufp_value)
|
|
||||||
|
|
||||||
if unifi_value is None:
|
if unifi_value is None:
|
||||||
unifi_value = TYPE_EMPTY_VALUE
|
unifi_value = TYPE_EMPTY_VALUE
|
||||||
elif isinstance(unifi_value, Liveview):
|
|
||||||
unifi_value = unifi_value.id
|
|
||||||
elif self.entity_description.key == _KEY_LIGHT_MOTION:
|
|
||||||
assert isinstance(self.device, Light)
|
|
||||||
|
|
||||||
# a bit of extra to allow On Motion Always/Dark
|
|
||||||
if (
|
|
||||||
self.device.light_mode_settings.mode == LightModeType.MOTION
|
|
||||||
and self.device.light_mode_settings.enable_at
|
|
||||||
== LightModeEnableType.DARK
|
|
||||||
):
|
|
||||||
unifi_value = f"{LightModeType.MOTION.value}Dark"
|
|
||||||
elif self.entity_description.key == _KEY_DOORBELL_TEXT:
|
|
||||||
assert isinstance(unifi_value, LCDMessage)
|
|
||||||
return unifi_value.text
|
|
||||||
return self._unifi_to_hass_options.get(unifi_value, unifi_value)
|
return self._unifi_to_hass_options.get(unifi_value, unifi_value)
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Change the Select Entity Option."""
|
"""Change the Select Entity Option."""
|
||||||
|
|
||||||
if isinstance(self.device, Light):
|
# Light Motion is a bit different
|
||||||
if self.entity_description.key == _KEY_LIGHT_MOTION:
|
if self.entity_description.key == _KEY_LIGHT_MOTION:
|
||||||
lightmode, timing = LIGHT_MODE_TO_SETTINGS[option]
|
assert self.entity_description.ufp_set_method_fn is not None
|
||||||
_LOGGER.debug("Changing Light Mode to %s", option)
|
await self.entity_description.ufp_set_method_fn(self.device, option)
|
||||||
await self.device.set_light_settings(
|
return
|
||||||
LightModeType(lightmode),
|
|
||||||
enable_at=None if timing is None else LightModeEnableType(timing),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
unifi_value = self._hass_to_unifi_options[option]
|
|
||||||
if self.entity_description.key == _KEY_PAIRED_CAMERA:
|
|
||||||
if unifi_value == TYPE_EMPTY_VALUE:
|
|
||||||
unifi_value = None
|
|
||||||
camera = self.data.api.bootstrap.cameras.get(unifi_value)
|
|
||||||
await self.device.set_paired_camera(camera)
|
|
||||||
_LOGGER.debug("Changed Paired Camera to to: %s", option)
|
|
||||||
return
|
|
||||||
|
|
||||||
unifi_value = self._hass_to_unifi_options[option]
|
unifi_value = self._hass_to_unifi_options[option]
|
||||||
if isinstance(self.device, Camera):
|
|
||||||
if self.entity_description.key == _KEY_DOORBELL_TEXT:
|
|
||||||
if unifi_value.startswith(DoorbellMessageType.CUSTOM_MESSAGE.value):
|
|
||||||
await self.device.set_lcd_text(
|
|
||||||
DoorbellMessageType.CUSTOM_MESSAGE, text=option
|
|
||||||
)
|
|
||||||
elif unifi_value == TYPE_EMPTY_VALUE:
|
|
||||||
await self.device.set_lcd_text(None)
|
|
||||||
else:
|
|
||||||
await self.device.set_lcd_text(DoorbellMessageType(unifi_value))
|
|
||||||
|
|
||||||
_LOGGER.debug("Changed Doorbell LCD Text to: %s", option)
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.entity_description.ufp_enum_type is not None:
|
if self.entity_description.ufp_enum_type is not None:
|
||||||
unifi_value = self.entity_description.ufp_enum_type(unifi_value)
|
unifi_value = self.entity_description.ufp_enum_type(unifi_value)
|
||||||
elif self.entity_description.key == _KEY_VIEWER:
|
await self.entity_description.ufp_set(self.device, unifi_value)
|
||||||
unifi_value = self.data.api.bootstrap.liveviews[unifi_value]
|
|
||||||
|
|
||||||
_LOGGER.debug("%s set to: %s", self.entity_description.key, option)
|
|
||||||
assert self.entity_description.ufp_set_function
|
|
||||||
coro = getattr(self.device, self.entity_description.ufp_set_function)
|
|
||||||
await coro(unifi_value)
|
|
||||||
|
|
||||||
async def async_set_doorbell_message(self, message: str, duration: str) -> None:
|
async def async_set_doorbell_message(self, message: str, duration: str) -> None:
|
||||||
"""Set LCD Message on Doorbell display."""
|
"""Set LCD Message on Doorbell display."""
|
||||||
|
|
||||||
if self.entity_description.key != _KEY_DOORBELL_TEXT:
|
if self.entity_description.device_class != DEVICE_CLASS_LCD_MESSAGE:
|
||||||
raise HomeAssistantError("Not a doorbell text select entity")
|
raise HomeAssistantError("Not a doorbell text select entity")
|
||||||
|
|
||||||
assert isinstance(self.device, Camera)
|
assert isinstance(self.device, Camera)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ from homeassistant.const import (
|
||||||
TIME_SECONDS,
|
TIME_SECONDS,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity import Entity, EntityCategory
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
@ -40,7 +40,6 @@ from .entity import (
|
||||||
async_all_device_entities,
|
async_all_device_entities,
|
||||||
)
|
)
|
||||||
from .models import ProtectRequiredKeysMixin
|
from .models import ProtectRequiredKeysMixin
|
||||||
from .utils import get_nested_attr
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DETECTED_OBJECT_NONE = "none"
|
DETECTED_OBJECT_NONE = "none"
|
||||||
|
@ -53,50 +52,54 @@ class ProtectSensorEntityDescription(ProtectRequiredKeysMixin, SensorEntityDescr
|
||||||
|
|
||||||
precision: int | None = None
|
precision: int | None = None
|
||||||
|
|
||||||
|
def get_ufp_value(self, obj: ProtectAdoptableDeviceModel | NVR) -> Any:
|
||||||
|
"""Return value from UniFi Protect device."""
|
||||||
|
value = super().get_ufp_value(obj)
|
||||||
|
|
||||||
_KEY_UPTIME = "uptime"
|
if isinstance(value, float) and self.precision:
|
||||||
_KEY_BLE = "ble_signal"
|
value = round(value, self.precision)
|
||||||
_KEY_WIRED = "phy_rate"
|
return value
|
||||||
_KEY_WIFI = "wifi_signal"
|
|
||||||
|
|
||||||
_KEY_RX = "stats_rx"
|
|
||||||
_KEY_TX = "stats_tx"
|
|
||||||
_KEY_OLDEST = "oldest_recording"
|
|
||||||
_KEY_USED = "storage_used"
|
|
||||||
_KEY_WRITE_RATE = "write_rate"
|
|
||||||
_KEY_VOLTAGE = "voltage"
|
|
||||||
|
|
||||||
_KEY_BATTERY = "battery_level"
|
def _get_uptime(obj: ProtectAdoptableDeviceModel | NVR) -> datetime | None:
|
||||||
_KEY_LIGHT = "light_level"
|
if obj.up_since is None:
|
||||||
_KEY_HUMIDITY = "humidity_level"
|
return None
|
||||||
_KEY_TEMP = "temperature_level"
|
|
||||||
|
|
||||||
_KEY_CPU = "cpu_utilization"
|
# up_since can vary slightly over time
|
||||||
_KEY_CPU_TEMP = "cpu_temperature"
|
# truncate to ensure no extra state_change events fire
|
||||||
_KEY_MEMORY = "memory_utilization"
|
return obj.up_since.replace(second=0, microsecond=0)
|
||||||
_KEY_DISK = "storage_utilization"
|
|
||||||
_KEY_RECORD_ROTATE = "record_rotating"
|
|
||||||
_KEY_RECORD_TIMELAPSE = "record_timelapse"
|
def _get_nvr_recording_capacity(obj: Any) -> int:
|
||||||
_KEY_RECORD_DETECTIONS = "record_detections"
|
assert isinstance(obj, NVR)
|
||||||
_KEY_RES_HD = "resolution_HD"
|
|
||||||
_KEY_RES_4K = "resolution_4K"
|
if obj.storage_stats.capacity is None:
|
||||||
_KEY_RES_FREE = "resolution_free"
|
return 0
|
||||||
_KEY_CAPACITY = "record_capacity"
|
|
||||||
|
return int(obj.storage_stats.capacity.total_seconds())
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nvr_memory(obj: Any) -> float | None:
|
||||||
|
assert isinstance(obj, NVR)
|
||||||
|
|
||||||
|
memory = obj.system_info.memory
|
||||||
|
if memory.available is None or memory.total is None:
|
||||||
|
return None
|
||||||
|
return (1 - memory.available / memory.total) * 100
|
||||||
|
|
||||||
_KEY_OBJECT = "detected_object"
|
|
||||||
|
|
||||||
ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_UPTIME,
|
key="uptime",
|
||||||
name="Uptime",
|
name="Uptime",
|
||||||
icon="mdi:clock",
|
icon="mdi:clock",
|
||||||
device_class=SensorDeviceClass.TIMESTAMP,
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
ufp_value="up_since",
|
ufp_value_fn=_get_uptime,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_BLE,
|
key="ble_signal",
|
||||||
name="Bluetooth Signal Strength",
|
name="Bluetooth Signal Strength",
|
||||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
|
@ -107,7 +110,7 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ufp_required_field="bluetooth_connection_state.signal_strength",
|
ufp_required_field="bluetooth_connection_state.signal_strength",
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_WIRED,
|
key="phy_rate",
|
||||||
name="Link Speed",
|
name="Link Speed",
|
||||||
native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND,
|
native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
@ -117,7 +120,7 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ufp_required_field="wired_connection_state.phy_rate",
|
ufp_required_field="wired_connection_state.phy_rate",
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_WIFI,
|
key="wifi_signal",
|
||||||
name="WiFi Signal Strength",
|
name="WiFi Signal Strength",
|
||||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
|
@ -131,14 +134,14 @@ ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
|
|
||||||
CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_OLDEST,
|
key="oldest_recording",
|
||||||
name="Oldest Recording",
|
name="Oldest Recording",
|
||||||
device_class=SensorDeviceClass.TIMESTAMP,
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
ufp_value="stats.video.recording_start",
|
ufp_value="stats.video.recording_start",
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_USED,
|
key="storage_used",
|
||||||
name="Storage Used",
|
name="Storage Used",
|
||||||
native_unit_of_measurement=DATA_BYTES,
|
native_unit_of_measurement=DATA_BYTES,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
@ -146,7 +149,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ufp_value="stats.storage.used",
|
ufp_value="stats.storage.used",
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_WRITE_RATE,
|
key="write_rate",
|
||||||
name="Disk Write Rate",
|
name="Disk Write Rate",
|
||||||
native_unit_of_measurement=DATA_RATE_BYTES_PER_SECOND,
|
native_unit_of_measurement=DATA_RATE_BYTES_PER_SECOND,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
@ -155,7 +158,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
precision=2,
|
precision=2,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_VOLTAGE,
|
key="voltage",
|
||||||
name="Voltage",
|
name="Voltage",
|
||||||
device_class=SensorDeviceClass.VOLTAGE,
|
device_class=SensorDeviceClass.VOLTAGE,
|
||||||
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
|
||||||
|
@ -171,7 +174,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
|
|
||||||
CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_RX,
|
key="stats_rx",
|
||||||
name="Received Data",
|
name="Received Data",
|
||||||
native_unit_of_measurement=DATA_BYTES,
|
native_unit_of_measurement=DATA_BYTES,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
|
@ -180,7 +183,7 @@ CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ufp_value="stats.rx_bytes",
|
ufp_value="stats.rx_bytes",
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_TX,
|
key="stats_tx",
|
||||||
name="Transferred Data",
|
name="Transferred Data",
|
||||||
native_unit_of_measurement=DATA_BYTES,
|
native_unit_of_measurement=DATA_BYTES,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
|
@ -192,7 +195,7 @@ CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
|
|
||||||
SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_BATTERY,
|
key="battery_level",
|
||||||
name="Battery Level",
|
name="Battery Level",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
@ -201,7 +204,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ufp_value="battery_status.percentage",
|
ufp_value="battery_status.percentage",
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_LIGHT,
|
key="light_level",
|
||||||
name="Light Level",
|
name="Light Level",
|
||||||
native_unit_of_measurement=LIGHT_LUX,
|
native_unit_of_measurement=LIGHT_LUX,
|
||||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||||
|
@ -209,7 +212,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ufp_value="stats.light.value",
|
ufp_value="stats.light.value",
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_HUMIDITY,
|
key="humidity_level",
|
||||||
name="Humidity Level",
|
name="Humidity Level",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
@ -217,7 +220,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ufp_value="stats.humidity.value",
|
ufp_value="stats.humidity.value",
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_TEMP,
|
key="temperature_level",
|
||||||
name="Temperature",
|
name="Temperature",
|
||||||
native_unit_of_measurement=TEMP_CELSIUS,
|
native_unit_of_measurement=TEMP_CELSIUS,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
@ -228,15 +231,15 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
|
|
||||||
NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_UPTIME,
|
key="uptime",
|
||||||
name="Uptime",
|
name="Uptime",
|
||||||
icon="mdi:clock",
|
icon="mdi:clock",
|
||||||
device_class=SensorDeviceClass.TIMESTAMP,
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
ufp_value="up_since",
|
ufp_value_fn=_get_uptime,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_DISK,
|
key="storage_utilization",
|
||||||
name="Storage Utilization",
|
name="Storage Utilization",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:harddisk",
|
icon="mdi:harddisk",
|
||||||
|
@ -246,7 +249,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
precision=2,
|
precision=2,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_RECORD_TIMELAPSE,
|
key="record_rotating",
|
||||||
name="Type: Timelapse Video",
|
name="Type: Timelapse Video",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:server",
|
icon="mdi:server",
|
||||||
|
@ -256,7 +259,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
precision=2,
|
precision=2,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_RECORD_ROTATE,
|
key="record_timelapse",
|
||||||
name="Type: Continuous Video",
|
name="Type: Continuous Video",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:server",
|
icon="mdi:server",
|
||||||
|
@ -266,7 +269,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
precision=2,
|
precision=2,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_RECORD_DETECTIONS,
|
key="record_detections",
|
||||||
name="Type: Detections Video",
|
name="Type: Detections Video",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:server",
|
icon="mdi:server",
|
||||||
|
@ -276,7 +279,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
precision=2,
|
precision=2,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_RES_HD,
|
key="resolution_HD",
|
||||||
name="Resolution: HD Video",
|
name="Resolution: HD Video",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:cctv",
|
icon="mdi:cctv",
|
||||||
|
@ -286,7 +289,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
precision=2,
|
precision=2,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_RES_4K,
|
key="resolution_4K",
|
||||||
name="Resolution: 4K Video",
|
name="Resolution: 4K Video",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:cctv",
|
icon="mdi:cctv",
|
||||||
|
@ -296,7 +299,7 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
precision=2,
|
precision=2,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_RES_FREE,
|
key="resolution_free",
|
||||||
name="Resolution: Free Space",
|
name="Resolution: Free Space",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:cctv",
|
icon="mdi:cctv",
|
||||||
|
@ -306,19 +309,19 @@ NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
precision=2,
|
precision=2,
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_CAPACITY,
|
key="record_capacity",
|
||||||
name="Recording Capacity",
|
name="Recording Capacity",
|
||||||
native_unit_of_measurement=TIME_SECONDS,
|
native_unit_of_measurement=TIME_SECONDS,
|
||||||
icon="mdi:record-rec",
|
icon="mdi:record-rec",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
ufp_value="storage_stats.capacity",
|
ufp_value_fn=_get_nvr_recording_capacity,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_CPU,
|
key="cpu_utilization",
|
||||||
name="CPU Utilization",
|
name="CPU Utilization",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:speedometer",
|
icon="mdi:speedometer",
|
||||||
|
@ -328,7 +331,7 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ufp_value="system_info.cpu.average_load",
|
ufp_value="system_info.cpu.average_load",
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_CPU_TEMP,
|
key="cpu_temperature",
|
||||||
name="CPU Temperature",
|
name="CPU Temperature",
|
||||||
native_unit_of_measurement=TEMP_CELSIUS,
|
native_unit_of_measurement=TEMP_CELSIUS,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
@ -338,20 +341,21 @@ NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ufp_value="system_info.cpu.temperature",
|
ufp_value="system_info.cpu.temperature",
|
||||||
),
|
),
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_MEMORY,
|
key="memory_utilization",
|
||||||
name="Memory Utilization",
|
name="Memory Utilization",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
icon="mdi:memory",
|
icon="mdi:memory",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
ufp_value_fn=_get_nvr_memory,
|
||||||
precision=2,
|
precision=2,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
MOTION_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
MOTION_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
ProtectSensorEntityDescription(
|
ProtectSensorEntityDescription(
|
||||||
key=_KEY_OBJECT,
|
key="detected_object",
|
||||||
name="Detected Object",
|
name="Detected Object",
|
||||||
device_class=DEVICE_CLASS_DETECTION,
|
device_class=DEVICE_CLASS_DETECTION,
|
||||||
),
|
),
|
||||||
|
@ -411,28 +415,11 @@ def _async_nvr_entities(
|
||||||
return entities
|
return entities
|
||||||
|
|
||||||
|
|
||||||
class SensorValueMixin(Entity):
|
class ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity):
|
||||||
"""A mixin to provide sensor values."""
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _clean_sensor_value(self, value: Any) -> Any:
|
|
||||||
if isinstance(value, timedelta):
|
|
||||||
value = int(value.total_seconds())
|
|
||||||
elif isinstance(value, datetime):
|
|
||||||
# UniFi Protect value can vary slightly over time
|
|
||||||
# truncate to ensure no extra state_change events fire
|
|
||||||
value = value.replace(second=0, microsecond=0)
|
|
||||||
|
|
||||||
assert isinstance(self.entity_description, ProtectSensorEntityDescription)
|
|
||||||
if isinstance(value, float) and self.entity_description.precision:
|
|
||||||
value = round(value, self.entity_description.precision)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class ProtectDeviceSensor(SensorValueMixin, ProtectDeviceEntity, SensorEntity):
|
|
||||||
"""A Ubiquiti UniFi Protect Sensor."""
|
"""A Ubiquiti UniFi Protect Sensor."""
|
||||||
|
|
||||||
|
entity_description: ProtectSensorEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: ProtectData,
|
data: ProtectData,
|
||||||
|
@ -440,21 +427,15 @@ class ProtectDeviceSensor(SensorValueMixin, ProtectDeviceEntity, SensorEntity):
|
||||||
description: ProtectSensorEntityDescription,
|
description: ProtectSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an UniFi Protect sensor."""
|
"""Initialize an UniFi Protect sensor."""
|
||||||
self.entity_description: ProtectSensorEntityDescription = description
|
super().__init__(data, device, description)
|
||||||
super().__init__(data, device)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self) -> None:
|
def _async_update_device_from_protect(self) -> None:
|
||||||
super()._async_update_device_from_protect()
|
super()._async_update_device_from_protect()
|
||||||
|
self._attr_native_value = self.entity_description.get_ufp_value(self.device)
|
||||||
if self.entity_description.ufp_value is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
value = get_nested_attr(self.device, self.entity_description.ufp_value)
|
|
||||||
self._attr_native_value = self._clean_sensor_value(value)
|
|
||||||
|
|
||||||
|
|
||||||
class ProtectNVRSensor(SensorValueMixin, ProtectNVREntity, SensorEntity):
|
class ProtectNVRSensor(ProtectNVREntity, SensorEntity):
|
||||||
"""A Ubiquiti UniFi Protect Sensor."""
|
"""A Ubiquiti UniFi Protect Sensor."""
|
||||||
|
|
||||||
entity_description: ProtectSensorEntityDescription
|
entity_description: ProtectSensorEntityDescription
|
||||||
|
@ -466,38 +447,18 @@ class ProtectNVRSensor(SensorValueMixin, ProtectNVREntity, SensorEntity):
|
||||||
description: ProtectSensorEntityDescription,
|
description: ProtectSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an UniFi Protect sensor."""
|
"""Initialize an UniFi Protect sensor."""
|
||||||
self.entity_description = description
|
super().__init__(data, device, description)
|
||||||
super().__init__(data, device)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self) -> None:
|
def _async_update_device_from_protect(self) -> None:
|
||||||
super()._async_update_device_from_protect()
|
super()._async_update_device_from_protect()
|
||||||
|
self._attr_native_value = self.entity_description.get_ufp_value(self.device)
|
||||||
# _KEY_MEMORY
|
|
||||||
if self.entity_description.ufp_value is None:
|
|
||||||
memory = self.device.system_info.memory
|
|
||||||
if memory.available is None or memory.total is None:
|
|
||||||
self._attr_available = False
|
|
||||||
return
|
|
||||||
value = (1 - memory.available / memory.total) * 100
|
|
||||||
else:
|
|
||||||
value = get_nested_attr(self.device, self.entity_description.ufp_value)
|
|
||||||
|
|
||||||
self._attr_native_value = self._clean_sensor_value(value)
|
|
||||||
|
|
||||||
|
|
||||||
class ProtectEventSensor(EventThumbnailMixin, ProtectDeviceSensor):
|
class ProtectEventSensor(ProtectDeviceSensor, EventThumbnailMixin):
|
||||||
"""A UniFi Protect Device Sensor with access tokens."""
|
"""A UniFi Protect Device Sensor with access tokens."""
|
||||||
|
|
||||||
def __init__(
|
device: Camera
|
||||||
self,
|
|
||||||
data: ProtectData,
|
|
||||||
device: Camera,
|
|
||||||
description: ProtectSensorEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Init an sensor that uses access tokens."""
|
|
||||||
self.device: Camera = device
|
|
||||||
super().__init__(data, device, description)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_get_event(self) -> Event | None:
|
def _async_get_event(self) -> Event | None:
|
||||||
|
@ -515,8 +476,8 @@ class ProtectEventSensor(EventThumbnailMixin, ProtectDeviceSensor):
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self) -> None:
|
def _async_update_device_from_protect(self) -> None:
|
||||||
super()._async_update_device_from_protect()
|
# do not call ProtectDeviceSensor method since we want event to get value here
|
||||||
|
EventThumbnailMixin._async_update_device_from_protect(self)
|
||||||
if self._event is None:
|
if self._event is None:
|
||||||
self._attr_native_value = DETECTED_OBJECT_NONE
|
self._attr_native_value = DETECTED_OBJECT_NONE
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -17,70 +17,71 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .data import ProtectData
|
from .data import ProtectData
|
||||||
from .entity import ProtectDeviceEntity, async_all_device_entities
|
from .entity import ProtectDeviceEntity, async_all_device_entities
|
||||||
from .models import ProtectRequiredKeysMixin
|
from .models import ProtectSetableKeysMixin
|
||||||
from .utils import get_nested_attr
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ProtectSwitchEntityDescription(ProtectRequiredKeysMixin, SwitchEntityDescription):
|
class ProtectSwitchEntityDescription(ProtectSetableKeysMixin, SwitchEntityDescription):
|
||||||
"""Describes UniFi Protect Switch entity."""
|
"""Describes UniFi Protect Switch entity."""
|
||||||
|
|
||||||
ufp_set_function: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
_KEY_STATUS_LIGHT = "status_light"
|
|
||||||
_KEY_HDR_MODE = "hdr_mode"
|
|
||||||
_KEY_HIGH_FPS = "high_fps"
|
|
||||||
_KEY_PRIVACY_MODE = "privacy_mode"
|
_KEY_PRIVACY_MODE = "privacy_mode"
|
||||||
_KEY_SYSTEM_SOUNDS = "system_sounds"
|
|
||||||
_KEY_OSD_NAME = "osd_name"
|
|
||||||
_KEY_OSD_DATE = "osd_date"
|
def _get_is_highfps(obj: Any) -> bool:
|
||||||
_KEY_OSD_LOGO = "osd_logo"
|
assert isinstance(obj, Camera)
|
||||||
_KEY_OSD_BITRATE = "osd_bitrate"
|
return bool(obj.video_mode == VideoMode.HIGH_FPS)
|
||||||
_KEY_SMART_PERSON = "smart_person"
|
|
||||||
_KEY_SMART_VEHICLE = "smart_vehicle"
|
|
||||||
_KEY_SSH = "ssh"
|
async def _set_highfps(obj: Any, value: bool) -> None:
|
||||||
|
assert isinstance(obj, Camera)
|
||||||
|
if value:
|
||||||
|
await obj.set_video_mode(VideoMode.HIGH_FPS)
|
||||||
|
else:
|
||||||
|
await obj.set_video_mode(VideoMode.DEFAULT)
|
||||||
|
|
||||||
|
|
||||||
ALL_DEVICES_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
ALL_DEVICES_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_SSH,
|
key="ssh",
|
||||||
name="SSH Enabled",
|
name="SSH Enabled",
|
||||||
icon="mdi:lock",
|
icon="mdi:lock",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_value="is_ssh_enabled",
|
ufp_value="is_ssh_enabled",
|
||||||
ufp_set_function="set_ssh",
|
ufp_set_method="set_ssh",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_STATUS_LIGHT,
|
key="status_light",
|
||||||
name="Status Light On",
|
name="Status Light On",
|
||||||
icon="mdi:led-on",
|
icon="mdi:led-on",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_required_field="feature_flags.has_led_status",
|
ufp_required_field="feature_flags.has_led_status",
|
||||||
ufp_value="led_settings.is_enabled",
|
ufp_value="led_settings.is_enabled",
|
||||||
ufp_set_function="set_status_light",
|
ufp_set_method="set_status_light",
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_HDR_MODE,
|
key="hdr_mode",
|
||||||
name="HDR Mode",
|
name="HDR Mode",
|
||||||
icon="mdi:brightness-7",
|
icon="mdi:brightness-7",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_required_field="feature_flags.has_hdr",
|
ufp_required_field="feature_flags.has_hdr",
|
||||||
ufp_value="hdr_mode",
|
ufp_value="hdr_mode",
|
||||||
ufp_set_function="set_hdr",
|
ufp_set_method="set_hdr",
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_HIGH_FPS,
|
key="high_fps",
|
||||||
name="High FPS",
|
name="High FPS",
|
||||||
icon="mdi:video-high-definition",
|
icon="mdi:video-high-definition",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_required_field="feature_flags.has_highfps",
|
ufp_required_field="feature_flags.has_highfps",
|
||||||
ufp_value="video_mode",
|
ufp_value_fn=_get_is_highfps,
|
||||||
|
ufp_set_method_fn=_set_highfps,
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_PRIVACY_MODE,
|
key=_KEY_PRIVACY_MODE,
|
||||||
|
@ -91,75 +92,75 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
||||||
ufp_value="is_privacy_on",
|
ufp_value="is_privacy_on",
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_SYSTEM_SOUNDS,
|
key="system_sounds",
|
||||||
name="System Sounds",
|
name="System Sounds",
|
||||||
icon="mdi:speaker",
|
icon="mdi:speaker",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_required_field="feature_flags.has_speaker",
|
ufp_required_field="feature_flags.has_speaker",
|
||||||
ufp_value="speaker_settings.are_system_sounds_enabled",
|
ufp_value="speaker_settings.are_system_sounds_enabled",
|
||||||
ufp_set_function="set_system_sounds",
|
ufp_set_method="set_system_sounds",
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_OSD_NAME,
|
key="osd_name",
|
||||||
name="Overlay: Show Name",
|
name="Overlay: Show Name",
|
||||||
icon="mdi:fullscreen",
|
icon="mdi:fullscreen",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_value="osd_settings.is_name_enabled",
|
ufp_value="osd_settings.is_name_enabled",
|
||||||
ufp_set_function="set_osd_name",
|
ufp_set_method="set_osd_name",
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_OSD_DATE,
|
key="osd_date",
|
||||||
name="Overlay: Show Date",
|
name="Overlay: Show Date",
|
||||||
icon="mdi:fullscreen",
|
icon="mdi:fullscreen",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_value="osd_settings.is_date_enabled",
|
ufp_value="osd_settings.is_date_enabled",
|
||||||
ufp_set_function="set_osd_date",
|
ufp_set_method="set_osd_date",
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_OSD_LOGO,
|
key="osd_logo",
|
||||||
name="Overlay: Show Logo",
|
name="Overlay: Show Logo",
|
||||||
icon="mdi:fullscreen",
|
icon="mdi:fullscreen",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_value="osd_settings.is_logo_enabled",
|
ufp_value="osd_settings.is_logo_enabled",
|
||||||
ufp_set_function="set_osd_logo",
|
ufp_set_method="set_osd_logo",
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_OSD_BITRATE,
|
key="osd_bitrate",
|
||||||
name="Overlay: Show Bitrate",
|
name="Overlay: Show Bitrate",
|
||||||
icon="mdi:fullscreen",
|
icon="mdi:fullscreen",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_value="osd_settings.is_debug_enabled",
|
ufp_value="osd_settings.is_debug_enabled",
|
||||||
ufp_set_function="set_osd_bitrate",
|
ufp_set_method="set_osd_bitrate",
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_SMART_PERSON,
|
key="smart_person",
|
||||||
name="Detections: Person",
|
name="Detections: Person",
|
||||||
icon="mdi:walk",
|
icon="mdi:walk",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_required_field="feature_flags.has_smart_detect",
|
ufp_required_field="feature_flags.has_smart_detect",
|
||||||
ufp_value="is_person_detection_on",
|
ufp_value="is_person_detection_on",
|
||||||
ufp_set_function="set_person_detection",
|
ufp_set_method="set_person_detection",
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_SMART_VEHICLE,
|
key="smart_vehicle",
|
||||||
name="Detections: Vehicle",
|
name="Detections: Vehicle",
|
||||||
icon="mdi:car",
|
icon="mdi:car",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_required_field="feature_flags.has_smart_detect",
|
ufp_required_field="feature_flags.has_smart_detect",
|
||||||
ufp_value="is_vehicle_detection_on",
|
ufp_value="is_vehicle_detection_on",
|
||||||
ufp_set_function="set_vehicle_detection",
|
ufp_set_method="set_vehicle_detection",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key=_KEY_STATUS_LIGHT,
|
key="status_light",
|
||||||
name="Status Light On",
|
name="Status Light On",
|
||||||
icon="mdi:led-on",
|
icon="mdi:led-on",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_value="light_device_settings.is_indicator_enabled",
|
ufp_value="light_device_settings.is_indicator_enabled",
|
||||||
ufp_set_function="set_status_light",
|
ufp_set_method="set_status_light",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -184,6 +185,8 @@ async def async_setup_entry(
|
||||||
class ProtectSwitch(ProtectDeviceEntity, SwitchEntity):
|
class ProtectSwitch(ProtectDeviceEntity, SwitchEntity):
|
||||||
"""A UniFi Protect Switch."""
|
"""A UniFi Protect Switch."""
|
||||||
|
|
||||||
|
entity_description: ProtectSwitchEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
data: ProtectData,
|
data: ProtectData,
|
||||||
|
@ -191,8 +194,7 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity):
|
||||||
description: ProtectSwitchEntityDescription,
|
description: ProtectSwitchEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an UniFi Protect Switch."""
|
"""Initialize an UniFi Protect Switch."""
|
||||||
self.entity_description: ProtectSwitchEntityDescription = description
|
super().__init__(data, device, description)
|
||||||
super().__init__(data, device)
|
|
||||||
self._attr_name = f"{self.device.name} {self.entity_description.name}"
|
self._attr_name = f"{self.device.name} {self.entity_description.name}"
|
||||||
self._switch_type = self.entity_description.key
|
self._switch_type = self.entity_description.key
|
||||||
|
|
||||||
|
@ -210,44 +212,26 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity):
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return true if device is on."""
|
"""Return true if device is on."""
|
||||||
assert self.entity_description.ufp_value is not None
|
return self.entity_description.get_ufp_value(self.device) is True
|
||||||
|
|
||||||
ufp_value = get_nested_attr(self.device, self.entity_description.ufp_value)
|
|
||||||
if self._switch_type == _KEY_HIGH_FPS:
|
|
||||||
return bool(ufp_value == VideoMode.HIGH_FPS)
|
|
||||||
return ufp_value is True
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
|
|
||||||
if self.entity_description.ufp_set_function is not None:
|
|
||||||
await getattr(self.device, self.entity_description.ufp_set_function)(True)
|
|
||||||
return
|
|
||||||
|
|
||||||
assert isinstance(self.device, Camera)
|
|
||||||
if self._switch_type == _KEY_HIGH_FPS:
|
|
||||||
_LOGGER.debug("Turning on High FPS mode")
|
|
||||||
await self.device.set_video_mode(VideoMode.HIGH_FPS)
|
|
||||||
return
|
|
||||||
if self._switch_type == _KEY_PRIVACY_MODE:
|
if self._switch_type == _KEY_PRIVACY_MODE:
|
||||||
_LOGGER.debug("Turning Privacy Mode on for %s", self.device.name)
|
assert isinstance(self.device, Camera)
|
||||||
self._previous_mic_level = self.device.mic_volume
|
self._previous_mic_level = self.device.mic_volume
|
||||||
self._previous_record_mode = self.device.recording_settings.mode
|
self._previous_record_mode = self.device.recording_settings.mode
|
||||||
await self.device.set_privacy(True, 0, RecordingMode.NEVER)
|
await self.device.set_privacy(True, 0, RecordingMode.NEVER)
|
||||||
|
else:
|
||||||
|
await self.entity_description.ufp_set(self.device, True)
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
|
|
||||||
if self.entity_description.ufp_set_function is not None:
|
if self._switch_type == _KEY_PRIVACY_MODE:
|
||||||
await getattr(self.device, self.entity_description.ufp_set_function)(False)
|
assert isinstance(self.device, Camera)
|
||||||
return
|
_LOGGER.debug("Setting Privacy Mode to false for %s", self.device.name)
|
||||||
|
|
||||||
assert isinstance(self.device, Camera)
|
|
||||||
if self._switch_type == _KEY_HIGH_FPS:
|
|
||||||
_LOGGER.debug("Turning off High FPS mode")
|
|
||||||
await self.device.set_video_mode(VideoMode.DEFAULT)
|
|
||||||
elif self._switch_type == _KEY_PRIVACY_MODE:
|
|
||||||
_LOGGER.debug("Turning Privacy Mode off for %s", self.device.name)
|
|
||||||
await self.device.set_privacy(
|
await self.device.set_privacy(
|
||||||
False, self._previous_mic_level, self._previous_record_mode
|
False, self._previous_mic_level, self._previous_record_mode
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
await self.entity_description.ufp_set(self.device, False)
|
||||||
|
|
|
@ -10,7 +10,6 @@ from pyunifiprotect.data import Camera, Light
|
||||||
|
|
||||||
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
|
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
|
||||||
from homeassistant.components.unifiprotect.number import (
|
from homeassistant.components.unifiprotect.number import (
|
||||||
_KEY_DURATION,
|
|
||||||
CAMERA_NUMBERS,
|
CAMERA_NUMBERS,
|
||||||
LIGHT_NUMBERS,
|
LIGHT_NUMBERS,
|
||||||
ProtectNumberEntityDescription,
|
ProtectNumberEntityDescription,
|
||||||
|
@ -79,7 +78,7 @@ async def camera_fixture(
|
||||||
camera_obj.isp_settings.wdr = 0
|
camera_obj.isp_settings.wdr = 0
|
||||||
camera_obj.mic_volume = 0
|
camera_obj.mic_volume = 0
|
||||||
camera_obj.isp_settings.zoom_position = 0
|
camera_obj.isp_settings.zoom_position = 0
|
||||||
camera_obj.chime_duration = timedelta(seconds=0)
|
camera_obj.chime_duration = 0
|
||||||
|
|
||||||
mock_entry.api.bootstrap.reset_objects()
|
mock_entry.api.bootstrap.reset_objects()
|
||||||
mock_entry.api.bootstrap.cameras = {
|
mock_entry.api.bootstrap.cameras = {
|
||||||
|
@ -199,17 +198,14 @@ async def test_number_setup_camera_missing_attr(
|
||||||
assert_entity_counts(hass, Platform.NUMBER, 0, 0)
|
assert_entity_counts(hass, Platform.NUMBER, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("description", LIGHT_NUMBERS)
|
async def test_number_light_sensitivity(hass: HomeAssistant, light: Light):
|
||||||
async def test_number_light_simple(
|
"""Test sensitivity number entity for lights."""
|
||||||
hass: HomeAssistant, light: Light, description: ProtectNumberEntityDescription
|
|
||||||
):
|
|
||||||
"""Tests all simple numbers for lights."""
|
|
||||||
|
|
||||||
assert description.ufp_set_function is not None
|
description = LIGHT_NUMBERS[0]
|
||||||
|
assert description.ufp_set_method is not None
|
||||||
|
|
||||||
light.__fields__[description.ufp_set_function] = Mock()
|
light.__fields__["set_sensitivity"] = Mock()
|
||||||
setattr(light, description.ufp_set_function, AsyncMock())
|
light.set_sensitivity = AsyncMock()
|
||||||
set_method = getattr(light, description.ufp_set_function)
|
|
||||||
|
|
||||||
_, entity_id = ids_from_device_description(Platform.NUMBER, light, description)
|
_, entity_id = ids_from_device_description(Platform.NUMBER, light, description)
|
||||||
|
|
||||||
|
@ -217,10 +213,24 @@ async def test_number_light_simple(
|
||||||
"number", "set_value", {ATTR_ENTITY_ID: entity_id, "value": 15.0}, blocking=True
|
"number", "set_value", {ATTR_ENTITY_ID: entity_id, "value": 15.0}, blocking=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if description.key == _KEY_DURATION:
|
light.set_sensitivity.assert_called_once_with(15.0)
|
||||||
set_method.assert_called_once_with(timedelta(seconds=15.0))
|
|
||||||
else:
|
|
||||||
set_method.assert_called_once_with(15.0)
|
async def test_number_light_duration(hass: HomeAssistant, light: Light):
|
||||||
|
"""Test chime duration number entity for lights."""
|
||||||
|
|
||||||
|
description = LIGHT_NUMBERS[1]
|
||||||
|
|
||||||
|
light.__fields__["set_duration"] = Mock()
|
||||||
|
light.set_duration = AsyncMock()
|
||||||
|
|
||||||
|
_, entity_id = ids_from_device_description(Platform.NUMBER, light, description)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"number", "set_value", {ATTR_ENTITY_ID: entity_id, "value": 15.0}, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
light.set_duration.assert_called_once_with(timedelta(seconds=15.0))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("description", CAMERA_NUMBERS)
|
@pytest.mark.parametrize("description", CAMERA_NUMBERS)
|
||||||
|
@ -229,11 +239,11 @@ async def test_number_camera_simple(
|
||||||
):
|
):
|
||||||
"""Tests all simple numbers for cameras."""
|
"""Tests all simple numbers for cameras."""
|
||||||
|
|
||||||
assert description.ufp_set_function is not None
|
assert description.ufp_set_method is not None
|
||||||
|
|
||||||
camera.__fields__[description.ufp_set_function] = Mock()
|
camera.__fields__[description.ufp_set_method] = Mock()
|
||||||
setattr(camera, description.ufp_set_function, AsyncMock())
|
setattr(camera, description.ufp_set_method, AsyncMock())
|
||||||
set_method = getattr(camera, description.ufp_set_function)
|
set_method = getattr(camera, description.ufp_set_method)
|
||||||
|
|
||||||
_, entity_id = ids_from_device_description(Platform.NUMBER, camera, description)
|
_, entity_id = ids_from_device_description(Platform.NUMBER, camera, description)
|
||||||
|
|
||||||
|
@ -241,7 +251,4 @@ async def test_number_camera_simple(
|
||||||
"number", "set_value", {ATTR_ENTITY_ID: entity_id, "value": 1.0}, blocking=True
|
"number", "set_value", {ATTR_ENTITY_ID: entity_id, "value": 1.0}, blocking=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if description.key == _KEY_DURATION:
|
set_method.assert_called_once_with(1.0)
|
||||||
set_method.assert_called_once_with(timedelta(seconds=1.0))
|
|
||||||
else:
|
|
||||||
set_method.assert_called_once_with(1.0)
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ from homeassistant.components.unifiprotect.sensor import (
|
||||||
NVR_SENSORS,
|
NVR_SENSORS,
|
||||||
SENSE_SENSORS,
|
SENSE_SENSORS,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNAVAILABLE, Platform
|
from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
@ -242,15 +242,17 @@ async def test_sensor_setup_nvr(
|
||||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
|
||||||
async def test_sensor_nvr_memory_unavaiable(
|
async def test_sensor_nvr_missing_values(
|
||||||
hass: HomeAssistant, mock_entry: MockEntityFixture, now: datetime
|
hass: HomeAssistant, mock_entry: MockEntityFixture, now: datetime
|
||||||
):
|
):
|
||||||
"""Test memory sensor for NVR if no data available."""
|
"""Test NVR sensor sensors if no data available."""
|
||||||
|
|
||||||
mock_entry.api.bootstrap.reset_objects()
|
mock_entry.api.bootstrap.reset_objects()
|
||||||
nvr: NVR = mock_entry.api.bootstrap.nvr
|
nvr: NVR = mock_entry.api.bootstrap.nvr
|
||||||
nvr.system_info.memory.available = None
|
nvr.system_info.memory.available = None
|
||||||
nvr.system_info.memory.total = None
|
nvr.system_info.memory.total = None
|
||||||
|
nvr.up_since = None
|
||||||
|
nvr.storage_stats.capacity = None
|
||||||
|
|
||||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -260,6 +262,39 @@ async def test_sensor_nvr_memory_unavaiable(
|
||||||
|
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
# Uptime
|
||||||
|
description = NVR_SENSORS[0]
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.SENSOR, nvr, description
|
||||||
|
)
|
||||||
|
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity
|
||||||
|
assert entity.unique_id == unique_id
|
||||||
|
|
||||||
|
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
# Memory
|
||||||
|
description = NVR_SENSORS[8]
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.SENSOR, nvr, description
|
||||||
|
)
|
||||||
|
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity
|
||||||
|
assert entity.unique_id == unique_id
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == "0"
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
# Memory
|
||||||
description = NVR_DISABLED_SENSORS[2]
|
description = NVR_DISABLED_SENSORS[2]
|
||||||
unique_id, entity_id = ids_from_device_description(
|
unique_id, entity_id = ids_from_device_description(
|
||||||
Platform.SENSOR, nvr, description
|
Platform.SENSOR, nvr, description
|
||||||
|
@ -267,14 +302,14 @@ async def test_sensor_nvr_memory_unavaiable(
|
||||||
|
|
||||||
entity = entity_registry.async_get(entity_id)
|
entity = entity_registry.async_get(entity_id)
|
||||||
assert entity
|
assert entity
|
||||||
assert entity.disabled is not description.entity_registry_enabled_default
|
assert entity.disabled is True
|
||||||
assert entity.unique_id == unique_id
|
assert entity.unique_id == unique_id
|
||||||
|
|
||||||
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
|
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
|
||||||
|
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert state
|
assert state
|
||||||
assert state.state == STATE_UNAVAILABLE
|
assert state.state == STATE_UNKNOWN
|
||||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
|
||||||
|
@ -286,7 +321,7 @@ async def test_sensor_setup_camera(
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
expected_values = (
|
expected_values = (
|
||||||
now.replace(second=0, microsecond=0).isoformat(),
|
now.replace(microsecond=0).isoformat(),
|
||||||
"100",
|
"100",
|
||||||
"100.0",
|
"100.0",
|
||||||
"20.0",
|
"20.0",
|
||||||
|
|
|
@ -384,11 +384,11 @@ async def test_switch_camera_simple(
|
||||||
if description.name in ("High FPS", "Privacy Mode"):
|
if description.name in ("High FPS", "Privacy Mode"):
|
||||||
return
|
return
|
||||||
|
|
||||||
assert description.ufp_set_function is not None
|
assert description.ufp_set_method is not None
|
||||||
|
|
||||||
camera.__fields__[description.ufp_set_function] = Mock()
|
camera.__fields__[description.ufp_set_method] = Mock()
|
||||||
setattr(camera, description.ufp_set_function, AsyncMock())
|
setattr(camera, description.ufp_set_method, AsyncMock())
|
||||||
set_method = getattr(camera, description.ufp_set_function)
|
set_method = getattr(camera, description.ufp_set_method)
|
||||||
|
|
||||||
_, entity_id = ids_from_device_description(Platform.SWITCH, camera, description)
|
_, entity_id = ids_from_device_description(Platform.SWITCH, camera, description)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue