1 """Component providing sensors for UniFi Protect."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Sequence
6 from dataclasses
import dataclass
7 from datetime
import datetime
8 from functools
import partial
10 from typing
import Any
12 from uiprotect.data
import (
17 ProtectAdoptableDeviceModel,
20 SmartDetectObjectType,
26 SensorEntityDescription,
32 SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
35 UnitOfElectricPotential,
43 from .data
import ProtectData, ProtectDeviceType, UFPConfigEntry
49 ProtectEntityDescription,
53 async_all_device_entities,
55 from .utils
import async_get_light_motion_current
57 _LOGGER = logging.getLogger(__name__)
58 OBJECT_TYPE_NONE =
"none"
61 @dataclass(frozen=True, kw_only=True)
63 ProtectEntityDescription[T], SensorEntityDescription
65 """Describes UniFi Protect Sensor entity."""
67 precision: int |
None =
None
70 """Ensure values are rounded if precision is set."""
72 if precision := self.precision:
79 def _rounded_value(self, precision: int, getter: Callable[[T], Any], obj: T) -> Any:
80 """Round value to precision if set."""
81 return None if (v := getter(obj))
is None else round(v, precision)
84 @dataclass(frozen=True, kw_only=True)
86 ProtectEventMixin[T], SensorEntityDescription
88 """Describes UniFi Protect Sensor entity."""
92 if obj.up_since
is None:
97 return obj.up_since.replace(second=0, microsecond=0)
101 if obj.storage_stats.capacity
is None:
104 return int(obj.storage_stats.capacity.total_seconds())
108 memory = obj.system_info.memory
109 if memory.available
is None or memory.total
is None:
111 return (1 - memory.available / memory.total) * 100
115 alarm_type = OBJECT_TYPE_NONE
117 obj.is_alarm_detected
118 and obj.last_alarm_event
is not None
119 and obj.last_alarm_event.metadata
is not None
121 alarm_type = obj.last_alarm_event.metadata.alarm_type
or OBJECT_TYPE_NONE
122 return alarm_type.lower()
125 ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
130 device_class=SensorDeviceClass.TIMESTAMP,
131 entity_category=EntityCategory.DIAGNOSTIC,
132 entity_registry_enabled_default=
False,
133 ufp_value_fn=_get_uptime,
137 name=
"Bluetooth signal strength",
138 native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
139 device_class=SensorDeviceClass.SIGNAL_STRENGTH,
140 entity_category=EntityCategory.DIAGNOSTIC,
141 entity_registry_enabled_default=
False,
142 state_class=SensorStateClass.MEASUREMENT,
143 ufp_value=
"bluetooth_connection_state.signal_strength",
144 ufp_required_field=
"bluetooth_connection_state.signal_strength",
149 device_class=SensorDeviceClass.DATA_RATE,
150 native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
151 entity_category=EntityCategory.DIAGNOSTIC,
152 entity_registry_enabled_default=
False,
153 state_class=SensorStateClass.MEASUREMENT,
154 ufp_value=
"wired_connection_state.phy_rate",
155 ufp_required_field=
"wired_connection_state.phy_rate",
159 name=
"WiFi signal strength",
160 native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
161 device_class=SensorDeviceClass.SIGNAL_STRENGTH,
162 entity_registry_enabled_default=
False,
163 entity_category=EntityCategory.DIAGNOSTIC,
164 state_class=SensorStateClass.MEASUREMENT,
165 ufp_value=
"wifi_connection_state.signal_strength",
166 ufp_required_field=
"wifi_connection_state.signal_strength",
170 CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
172 key=
"oldest_recording",
173 name=
"Oldest recording",
174 device_class=SensorDeviceClass.TIMESTAMP,
175 entity_category=EntityCategory.DIAGNOSTIC,
176 entity_registry_enabled_default=
False,
177 ufp_value=
"stats.video.recording_start",
182 native_unit_of_measurement=UnitOfInformation.BYTES,
183 device_class=SensorDeviceClass.DATA_SIZE,
184 entity_category=EntityCategory.DIAGNOSTIC,
185 state_class=SensorStateClass.MEASUREMENT,
186 ufp_value=
"stats.storage.used",
187 suggested_unit_of_measurement=UnitOfInformation.MEGABYTES,
188 suggested_display_precision=2,
192 name=
"Disk write rate",
193 device_class=SensorDeviceClass.DATA_RATE,
194 native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
195 entity_category=EntityCategory.DIAGNOSTIC,
196 state_class=SensorStateClass.MEASUREMENT,
197 ufp_value=
"stats.storage.rate_per_second",
199 suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
200 suggested_display_precision=2,
205 device_class=SensorDeviceClass.VOLTAGE,
206 native_unit_of_measurement=UnitOfElectricPotential.VOLT,
207 entity_category=EntityCategory.DIAGNOSTIC,
208 state_class=SensorStateClass.MEASUREMENT,
212 ufp_required_field=
"voltage",
216 key=
"doorbell_last_trip_time",
217 name=
"Last doorbell ring",
218 device_class=SensorDeviceClass.TIMESTAMP,
219 icon=
"mdi:doorbell-video",
220 ufp_required_field=
"feature_flags.is_doorbell",
221 ufp_value=
"last_ring",
222 entity_registry_enabled_default=
False,
227 entity_category=EntityCategory.DIAGNOSTIC,
228 icon=
"mdi:camera-iris",
229 ufp_required_field=
"has_removable_lens",
230 ufp_value=
"feature_flags.lens_type",
234 name=
"Microphone level",
235 icon=
"mdi:microphone",
236 native_unit_of_measurement=PERCENTAGE,
237 entity_category=EntityCategory.DIAGNOSTIC,
238 ufp_required_field=
"has_mic",
239 ufp_value=
"mic_volume",
240 ufp_enabled=
"feature_flags.has_mic",
241 ufp_perm=PermRequired.NO_WRITE,
244 key=
"recording_mode",
245 name=
"Recording mode",
246 icon=
"mdi:video-outline",
247 entity_category=EntityCategory.DIAGNOSTIC,
248 ufp_value=
"recording_settings.mode.value",
249 ufp_perm=PermRequired.NO_WRITE,
253 name=
"Infrared mode",
254 icon=
"mdi:circle-opacity",
255 entity_category=EntityCategory.DIAGNOSTIC,
256 ufp_required_field=
"feature_flags.has_led_ir",
257 ufp_value=
"isp_settings.ir_led_mode.value",
258 ufp_perm=PermRequired.NO_WRITE,
262 name=
"Doorbell text",
263 icon=
"mdi:card-text",
264 entity_category=EntityCategory.DIAGNOSTIC,
265 ufp_required_field=
"feature_flags.has_lcd_screen",
266 ufp_value=
"lcd_message.text",
267 ufp_perm=PermRequired.NO_WRITE,
273 entity_category=EntityCategory.DIAGNOSTIC,
274 entity_registry_enabled_default=
False,
275 ufp_required_field=
"feature_flags.has_chime",
276 ufp_value=
"chime_type",
280 CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
283 name=
"Received data",
284 native_unit_of_measurement=UnitOfInformation.BYTES,
285 device_class=SensorDeviceClass.DATA_SIZE,
286 entity_registry_enabled_default=
False,
287 entity_category=EntityCategory.DIAGNOSTIC,
288 state_class=SensorStateClass.TOTAL_INCREASING,
289 ufp_value=
"stats.rx_bytes",
290 suggested_unit_of_measurement=UnitOfInformation.MEGABYTES,
291 suggested_display_precision=2,
295 name=
"Transferred data",
296 native_unit_of_measurement=UnitOfInformation.BYTES,
297 device_class=SensorDeviceClass.DATA_SIZE,
298 entity_registry_enabled_default=
False,
299 entity_category=EntityCategory.DIAGNOSTIC,
300 state_class=SensorStateClass.TOTAL_INCREASING,
301 ufp_value=
"stats.tx_bytes",
302 suggested_unit_of_measurement=UnitOfInformation.MEGABYTES,
303 suggested_display_precision=2,
307 SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
310 name=
"Battery level",
311 native_unit_of_measurement=PERCENTAGE,
312 device_class=SensorDeviceClass.BATTERY,
313 entity_category=EntityCategory.DIAGNOSTIC,
314 state_class=SensorStateClass.MEASUREMENT,
315 ufp_value=
"battery_status.percentage",
320 native_unit_of_measurement=LIGHT_LUX,
321 device_class=SensorDeviceClass.ILLUMINANCE,
322 state_class=SensorStateClass.MEASUREMENT,
323 ufp_value=
"stats.light.value",
324 ufp_enabled=
"is_light_sensor_enabled",
327 key=
"humidity_level",
328 name=
"Humidity level",
329 native_unit_of_measurement=PERCENTAGE,
330 device_class=SensorDeviceClass.HUMIDITY,
331 state_class=SensorStateClass.MEASUREMENT,
332 ufp_value=
"stats.humidity.value",
333 ufp_enabled=
"is_humidity_sensor_enabled",
336 key=
"temperature_level",
338 native_unit_of_measurement=UnitOfTemperature.CELSIUS,
339 device_class=SensorDeviceClass.TEMPERATURE,
340 state_class=SensorStateClass.MEASUREMENT,
341 ufp_value=
"stats.temperature.value",
342 ufp_enabled=
"is_temperature_sensor_enabled",
344 ProtectSensorEntityDescription[Sensor](
346 name=
"Alarm sound detected",
347 ufp_value_fn=_get_alarm_sound,
348 ufp_enabled=
"is_alarm_sensor_enabled",
351 key=
"door_last_trip_time",
353 device_class=SensorDeviceClass.TIMESTAMP,
354 ufp_value=
"open_status_changed_at",
355 entity_registry_enabled_default=
False,
358 key=
"motion_last_trip_time",
359 name=
"Last motion detected",
360 device_class=SensorDeviceClass.TIMESTAMP,
361 ufp_value=
"motion_detected_at",
362 entity_registry_enabled_default=
False,
365 key=
"tampering_last_trip_time",
366 name=
"Last tampering detected",
367 device_class=SensorDeviceClass.TIMESTAMP,
368 ufp_value=
"tampering_detected_at",
369 entity_registry_enabled_default=
False,
373 name=
"Motion sensitivity",
375 native_unit_of_measurement=PERCENTAGE,
376 entity_category=EntityCategory.DIAGNOSTIC,
377 ufp_value=
"motion_settings.sensitivity",
378 ufp_perm=PermRequired.NO_WRITE,
383 icon=
"mdi:screwdriver",
384 entity_category=EntityCategory.DIAGNOSTIC,
385 ufp_value=
"mount_type",
386 ufp_perm=PermRequired.NO_WRITE,
390 name=
"Paired camera",
392 entity_category=EntityCategory.DIAGNOSTIC,
393 ufp_value=
"camera.display_name",
394 ufp_perm=PermRequired.NO_WRITE,
398 DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
401 name=
"Battery level",
402 native_unit_of_measurement=PERCENTAGE,
403 device_class=SensorDeviceClass.BATTERY,
404 entity_category=EntityCategory.DIAGNOSTIC,
405 state_class=SensorStateClass.MEASUREMENT,
406 ufp_value=
"battery_status.percentage",
410 name=
"Paired camera",
412 entity_category=EntityCategory.DIAGNOSTIC,
413 ufp_value=
"camera.display_name",
414 ufp_perm=PermRequired.NO_WRITE,
418 NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
423 device_class=SensorDeviceClass.TIMESTAMP,
424 entity_category=EntityCategory.DIAGNOSTIC,
425 ufp_value_fn=_get_uptime,
428 key=
"storage_utilization",
429 name=
"Storage utilization",
430 native_unit_of_measurement=PERCENTAGE,
432 entity_category=EntityCategory.DIAGNOSTIC,
433 state_class=SensorStateClass.MEASUREMENT,
434 ufp_value=
"storage_stats.utilization",
438 key=
"record_rotating",
439 name=
"Type: timelapse video",
440 native_unit_of_measurement=PERCENTAGE,
442 entity_category=EntityCategory.DIAGNOSTIC,
443 state_class=SensorStateClass.MEASUREMENT,
444 ufp_value=
"storage_stats.storage_distribution.timelapse_recordings.percentage",
448 key=
"record_timelapse",
449 name=
"Type: continuous video",
450 native_unit_of_measurement=PERCENTAGE,
452 entity_category=EntityCategory.DIAGNOSTIC,
453 state_class=SensorStateClass.MEASUREMENT,
454 ufp_value=
"storage_stats.storage_distribution.continuous_recordings.percentage",
458 key=
"record_detections",
459 name=
"Type: detections video",
460 native_unit_of_measurement=PERCENTAGE,
462 entity_category=EntityCategory.DIAGNOSTIC,
463 state_class=SensorStateClass.MEASUREMENT,
464 ufp_value=
"storage_stats.storage_distribution.detections_recordings.percentage",
469 name=
"Resolution: HD video",
470 native_unit_of_measurement=PERCENTAGE,
472 entity_category=EntityCategory.DIAGNOSTIC,
473 state_class=SensorStateClass.MEASUREMENT,
474 ufp_value=
"storage_stats.storage_distribution.hd_usage.percentage",
479 name=
"Resolution: 4K video",
480 native_unit_of_measurement=PERCENTAGE,
482 entity_category=EntityCategory.DIAGNOSTIC,
483 state_class=SensorStateClass.MEASUREMENT,
484 ufp_value=
"storage_stats.storage_distribution.uhd_usage.percentage",
488 key=
"resolution_free",
489 name=
"Resolution: free space",
490 native_unit_of_measurement=PERCENTAGE,
492 entity_category=EntityCategory.DIAGNOSTIC,
493 state_class=SensorStateClass.MEASUREMENT,
494 ufp_value=
"storage_stats.storage_distribution.free.percentage",
497 ProtectSensorEntityDescription[NVR](
498 key=
"record_capacity",
499 name=
"Recording capacity",
500 native_unit_of_measurement=UnitOfTime.SECONDS,
501 icon=
"mdi:record-rec",
502 entity_category=EntityCategory.DIAGNOSTIC,
503 state_class=SensorStateClass.MEASUREMENT,
504 ufp_value_fn=_get_nvr_recording_capacity,
508 NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
510 key=
"cpu_utilization",
511 name=
"CPU utilization",
512 native_unit_of_measurement=PERCENTAGE,
513 icon=
"mdi:speedometer",
514 entity_registry_enabled_default=
False,
515 entity_category=EntityCategory.DIAGNOSTIC,
516 state_class=SensorStateClass.MEASUREMENT,
517 ufp_value=
"system_info.cpu.average_load",
520 key=
"cpu_temperature",
521 name=
"CPU temperature",
522 native_unit_of_measurement=UnitOfTemperature.CELSIUS,
523 device_class=SensorDeviceClass.TEMPERATURE,
524 entity_registry_enabled_default=
False,
525 entity_category=EntityCategory.DIAGNOSTIC,
526 state_class=SensorStateClass.MEASUREMENT,
527 ufp_value=
"system_info.cpu.temperature",
529 ProtectSensorEntityDescription[NVR](
530 key=
"memory_utilization",
531 name=
"Memory utilization",
532 native_unit_of_measurement=PERCENTAGE,
534 entity_registry_enabled_default=
False,
535 entity_category=EntityCategory.DIAGNOSTIC,
536 state_class=SensorStateClass.MEASUREMENT,
537 ufp_value_fn=_get_nvr_memory,
542 LICENSE_PLATE_EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
544 key=
"smart_obj_licenseplate",
545 name=
"License plate detected",
547 translation_key=
"license_plate",
548 ufp_obj_type=SmartDetectObjectType.LICENSE_PLATE,
549 ufp_required_field=
"can_detect_license_plate",
550 ufp_event_obj=
"last_license_plate_detect_event",
555 LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
557 key=
"motion_last_trip_time",
558 name=
"Last motion detected",
559 device_class=SensorDeviceClass.TIMESTAMP,
560 ufp_value=
"last_motion",
561 entity_registry_enabled_default=
False,
565 name=
"Motion sensitivity",
567 native_unit_of_measurement=PERCENTAGE,
568 entity_category=EntityCategory.DIAGNOSTIC,
569 ufp_value=
"light_device_settings.pir_sensitivity",
570 ufp_perm=PermRequired.NO_WRITE,
572 ProtectSensorEntityDescription[Light](
575 icon=
"mdi:spotlight",
576 entity_category=EntityCategory.DIAGNOSTIC,
577 ufp_value_fn=async_get_light_motion_current,
578 ufp_perm=PermRequired.NO_WRITE,
582 name=
"Paired camera",
584 entity_category=EntityCategory.DIAGNOSTIC,
585 ufp_value=
"camera.display_name",
586 ufp_perm=PermRequired.NO_WRITE,
590 MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
592 key=
"motion_last_trip_time",
593 name=
"Last motion detected",
594 device_class=SensorDeviceClass.TIMESTAMP,
595 ufp_value=
"last_motion",
596 entity_registry_enabled_default=
False,
600 CHIME_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
604 device_class=SensorDeviceClass.TIMESTAMP,
606 ufp_value=
"last_ring",
612 native_unit_of_measurement=PERCENTAGE,
613 entity_category=EntityCategory.DIAGNOSTIC,
615 ufp_perm=PermRequired.NO_WRITE,
619 VIEWER_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
623 icon=
"mdi:view-dashboard",
624 entity_category=EntityCategory.DIAGNOSTIC,
625 ufp_value=
"liveview.name",
626 ufp_perm=PermRequired.NO_WRITE,
630 _MODEL_DESCRIPTIONS: dict[ModelType, Sequence[ProtectEntityDescription]] = {
631 ModelType.CAMERA: CAMERA_SENSORS + CAMERA_DISABLED_SENSORS,
632 ModelType.SENSOR: SENSE_SENSORS,
633 ModelType.LIGHT: LIGHT_SENSORS,
634 ModelType.DOORLOCK: DOORLOCK_SENSORS,
635 ModelType.CHIME: CHIME_SENSORS,
636 ModelType.VIEWPORT: VIEWER_SENSORS,
642 entry: UFPConfigEntry,
643 async_add_entities: AddEntitiesCallback,
645 """Set up sensors for UniFi Protect integration."""
646 data = entry.runtime_data
649 def _add_new_device(device: ProtectAdoptableDeviceModel) ->
None:
653 all_descs=ALL_DEVICES_SENSORS,
654 model_descriptions=_MODEL_DESCRIPTIONS,
657 if device.is_adopted_by_us
and isinstance(device, Camera):
661 data.async_subscribe_adopt(_add_new_device)
665 all_descs=ALL_DEVICES_SENSORS,
666 model_descriptions=_MODEL_DESCRIPTIONS,
677 ufp_device: Camera |
None =
None,
678 ) -> list[ProtectDeviceEntity]:
679 entities: list[ProtectDeviceEntity] = []
680 cameras = data.get_cameras()
if ufp_device
is None else [ufp_device]
681 for camera
in cameras:
682 for description
in MOTION_TRIP_SENSORS:
685 "Adding trip sensor entity %s for %s",
690 if not camera.feature_flags.has_smart_detect:
693 for event_desc
in LICENSE_PLATE_EVENT_SENSORS:
694 if not event_desc.has_required(camera):
699 "Adding sensor entity %s for %s",
710 ) -> list[BaseProtectEntity]:
711 entities: list[BaseProtectEntity] = []
712 device = data.api.bootstrap.nvr
713 for description
in NVR_SENSORS + NVR_DISABLED_SENSORS:
715 _LOGGER.debug(
"Adding NVR sensor entity %s", description.name)
721 """A UniFi Protect Sensor Entity."""
723 entity_description: ProtectSensorEntityDescription
724 _state_attrs = (
"_attr_available",
"_attr_native_value")
732 """A Ubiquiti UniFi Protect Sensor."""
735 class ProtectNVRSensor(BaseProtectSensor, ProtectNVREntity):
736 """A Ubiquiti UniFi Protect Sensor."""
740 """A UniFi Protect Device Sensor with access tokens."""
742 entity_description: ProtectSensorEventEntityDescription
745 "_attr_native_value",
746 "_attr_extra_state_attributes",
751 """A UniFi Protect license plate sensor."""
764 prev_event = self.
_event_event
767 if event := description.get_event_obj(device):
773 and (metadata := event.metadata)
774 and (license_plate := metadata.license_plate)
775 and description.has_matching_smart(event)
None _async_event_with_immediate_end(self)
_attr_extra_state_attributes
None _set_event_done(self)
bool _event_already_ended(self, Event|None prev_event, datetime|None prev_event_end)
None _set_event_attrs(self, Event event)
Any get_ufp_value(self, T obj)
None _async_update_device_from_protect(self, ProtectDeviceType device)
_attr_extra_state_attributes
None _set_event_done(self)
None _async_update_device_from_protect(self, ProtectDeviceType device)
Any _rounded_value(self, int precision, Callable[[T], Any] getter, T obj)
list[BaseProtectEntity] async_all_device_entities(ProtectData data, type[BaseProtectEntity] klass, dict[ModelType, Sequence[ProtectEntityDescription]]|None model_descriptions=None, Sequence[ProtectEntityDescription]|None all_descs=None, list[ProtectEntityDescription]|None unadopted_descs=None, ProtectAdoptableDeviceModel|None ufp_device=None)
list[ProtectDeviceEntity] _async_event_entities(ProtectData data, Camera|None ufp_device=None)
int _get_nvr_recording_capacity(NVR obj)
float|None _get_nvr_memory(NVR obj)
list[BaseProtectEntity] _async_nvr_entities(ProtectData data)
datetime|None _get_uptime(ProtectDeviceModel obj)
None async_setup_entry(HomeAssistant hass, UFPConfigEntry entry, AddEntitiesCallback async_add_entities)
str _get_alarm_sound(Sensor obj)