Add UniFi Protect sensor platform (#63524)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
da71e89f53
commit
7d442122c0
3 changed files with 728 additions and 0 deletions
|
@ -61,6 +61,7 @@ PLATFORMS = [
|
||||||
Platform.MEDIA_PLAYER,
|
Platform.MEDIA_PLAYER,
|
||||||
Platform.NUMBER,
|
Platform.NUMBER,
|
||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
|
Platform.SENSOR,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
444
homeassistant/components/unifiprotect/sensor.py
Normal file
444
homeassistant/components/unifiprotect/sensor.py
Normal file
|
@ -0,0 +1,444 @@
|
||||||
|
"""This component provides sensors for UniFi Protect."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pyunifiprotect.data import NVR
|
||||||
|
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import (
|
||||||
|
DATA_BYTES,
|
||||||
|
DATA_RATE_BYTES_PER_SECOND,
|
||||||
|
DATA_RATE_MEGABITS_PER_SECOND,
|
||||||
|
DEVICE_CLASS_BATTERY,
|
||||||
|
DEVICE_CLASS_HUMIDITY,
|
||||||
|
DEVICE_CLASS_ILLUMINANCE,
|
||||||
|
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_TIMESTAMP,
|
||||||
|
DEVICE_CLASS_VOLTAGE,
|
||||||
|
ELECTRIC_POTENTIAL_VOLT,
|
||||||
|
LIGHT_LUX,
|
||||||
|
PERCENTAGE,
|
||||||
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
TEMP_CELSIUS,
|
||||||
|
TIME_SECONDS,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity import Entity, EntityCategory
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .data import ProtectData
|
||||||
|
from .entity import ProtectDeviceEntity, ProtectNVREntity, async_all_device_entities
|
||||||
|
from .models import ProtectRequiredKeysMixin
|
||||||
|
from .utils import get_nested_attr
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProtectSensorEntityDescription(ProtectRequiredKeysMixin, SensorEntityDescription):
|
||||||
|
"""Describes UniFi Protect Sensor entity."""
|
||||||
|
|
||||||
|
precision: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
_KEY_UPTIME = "uptime"
|
||||||
|
_KEY_BLE = "ble_signal"
|
||||||
|
_KEY_WIRED = "phy_rate"
|
||||||
|
_KEY_WIFI = "wifi_signal"
|
||||||
|
|
||||||
|
_KEY_RX = "stats_rx"
|
||||||
|
_KEY_TX = "stats_tx"
|
||||||
|
_KEY_OLDEST = "oldest_recording"
|
||||||
|
_KEY_USED = "storage_used"
|
||||||
|
_KEY_WRITE_RATE = "write_rate"
|
||||||
|
_KEY_VOLTAGE = "voltage"
|
||||||
|
|
||||||
|
_KEY_BATTERY = "battery_level"
|
||||||
|
_KEY_LIGHT = "light_level"
|
||||||
|
_KEY_HUMIDITY = "humidity_level"
|
||||||
|
_KEY_TEMP = "temperature_level"
|
||||||
|
|
||||||
|
_KEY_CPU = "cpu_utilization"
|
||||||
|
_KEY_CPU_TEMP = "cpu_temperature"
|
||||||
|
_KEY_MEMORY = "memory_utilization"
|
||||||
|
_KEY_DISK = "storage_utilization"
|
||||||
|
_KEY_RECORD_ROTATE = "record_rotating"
|
||||||
|
_KEY_RECORD_TIMELAPSE = "record_timelapse"
|
||||||
|
_KEY_RECORD_DETECTIONS = "record_detections"
|
||||||
|
_KEY_RES_HD = "resolution_HD"
|
||||||
|
_KEY_RES_4K = "resolution_4K"
|
||||||
|
_KEY_RES_FREE = "resolution_free"
|
||||||
|
_KEY_CAPACITY = "record_capacity"
|
||||||
|
|
||||||
|
ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_UPTIME,
|
||||||
|
name="Uptime",
|
||||||
|
icon="mdi:clock",
|
||||||
|
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
ufp_value="up_since",
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_BLE,
|
||||||
|
name="Bluetooth Signal Strength",
|
||||||
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
device_class=DEVICE_CLASS_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=_KEY_WIRED,
|
||||||
|
name="Link Speed",
|
||||||
|
native_unit_of_measurement=DATA_RATE_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=_KEY_WIFI,
|
||||||
|
name="WiFi Signal Strength",
|
||||||
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
device_class=DEVICE_CLASS_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=_KEY_RX,
|
||||||
|
name="Received Data",
|
||||||
|
native_unit_of_measurement=DATA_BYTES,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
|
ufp_value="stats.rx_bytes",
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_TX,
|
||||||
|
name="Transferred Data",
|
||||||
|
native_unit_of_measurement=DATA_BYTES,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
|
ufp_value="stats.tx_bytes",
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_OLDEST,
|
||||||
|
name="Oldest Recording",
|
||||||
|
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
ufp_value="stats.video.recording_start",
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_USED,
|
||||||
|
name="Storage Used",
|
||||||
|
native_unit_of_measurement=DATA_BYTES,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
ufp_value="stats.storage.used",
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_WRITE_RATE,
|
||||||
|
name="Disk Write Rate",
|
||||||
|
native_unit_of_measurement=DATA_RATE_BYTES_PER_SECOND,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
ufp_value="stats.storage.rate",
|
||||||
|
precision=2,
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_VOLTAGE,
|
||||||
|
name="Voltage",
|
||||||
|
device_class=DEVICE_CLASS_VOLTAGE,
|
||||||
|
native_unit_of_measurement=ELECTRIC_POTENTIAL_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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_BATTERY,
|
||||||
|
name="Battery Level",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
device_class=DEVICE_CLASS_BATTERY,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
ufp_value="battery_status.percentage",
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_LIGHT,
|
||||||
|
name="Light Level",
|
||||||
|
native_unit_of_measurement=LIGHT_LUX,
|
||||||
|
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
ufp_value="stats.light.value",
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_HUMIDITY,
|
||||||
|
name="Humidity Level",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
device_class=DEVICE_CLASS_HUMIDITY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
ufp_value="stats.humidity.value",
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_TEMP,
|
||||||
|
name="Temperature",
|
||||||
|
native_unit_of_measurement=TEMP_CELSIUS,
|
||||||
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
ufp_value="stats.temperature.value",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_UPTIME,
|
||||||
|
name="Uptime",
|
||||||
|
icon="mdi:clock",
|
||||||
|
device_class=DEVICE_CLASS_TIMESTAMP,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
ufp_value="up_since",
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_CPU,
|
||||||
|
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=_KEY_CPU_TEMP,
|
||||||
|
name="CPU Temperature",
|
||||||
|
native_unit_of_measurement=TEMP_CELSIUS,
|
||||||
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
ufp_value="system_info.cpu.temperature",
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_MEMORY,
|
||||||
|
name="Memory Utilization",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
icon="mdi:memory",
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
precision=2,
|
||||||
|
),
|
||||||
|
ProtectSensorEntityDescription(
|
||||||
|
key=_KEY_DISK,
|
||||||
|
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=_KEY_RECORD_TIMELAPSE,
|
||||||
|
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=_KEY_RECORD_ROTATE,
|
||||||
|
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=_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=_KEY_RES_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=_KEY_RES_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=_KEY_RES_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(
|
||||||
|
key=_KEY_CAPACITY,
|
||||||
|
name="Recording Capacity",
|
||||||
|
native_unit_of_measurement=TIME_SECONDS,
|
||||||
|
icon="mdi:record-rec",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
ufp_value="storage_stats.capacity",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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]
|
||||||
|
entities: list[ProtectDeviceEntity] = async_all_device_entities(
|
||||||
|
data,
|
||||||
|
ProtectDeviceSensor,
|
||||||
|
all_descs=ALL_DEVICES_SENSORS,
|
||||||
|
camera_descs=CAMERA_SENSORS,
|
||||||
|
sense_descs=SENSE_SENSORS,
|
||||||
|
)
|
||||||
|
entities += _async_nvr_entities(data)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_nvr_entities(
|
||||||
|
data: ProtectData,
|
||||||
|
) -> list[ProtectDeviceEntity]:
|
||||||
|
entities: list[ProtectDeviceEntity] = []
|
||||||
|
device = data.api.bootstrap.nvr
|
||||||
|
for description in NVR_SENSORS:
|
||||||
|
entities.append(ProtectNVRSensor(data, device, description))
|
||||||
|
_LOGGER.debug("Adding NVR sensor entity %s", description.name)
|
||||||
|
|
||||||
|
return entities
|
||||||
|
|
||||||
|
|
||||||
|
class SensorValueMixin(Entity):
|
||||||
|
"""A mixin to provide sensor values."""
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _clean_sensor_value(self, value: Any) -> Any:
|
||||||
|
if isinstance(value, timedelta):
|
||||||
|
value = int(value.total_seconds())
|
||||||
|
elif isinstance(value, datetime):
|
||||||
|
# UniFi Protect value can vary slightly over time
|
||||||
|
# truncate to ensure no extra state_change events fire
|
||||||
|
value = value.replace(second=0, microsecond=0)
|
||||||
|
|
||||||
|
assert isinstance(self.entity_description, ProtectSensorEntityDescription)
|
||||||
|
if isinstance(value, float) and self.entity_description.precision:
|
||||||
|
value = round(value, self.entity_description.precision)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ProtectDeviceSensor(SensorValueMixin, ProtectDeviceEntity, SensorEntity):
|
||||||
|
"""A Ubiquiti UniFi Protect Sensor."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
data: ProtectData,
|
||||||
|
device: ProtectAdoptableDeviceModel,
|
||||||
|
description: ProtectSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize an UniFi Protect sensor."""
|
||||||
|
self.entity_description: ProtectSensorEntityDescription = description
|
||||||
|
super().__init__(data, device)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_device_from_protect(self) -> None:
|
||||||
|
super()._async_update_device_from_protect()
|
||||||
|
|
||||||
|
assert self.entity_description.ufp_value is not None
|
||||||
|
|
||||||
|
value = get_nested_attr(self.device, self.entity_description.ufp_value)
|
||||||
|
self._attr_native_value = self._clean_sensor_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
class ProtectNVRSensor(SensorValueMixin, 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."""
|
||||||
|
self.entity_description = description
|
||||||
|
super().__init__(data, device)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_device_from_protect(self) -> None:
|
||||||
|
super()._async_update_device_from_protect()
|
||||||
|
|
||||||
|
# _KEY_MEMORY
|
||||||
|
if self.entity_description.ufp_value is None:
|
||||||
|
memory = self.device.system_info.memory
|
||||||
|
value = (1 - memory.available / memory.total) * 100
|
||||||
|
else:
|
||||||
|
value = get_nested_attr(self.device, self.entity_description.ufp_value)
|
||||||
|
|
||||||
|
self._attr_native_value = self._clean_sensor_value(value)
|
283
tests/components/unifiprotect/test_sensor.py
Normal file
283
tests/components/unifiprotect/test_sensor.py
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
"""Test the UniFi Protect sensor platform."""
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pyunifiprotect.data.base import WifiConnectionState, WiredConnectionState
|
||||||
|
from pyunifiprotect.data.devices import Camera, Sensor
|
||||||
|
from pyunifiprotect.data.nvr import NVR
|
||||||
|
|
||||||
|
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
|
||||||
|
from homeassistant.components.unifiprotect.sensor import (
|
||||||
|
ALL_DEVICES_SENSORS,
|
||||||
|
CAMERA_SENSORS,
|
||||||
|
NVR_SENSORS,
|
||||||
|
SENSE_SENSORS,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_ATTRIBUTION, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from .conftest import (
|
||||||
|
MockEntityFixture,
|
||||||
|
assert_entity_counts,
|
||||||
|
enable_entity,
|
||||||
|
ids_from_device_description,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="sensor")
|
||||||
|
async def sensor_fixture(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_entry: MockEntityFixture,
|
||||||
|
mock_sensor: Sensor,
|
||||||
|
now: datetime,
|
||||||
|
):
|
||||||
|
"""Fixture for a single sensor for testing the sensor platform."""
|
||||||
|
|
||||||
|
# disable pydantic validation so mocking can happen
|
||||||
|
Sensor.__config__.validate_assignment = False
|
||||||
|
|
||||||
|
sensor_obj = mock_sensor.copy(deep=True)
|
||||||
|
sensor_obj._api = mock_entry.api
|
||||||
|
sensor_obj.name = "Test Sensor"
|
||||||
|
sensor_obj.battery_status.percentage = 10.0
|
||||||
|
sensor_obj.stats.light.value = 10.0
|
||||||
|
sensor_obj.stats.humidity.value = 10.0
|
||||||
|
sensor_obj.stats.temperature.value = 10.0
|
||||||
|
sensor_obj.up_since = now
|
||||||
|
sensor_obj.bluetooth_connection_state.signal_strength = -50.0
|
||||||
|
|
||||||
|
mock_entry.api.bootstrap.reset_objects()
|
||||||
|
mock_entry.api.bootstrap.sensors = {
|
||||||
|
sensor_obj.id: sensor_obj,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# 2 from all, 4 from sense, 12 NVR
|
||||||
|
assert_entity_counts(hass, Platform.SENSOR, 18, 13)
|
||||||
|
|
||||||
|
yield sensor_obj
|
||||||
|
|
||||||
|
Sensor.__config__.validate_assignment = True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="camera")
|
||||||
|
async def camera_fixture(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_entry: MockEntityFixture,
|
||||||
|
mock_camera: Camera,
|
||||||
|
now: datetime,
|
||||||
|
):
|
||||||
|
"""Fixture for a single camera for testing the sensor platform."""
|
||||||
|
|
||||||
|
# disable pydantic validation so mocking can happen
|
||||||
|
Camera.__config__.validate_assignment = False
|
||||||
|
|
||||||
|
camera_obj = mock_camera.copy(deep=True)
|
||||||
|
camera_obj._api = mock_entry.api
|
||||||
|
camera_obj.channels[0]._api = mock_entry.api
|
||||||
|
camera_obj.channels[1]._api = mock_entry.api
|
||||||
|
camera_obj.channels[2]._api = mock_entry.api
|
||||||
|
camera_obj.name = "Test Camera"
|
||||||
|
camera_obj.wired_connection_state = WiredConnectionState(phy_rate=1000)
|
||||||
|
camera_obj.wifi_connection_state = WifiConnectionState(
|
||||||
|
signal_quality=100, signal_strength=-50
|
||||||
|
)
|
||||||
|
camera_obj.stats.rx_bytes = 100.0
|
||||||
|
camera_obj.stats.tx_bytes = 100.0
|
||||||
|
camera_obj.stats.video.recording_start = now
|
||||||
|
camera_obj.stats.storage.used = 100.0
|
||||||
|
camera_obj.stats.storage.used = 100.0
|
||||||
|
camera_obj.stats.storage.rate = 100.0
|
||||||
|
camera_obj.voltage = 20.0
|
||||||
|
|
||||||
|
mock_entry.api.bootstrap.reset_objects()
|
||||||
|
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
|
||||||
|
mock_entry.api.bootstrap.cameras = {
|
||||||
|
camera_obj.id: camera_obj,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# 3 from all, 6 from camera, 12 NVR
|
||||||
|
assert_entity_counts(hass, Platform.SENSOR, 21, 13)
|
||||||
|
|
||||||
|
yield camera_obj
|
||||||
|
|
||||||
|
Camera.__config__.validate_assignment = True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensor_setup_sensor(
|
||||||
|
hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor
|
||||||
|
):
|
||||||
|
"""Test sensor entity setup for sensor devices."""
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
expected_values = ("10", "10.0", "10.0", "10.0")
|
||||||
|
for index, description in enumerate(SENSE_SENSORS):
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.SENSOR, sensor, description
|
||||||
|
)
|
||||||
|
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity
|
||||||
|
assert entity.unique_id == unique_id
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == expected_values[index]
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
# BLE signal
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.SENSOR, sensor, ALL_DEVICES_SENSORS[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity
|
||||||
|
assert entity.disabled is True
|
||||||
|
assert entity.unique_id == unique_id
|
||||||
|
|
||||||
|
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == "-50"
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensor_setup_nvr(
|
||||||
|
hass: HomeAssistant, mock_entry: MockEntityFixture, now: datetime
|
||||||
|
):
|
||||||
|
"""Test sensor entity setup for NVR device."""
|
||||||
|
|
||||||
|
mock_entry.api.bootstrap.reset_objects()
|
||||||
|
nvr: NVR = mock_entry.api.bootstrap.nvr
|
||||||
|
nvr.up_since = now
|
||||||
|
nvr.system_info.cpu.average_load = 50.0
|
||||||
|
nvr.system_info.cpu.temperature = 50.0
|
||||||
|
nvr.storage_stats.utilization = 50.0
|
||||||
|
nvr.system_info.memory.available = 50.0
|
||||||
|
nvr.system_info.memory.total = 100.0
|
||||||
|
nvr.storage_stats.storage_distribution.timelapse_recordings.percentage = 50.0
|
||||||
|
nvr.storage_stats.storage_distribution.continuous_recordings.percentage = 50.0
|
||||||
|
nvr.storage_stats.storage_distribution.detections_recordings.percentage = 50.0
|
||||||
|
nvr.storage_stats.storage_distribution.hd_usage.percentage = 50.0
|
||||||
|
nvr.storage_stats.storage_distribution.uhd_usage.percentage = 50.0
|
||||||
|
nvr.storage_stats.storage_distribution.free.percentage = 50.0
|
||||||
|
nvr.storage_stats.capacity = 50.0
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# 2 from all, 4 from sense, 12 NVR
|
||||||
|
assert_entity_counts(hass, Platform.SENSOR, 12, 9)
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
expected_values = (
|
||||||
|
now.replace(second=0, microsecond=0).isoformat(),
|
||||||
|
"50.0",
|
||||||
|
"50.0",
|
||||||
|
"50.0",
|
||||||
|
"50.0",
|
||||||
|
"50.0",
|
||||||
|
"50.0",
|
||||||
|
"50.0",
|
||||||
|
"50.0",
|
||||||
|
"50.0",
|
||||||
|
"50.0",
|
||||||
|
"50",
|
||||||
|
)
|
||||||
|
for index, description in enumerate(NVR_SENSORS):
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.SENSOR, nvr, description
|
||||||
|
)
|
||||||
|
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity
|
||||||
|
assert entity.disabled is not description.entity_registry_enabled_default
|
||||||
|
assert entity.unique_id == unique_id
|
||||||
|
|
||||||
|
if not description.entity_registry_enabled_default:
|
||||||
|
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == expected_values[index]
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensor_setup_camera(
|
||||||
|
hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime
|
||||||
|
):
|
||||||
|
"""Test sensor entity setup for camera devices."""
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
|
expected_values = (
|
||||||
|
"100",
|
||||||
|
"100",
|
||||||
|
now.replace(second=0, microsecond=0).isoformat(),
|
||||||
|
"100",
|
||||||
|
"100.0",
|
||||||
|
"20.0",
|
||||||
|
)
|
||||||
|
for index, description in enumerate(CAMERA_SENSORS):
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.SENSOR, camera, description
|
||||||
|
)
|
||||||
|
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity
|
||||||
|
assert entity.disabled is not description.entity_registry_enabled_default
|
||||||
|
assert entity.unique_id == unique_id
|
||||||
|
|
||||||
|
if not description.entity_registry_enabled_default:
|
||||||
|
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == expected_values[index]
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
# Wired signal
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.SENSOR, camera, ALL_DEVICES_SENSORS[2]
|
||||||
|
)
|
||||||
|
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity
|
||||||
|
assert entity.disabled is True
|
||||||
|
assert entity.unique_id == unique_id
|
||||||
|
|
||||||
|
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == "1000"
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
# WiFi signal
|
||||||
|
unique_id, entity_id = ids_from_device_description(
|
||||||
|
Platform.SENSOR, camera, ALL_DEVICES_SENSORS[3]
|
||||||
|
)
|
||||||
|
|
||||||
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
assert entity
|
||||||
|
assert entity.disabled is True
|
||||||
|
assert entity.unique_id == unique_id
|
||||||
|
|
||||||
|
await enable_entity(hass, mock_entry.entry.entry_id, entity_id)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == "-50"
|
||||||
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
Loading…
Add table
Add a link
Reference in a new issue