803 lines
27 KiB
Python
803 lines
27 KiB
Python
"""This component provides sensors for UniFi Protect."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
import logging
|
|
from typing import Any, cast
|
|
|
|
from pyunifiprotect.data import (
|
|
NVR,
|
|
Camera,
|
|
Light,
|
|
ModelType,
|
|
ProtectAdoptableDeviceModel,
|
|
ProtectDeviceModel,
|
|
ProtectModelWithId,
|
|
Sensor,
|
|
SmartDetectObjectType,
|
|
)
|
|
|
|
from homeassistant.components.sensor import (
|
|
SensorDeviceClass,
|
|
SensorEntity,
|
|
SensorEntityDescription,
|
|
SensorStateClass,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import (
|
|
LIGHT_LUX,
|
|
PERCENTAGE,
|
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
|
UnitOfDataRate,
|
|
UnitOfElectricPotential,
|
|
UnitOfInformation,
|
|
UnitOfTemperature,
|
|
UnitOfTime,
|
|
)
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
from homeassistant.helpers.entity import EntityCategory
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
|
|
from .const import DISPATCH_ADOPT, DOMAIN
|
|
from .data import ProtectData
|
|
from .entity import (
|
|
EventEntityMixin,
|
|
ProtectDeviceEntity,
|
|
ProtectNVREntity,
|
|
async_all_device_entities,
|
|
)
|
|
from .models import PermRequired, ProtectEventMixin, ProtectRequiredKeysMixin, T
|
|
from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
OBJECT_TYPE_NONE = "none"
|
|
DEVICE_CLASS_DETECTION = "unifiprotect__detection"
|
|
|
|
|
|
@dataclass
|
|
class ProtectSensorEntityDescription(
|
|
ProtectRequiredKeysMixin[T], SensorEntityDescription
|
|
):
|
|
"""Describes UniFi Protect Sensor entity."""
|
|
|
|
precision: int | None = None
|
|
|
|
def get_ufp_value(self, obj: T) -> Any:
|
|
"""Return value from UniFi Protect device."""
|
|
value = super().get_ufp_value(obj)
|
|
|
|
if isinstance(value, float) and self.precision:
|
|
value = round(value, self.precision)
|
|
return value
|
|
|
|
|
|
@dataclass
|
|
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",
|
|
),
|
|
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,
|
|
),
|
|
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",
|
|
),
|
|
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",
|
|
),
|
|
)
|
|
|
|
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,
|
|
),
|
|
)
|
|
|
|
EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
|
|
ProtectSensorEventEntityDescription(
|
|
key="detected_object",
|
|
name="Detected Object",
|
|
device_class=DEVICE_CLASS_DETECTION,
|
|
entity_registry_enabled_default=False,
|
|
ufp_value="is_smart_detected",
|
|
ufp_event_obj="last_smart_detect_event",
|
|
),
|
|
ProtectSensorEventEntityDescription(
|
|
key="smart_obj_licenseplate",
|
|
name="License Plate Detected",
|
|
icon="mdi:car",
|
|
translation_key="license_plate",
|
|
ufp_smart_type=SmartDetectObjectType.LICENSE_PLATE,
|
|
ufp_value="is_smart_detected",
|
|
ufp_required_field="can_detect_license_plate",
|
|
ufp_event_obj="last_smart_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,
|
|
),
|
|
)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up sensors for UniFi Protect integration."""
|
|
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
|
|
|
async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None:
|
|
entities = async_all_device_entities(
|
|
data,
|
|
ProtectDeviceSensor,
|
|
all_descs=ALL_DEVICES_SENSORS,
|
|
camera_descs=CAMERA_SENSORS + CAMERA_DISABLED_SENSORS,
|
|
sense_descs=SENSE_SENSORS,
|
|
light_descs=LIGHT_SENSORS,
|
|
lock_descs=DOORLOCK_SENSORS,
|
|
chime_descs=CHIME_SENSORS,
|
|
viewer_descs=VIEWER_SENSORS,
|
|
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)
|
|
|
|
entry.async_on_unload(
|
|
async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device)
|
|
)
|
|
|
|
entities: list[ProtectDeviceEntity] = async_all_device_entities(
|
|
data,
|
|
ProtectDeviceSensor,
|
|
all_descs=ALL_DEVICES_SENSORS,
|
|
camera_descs=CAMERA_SENSORS + CAMERA_DISABLED_SENSORS,
|
|
sense_descs=SENSE_SENSORS,
|
|
light_descs=LIGHT_SENSORS,
|
|
lock_descs=DOORLOCK_SENSORS,
|
|
chime_descs=CHIME_SENSORS,
|
|
viewer_descs=VIEWER_SENSORS,
|
|
)
|
|
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] = []
|
|
devices = (
|
|
data.get_by_types({ModelType.CAMERA}) if ufp_device is None else [ufp_device]
|
|
)
|
|
for device in devices:
|
|
device = cast(Camera, device)
|
|
for description in MOTION_TRIP_SENSORS:
|
|
entities.append(ProtectDeviceSensor(data, device, description))
|
|
_LOGGER.debug(
|
|
"Adding trip sensor entity %s for %s",
|
|
description.name,
|
|
device.display_name,
|
|
)
|
|
|
|
if not device.feature_flags.has_smart_detect:
|
|
continue
|
|
|
|
for event_desc in EVENT_SENSORS:
|
|
if not event_desc.has_required(device):
|
|
continue
|
|
|
|
entities.append(ProtectEventSensor(data, device, event_desc))
|
|
_LOGGER.debug(
|
|
"Adding sensor entity %s for %s",
|
|
description.name,
|
|
device.display_name,
|
|
)
|
|
|
|
return entities
|
|
|
|
|
|
@callback
|
|
def _async_nvr_entities(
|
|
data: ProtectData,
|
|
) -> list[ProtectDeviceEntity]:
|
|
entities: list[ProtectDeviceEntity] = []
|
|
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 ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity):
|
|
"""A Ubiquiti UniFi Protect Sensor."""
|
|
|
|
entity_description: ProtectSensorEntityDescription
|
|
|
|
def __init__(
|
|
self,
|
|
data: ProtectData,
|
|
device: ProtectAdoptableDeviceModel,
|
|
description: ProtectSensorEntityDescription,
|
|
) -> None:
|
|
"""Initialize an UniFi Protect sensor."""
|
|
super().__init__(data, device, description)
|
|
|
|
@callback
|
|
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
|
super()._async_update_device_from_protect(device)
|
|
self._attr_native_value = self.entity_description.get_ufp_value(self.device)
|
|
|
|
|
|
class ProtectNVRSensor(ProtectNVREntity, SensorEntity):
|
|
"""A Ubiquiti UniFi Protect Sensor."""
|
|
|
|
entity_description: ProtectSensorEntityDescription
|
|
|
|
def __init__(
|
|
self,
|
|
data: ProtectData,
|
|
device: NVR,
|
|
description: ProtectSensorEntityDescription,
|
|
) -> None:
|
|
"""Initialize an UniFi Protect sensor."""
|
|
super().__init__(data, device, description)
|
|
|
|
@callback
|
|
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
|
super()._async_update_device_from_protect(device)
|
|
self._attr_native_value = self.entity_description.get_ufp_value(self.device)
|
|
|
|
|
|
class ProtectEventSensor(EventEntityMixin, SensorEntity):
|
|
"""A UniFi Protect Device Sensor with access tokens."""
|
|
|
|
entity_description: ProtectSensorEventEntityDescription
|
|
|
|
def __init__(
|
|
self,
|
|
data: ProtectData,
|
|
device: ProtectAdoptableDeviceModel,
|
|
description: ProtectSensorEventEntityDescription,
|
|
) -> None:
|
|
"""Initialize an UniFi Protect sensor."""
|
|
super().__init__(data, device, description)
|
|
|
|
@callback
|
|
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
|
# do not call ProtectDeviceSensor method since we want event to get value here
|
|
EventEntityMixin._async_update_device_from_protect(self, device)
|
|
is_on = self.entity_description.get_is_on(device)
|
|
is_license_plate = (
|
|
self.entity_description.ufp_smart_type
|
|
== SmartDetectObjectType.LICENSE_PLATE
|
|
)
|
|
if (
|
|
not is_on
|
|
or self._event is None
|
|
or (
|
|
is_license_plate
|
|
and (
|
|
self._event.metadata is None
|
|
or self._event.metadata.license_plate is None
|
|
)
|
|
)
|
|
):
|
|
self._attr_native_value = OBJECT_TYPE_NONE
|
|
self._event = None
|
|
self._attr_extra_state_attributes = {}
|
|
return
|
|
|
|
if is_license_plate:
|
|
# type verified above
|
|
self._attr_native_value = self._event.metadata.license_plate.name # type: ignore[union-attr]
|
|
else:
|
|
self._attr_native_value = self._event.smart_detect_types[0].value
|