"""Component providing sensors for UniFi Protect."""

from collections.abc import Callable, Sequence
from dataclasses import dataclass
from datetime import datetime
from functools import partial
import logging
from typing import Any, cast, override

from uiprotect.data import (
    NVR,
    Camera,
    Light,
    ModelType,
    ProtectAdoptableDeviceModel,
    ProtectDeviceModel,
    Sensor,
)
from uiprotect.data.public_devices import (
    PublicDeviceModel,
    PublicLight,
    SensorFeatureCapability,
)
from uiprotect.utils import convert_to_datetime

from homeassistant.components.sensor import (
    SensorDeviceClass,
    SensorEntity,
    SensorEntityDescription,
    SensorStateClass,
)
from homeassistant.const import (
    LIGHT_LUX,
    PERCENTAGE,
    SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
    EntityCategory,
    Platform,
    UnitOfDataRate,
    UnitOfElectricPotential,
    UnitOfInformation,
    UnitOfTemperature,
    UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .data import ProtectData, ProtectDeviceType, UFPConfigEntry
from .entity import (
    BaseProtectEntity,
    EventEntityMixin,
    PermRequired,
    ProtectDeviceEntity,
    ProtectEntityDescription,
    ProtectEventMixin,
    ProtectNVREntity,
    T,
    async_all_device_entities,
    async_remove_unsupported_sense_entities,
)
from .utils import async_get_light_motion_current_public

_LOGGER = logging.getLogger(__name__)
OBJECT_TYPE_NONE = "none"
PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
class ProtectSensorEntityDescription(
    ProtectEntityDescription[T], SensorEntityDescription
):
    """Describes UniFi Protect Sensor entity."""

    precision: int | None = None

    @override
    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_last_motion_public(obj: PublicDeviceModel) -> datetime | None:
    # Public API reports last motion as a JS epoch (ms); private side a datetime.
    return convert_to_datetime(cast(PublicLight, obj).last_motion)


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",
        translation_key="uptime",
        device_class=SensorDeviceClass.TIMESTAMP,
        entity_category=EntityCategory.DIAGNOSTIC,
        entity_registry_enabled_default=False,
        ufp_value_fn=_get_uptime,
    ),
    ProtectSensorEntityDescription(
        key="ble_signal",
        translation_key="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",
        translation_key="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",
        translation_key="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",
        translation_key="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",
        translation_key="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",
        translation_key="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",
        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",
        translation_key="last_doorbell_ring",
        device_class=SensorDeviceClass.TIMESTAMP,
        ufp_required_field="feature_flags.is_doorbell",
        ufp_value="last_ring",
        entity_registry_enabled_default=False,
    ),
    ProtectSensorEntityDescription(
        key="lens_type",
        translation_key="lens_type",
        entity_category=EntityCategory.DIAGNOSTIC,
        ufp_required_field="has_removable_lens",
        ufp_value="feature_flags.lens_type",
    ),
    ProtectSensorEntityDescription(
        key="mic_level",
        translation_key="microphone_level",
        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",
        translation_key="recording_mode",
        entity_category=EntityCategory.DIAGNOSTIC,
        ufp_value="recording_settings.mode.value",
        ufp_perm=PermRequired.NO_WRITE,
    ),
    ProtectSensorEntityDescription(
        key="infrared",
        translation_key="infrared_mode",
        entity_category=EntityCategory.DIAGNOSTIC,
        ufp_required_field="feature_flags.has_led_ir",
        ufp_value="isp_settings.ir_led_mode.value",
        ufp_perm=PermRequired.NO_WRITE,
    ),
    ProtectSensorEntityDescription(
        key="doorbell_text",
        translation_key="doorbell_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",
        translation_key="chime_type",
        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",
        translation_key="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",
        translation_key="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",
        native_unit_of_measurement=PERCENTAGE,
        device_class=SensorDeviceClass.BATTERY,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_public_value="wireless_connection_state.battery_status.percentage",
    ),
    ProtectSensorEntityDescription(
        key="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",
        ufp_capability=SensorFeatureCapability.LIGHT,
    ),
    ProtectSensorEntityDescription(
        key="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",
        ufp_capability=SensorFeatureCapability.HUMIDITY,
    ),
    ProtectSensorEntityDescription(
        key="temperature_level",
        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",
        ufp_capability=SensorFeatureCapability.TEMPERATURE,
    ),
    ProtectSensorEntityDescription[Sensor](
        key="alarm_sound",
        translation_key="alarm_sound_detected",
        ufp_value_fn=_get_alarm_sound,
        ufp_enabled="is_alarm_sensor_enabled",
        ufp_capability=SensorFeatureCapability.SMOKE,
    ),
    ProtectSensorEntityDescription(
        key="door_last_trip_time",
        translation_key="last_open",
        device_class=SensorDeviceClass.TIMESTAMP,
        ufp_value="open_status_changed_at",
        ufp_capability=SensorFeatureCapability.OPEN,
        entity_registry_enabled_default=False,
    ),
    ProtectSensorEntityDescription(
        key="motion_last_trip_time",
        translation_key="last_motion_detected",
        device_class=SensorDeviceClass.TIMESTAMP,
        ufp_value="motion_detected_at",
        ufp_capability=SensorFeatureCapability.MOTION,
        entity_registry_enabled_default=False,
    ),
    ProtectSensorEntityDescription(
        key="tampering_last_trip_time",
        translation_key="last_tampering_detected",
        ufp_capability=SensorFeatureCapability.TAMPER,
        device_class=SensorDeviceClass.TIMESTAMP,
        ufp_value="tampering_detected_at",
        entity_registry_enabled_default=False,
    ),
    ProtectSensorEntityDescription(
        key="sensitivity",
        translation_key="sensitivity",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        ufp_value="motion_settings.sensitivity",
        ufp_capability=SensorFeatureCapability.MOTION,
        ufp_perm=PermRequired.NO_WRITE,
    ),
    ProtectSensorEntityDescription(
        key="mount_type",
        translation_key="mount_type",
        entity_category=EntityCategory.DIAGNOSTIC,
        ufp_value="mount_type",
        ufp_capability=SensorFeatureCapability.OPEN,
        ufp_perm=PermRequired.NO_WRITE,
    ),
    ProtectSensorEntityDescription(
        key="paired_camera",
        translation_key="paired_camera",
        entity_category=EntityCategory.DIAGNOSTIC,
        ufp_value="camera.display_name",
        ufp_perm=PermRequired.NO_WRITE,
    ),
)

NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
    ProtectSensorEntityDescription(
        key="uptime",
        translation_key="uptime",
        device_class=SensorDeviceClass.TIMESTAMP,
        entity_category=EntityCategory.DIAGNOSTIC,
        ufp_value_fn=_get_uptime,
    ),
    ProtectSensorEntityDescription(
        key="storage_utilization",
        translation_key="storage_utilization",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_value="storage_stats.utilization",
        precision=2,
    ),
    ProtectSensorEntityDescription(
        key="record_rotating",
        translation_key="type_timelapse_video",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_value="storage_stats.storage_distribution.timelapse_recordings.percentage",
        precision=2,
    ),
    ProtectSensorEntityDescription(
        key="record_timelapse",
        translation_key="type_continuous_video",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_value="storage_stats.storage_distribution.continuous_recordings.percentage",
        precision=2,
    ),
    ProtectSensorEntityDescription(
        key="record_detections",
        translation_key="type_detections_video",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_value="storage_stats.storage_distribution.detections_recordings.percentage",
        precision=2,
    ),
    ProtectSensorEntityDescription(
        key="resolution_HD",
        translation_key="resolution_hd_video",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_value="storage_stats.storage_distribution.hd_usage.percentage",
        precision=2,
    ),
    ProtectSensorEntityDescription(
        key="resolution_4K",
        translation_key="resolution_4k_video",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_value="storage_stats.storage_distribution.uhd_usage.percentage",
        precision=2,
    ),
    ProtectSensorEntityDescription(
        key="resolution_free",
        translation_key="resolution_free_space",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_value="storage_stats.storage_distribution.free.percentage",
        precision=2,
    ),
    ProtectSensorEntityDescription[NVR](
        key="record_capacity",
        translation_key="recording_capacity",
        native_unit_of_measurement=UnitOfTime.SECONDS,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_value_fn=_get_nvr_recording_capacity,
    ),
)

NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
    ProtectSensorEntityDescription(
        key="cpu_utilization",
        translation_key="cpu_utilization",
        native_unit_of_measurement=PERCENTAGE,
        entity_registry_enabled_default=False,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_value="system_info.cpu.average_load",
    ),
    ProtectSensorEntityDescription(
        key="cpu_temperature",
        translation_key="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",
        translation_key="memory_utilization",
        native_unit_of_measurement=PERCENTAGE,
        entity_registry_enabled_default=False,
        entity_category=EntityCategory.DIAGNOSTIC,
        state_class=SensorStateClass.MEASUREMENT,
        ufp_value_fn=_get_nvr_memory,
        precision=2,
    ),
)

LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
    ProtectSensorEntityDescription(
        key="motion_last_trip_time",
        translation_key="last_motion_detected",
        device_class=SensorDeviceClass.TIMESTAMP,
        ufp_public_value_fn=_get_last_motion_public,
        entity_registry_enabled_default=False,
    ),
    ProtectSensorEntityDescription(
        key="sensitivity",
        translation_key="motion_sensitivity",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        ufp_public_value="light_device_settings.pir_sensitivity",
        ufp_perm=PermRequired.NO_WRITE,
    ),
    ProtectSensorEntityDescription[Light](
        key="light_motion",
        translation_key="light_mode",
        entity_category=EntityCategory.DIAGNOSTIC,
        ufp_public_value_fn=async_get_light_motion_current_public,
        ufp_perm=PermRequired.NO_WRITE,
    ),
    ProtectSensorEntityDescription(
        key="paired_camera",
        translation_key="paired_camera",
        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",
        translation_key="last_motion_detected",
        device_class=SensorDeviceClass.TIMESTAMP,
        ufp_value="last_motion",
        entity_registry_enabled_default=False,
    ),
)

CHIME_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
    ProtectSensorEntityDescription(
        key="last_ring",
        translation_key="last_ring",
        device_class=SensorDeviceClass.TIMESTAMP,
        ufp_value="last_ring",
    ),
    ProtectSensorEntityDescription(
        key="volume",
        translation_key="volume",
        native_unit_of_measurement=PERCENTAGE,
        entity_category=EntityCategory.DIAGNOSTIC,
        ufp_value="volume",
        ufp_perm=PermRequired.NO_WRITE,
    ),
)

VIEWER_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
    ProtectSensorEntityDescription(
        key="viewer",
        translation_key="liveview",
        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.CHIME: CHIME_SENSORS,
    ModelType.VIEWPORT: VIEWER_SENSORS,
}


async def async_setup_entry(
    hass: HomeAssistant,
    entry: UFPConfigEntry,
    async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
    """Set up sensors for UniFi Protect integration."""
    data = entry.runtime_data
    async_remove_unsupported_sense_entities(hass, Platform.SENSOR, data, SENSE_SENSORS)

    @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,
        )
        # AiPort inherits from Camera but should not create camera-specific entities
        if (
            device.model is ModelType.CAMERA
            and isinstance(device, Camera)
            and device.is_adopted_by_us
        ):
            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,
            )

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

    @override
    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_value(
            self.device, self._ufp_public_obj
        )


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