"""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