diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 1ca2612a10a..891912ebf09 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -61,6 +61,7 @@ PLATFORMS = [ Platform.MEDIA_PLAYER, Platform.NUMBER, Platform.SELECT, + Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py new file mode 100644 index 00000000000..8c5c90c8ebe --- /dev/null +++ b/homeassistant/components/unifiprotect/sensor.py @@ -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) diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py new file mode 100644 index 00000000000..2eea398dd40 --- /dev/null +++ b/tests/components/unifiprotect/test_sensor.py @@ -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