Improve Deconz sensors (#65259)

This commit is contained in:
Robert Svensson 2022-02-15 08:32:56 +01:00 committed by GitHub
parent 334a8ab13f
commit 1bc936ca8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 849 additions and 463 deletions

View File

@ -3,10 +3,10 @@ from __future__ import annotations
from collections.abc import Callable, ValuesView from collections.abc import Callable, ValuesView
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from pydeconz.sensor import ( from pydeconz.sensor import (
AirQuality, AirQuality,
Battery,
Consumption, Consumption,
Daylight, Daylight,
DeconzSensor as PydeconzSensor, DeconzSensor as PydeconzSensor,
@ -17,7 +17,6 @@ from pydeconz.sensor import (
Pressure, Pressure,
Switch, Switch,
Temperature, Temperature,
Thermostat,
Time, Time,
) )
@ -48,22 +47,21 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
import homeassistant.util.dt as dt_util
from .const import ATTR_DARK, ATTR_ON from .const import ATTR_DARK, ATTR_ON
from .deconz_device import DeconzDevice from .deconz_device import DeconzDevice
from .gateway import DeconzGateway, get_gateway_from_config_entry from .gateway import DeconzGateway, get_gateway_from_config_entry
DECONZ_SENSORS = ( PROVIDES_EXTRA_ATTRIBUTES = (
AirQuality, "battery",
Consumption, "consumption",
Daylight, "status",
GenericStatus, "humidity",
Humidity, "light_level",
LightLevel, "power",
Power, "pressure",
Pressure, "temperature",
Temperature,
Time,
) )
ATTR_CURRENT = "current" ATTR_CURRENT = "current"
@ -76,9 +74,7 @@ ATTR_EVENT_ID = "event_id"
class DeconzSensorDescriptionMixin: class DeconzSensorDescriptionMixin:
"""Required values when describing secondary sensor attributes.""" """Required values when describing secondary sensor attributes."""
suffix: str
update_key: str update_key: str
required_attr: str
value_fn: Callable[[PydeconzSensor], float | int | None] value_fn: Callable[[PydeconzSensor], float | int | None]
@ -89,78 +85,133 @@ class DeconzSensorDescription(
): ):
"""Class describing deCONZ binary sensor entities.""" """Class describing deCONZ binary sensor entities."""
suffix: str = ""
ENTITY_DESCRIPTIONS = { ENTITY_DESCRIPTIONS = {
Battery: SensorEntityDescription( AirQuality: [
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( DeconzSensorDescription(
key="temperature", key="air_quality",
required_attr="secondary_temperature", value_fn=lambda device: device.air_quality, # type: ignore[no-any-return]
value_fn=lambda device: device.secondary_temperature, update_key="airquality",
suffix="Temperature",
update_key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=TEMP_CELSIUS,
), ),
DeconzSensorDescription( DeconzSensorDescription(
key="air_quality_ppb", key="air_quality_ppb",
required_attr="air_quality_ppb", value_fn=lambda device: device.air_quality_ppb, # type: ignore[no-any-return]
value_fn=lambda device: device.air_quality_ppb,
suffix="PPB", suffix="PPB",
update_key="airqualityppb", update_key="airqualityppb",
device_class=SensorDeviceClass.AQI, device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, 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,
),
DeconzSensorDescription(
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,
),
] ]
@ -185,42 +236,33 @@ async def async_setup_entry(
Create DeconzBattery if sensor has a battery attribute. Create DeconzBattery if sensor has a battery attribute.
Create DeconzSensor if not a battery, switch or thermostat and not a binary sensor. 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: for sensor in sensors:
if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"):
continue continue
if sensor.battery is not None: if sensor.battery is 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:
battery_handler.create_tracker(sensor) battery_handler.create_tracker(sensor)
if ( known_entities = set(gateway.entities[DOMAIN])
isinstance(sensor, DECONZ_SENSORS) for description in (
and not isinstance(sensor, Thermostat) ENTITY_DESCRIPTIONS.get(type(sensor), []) + SENSOR_DESCRIPTIONS
and sensor.unique_id not in gateway.entities[DOMAIN]
): ):
entities.append(DeconzSensor(sensor, gateway))
known_sensor_entities = set(gateway.entities[DOMAIN]) if (
for sensor_description in SENSOR_DESCRIPTIONS: not hasattr(sensor, description.key)
or description.value_fn(sensor) is None
if not hasattr( ):
sensor, sensor_description.required_attr
) or not sensor_description.value_fn(sensor):
continue continue
new_sensor = DeconzPropertySensor(sensor, gateway, sensor_description) new_entity = DeconzSensor(sensor, gateway, description)
if new_sensor.unique_id not in known_sensor_entities: if new_entity.unique_id not in known_entities:
entities.append(new_sensor) entities.append(new_entity)
if description.key == "battery":
battery_handler.remove_tracker(sensor)
if entities: if entities:
async_add_entities(entities) async_add_entities(entities)
@ -243,30 +285,66 @@ class DeconzSensor(DeconzDevice, SensorEntity):
TYPE = DOMAIN TYPE = DOMAIN
_device: PydeconzSensor _device: PydeconzSensor
entity_description: DeconzSensorDescription
def __init__(self, device: PydeconzSensor, gateway: DeconzGateway) -> None: def __init__(
"""Initialize deCONZ binary sensor.""" self,
device: PydeconzSensor,
gateway: DeconzGateway,
description: DeconzSensorDescription,
) -> None:
"""Initialize deCONZ sensor."""
self.entity_description = description
super().__init__(device, gateway) super().__init__(device, gateway)
if entity_description := ENTITY_DESCRIPTIONS.get(type(device)): if description.suffix:
self.entity_description = entity_description 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 @callback
def async_update_callback(self) -> None: def async_update_callback(self) -> None:
"""Update the sensor's state.""" """Update the sensor's state."""
keys = {"on", "reachable", "state"} if self._device.changed_keys.intersection(self._update_keys):
if self._device.changed_keys.intersection(keys):
super().async_update_callback() super().async_update_callback()
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType | datetime:
"""Return the state of the sensor.""" """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 @property
def extra_state_attributes(self) -> dict[str, bool | float | int | None]: def extra_state_attributes(self) -> dict[str, bool | float | int | None]:
"""Return the state attributes of the sensor.""" """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: if self._device.on is not None:
attr[ATTR_ON] = self._device.on attr[ATTR_ON] = self._device.on
@ -292,93 +370,7 @@ class DeconzSensor(DeconzDevice, SensorEntity):
attr[ATTR_CURRENT] = self._device.current attr[ATTR_CURRENT] = self._device.current
attr[ATTR_VOLTAGE] = self._device.voltage attr[ATTR_VOLTAGE] = self._device.voltage
return attr elif isinstance(self._device, Switch):
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):
for event in self.gateway.events: for event in self.gateway.events:
if self._device == event.device: if self._device == event.device:
attr[ATTR_EVENT_ID] = event.event_id attr[ATTR_EVENT_ID] = event.event_id

View File

@ -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["current_temperature"] == 21.0
assert climate_thermostat.attributes["temperature"] == 21.0 assert climate_thermostat.attributes["temperature"] == 21.0
assert climate_thermostat.attributes["locked"] is True 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 # 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["current_temperature"] == 22.6
assert climate_thermostat.attributes["temperature"] == 22.0 assert climate_thermostat.attributes["temperature"] == 22.0
assert hass.states.get("sensor.thermostat") is None 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.presence_sensor") is None
assert hass.states.get("climate.clip_thermostat") 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["current_temperature"] == 23.2
assert climate_thermostat.attributes["temperature"] == 22.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 # 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 len(hass.states.async_all()) == 2
assert hass.states.get("climate.thermostat").state == HVAC_MODE_AUTO 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"

View File

@ -80,9 +80,9 @@ async def test_deconz_events(hass, aioclient_mock, mock_deconz_websocket):
assert ( assert (
len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 7 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_2_battery").state == "100"
assert hass.states.get("sensor.switch_3_battery_level").state == "100" assert hass.states.get("sensor.switch_3_battery").state == "100"
assert hass.states.get("sensor.switch_4_battery_level").state == "100" assert hass.states.get("sensor.switch_4_battery").state == "100"
captured_events = async_capture_events(hass, CONF_DECONZ_EVENT) captured_events = async_capture_events(hass, CONF_DECONZ_EVENT)

View File

@ -120,7 +120,7 @@ async def test_get_triggers(hass, aioclient_mock):
{ {
CONF_DEVICE_ID: device.id, CONF_DEVICE_ID: device.id,
CONF_DOMAIN: SENSOR_DOMAIN, 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_PLATFORM: "device",
CONF_TYPE: ATTR_BATTERY_LEVEL, CONF_TYPE: ATTR_BATTERY_LEVEL,
}, },

View File

@ -3,12 +3,17 @@
from datetime import timedelta from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
import pytest
from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR from homeassistant.components.deconz.const import CONF_ALLOW_CLIP_SENSOR
from homeassistant.components.deconz.sensor import ATTR_DAYLIGHT from homeassistant.components.sensor import (
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass DOMAIN as SENSOR_DOMAIN,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY
from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE 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.helpers.entity import EntityCategory
from homeassistant.util import dt from homeassistant.util import dt
@ -23,159 +28,639 @@ async def test_no_sensors(hass, aioclient_mock):
assert len(hass.states.async_all()) == 0 assert len(hass.states.async_all()) == 0
async def test_sensors(hass, aioclient_mock, mock_deconz_websocket): TEST_DATA = [
"""Test successful creation of sensor entities.""" ( # Air quality sensor
data = { {
"sensors": { "config": {
"1": { "on": True,
"name": "Light level sensor", "reachable": True,
"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": { "ep": 2,
"name": "Presence sensor", "etag": "c2d2e42396f7c78e11e46c66e2ec0200",
"type": "ZHAPresence", "lastseen": "2020-11-20T22:48Z",
"state": {"presence": False}, "manufacturername": "BOSCH",
"config": {}, "modelid": "AIR",
"uniqueid": "00:00:00:00:00:00:00:01-00", "name": "BOSCH Air quality sensor",
"state": {
"airquality": "poor",
"airqualityppb": 809,
"lastupdated": "2020-11-20T22:48:00.209",
}, },
"3": { "swversion": "20200402",
"name": "Switch 1", "type": "ZHAAirQuality",
"type": "ZHASwitch", "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef",
"state": {"buttonevent": 1000},
"config": {},
"uniqueid": "00:00:00:00:00:00:00:02-00",
}, },
"4": { {
"name": "Switch 2", "entity_count": 2,
"type": "ZHASwitch", "device_count": 3,
"state": {"buttonevent": 1000}, "entity_id": "sensor.bosch_air_quality_sensor",
"config": {"battery": 100}, "unique_id": "00:12:4b:00:14:4d:00:07-02-fdef",
"uniqueid": "00:00:00:00:00:00:00:03-00", "state": "poor",
"entity_category": None,
"device_class": None,
"state_class": SensorStateClass.MEASUREMENT,
"attributes": {
"state_class": "measurement",
"friendly_name": "BOSCH Air quality sensor",
}, },
"5": { "websocket_event": {"state": {"airquality": "excellent"}},
"name": "Power sensor", "next_state": "excellent",
"type": "ZHAPower", },
"state": {"current": 2, "power": 6, "voltage": 3}, ),
"config": {"reachable": True}, ( # Air quality PPB sensor
"uniqueid": "00:00:00:00:00:00:00:05-00", {
"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,
}, },
"6": {
"name": "Consumption sensor",
"type": "ZHAConsumption", "type": "ZHAConsumption",
"state": {"consumption": 2, "power": 6}, "uniqueid": "00:0d:6f:00:0b:7a:64:29-01-0702",
"config": {"reachable": True},
"uniqueid": "00:00:00:00:00:00:00:06-00",
}, },
"7": { {
"id": "CLIP light sensor id", "entity_count": 1,
"name": "CLIP light level sensor", "device_count": 3,
"type": "CLIPLightLevel", "entity_id": "sensor.consumption_15",
"state": {"lightlevel": 30000}, "unique_id": "00:0d:6f:00:0b:7a:64:29-01-0702",
"config": {"reachable": True}, "state": "11.342",
"uniqueid": "00:00:00:00:00:00:00:07-00", "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",
},
),
]
with patch.dict(DECONZ_WEB_REQUEST, data):
config_entry = await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 6
@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."""
ent_reg = er.async_get(hass) ent_reg = er.async_get(hass)
dev_reg = dr.async_get(hass)
light_level_sensor = hass.states.get("sensor.light_level_sensor") with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}):
assert light_level_sensor.state == "999.8" config_entry = await setup_deconz_integration(
assert ( hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True}
light_level_sensor.attributes[ATTR_DEVICE_CLASS]
== SensorDeviceClass.ILLUMINANCE
)
assert light_level_sensor.attributes[ATTR_DAYLIGHT] == 6955
light_level_temp = hass.states.get("sensor.light_level_sensor_temperature")
assert light_level_temp.state == "0.1"
assert (
light_level_temp.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE
) )
assert not hass.states.get("sensor.presence_sensor") # Enable in entity registry
assert not hass.states.get("sensor.switch_1") if expected.get("enable_entity"):
assert not hass.states.get("sensor.switch_1_battery_level") ent_reg.async_update_entity(entity_id=expected["entity_id"], disabled_by=None)
assert not hass.states.get("sensor.switch_2") await hass.async_block_till_done()
switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") async_fire_time_changed(
assert switch_2_battery_level.state == "100" hass,
assert ( dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
switch_2_battery_level.attributes[ATTR_DEVICE_CLASS]
== SensorDeviceClass.BATTERY
) )
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 ( assert (
ent_reg.async_get("sensor.switch_2_battery_level").entity_category ent_reg.async_get(expected["entity_id"]).entity_category
== EntityCategory.DIAGNOSTIC is expected["entity_category"]
)
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"]
# Verify device registry
assert (
len(dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id))
== expected["device_count"]
) )
assert not hass.states.get("sensor.daylight_sensor") # Change state
power_sensor = hass.states.get("sensor.power_sensor") event_changed_sensor = {"t": "event", "e": "changed", "r": "sensors", "id": "1"}
assert power_sensor.state == "6" event_changed_sensor |= expected["websocket_event"]
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},
}
await mock_deconz_websocket(data=event_changed_sensor) await mock_deconz_websocket(data=event_changed_sensor)
await hass.async_block_till_done()
assert hass.states.get("sensor.light_level_sensor").state == "1.6" assert hass.states.get(expected["entity_id"]).state == expected["next_state"]
# 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"
# Unload entry # Unload entry
await hass.config_entries.async_unload(config_entry.entry_id) await hass.config_entries.async_unload(config_entry.entry_id)
assert hass.states.get(expected["entity_id"]).state == STATE_UNAVAILABLE
states = hass.states.async_all()
assert len(states) == 6
for state in states:
assert state.state == STATE_UNAVAILABLE
# Remove entry # Remove entry
@ -184,6 +669,28 @@ async def test_sensors(hass, aioclient_mock, mock_deconz_websocket):
assert len(hass.states.async_all()) == 0 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): async def test_allow_clip_sensors(hass, aioclient_mock):
"""Test that CLIP sensors can be allowed.""" """Test that CLIP sensors can be allowed."""
data = { data = {
@ -295,7 +802,7 @@ async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket):
await setup_deconz_integration(hass, aioclient_mock) await setup_deconz_integration(hass, aioclient_mock)
assert len(hass.states.async_all()) == 0 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 = { event_changed_sensor = {
"t": "event", "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 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. """Test the special Danfoss battery creation works.
Normally there should only be one battery sensor per device from deCONZ. 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", "etag": "982d9acc38bee5b251e24a9be26558e4",
"lastseen": "2021-02-15T12:23Z", "lastseen": "2021-02-15T12:23Z",
"manufacturername": "Danfoss", "manufacturername": "Danfoss",
"modelid": "0x8030", "modelid": model_id,
"name": "0x8030", "name": "0x8030",
"state": { "state": {
"lastupdated": "2021-02-15T12:23:07.994", "lastupdated": "2021-02-15T12:23:07.994",
@ -359,7 +867,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock):
"etag": "62f12749f9f51c950086aff37dd02b61", "etag": "62f12749f9f51c950086aff37dd02b61",
"lastseen": "2021-02-15T12:23Z", "lastseen": "2021-02-15T12:23Z",
"manufacturername": "Danfoss", "manufacturername": "Danfoss",
"modelid": "0x8030", "modelid": model_id,
"name": "0x8030", "name": "0x8030",
"state": { "state": {
"lastupdated": "2021-02-15T12:23:22.399", "lastupdated": "2021-02-15T12:23:22.399",
@ -384,7 +892,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock):
"etag": "f50061174bb7f18a3d95789bab8b646d", "etag": "f50061174bb7f18a3d95789bab8b646d",
"lastseen": "2021-02-15T12:23Z", "lastseen": "2021-02-15T12:23Z",
"manufacturername": "Danfoss", "manufacturername": "Danfoss",
"modelid": "0x8030", "modelid": model_id,
"name": "0x8030", "name": "0x8030",
"state": { "state": {
"lastupdated": "2021-02-15T12:23:25.466", "lastupdated": "2021-02-15T12:23:25.466",
@ -409,7 +917,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock):
"etag": "eea97adf8ce1b971b8b6a3a31793f96b", "etag": "eea97adf8ce1b971b8b6a3a31793f96b",
"lastseen": "2021-02-15T12:23Z", "lastseen": "2021-02-15T12:23Z",
"manufacturername": "Danfoss", "manufacturername": "Danfoss",
"modelid": "0x8030", "modelid": model_id,
"name": "0x8030", "name": "0x8030",
"state": { "state": {
"lastupdated": "2021-02-15T12:23:41.939", "lastupdated": "2021-02-15T12:23:41.939",
@ -434,7 +942,7 @@ async def test_special_danfoss_battery_creation(hass, aioclient_mock):
"etag": "1f7cd1a5d66dc27ac5eb44b8c47362fb", "etag": "1f7cd1a5d66dc27ac5eb44b8c47362fb",
"lastseen": "2021-02-15T12:23Z", "lastseen": "2021-02-15T12:23Z",
"manufacturername": "Danfoss", "manufacturername": "Danfoss",
"modelid": "0x8030", "modelid": model_id,
"name": "0x8030", "name": "0x8030",
"state": {"lastupdated": "none", "on": False, "temperature": 2325}, "state": {"lastupdated": "none", "on": False, "temperature": 2325},
"swversion": "YYYYMMDD", "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 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): async def test_unsupported_sensor(hass, aioclient_mock):
"""Test that unsupported sensors doesn't break anything.""" """Test that unsupported sensors doesn't break anything."""
data = { data = {