782 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			782 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
"""Component providing sensors for UniFi Protect."""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
from collections.abc import Callable, Sequence
 | 
						|
from dataclasses import dataclass
 | 
						|
from datetime import datetime
 | 
						|
from functools import partial
 | 
						|
import logging
 | 
						|
from typing import Any
 | 
						|
 | 
						|
from uiprotect.data import (
 | 
						|
    NVR,
 | 
						|
    Camera,
 | 
						|
    Light,
 | 
						|
    ModelType,
 | 
						|
    ProtectAdoptableDeviceModel,
 | 
						|
    ProtectDeviceModel,
 | 
						|
    Sensor,
 | 
						|
    SmartDetectObjectType,
 | 
						|
)
 | 
						|
 | 
						|
from homeassistant.components.sensor import (
 | 
						|
    SensorDeviceClass,
 | 
						|
    SensorEntity,
 | 
						|
    SensorEntityDescription,
 | 
						|
    SensorStateClass,
 | 
						|
)
 | 
						|
from homeassistant.const import (
 | 
						|
    LIGHT_LUX,
 | 
						|
    PERCENTAGE,
 | 
						|
    SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
 | 
						|
    EntityCategory,
 | 
						|
    UnitOfDataRate,
 | 
						|
    UnitOfElectricPotential,
 | 
						|
    UnitOfInformation,
 | 
						|
    UnitOfTemperature,
 | 
						|
    UnitOfTime,
 | 
						|
)
 | 
						|
from homeassistant.core import HomeAssistant, callback
 | 
						|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
 | 
						|
 | 
						|
from .data import ProtectData, ProtectDeviceType, UFPConfigEntry
 | 
						|
from .entity import (
 | 
						|
    BaseProtectEntity,
 | 
						|
    EventEntityMixin,
 | 
						|
    ProtectDeviceEntity,
 | 
						|
    ProtectNVREntity,
 | 
						|
    async_all_device_entities,
 | 
						|
)
 | 
						|
from .models import PermRequired, ProtectEntityDescription, ProtectEventMixin, T
 | 
						|
from .utils import async_get_light_motion_current
 | 
						|
 | 
						|
_LOGGER = logging.getLogger(__name__)
 | 
						|
OBJECT_TYPE_NONE = "none"
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True, kw_only=True)
 | 
						|
class ProtectSensorEntityDescription(
 | 
						|
    ProtectEntityDescription[T], SensorEntityDescription
 | 
						|
):
 | 
						|
    """Describes UniFi Protect Sensor entity."""
 | 
						|
 | 
						|
    precision: int | None = None
 | 
						|
 | 
						|
    def __post_init__(self) -> None:
 | 
						|
        """Ensure values are rounded if precision is set."""
 | 
						|
        super().__post_init__()
 | 
						|
        if precision := self.precision:
 | 
						|
            object.__setattr__(
 | 
						|
                self,
 | 
						|
                "get_ufp_value",
 | 
						|
                partial(self._rounded_value, precision, self.get_ufp_value),
 | 
						|
            )
 | 
						|
 | 
						|
    def _rounded_value(self, precision: int, getter: Callable[[T], Any], obj: T) -> Any:
 | 
						|
        """Round value to precision if set."""
 | 
						|
        return None if (v := getter(obj)) is None else round(v, precision)
 | 
						|
 | 
						|
 | 
						|
@dataclass(frozen=True, kw_only=True)
 | 
						|
class ProtectSensorEventEntityDescription(
 | 
						|
    ProtectEventMixin[T], SensorEntityDescription
 | 
						|
):
 | 
						|
    """Describes UniFi Protect Sensor entity."""
 | 
						|
 | 
						|
 | 
						|
def _get_uptime(obj: ProtectDeviceModel) -> datetime | None:
 | 
						|
    if obj.up_since is None:
 | 
						|
        return None
 | 
						|
 | 
						|
    # up_since can vary slightly over time
 | 
						|
    # truncate to ensure no extra state_change events fire
 | 
						|
    return obj.up_since.replace(second=0, microsecond=0)
 | 
						|
 | 
						|
 | 
						|
def _get_nvr_recording_capacity(obj: NVR) -> int:
 | 
						|
    if obj.storage_stats.capacity is None:
 | 
						|
        return 0
 | 
						|
 | 
						|
    return int(obj.storage_stats.capacity.total_seconds())
 | 
						|
 | 
						|
 | 
						|
def _get_nvr_memory(obj: NVR) -> float | None:
 | 
						|
    memory = obj.system_info.memory
 | 
						|
    if memory.available is None or memory.total is None:
 | 
						|
        return None
 | 
						|
    return (1 - memory.available / memory.total) * 100
 | 
						|
 | 
						|
 | 
						|
def _get_alarm_sound(obj: Sensor) -> str:
 | 
						|
    alarm_type = OBJECT_TYPE_NONE
 | 
						|
    if (
 | 
						|
        obj.is_alarm_detected
 | 
						|
        and obj.last_alarm_event is not None
 | 
						|
        and obj.last_alarm_event.metadata is not None
 | 
						|
    ):
 | 
						|
        alarm_type = obj.last_alarm_event.metadata.alarm_type or OBJECT_TYPE_NONE
 | 
						|
    return alarm_type.lower()
 | 
						|
 | 
						|
 | 
						|
ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="uptime",
 | 
						|
        name="Uptime",
 | 
						|
        icon="mdi:clock",
 | 
						|
        device_class=SensorDeviceClass.TIMESTAMP,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        ufp_value_fn=_get_uptime,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="ble_signal",
 | 
						|
        name="Bluetooth signal strength",
 | 
						|
        native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
 | 
						|
        device_class=SensorDeviceClass.SIGNAL_STRENGTH,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="bluetooth_connection_state.signal_strength",
 | 
						|
        ufp_required_field="bluetooth_connection_state.signal_strength",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="phy_rate",
 | 
						|
        name="Link speed",
 | 
						|
        device_class=SensorDeviceClass.DATA_RATE,
 | 
						|
        native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="wired_connection_state.phy_rate",
 | 
						|
        ufp_required_field="wired_connection_state.phy_rate",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="wifi_signal",
 | 
						|
        name="WiFi signal strength",
 | 
						|
        native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
 | 
						|
        device_class=SensorDeviceClass.SIGNAL_STRENGTH,
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="wifi_connection_state.signal_strength",
 | 
						|
        ufp_required_field="wifi_connection_state.signal_strength",
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="oldest_recording",
 | 
						|
        name="Oldest recording",
 | 
						|
        device_class=SensorDeviceClass.TIMESTAMP,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        ufp_value="stats.video.recording_start",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="storage_used",
 | 
						|
        name="Storage used",
 | 
						|
        native_unit_of_measurement=UnitOfInformation.BYTES,
 | 
						|
        device_class=SensorDeviceClass.DATA_SIZE,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="stats.storage.used",
 | 
						|
        suggested_unit_of_measurement=UnitOfInformation.MEGABYTES,
 | 
						|
        suggested_display_precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="write_rate",
 | 
						|
        name="Disk write rate",
 | 
						|
        device_class=SensorDeviceClass.DATA_RATE,
 | 
						|
        native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="stats.storage.rate_per_second",
 | 
						|
        precision=2,
 | 
						|
        suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
 | 
						|
        suggested_display_precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="voltage",
 | 
						|
        name="Voltage",
 | 
						|
        device_class=SensorDeviceClass.VOLTAGE,
 | 
						|
        native_unit_of_measurement=UnitOfElectricPotential.VOLT,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="voltage",
 | 
						|
        # no feature flag, but voltage will be null if device does not have
 | 
						|
        # voltage sensor (i.e. is not G4 Doorbell or not on 1.20.1+)
 | 
						|
        ufp_required_field="voltage",
 | 
						|
        precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="doorbell_last_trip_time",
 | 
						|
        name="Last doorbell ring",
 | 
						|
        device_class=SensorDeviceClass.TIMESTAMP,
 | 
						|
        icon="mdi:doorbell-video",
 | 
						|
        ufp_required_field="feature_flags.is_doorbell",
 | 
						|
        ufp_value="last_ring",
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="lens_type",
 | 
						|
        name="Lens type",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        icon="mdi:camera-iris",
 | 
						|
        ufp_required_field="has_removable_lens",
 | 
						|
        ufp_value="feature_flags.lens_type",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="mic_level",
 | 
						|
        name="Microphone level",
 | 
						|
        icon="mdi:microphone",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_required_field="has_mic",
 | 
						|
        ufp_value="mic_volume",
 | 
						|
        ufp_enabled="feature_flags.has_mic",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="recording_mode",
 | 
						|
        name="Recording mode",
 | 
						|
        icon="mdi:video-outline",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value="recording_settings.mode",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="infrared",
 | 
						|
        name="Infrared mode",
 | 
						|
        icon="mdi:circle-opacity",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_required_field="feature_flags.has_led_ir",
 | 
						|
        ufp_value="isp_settings.ir_led_mode",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="doorbell_text",
 | 
						|
        name="Doorbell text",
 | 
						|
        icon="mdi:card-text",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_required_field="feature_flags.has_lcd_screen",
 | 
						|
        ufp_value="lcd_message.text",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="chime_type",
 | 
						|
        name="Chime type",
 | 
						|
        icon="mdi:bell",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        ufp_required_field="feature_flags.has_chime",
 | 
						|
        ufp_value="chime_type",
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="stats_rx",
 | 
						|
        name="Received data",
 | 
						|
        native_unit_of_measurement=UnitOfInformation.BYTES,
 | 
						|
        device_class=SensorDeviceClass.DATA_SIZE,
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.TOTAL_INCREASING,
 | 
						|
        ufp_value="stats.rx_bytes",
 | 
						|
        suggested_unit_of_measurement=UnitOfInformation.MEGABYTES,
 | 
						|
        suggested_display_precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="stats_tx",
 | 
						|
        name="Transferred data",
 | 
						|
        native_unit_of_measurement=UnitOfInformation.BYTES,
 | 
						|
        device_class=SensorDeviceClass.DATA_SIZE,
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.TOTAL_INCREASING,
 | 
						|
        ufp_value="stats.tx_bytes",
 | 
						|
        suggested_unit_of_measurement=UnitOfInformation.MEGABYTES,
 | 
						|
        suggested_display_precision=2,
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="battery_level",
 | 
						|
        name="Battery level",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        device_class=SensorDeviceClass.BATTERY,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="battery_status.percentage",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="light_level",
 | 
						|
        name="Light level",
 | 
						|
        native_unit_of_measurement=LIGHT_LUX,
 | 
						|
        device_class=SensorDeviceClass.ILLUMINANCE,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="stats.light.value",
 | 
						|
        ufp_enabled="is_light_sensor_enabled",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="humidity_level",
 | 
						|
        name="Humidity level",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        device_class=SensorDeviceClass.HUMIDITY,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="stats.humidity.value",
 | 
						|
        ufp_enabled="is_humidity_sensor_enabled",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="temperature_level",
 | 
						|
        name="Temperature",
 | 
						|
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 | 
						|
        device_class=SensorDeviceClass.TEMPERATURE,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="stats.temperature.value",
 | 
						|
        ufp_enabled="is_temperature_sensor_enabled",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription[Sensor](
 | 
						|
        key="alarm_sound",
 | 
						|
        name="Alarm sound detected",
 | 
						|
        ufp_value_fn=_get_alarm_sound,
 | 
						|
        ufp_enabled="is_alarm_sensor_enabled",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="door_last_trip_time",
 | 
						|
        name="Last open",
 | 
						|
        device_class=SensorDeviceClass.TIMESTAMP,
 | 
						|
        ufp_value="open_status_changed_at",
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="motion_last_trip_time",
 | 
						|
        name="Last motion detected",
 | 
						|
        device_class=SensorDeviceClass.TIMESTAMP,
 | 
						|
        ufp_value="motion_detected_at",
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="tampering_last_trip_time",
 | 
						|
        name="Last tampering detected",
 | 
						|
        device_class=SensorDeviceClass.TIMESTAMP,
 | 
						|
        ufp_value="tampering_detected_at",
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="sensitivity",
 | 
						|
        name="Motion sensitivity",
 | 
						|
        icon="mdi:walk",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value="motion_settings.sensitivity",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="mount_type",
 | 
						|
        name="Mount type",
 | 
						|
        icon="mdi:screwdriver",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value="mount_type",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="paired_camera",
 | 
						|
        name="Paired camera",
 | 
						|
        icon="mdi:cctv",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value="camera.display_name",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="battery_level",
 | 
						|
        name="Battery level",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        device_class=SensorDeviceClass.BATTERY,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="battery_status.percentage",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="paired_camera",
 | 
						|
        name="Paired camera",
 | 
						|
        icon="mdi:cctv",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value="camera.display_name",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="uptime",
 | 
						|
        name="Uptime",
 | 
						|
        icon="mdi:clock",
 | 
						|
        device_class=SensorDeviceClass.TIMESTAMP,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value_fn=_get_uptime,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="storage_utilization",
 | 
						|
        name="Storage utilization",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        icon="mdi:harddisk",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="storage_stats.utilization",
 | 
						|
        precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="record_rotating",
 | 
						|
        name="Type: timelapse video",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        icon="mdi:server",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="storage_stats.storage_distribution.timelapse_recordings.percentage",
 | 
						|
        precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="record_timelapse",
 | 
						|
        name="Type: continuous video",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        icon="mdi:server",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="storage_stats.storage_distribution.continuous_recordings.percentage",
 | 
						|
        precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="record_detections",
 | 
						|
        name="Type: detections video",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        icon="mdi:server",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="storage_stats.storage_distribution.detections_recordings.percentage",
 | 
						|
        precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="resolution_HD",
 | 
						|
        name="Resolution: HD video",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        icon="mdi:cctv",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="storage_stats.storage_distribution.hd_usage.percentage",
 | 
						|
        precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="resolution_4K",
 | 
						|
        name="Resolution: 4K video",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        icon="mdi:cctv",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="storage_stats.storage_distribution.uhd_usage.percentage",
 | 
						|
        precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="resolution_free",
 | 
						|
        name="Resolution: free space",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        icon="mdi:cctv",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="storage_stats.storage_distribution.free.percentage",
 | 
						|
        precision=2,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription[NVR](
 | 
						|
        key="record_capacity",
 | 
						|
        name="Recording capacity",
 | 
						|
        native_unit_of_measurement=UnitOfTime.SECONDS,
 | 
						|
        icon="mdi:record-rec",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value_fn=_get_nvr_recording_capacity,
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="cpu_utilization",
 | 
						|
        name="CPU utilization",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        icon="mdi:speedometer",
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="system_info.cpu.average_load",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="cpu_temperature",
 | 
						|
        name="CPU temperature",
 | 
						|
        native_unit_of_measurement=UnitOfTemperature.CELSIUS,
 | 
						|
        device_class=SensorDeviceClass.TEMPERATURE,
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value="system_info.cpu.temperature",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription[NVR](
 | 
						|
        key="memory_utilization",
 | 
						|
        name="Memory utilization",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        icon="mdi:memory",
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        state_class=SensorStateClass.MEASUREMENT,
 | 
						|
        ufp_value_fn=_get_nvr_memory,
 | 
						|
        precision=2,
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
LICENSE_PLATE_EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
 | 
						|
    ProtectSensorEventEntityDescription(
 | 
						|
        key="smart_obj_licenseplate",
 | 
						|
        name="License plate detected",
 | 
						|
        icon="mdi:car",
 | 
						|
        translation_key="license_plate",
 | 
						|
        ufp_obj_type=SmartDetectObjectType.LICENSE_PLATE,
 | 
						|
        ufp_required_field="can_detect_license_plate",
 | 
						|
        ufp_event_obj="last_license_plate_detect_event",
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
 | 
						|
LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="motion_last_trip_time",
 | 
						|
        name="Last motion detected",
 | 
						|
        device_class=SensorDeviceClass.TIMESTAMP,
 | 
						|
        ufp_value="last_motion",
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="sensitivity",
 | 
						|
        name="Motion sensitivity",
 | 
						|
        icon="mdi:walk",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value="light_device_settings.pir_sensitivity",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription[Light](
 | 
						|
        key="light_motion",
 | 
						|
        name="Light mode",
 | 
						|
        icon="mdi:spotlight",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value_fn=async_get_light_motion_current,
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="paired_camera",
 | 
						|
        name="Paired camera",
 | 
						|
        icon="mdi:cctv",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value="camera.display_name",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="motion_last_trip_time",
 | 
						|
        name="Last motion detected",
 | 
						|
        device_class=SensorDeviceClass.TIMESTAMP,
 | 
						|
        ufp_value="last_motion",
 | 
						|
        entity_registry_enabled_default=False,
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
CHIME_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="last_ring",
 | 
						|
        name="Last ring",
 | 
						|
        device_class=SensorDeviceClass.TIMESTAMP,
 | 
						|
        icon="mdi:bell",
 | 
						|
        ufp_value="last_ring",
 | 
						|
    ),
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="volume",
 | 
						|
        name="Volume",
 | 
						|
        icon="mdi:speaker",
 | 
						|
        native_unit_of_measurement=PERCENTAGE,
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value="volume",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
VIEWER_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
 | 
						|
    ProtectSensorEntityDescription(
 | 
						|
        key="viewer",
 | 
						|
        name="Liveview",
 | 
						|
        icon="mdi:view-dashboard",
 | 
						|
        entity_category=EntityCategory.DIAGNOSTIC,
 | 
						|
        ufp_value="liveview.name",
 | 
						|
        ufp_perm=PermRequired.NO_WRITE,
 | 
						|
    ),
 | 
						|
)
 | 
						|
 | 
						|
_MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = {
 | 
						|
    ModelType.CAMERA: CAMERA_SENSORS + CAMERA_DISABLED_SENSORS,
 | 
						|
    ModelType.SENSOR: SENSE_SENSORS,
 | 
						|
    ModelType.LIGHT: LIGHT_SENSORS,
 | 
						|
    ModelType.DOORLOCK: DOORLOCK_SENSORS,
 | 
						|
    ModelType.CHIME: CHIME_SENSORS,
 | 
						|
    ModelType.VIEWPORT: VIEWER_SENSORS,
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
async def async_setup_entry(
 | 
						|
    hass: HomeAssistant,
 | 
						|
    entry: UFPConfigEntry,
 | 
						|
    async_add_entities: AddEntitiesCallback,
 | 
						|
) -> None:
 | 
						|
    """Set up sensors for UniFi Protect integration."""
 | 
						|
    data = entry.runtime_data
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
 | 
						|
        entities = async_all_device_entities(
 | 
						|
            data,
 | 
						|
            ProtectDeviceSensor,
 | 
						|
            all_descs=ALL_DEVICES_SENSORS,
 | 
						|
            model_descriptions=_MODEL_DESCRIPTIONS,
 | 
						|
            ufp_device=device,
 | 
						|
        )
 | 
						|
        if device.is_adopted_by_us and isinstance(device, Camera):
 | 
						|
            entities += _async_event_entities(data, ufp_device=device)
 | 
						|
        async_add_entities(entities)
 | 
						|
 | 
						|
    data.async_subscribe_adopt(_add_new_device)
 | 
						|
    entities = async_all_device_entities(
 | 
						|
        data,
 | 
						|
        ProtectDeviceSensor,
 | 
						|
        all_descs=ALL_DEVICES_SENSORS,
 | 
						|
        model_descriptions=_MODEL_DESCRIPTIONS,
 | 
						|
    )
 | 
						|
    entities += _async_event_entities(data)
 | 
						|
    entities += _async_nvr_entities(data)
 | 
						|
 | 
						|
    async_add_entities(entities)
 | 
						|
 | 
						|
 | 
						|
@callback
 | 
						|
def _async_event_entities(
 | 
						|
    data: ProtectData,
 | 
						|
    ufp_device: Camera | None = None,
 | 
						|
) -> list[ProtectDeviceEntity]:
 | 
						|
    entities: list[ProtectDeviceEntity] = []
 | 
						|
    cameras = data.get_cameras() if ufp_device is None else [ufp_device]
 | 
						|
    for camera in cameras:
 | 
						|
        for description in MOTION_TRIP_SENSORS:
 | 
						|
            entities.append(ProtectDeviceSensor(data, camera, description))
 | 
						|
            _LOGGER.debug(
 | 
						|
                "Adding trip sensor entity %s for %s",
 | 
						|
                description.name,
 | 
						|
                camera.display_name,
 | 
						|
            )
 | 
						|
 | 
						|
        if not camera.feature_flags.has_smart_detect:
 | 
						|
            continue
 | 
						|
 | 
						|
        for event_desc in LICENSE_PLATE_EVENT_SENSORS:
 | 
						|
            if not event_desc.has_required(camera):
 | 
						|
                continue
 | 
						|
 | 
						|
            entities.append(ProtectLicensePlateEventSensor(data, camera, event_desc))
 | 
						|
            _LOGGER.debug(
 | 
						|
                "Adding sensor entity %s for %s",
 | 
						|
                description.name,
 | 
						|
                camera.display_name,
 | 
						|
            )
 | 
						|
 | 
						|
    return entities
 | 
						|
 | 
						|
 | 
						|
@callback
 | 
						|
def _async_nvr_entities(
 | 
						|
    data: ProtectData,
 | 
						|
) -> list[BaseProtectEntity]:
 | 
						|
    entities: list[BaseProtectEntity] = []
 | 
						|
    device = data.api.bootstrap.nvr
 | 
						|
    for description in NVR_SENSORS + NVR_DISABLED_SENSORS:
 | 
						|
        entities.append(ProtectNVRSensor(data, device, description))
 | 
						|
        _LOGGER.debug("Adding NVR sensor entity %s", description.name)
 | 
						|
 | 
						|
    return entities
 | 
						|
 | 
						|
 | 
						|
class BaseProtectSensor(BaseProtectEntity, SensorEntity):
 | 
						|
    """A UniFi Protect Sensor Entity."""
 | 
						|
 | 
						|
    entity_description: ProtectSensorEntityDescription
 | 
						|
    _state_attrs = ("_attr_available", "_attr_native_value")
 | 
						|
 | 
						|
    def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None:
 | 
						|
        super()._async_update_device_from_protect(device)
 | 
						|
        self._attr_native_value = self.entity_description.get_ufp_value(self.device)
 | 
						|
 | 
						|
 | 
						|
class ProtectDeviceSensor(BaseProtectSensor, ProtectDeviceEntity):
 | 
						|
    """A Ubiquiti UniFi Protect Sensor."""
 | 
						|
 | 
						|
 | 
						|
class ProtectNVRSensor(BaseProtectSensor, ProtectNVREntity):
 | 
						|
    """A Ubiquiti UniFi Protect Sensor."""
 | 
						|
 | 
						|
 | 
						|
class ProtectEventSensor(EventEntityMixin, SensorEntity):
 | 
						|
    """A UniFi Protect Device Sensor with access tokens."""
 | 
						|
 | 
						|
    entity_description: ProtectSensorEventEntityDescription
 | 
						|
    _state_attrs = (
 | 
						|
        "_attr_available",
 | 
						|
        "_attr_native_value",
 | 
						|
        "_attr_extra_state_attributes",
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
class ProtectLicensePlateEventSensor(ProtectEventSensor):
 | 
						|
    """A UniFi Protect license plate sensor."""
 | 
						|
 | 
						|
    device: Camera
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _set_event_done(self) -> None:
 | 
						|
        self._attr_native_value = OBJECT_TYPE_NONE
 | 
						|
        self._attr_extra_state_attributes = {}
 | 
						|
 | 
						|
    @callback
 | 
						|
    def _async_update_device_from_protect(self, device: ProtectDeviceType) -> None:
 | 
						|
        description = self.entity_description
 | 
						|
 | 
						|
        prev_event = self._event
 | 
						|
        prev_event_end = self._event_end
 | 
						|
        super()._async_update_device_from_protect(device)
 | 
						|
        if event := description.get_event_obj(device):
 | 
						|
            self._event = event
 | 
						|
            self._event_end = event.end
 | 
						|
 | 
						|
        if not (
 | 
						|
            event
 | 
						|
            and (metadata := event.metadata)
 | 
						|
            and (license_plate := metadata.license_plate)
 | 
						|
            and description.has_matching_smart(event)
 | 
						|
            and not self._event_already_ended(prev_event, prev_event_end)
 | 
						|
        ):
 | 
						|
            self._set_event_done()
 | 
						|
            return
 | 
						|
 | 
						|
        self._attr_native_value = license_plate.name
 | 
						|
        self._set_event_attrs(event)
 | 
						|
        if event.end:
 | 
						|
            self._async_event_with_immediate_end()
 |