core/homeassistant/components/unifiprotect/sensor.py

806 lines
27 KiB
Python

"""Component providing 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,
)
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,
EntityCategory,
UnitOfDataRate,
UnitOfElectricPotential,
UnitOfInformation,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
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"
@dataclass(frozen=True)
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(frozen=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",
),
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="smart_obj_licenseplate",
name="License Plate Detected",
icon="mdi:car",
translation_key="license_plate",
ufp_value="is_smart_detected",
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,
),
)
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 _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)
@callback
def _async_updated_event(self, device: ProtectModelWithId) -> None:
"""Call back for incoming data that only writes when state has changed.
Only the native value and available are ever updated for these
entities, and since the websocket update for the device will trigger
an update for all entities connected to the device, we want to avoid
writing state unless something has actually changed.
"""
previous_value = self._attr_native_value
previous_available = self._attr_available
self._async_update_device_from_protect(device)
if (
self._attr_native_value != previous_value
or self._attr_available != previous_available
):
_LOGGER.debug(
"Updating state [%s (%s)] %s (%s) -> %s (%s)",
device.name,
device.mac,
previous_value,
previous_available,
self._attr_native_value,
self._attr_available,
)
self.async_write_ha_state()
class ProtectNVRSensor(ProtectNVREntity, SensorEntity):
"""A Ubiquiti UniFi Protect Sensor."""
entity_description: ProtectSensorEntityDescription
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)
@callback
def _async_updated_event(self, device: ProtectModelWithId) -> None:
"""Call back for incoming data that only writes when state has changed.
Only the native value and available are ever updated for these
entities, and since the websocket update for the device will trigger
an update for all entities connected to the device, we want to avoid
writing state unless something has actually changed.
"""
previous_value = self._attr_native_value
previous_available = self._attr_available
self._async_update_device_from_protect(device)
if (
self._attr_native_value != previous_value
or self._attr_available != previous_available
):
self.async_write_ha_state()
class ProtectEventSensor(EventEntityMixin, SensorEntity):
"""A UniFi Protect Device Sensor with access tokens."""
entity_description: ProtectSensorEventEntityDescription
@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)
event = self._event
entity_description = self.entity_description
is_on = entity_description.get_is_on(event)
is_license_plate = (
entity_description.ufp_event_obj == "last_license_plate_detect_event"
)
if (
not is_on
or event is None
or (
is_license_plate
and (event.metadata is None or 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 = event.metadata.license_plate.name # type: ignore[union-attr]
else:
self._attr_native_value = event.smart_detect_types[0].value