diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 5c870ffd937..b0df644f1bd 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -3,10 +3,10 @@ from __future__ import annotations from collections.abc import Callable, ValuesView from dataclasses import dataclass +from datetime import datetime from pydeconz.sensor import ( AirQuality, - Battery, Consumption, Daylight, DeconzSensor as PydeconzSensor, @@ -17,7 +17,6 @@ from pydeconz.sensor import ( Pressure, Switch, Temperature, - Thermostat, Time, ) @@ -48,22 +47,21 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +import homeassistant.util.dt as dt_util from .const import ATTR_DARK, ATTR_ON from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry -DECONZ_SENSORS = ( - AirQuality, - Consumption, - Daylight, - GenericStatus, - Humidity, - LightLevel, - Power, - Pressure, - Temperature, - Time, +PROVIDES_EXTRA_ATTRIBUTES = ( + "battery", + "consumption", + "status", + "humidity", + "light_level", + "power", + "pressure", + "temperature", ) ATTR_CURRENT = "current" @@ -76,9 +74,7 @@ ATTR_EVENT_ID = "event_id" class DeconzSensorDescriptionMixin: """Required values when describing secondary sensor attributes.""" - suffix: str update_key: str - required_attr: str value_fn: Callable[[PydeconzSensor], float | int | None] @@ -89,78 +85,133 @@ class DeconzSensorDescription( ): """Class describing deCONZ binary sensor entities.""" + suffix: str = "" + ENTITY_DESCRIPTIONS = { - Battery: SensorEntityDescription( + AirQuality: [ + DeconzSensorDescription( + key="air_quality", + value_fn=lambda device: device.air_quality, # type: ignore[no-any-return] + update_key="airquality", + state_class=SensorStateClass.MEASUREMENT, + ), + DeconzSensorDescription( + key="air_quality_ppb", + value_fn=lambda device: device.air_quality_ppb, # type: ignore[no-any-return] + suffix="PPB", + update_key="airqualityppb", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + ), + ], + Consumption: [ + DeconzSensorDescription( + key="consumption", + value_fn=lambda device: device.scaled_consumption, # type: ignore[no-any-return] + update_key="consumption", + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ) + ], + Daylight: [ + DeconzSensorDescription( + key="status", + value_fn=lambda device: device.status, # type: ignore[no-any-return] + update_key="status", + icon="mdi:white-balance-sunny", + entity_registry_enabled_default=False, + ) + ], + GenericStatus: [ + DeconzSensorDescription( + key="status", + value_fn=lambda device: device.status, # type: ignore[no-any-return] + update_key="status", + ) + ], + Humidity: [ + DeconzSensorDescription( + key="humidity", + value_fn=lambda device: device.scaled_humidity, # type: ignore[no-any-return] + update_key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + ) + ], + LightLevel: [ + DeconzSensorDescription( + key="light_level", + value_fn=lambda device: device.scaled_light_level, # type: ignore[no-any-return] + update_key="lightlevel", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + ) + ], + Power: [ + DeconzSensorDescription( + key="power", + value_fn=lambda device: device.power, # type: ignore[no-any-return] + update_key="power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + ) + ], + Pressure: [ + DeconzSensorDescription( + key="pressure", + value_fn=lambda device: device.pressure, # type: ignore[no-any-return] + update_key="pressure", + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PRESSURE_HPA, + ) + ], + Temperature: [ + DeconzSensorDescription( + key="temperature", + value_fn=lambda device: device.temperature, # type: ignore[no-any-return] + update_key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ) + ], + Time: [ + DeconzSensorDescription( + key="last_set", + value_fn=lambda device: device.last_set, # type: ignore[no-any-return] + update_key="lastset", + device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.TOTAL_INCREASING, + ) + ], +} + +SENSOR_DESCRIPTIONS = [ + DeconzSensorDescription( key="battery", + value_fn=lambda device: device.battery, # type: ignore[no-any-return] + suffix="Battery", + update_key="battery", device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, ), - Consumption: SensorEntityDescription( - key="consumption", - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - ), - Daylight: SensorEntityDescription( - key="daylight", - icon="mdi:white-balance-sunny", - entity_registry_enabled_default=False, - ), - Humidity: SensorEntityDescription( - key="humidity", - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ), - LightLevel: SensorEntityDescription( - key="lightlevel", - device_class=SensorDeviceClass.ILLUMINANCE, - native_unit_of_measurement=LIGHT_LUX, - ), - Power: SensorEntityDescription( - key="power", - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, - ), - Pressure: SensorEntityDescription( - key="pressure", - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PRESSURE_HPA, - ), - Temperature: SensorEntityDescription( - key="temperature", - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=TEMP_CELSIUS, - ), -} - -SENSOR_DESCRIPTIONS = [ DeconzSensorDescription( - key="temperature", - required_attr="secondary_temperature", - value_fn=lambda device: device.secondary_temperature, + key="secondary_temperature", + value_fn=lambda device: device.secondary_temperature, # type: ignore[no-any-return] suffix="Temperature", update_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), - DeconzSensorDescription( - key="air_quality_ppb", - required_attr="air_quality_ppb", - value_fn=lambda device: device.air_quality_ppb, - suffix="PPB", - update_key="airqualityppb", - device_class=SensorDeviceClass.AQI, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - ), ] @@ -185,42 +236,33 @@ async def async_setup_entry( Create DeconzBattery if sensor has a battery attribute. Create DeconzSensor if not a battery, switch or thermostat and not a binary sensor. """ - entities: list[DeconzBattery | DeconzSensor | DeconzPropertySensor] = [] + entities: list[DeconzSensor] = [] for sensor in sensors: if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): continue - if sensor.battery is not None: - battery_handler.remove_tracker(sensor) - - known_batteries = set(gateway.entities[DOMAIN]) - new_battery = DeconzBattery(sensor, gateway) - if new_battery.unique_id not in known_batteries: - entities.append(new_battery) - - else: + if sensor.battery is None: battery_handler.create_tracker(sensor) - if ( - isinstance(sensor, DECONZ_SENSORS) - and not isinstance(sensor, Thermostat) - and sensor.unique_id not in gateway.entities[DOMAIN] + known_entities = set(gateway.entities[DOMAIN]) + for description in ( + ENTITY_DESCRIPTIONS.get(type(sensor), []) + SENSOR_DESCRIPTIONS ): - entities.append(DeconzSensor(sensor, gateway)) - known_sensor_entities = set(gateway.entities[DOMAIN]) - for sensor_description in SENSOR_DESCRIPTIONS: - - if not hasattr( - sensor, sensor_description.required_attr - ) or not sensor_description.value_fn(sensor): + if ( + not hasattr(sensor, description.key) + or description.value_fn(sensor) is None + ): continue - new_sensor = DeconzPropertySensor(sensor, gateway, sensor_description) - if new_sensor.unique_id not in known_sensor_entities: - entities.append(new_sensor) + new_entity = DeconzSensor(sensor, gateway, description) + if new_entity.unique_id not in known_entities: + entities.append(new_entity) + + if description.key == "battery": + battery_handler.remove_tracker(sensor) if entities: async_add_entities(entities) @@ -243,30 +285,66 @@ class DeconzSensor(DeconzDevice, SensorEntity): TYPE = DOMAIN _device: PydeconzSensor + entity_description: DeconzSensorDescription - def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: - """Initialize deCONZ binary sensor.""" + def __init__( + self, + device: PydeconzSensor, + gateway: DeconzGateway, + description: DeconzSensorDescription, + ) -> None: + """Initialize deCONZ sensor.""" + self.entity_description = description super().__init__(device, gateway) - if entity_description := ENTITY_DESCRIPTIONS.get(type(device)): - self.entity_description = entity_description + if description.suffix: + self._attr_name = f"{device.name} {description.suffix}" + + self._update_keys = {description.update_key, "reachable"} + if self.entity_description.key in PROVIDES_EXTRA_ATTRIBUTES: + self._update_keys.update({"on", "state"}) + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + if ( + self.entity_description.key == "battery" + and self._device.manufacturer == "Danfoss" + and self._device.model_id + in [ + "0x8030", + "0x8031", + "0x8034", + "0x8035", + ] + ): + return f"{super().unique_id}-battery" + if self.entity_description.suffix: + return f"{self.serial}-{self.entity_description.suffix.lower()}" + return super().unique_id @callback def async_update_callback(self) -> None: """Update the sensor's state.""" - keys = {"on", "reachable", "state"} - if self._device.changed_keys.intersection(keys): + if self._device.changed_keys.intersection(self._update_keys): super().async_update_callback() @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return the state of the sensor.""" - return self._device.state # type: ignore[no-any-return] + if self.entity_description.device_class is SensorDeviceClass.TIMESTAMP: + return dt_util.parse_datetime( + self.entity_description.value_fn(self._device) + ) + return self.entity_description.value_fn(self._device) @property def extra_state_attributes(self) -> dict[str, bool | float | int | None]: """Return the state attributes of the sensor.""" - attr = {} + attr: dict[str, bool | float | int | None] = {} + + if self.entity_description.key not in PROVIDES_EXTRA_ATTRIBUTES: + return attr if self._device.on is not None: attr[ATTR_ON] = self._device.on @@ -292,93 +370,7 @@ class DeconzSensor(DeconzDevice, SensorEntity): attr[ATTR_CURRENT] = self._device.current attr[ATTR_VOLTAGE] = self._device.voltage - return attr - - -class DeconzPropertySensor(DeconzDevice, SensorEntity): - """Representation of a deCONZ secondary attribute sensor.""" - - TYPE = DOMAIN - _device: PydeconzSensor - entity_description: DeconzSensorDescription - - def __init__( - self, - device: PydeconzSensor, - gateway: DeconzGateway, - description: DeconzSensorDescription, - ) -> None: - """Initialize deCONZ sensor.""" - self.entity_description = description - super().__init__(device, gateway) - - self._attr_name = f"{self._device.name} {description.suffix}" - self._update_keys = {description.update_key, "reachable"} - - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return f"{self.serial}-{self.entity_description.suffix.lower()}" - - @callback - def async_update_callback(self) -> None: - """Update the sensor's state.""" - if self._device.changed_keys.intersection(self._update_keys): - super().async_update_callback() - - @property - def native_value(self) -> StateType: - """Return the state of the sensor.""" - return self.entity_description.value_fn(self._device) - - -class DeconzBattery(DeconzDevice, SensorEntity): - """Battery class for when a device is only represented as an event.""" - - TYPE = DOMAIN - _device: PydeconzSensor - - def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: - """Initialize deCONZ battery level sensor.""" - super().__init__(device, gateway) - - self.entity_description = ENTITY_DESCRIPTIONS[Battery] - self._attr_name = f"{self._device.name} Battery Level" - - @callback - def async_update_callback(self) -> None: - """Update the battery's state, if needed.""" - keys = {"battery", "reachable"} - if self._device.changed_keys.intersection(keys): - super().async_update_callback() - - @property - def unique_id(self) -> str: - """Return a unique identifier for this device. - - Normally there should only be one battery sensor per device from deCONZ. - With specific Danfoss devices each endpoint can report its own battery state. - """ - if self._device.manufacturer == "Danfoss" and self._device.model_id in [ - "0x8030", - "0x8031", - "0x8034", - "0x8035", - ]: - return f"{super().unique_id}-battery" - return f"{self.serial}-battery" - - @property - def native_value(self) -> StateType: - """Return the state of the battery.""" - return self._device.battery # type: ignore[no-any-return] - - @property - def extra_state_attributes(self) -> dict[str, str]: - """Return the state attributes of the battery.""" - attr = {} - - if isinstance(self._device, Switch): + elif isinstance(self._device, Switch): for event in self.gateway.events: if self._device == event.device: attr[ATTR_EVENT_ID] = event.event_id diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 3febbae510b..a21d981900c 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -111,7 +111,7 @@ async def test_simple_climate_device(hass, aioclient_mock, mock_deconz_websocket assert climate_thermostat.attributes["current_temperature"] == 21.0 assert climate_thermostat.attributes["temperature"] == 21.0 assert climate_thermostat.attributes["locked"] is True - assert hass.states.get("sensor.thermostat_battery_level").state == "59" + assert hass.states.get("sensor.thermostat_battery").state == "59" # Event signals thermostat configured off @@ -211,7 +211,7 @@ async def test_climate_device_without_cooling_support( assert climate_thermostat.attributes["current_temperature"] == 22.6 assert climate_thermostat.attributes["temperature"] == 22.0 assert hass.states.get("sensor.thermostat") is None - assert hass.states.get("sensor.thermostat_battery_level").state == "100" + assert hass.states.get("sensor.thermostat_battery").state == "100" assert hass.states.get("climate.presence_sensor") is None assert hass.states.get("climate.clip_thermostat") is None @@ -385,7 +385,7 @@ async def test_climate_device_with_cooling_support( ] assert climate_thermostat.attributes["current_temperature"] == 23.2 assert climate_thermostat.attributes["temperature"] == 22.2 - assert hass.states.get("sensor.zen_01_battery_level").state == "25" + assert hass.states.get("sensor.zen_01_battery").state == "25" # Event signals thermostat state cool @@ -787,4 +787,4 @@ async def test_add_new_climate_device(hass, aioclient_mock, mock_deconz_websocke assert len(hass.states.async_all()) == 2 assert hass.states.get("climate.thermostat").state == HVAC_MODE_AUTO - assert hass.states.get("sensor.thermostat_battery_level").state == "100" + assert hass.states.get("sensor.thermostat_battery").state == "100" diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 76babab36be..1d3c4f7a811 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -80,9 +80,9 @@ async def test_deconz_events(hass, aioclient_mock, mock_deconz_websocket): assert ( len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 7 ) - assert hass.states.get("sensor.switch_2_battery_level").state == "100" - assert hass.states.get("sensor.switch_3_battery_level").state == "100" - assert hass.states.get("sensor.switch_4_battery_level").state == "100" + assert hass.states.get("sensor.switch_2_battery").state == "100" + assert hass.states.get("sensor.switch_3_battery").state == "100" + assert hass.states.get("sensor.switch_4_battery").state == "100" captured_events = async_capture_events(hass, CONF_DECONZ_EVENT) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 15e63a6a81f..4ae8fd32e45 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -120,7 +120,7 @@ async def test_get_triggers(hass, aioclient_mock): { CONF_DEVICE_ID: device.id, CONF_DOMAIN: SENSOR_DOMAIN, - ATTR_ENTITY_ID: "sensor.tradfri_on_off_switch_battery_level", + ATTR_ENTITY_ID: "sensor.tradfri_on_off_switch_battery", CONF_PLATFORM: "device", CONF_TYPE: ATTR_BATTERY_LEVEL, }, diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index ee66a159c18..bd51bab44e4 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -3,12 +3,17 @@ from datetime import timedelta from unittest.mock import patch +import pytest + from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR -from homeassistant.components.deconz.sensor import ATTR_DAYLIGHT -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass +from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory from homeassistant.util import dt @@ -23,159 +28,639 @@ async def test_no_sensors(hass, aioclient_mock): assert len(hass.states.async_all()) == 0 -async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): +TEST_DATA = [ + ( # Air quality sensor + { + "config": { + "on": True, + "reachable": True, + }, + "ep": 2, + "etag": "c2d2e42396f7c78e11e46c66e2ec0200", + "lastseen": "2020-11-20T22:48Z", + "manufacturername": "BOSCH", + "modelid": "AIR", + "name": "BOSCH Air quality sensor", + "state": { + "airquality": "poor", + "airqualityppb": 809, + "lastupdated": "2020-11-20T22:48:00.209", + }, + "swversion": "20200402", + "type": "ZHAAirQuality", + "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.bosch_air_quality_sensor", + "unique_id": "00:12:4b:00:14:4d:00:07-02-fdef", + "state": "poor", + "entity_category": None, + "device_class": None, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "friendly_name": "BOSCH Air quality sensor", + }, + "websocket_event": {"state": {"airquality": "excellent"}}, + "next_state": "excellent", + }, + ), + ( # Air quality PPB sensor + { + "config": { + "on": True, + "reachable": True, + }, + "ep": 2, + "etag": "c2d2e42396f7c78e11e46c66e2ec0200", + "lastseen": "2020-11-20T22:48Z", + "manufacturername": "BOSCH", + "modelid": "AIR", + "name": "BOSCH Air quality sensor", + "state": { + "airquality": "poor", + "airqualityppb": 809, + "lastupdated": "2020-11-20T22:48:00.209", + }, + "swversion": "20200402", + "type": "ZHAAirQuality", + "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.bosch_air_quality_sensor_ppb", + "unique_id": "00:12:4b:00:14:4d:00:07-ppb", + "state": "809", + "entity_category": None, + "device_class": SensorDeviceClass.AQI, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "unit_of_measurement": "ppb", + "device_class": "aqi", + "friendly_name": "BOSCH Air quality sensor PPB", + }, + "websocket_event": {"state": {"airqualityppb": 1000}}, + "next_state": "1000", + }, + ), + ( # Battery sensor + { + "config": { + "alert": "none", + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "23a8659f1cb22df2f51bc2da0e241bb4", + "manufacturername": "IKEA of Sweden", + "modelid": "FYRTUR block-out roller blind", + "name": "FYRTUR block-out roller blind", + "state": { + "battery": 100, + "lastupdated": "none", + }, + "swversion": "2.2.007", + "type": "ZHABattery", + "uniqueid": "00:0d:6f:ff:fe:01:23:45-01-0001", + }, + { + "entity_count": 1, + "device_count": 3, + "entity_id": "sensor.fyrtur_block_out_roller_blind_battery", + "unique_id": "00:0d:6f:ff:fe:01:23:45-battery", + "state": "100", + "entity_category": EntityCategory.DIAGNOSTIC, + "device_class": SensorDeviceClass.BATTERY, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "unit_of_measurement": "%", + "device_class": "battery", + "friendly_name": "FYRTUR block-out roller blind Battery", + }, + "websocket_event": {"state": {"battery": 50}}, + "next_state": "50", + }, + ), + ( # Consumption sensor + { + "config": {"on": True, "reachable": True}, + "ep": 1, + "etag": "a99e5bc463d15c23af7e89946e784cca", + "manufacturername": "Heiman", + "modelid": "SmartPlug", + "name": "Consumption 15", + "state": { + "consumption": 11342, + "lastupdated": "2018-03-12T19:19:08", + "power": 123, + }, + "type": "ZHAConsumption", + "uniqueid": "00:0d:6f:00:0b:7a:64:29-01-0702", + }, + { + "entity_count": 1, + "device_count": 3, + "entity_id": "sensor.consumption_15", + "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702", + "state": "11.342", + "entity_category": None, + "device_class": SensorDeviceClass.ENERGY, + "state_class": SensorStateClass.TOTAL_INCREASING, + "attributes": { + "state_class": "total_increasing", + "on": True, + "power": 123, + "unit_of_measurement": "kWh", + "device_class": "energy", + "friendly_name": "Consumption 15", + }, + "websocket_event": {"state": {"consumption": 10000}}, + "next_state": "10.0", + }, + ), + ( # Daylight sensor + { + "config": { + "configured": True, + "on": True, + "sunriseoffset": 30, + "sunsetoffset": -30, + }, + "etag": "55047cf652a7e594d0ee7e6fae01dd38", + "manufacturername": "Philips", + "modelid": "PHDL00", + "name": "Daylight", + "state": { + "daylight": True, + "lastupdated": "2018-03-24T17:26:12", + "status": 170, + }, + "swversion": "1.0", + "type": "Daylight", + }, + { + "enable_entity": True, + "entity_count": 1, + "device_count": 2, + "entity_id": "sensor.daylight", + "unique_id": "", + "state": "solar_noon", + "entity_category": None, + "device_class": None, + "state_class": None, + "attributes": { + "on": True, + "daylight": True, + "icon": "mdi:white-balance-sunny", + "friendly_name": "Daylight", + }, + "websocket_event": {"state": {"status": 210}}, + "next_state": "dusk", + }, + ), + ( # Generic status sensor + { + "config": { + "on": True, + "reachable": True, + }, + "etag": "aacc83bc7d6e4af7e44014e9f776b206", + "manufacturername": "Phoscon", + "modelid": "PHOSCON_FSM_STATE", + "name": "FSM_STATE Motion stair", + "state": { + "lastupdated": "2019-04-24T00:00:25", + "status": 0, + }, + "swversion": "1.0", + "type": "CLIPGenericStatus", + "uniqueid": "fsm-state-1520195376277", + }, + { + "entity_count": 1, + "device_count": 2, + "entity_id": "sensor.fsm_state_motion_stair", + "unique_id": "fsm-state-1520195376277", + "state": "0", + "entity_category": None, + "device_class": None, + "state_class": None, + "attributes": { + "on": True, + "friendly_name": "FSM_STATE Motion stair", + }, + "websocket_event": {"state": {"status": 1}}, + "next_state": "1", + }, + ), + ( # Humidity sensor + { + "config": { + "battery": 100, + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1220e5d026493b6e86207993703a8a71", + "manufacturername": "LUMI", + "modelid": "lumi.weather", + "name": "Mi temperature 1", + "state": { + "humidity": 3555, + "lastupdated": "2019-05-05T14:39:00", + }, + "swversion": "20161129", + "type": "ZHAHumidity", + "uniqueid": "00:15:8d:00:02:45:dc:53-01-0405", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.mi_temperature_1", + "unique_id": "00:15:8d:00:02:45:dc:53-01-0405", + "state": "35.5", + "entity_category": None, + "device_class": SensorDeviceClass.HUMIDITY, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "unit_of_measurement": "%", + "device_class": "humidity", + "friendly_name": "Mi temperature 1", + }, + "websocket_event": {"state": {"humidity": 1000}}, + "next_state": "10.0", + }, + ), + ( # Light level sensor + { + "config": { + "alert": "none", + "battery": 100, + "ledindication": False, + "on": True, + "pending": [], + "reachable": True, + "tholddark": 12000, + "tholdoffset": 7000, + "usertest": False, + }, + "ep": 2, + "etag": "5cfb81765e86aa53ace427cfd52c6d52", + "manufacturername": "Philips", + "modelid": "SML001", + "name": "Motion sensor 4", + "state": { + "dark": True, + "daylight": False, + "lastupdated": "2019-05-05T14:37:06", + "lightlevel": 6955, + "lux": 5, + }, + "swversion": "6.1.0.18912", + "type": "ZHALightLevel", + "uniqueid": "00:17:88:01:03:28:8c:9b-02-0400", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.motion_sensor_4", + "unique_id": "00:17:88:01:03:28:8c:9b-02-0400", + "state": "5.0", + "entity_category": None, + "device_class": SensorDeviceClass.ILLUMINANCE, + "state_class": None, + "attributes": { + "on": True, + "dark": True, + "daylight": False, + "unit_of_measurement": "lx", + "device_class": "illuminance", + "friendly_name": "Motion sensor 4", + }, + "websocket_event": {"state": {"lightlevel": 1000}}, + "next_state": "1.3", + }, + ), + ( # Power sensor + { + "config": { + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "96e71c7db4685b334d3d0decc3f11868", + "manufacturername": "Heiman", + "modelid": "SmartPlug", + "name": "Power 16", + "state": { + "current": 34, + "lastupdated": "2018-03-12T19:22:13", + "power": 64, + "voltage": 231, + }, + "type": "ZHAPower", + "uniqueid": "00:0d:6f:00:0b:7a:64:29-01-0b04", + }, + { + "entity_count": 1, + "device_count": 3, + "entity_id": "sensor.power_16", + "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0b04", + "state": "64", + "entity_category": None, + "device_class": SensorDeviceClass.POWER, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "current": 34, + "voltage": 231, + "unit_of_measurement": "W", + "device_class": "power", + "friendly_name": "Power 16", + }, + "websocket_event": {"state": {"power": 1000}}, + "next_state": "1000", + }, + ), + ( # Pressure sensor + { + "config": { + "battery": 100, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1220e5d026493b6e86207993703a8a71", + "manufacturername": "LUMI", + "modelid": "lumi.weather", + "name": "Mi temperature 1", + "state": { + "lastupdated": "2019-05-05T14:39:00", + "pressure": 1010, + }, + "swversion": "20161129", + "type": "ZHAPressure", + "uniqueid": "00:15:8d:00:02:45:dc:53-01-0403", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.mi_temperature_1", + "unique_id": "00:15:8d:00:02:45:dc:53-01-0403", + "state": "1010", + "entity_category": None, + "device_class": SensorDeviceClass.PRESSURE, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "unit_of_measurement": "hPa", + "device_class": "pressure", + "friendly_name": "Mi temperature 1", + }, + "websocket_event": {"state": {"pressure": 500}}, + "next_state": "500", + }, + ), + ( # Temperature sensor + { + "config": { + "battery": 100, + "offset": 0, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1220e5d026493b6e86207993703a8a71", + "manufacturername": "LUMI", + "modelid": "lumi.weather", + "name": "Mi temperature 1", + "state": { + "lastupdated": "2019-05-05T14:39:00", + "temperature": 2182, + }, + "swversion": "20161129", + "type": "ZHATemperature", + "uniqueid": "00:15:8d:00:02:45:dc:53-01-0402", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.mi_temperature_1", + "unique_id": "00:15:8d:00:02:45:dc:53-01-0402", + "state": "21.8", + "entity_category": None, + "device_class": SensorDeviceClass.TEMPERATURE, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "unit_of_measurement": "°C", + "device_class": "temperature", + "friendly_name": "Mi temperature 1", + }, + "websocket_event": {"state": {"temperature": 1800}}, + "next_state": "18.0", + }, + ), + ( # Time sensor + { + "config": { + "battery": 40, + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "28e796678d9a24712feef59294343bb6", + "lastseen": "2020-11-22T11:26Z", + "manufacturername": "Danfoss", + "modelid": "eTRV0100", + "name": "eTRV Séjour", + "state": { + "lastset": "2020-11-19T08:07:08Z", + "lastupdated": "2020-11-22T10:51:03.444", + "localtime": "2020-11-22T10:51:01", + "utc": "2020-11-22T10:51:01Z", + }, + "swversion": "20200429", + "type": "ZHATime", + "uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", + }, + { + "entity_count": 2, + "device_count": 3, + "entity_id": "sensor.etrv_sejour", + "unique_id": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", + "state": "2020-11-19T08:07:08+00:00", + "entity_category": None, + "device_class": SensorDeviceClass.TIMESTAMP, + "state_class": SensorStateClass.TOTAL_INCREASING, + "attributes": { + "state_class": "total_increasing", + "device_class": "timestamp", + "friendly_name": "eTRV Séjour", + }, + "websocket_event": {"state": {"lastset": "2020-12-14T10:12:14Z"}}, + "next_state": "2020-12-14T10:12:14+00:00", + }, + ), + ( # Secondary temperature sensor + { + "config": { + "battery": 100, + "on": True, + "reachable": True, + "temperature": 2600, + }, + "ep": 1, + "etag": "18c0f3c2100904e31a7f938db2ba9ba9", + "manufacturername": "dresden elektronik", + "modelid": "lumi.sensor_motion.aq2", + "name": "Alarm 10", + "state": { + "alarm": False, + "lastupdated": "none", + "lowbattery": None, + "tampered": None, + }, + "swversion": "20170627", + "type": "ZHAAlarm", + "uniqueid": "00:15:8d:00:02:b5:d1:80-01-0500", + }, + { + "entity_count": 3, + "device_count": 3, + "entity_id": "sensor.alarm_10_temperature", + "unique_id": "00:15:8d:00:02:b5:d1:80-temperature", + "state": "26.0", + "entity_category": None, + "device_class": SensorDeviceClass.TEMPERATURE, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "unit_of_measurement": "°C", + "device_class": "temperature", + "friendly_name": "Alarm 10 Temperature", + }, + "websocket_event": {"state": {"temperature": 1800}}, + "next_state": "26.0", + }, + ), + ( # Battery from switch + { + "config": { + "battery": 90, + "group": "201", + "on": True, + "reachable": True, + }, + "ep": 2, + "etag": "233ae541bbb7ac98c42977753884b8d2", + "manufacturername": "Philips", + "mode": 1, + "modelid": "RWL021", + "name": "Dimmer switch 3", + "state": { + "buttonevent": 1002, + "lastupdated": "2019-04-28T20:29:13", + }, + "swversion": "5.45.1.17846", + "type": "ZHASwitch", + "uniqueid": "00:17:88:01:02:0e:32:a3-02-fc00", + }, + { + "entity_count": 1, + "device_count": 3, + "entity_id": "sensor.dimmer_switch_3_battery", + "unique_id": "00:17:88:01:02:0e:32:a3-battery", + "state": "90", + "entity_category": EntityCategory.DIAGNOSTIC, + "device_class": SensorDeviceClass.BATTERY, + "state_class": SensorStateClass.MEASUREMENT, + "attributes": { + "state_class": "measurement", + "on": True, + "event_id": "dimmer_switch_3", + "unit_of_measurement": "%", + "device_class": "battery", + "friendly_name": "Dimmer switch 3 Battery", + }, + "websocket_event": {"config": {"battery": 80}}, + "next_state": "80", + }, + ), +] + + +@pytest.mark.parametrize("sensor_data, expected", TEST_DATA) +async def test_sensors( + hass, aioclient_mock, mock_deconz_websocket, sensor_data, expected +): """Test successful creation of sensor entities.""" - data = { - "sensors": { - "1": { - "name": "Light level sensor", - "type": "ZHALightLevel", - "state": {"daylight": 6955, "lightlevel": 30000, "dark": False}, - "config": {"on": True, "reachable": True, "temperature": 10}, - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - "2": { - "name": "Presence sensor", - "type": "ZHAPresence", - "state": {"presence": False}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:01-00", - }, - "3": { - "name": "Switch 1", - "type": "ZHASwitch", - "state": {"buttonevent": 1000}, - "config": {}, - "uniqueid": "00:00:00:00:00:00:00:02-00", - }, - "4": { - "name": "Switch 2", - "type": "ZHASwitch", - "state": {"buttonevent": 1000}, - "config": {"battery": 100}, - "uniqueid": "00:00:00:00:00:00:00:03-00", - }, - "5": { - "name": "Power sensor", - "type": "ZHAPower", - "state": {"current": 2, "power": 6, "voltage": 3}, - "config": {"reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:05-00", - }, - "6": { - "name": "Consumption sensor", - "type": "ZHAConsumption", - "state": {"consumption": 2, "power": 6}, - "config": {"reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:06-00", - }, - "7": { - "id": "CLIP light sensor id", - "name": "CLIP light level sensor", - "type": "CLIPLightLevel", - "state": {"lightlevel": 30000}, - "config": {"reachable": True}, - "uniqueid": "00:00:00:00:00:00:00:07-00", - }, - } - } - - with patch.dict(DECONZ_WEB_REQUEST, data): - config_entry = await setup_deconz_integration(hass, aioclient_mock) - - assert len(hass.states.async_all()) == 6 - ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) - light_level_sensor = hass.states.get("sensor.light_level_sensor") - assert light_level_sensor.state == "999.8" + with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}): + config_entry = await setup_deconz_integration( + hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} + ) + + # Enable in entity registry + if expected.get("enable_entity"): + ent_reg.async_update_entity(entity_id=expected["entity_id"], disabled_by=None) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == expected["entity_count"] + + # Verify entity state + sensor = hass.states.get(expected["entity_id"]) + assert sensor.state == expected["state"] + assert sensor.attributes.get(ATTR_DEVICE_CLASS) == expected["device_class"] + assert sensor.attributes == expected["attributes"] + + # Verify entity registry assert ( - light_level_sensor.attributes[ATTR_DEVICE_CLASS] - == SensorDeviceClass.ILLUMINANCE + ent_reg.async_get(expected["entity_id"]).entity_category + is expected["entity_category"] ) - assert light_level_sensor.attributes[ATTR_DAYLIGHT] == 6955 + ent_reg_entry = ent_reg.async_get(expected["entity_id"]) + assert ent_reg_entry.entity_category is expected["entity_category"] + assert ent_reg_entry.unique_id == expected["unique_id"] - light_level_temp = hass.states.get("sensor.light_level_sensor_temperature") - assert light_level_temp.state == "0.1" + # Verify device registry assert ( - light_level_temp.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE + len(dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)) + == expected["device_count"] ) - assert not hass.states.get("sensor.presence_sensor") - assert not hass.states.get("sensor.switch_1") - assert not hass.states.get("sensor.switch_1_battery_level") - assert not hass.states.get("sensor.switch_2") + # Change state - switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") - assert switch_2_battery_level.state == "100" - assert ( - switch_2_battery_level.attributes[ATTR_DEVICE_CLASS] - == SensorDeviceClass.BATTERY - ) - assert ( - ent_reg.async_get("sensor.switch_2_battery_level").entity_category - == EntityCategory.DIAGNOSTIC - ) - - assert not hass.states.get("sensor.daylight_sensor") - - power_sensor = hass.states.get("sensor.power_sensor") - assert power_sensor.state == "6" - assert power_sensor.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER - - consumption_sensor = hass.states.get("sensor.consumption_sensor") - assert consumption_sensor.state == "0.002" - assert consumption_sensor.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY - - assert not hass.states.get("sensor.clip_light_level_sensor") - - # Event signals new light level - - event_changed_sensor = { - "t": "event", - "e": "changed", - "r": "sensors", - "id": "1", - "state": {"lightlevel": 2000}, - } + event_changed_sensor = {"t": "event", "e": "changed", "r": "sensors", "id": "1"} + event_changed_sensor |= expected["websocket_event"] await mock_deconz_websocket(data=event_changed_sensor) - - assert hass.states.get("sensor.light_level_sensor").state == "1.6" - - # Event signals new temperature value - - event_changed_sensor = { - "t": "event", - "e": "changed", - "r": "sensors", - "id": "1", - "config": {"temperature": 100}, - } - await mock_deconz_websocket(data=event_changed_sensor) - - assert hass.states.get("sensor.light_level_sensor_temperature").state == "1.0" - - # Event signals new battery level - - event_changed_sensor = { - "t": "event", - "e": "changed", - "r": "sensors", - "id": "4", - "config": {"battery": 75}, - } - await mock_deconz_websocket(data=event_changed_sensor) - - assert hass.states.get("sensor.switch_2_battery_level").state == "75" + await hass.async_block_till_done() + assert hass.states.get(expected["entity_id"]).state == expected["next_state"] # Unload entry await hass.config_entries.async_unload(config_entry.entry_id) - - states = hass.states.async_all() - assert len(states) == 6 - for state in states: - assert state.state == STATE_UNAVAILABLE + assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE # Remove entry @@ -184,6 +669,28 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 0 +async def test_not_allow_clip_sensor(hass, aioclient_mock): + """Test that CLIP sensors are not allowed.""" + data = { + "sensors": { + "1": { + "name": "CLIP temperature sensor", + "type": "CLIPTemperature", + "state": {"temperature": 2600}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, + } + } + + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration( + hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: False} + ) + + assert len(hass.states.async_all()) == 0 + + async def test_allow_clip_sensors(hass, aioclient_mock): """Test that CLIP sensors can be allowed.""" data = { @@ -295,7 +802,7 @@ async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): await setup_deconz_integration(hass, aioclient_mock) assert len(hass.states.async_all()) == 0 - assert not hass.states.get("sensor.switch_1_battery_level") + assert not hass.states.get("sensor.switch_1_battery") event_changed_sensor = { "t": "event", @@ -309,10 +816,11 @@ async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 1 - assert hass.states.get("sensor.switch_1_battery_level").state == "50" + assert hass.states.get("sensor.switch_1_battery").state == "50" -async def test_special_danfoss_battery_creation(hass, aioclient_mock): +@pytest.mark.parametrize("model_id", ["0x8030", "0x8031", "0x8034", "0x8035"]) +async def test_special_danfoss_battery_creation(hass, aioclient_mock, model_id): """Test the special Danfoss battery creation works. Normally there should only be one battery sensor per device from deCONZ. @@ -334,7 +842,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): "etag": "982d9acc38bee5b251e24a9be26558e4", "lastseen": "2021-02-15T12:23Z", "manufacturername": "Danfoss", - "modelid": "0x8030", + "modelid": model_id, "name": "0x8030", "state": { "lastupdated": "2021-02-15T12:23:07.994", @@ -359,7 +867,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): "etag": "62f12749f9f51c950086aff37dd02b61", "lastseen": "2021-02-15T12:23Z", "manufacturername": "Danfoss", - "modelid": "0x8030", + "modelid": model_id, "name": "0x8030", "state": { "lastupdated": "2021-02-15T12:23:22.399", @@ -384,7 +892,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): "etag": "f50061174bb7f18a3d95789bab8b646d", "lastseen": "2021-02-15T12:23Z", "manufacturername": "Danfoss", - "modelid": "0x8030", + "modelid": model_id, "name": "0x8030", "state": { "lastupdated": "2021-02-15T12:23:25.466", @@ -409,7 +917,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): "etag": "eea97adf8ce1b971b8b6a3a31793f96b", "lastseen": "2021-02-15T12:23Z", "manufacturername": "Danfoss", - "modelid": "0x8030", + "modelid": model_id, "name": "0x8030", "state": { "lastupdated": "2021-02-15T12:23:41.939", @@ -434,7 +942,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): "etag": "1f7cd1a5d66dc27ac5eb44b8c47362fb", "lastseen": "2021-02-15T12:23Z", "manufacturername": "Danfoss", - "modelid": "0x8030", + "modelid": model_id, "name": "0x8030", "state": {"lastupdated": "none", "on": False, "temperature": 2325}, "swversion": "YYYYMMDD", @@ -450,120 +958,6 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock): assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 5 -async def test_air_quality_sensor(hass, aioclient_mock): - """Test successful creation of air quality sensor entities.""" - data = { - "sensors": { - "0": { - "config": {"on": True, "reachable": True}, - "ep": 2, - "etag": "c2d2e42396f7c78e11e46c66e2ec0200", - "lastseen": "2020-11-20T22:48Z", - "manufacturername": "BOSCH", - "modelid": "AIR", - "name": "Air quality", - "state": { - "airquality": "poor", - "airqualityppb": 809, - "lastupdated": "2020-11-20T22:48:00.209", - }, - "swversion": "20200402", - "type": "ZHAAirQuality", - "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef", - } - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - - assert len(hass.states.async_all()) == 2 - assert hass.states.get("sensor.air_quality").state == "poor" - assert hass.states.get("sensor.air_quality_ppb").state == "809" - - -async def test_daylight_sensor(hass, aioclient_mock): - """Test daylight sensor is disabled by default and when created has expected attributes.""" - data = { - "sensors": { - "0": { - "config": { - "configured": True, - "on": True, - "sunriseoffset": 30, - "sunsetoffset": -30, - }, - "etag": "55047cf652a7e594d0ee7e6fae01dd38", - "manufacturername": "Philips", - "modelid": "PHDL00", - "name": "Daylight sensor", - "state": { - "daylight": True, - "lastupdated": "2018-03-24T17:26:12", - "status": 170, - }, - "swversion": "1.0", - "type": "Daylight", - "uniqueid": "00:00:00:00:00:00:00:00-00", - } - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - - assert len(hass.states.async_all()) == 0 - assert not hass.states.get("sensor.daylight_sensor") - - # Enable in entity registry - - entity_registry = er.async_get(hass) - entity_registry.async_update_entity( - entity_id="sensor.daylight_sensor", disabled_by=None - ) - await hass.async_block_till_done() - - async_fire_time_changed( - hass, - dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), - ) - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 1 - assert hass.states.get("sensor.daylight_sensor") - assert hass.states.get("sensor.daylight_sensor").attributes[ATTR_DAYLIGHT] - - -async def test_time_sensor(hass, aioclient_mock): - """Test successful creation of time sensor entities.""" - data = { - "sensors": { - "0": { - "config": {"battery": 40, "on": True, "reachable": True}, - "ep": 1, - "etag": "28e796678d9a24712feef59294343bb6", - "lastseen": "2020-11-22T11:26Z", - "manufacturername": "Danfoss", - "modelid": "eTRV0100", - "name": "Time", - "state": { - "lastset": "2020-11-19T08:07:08Z", - "lastupdated": "2020-11-22T10:51:03.444", - "localtime": "2020-11-22T10:51:01", - "utc": "2020-11-22T10:51:01Z", - }, - "swversion": "20200429", - "type": "ZHATime", - "uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a", - } - } - } - with patch.dict(DECONZ_WEB_REQUEST, data): - await setup_deconz_integration(hass, aioclient_mock) - - assert len(hass.states.async_all()) == 2 - assert hass.states.get("sensor.time").state == "2020-11-19T08:07:08Z" - assert hass.states.get("sensor.time_battery_level").state == "40" - - async def test_unsupported_sensor(hass, aioclient_mock): """Test that unsupported sensors doesn't break anything.""" data = {