mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Improve Deconz sensors (#65259)
This commit is contained in:
parent
334a8ab13f
commit
1bc936ca8d
@ -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(
|
||||
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 = [
|
||||
AirQuality: [
|
||||
DeconzSensorDescription(
|
||||
key="temperature",
|
||||
required_attr="secondary_temperature",
|
||||
value_fn=lambda device: device.secondary_temperature,
|
||||
suffix="Temperature",
|
||||
update_key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key="air_quality",
|
||||
value_fn=lambda device: device.air_quality, # type: ignore[no-any-return]
|
||||
update_key="airquality",
|
||||
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,
|
||||
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,
|
||||
),
|
||||
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 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
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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 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",
|
||||
TEST_DATA = [
|
||||
( # Air quality sensor
|
||||
{
|
||||
"config": {
|
||||
"on": True,
|
||||
"reachable": True,
|
||||
},
|
||||
"2": {
|
||||
"name": "Presence sensor",
|
||||
"type": "ZHAPresence",
|
||||
"state": {"presence": False},
|
||||
"config": {},
|
||||
"uniqueid": "00:00:00:00:00:00:00:01-00",
|
||||
"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",
|
||||
},
|
||||
"3": {
|
||||
"name": "Switch 1",
|
||||
"type": "ZHASwitch",
|
||||
"state": {"buttonevent": 1000},
|
||||
"config": {},
|
||||
"uniqueid": "00:00:00:00:00:00:00:02-00",
|
||||
"swversion": "20200402",
|
||||
"type": "ZHAAirQuality",
|
||||
"uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef",
|
||||
},
|
||||
"4": {
|
||||
"name": "Switch 2",
|
||||
"type": "ZHASwitch",
|
||||
"state": {"buttonevent": 1000},
|
||||
"config": {"battery": 100},
|
||||
"uniqueid": "00:00:00:00:00:00:00:03-00",
|
||||
{
|
||||
"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",
|
||||
},
|
||||
"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",
|
||||
"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,
|
||||
},
|
||||
"6": {
|
||||
"name": "Consumption sensor",
|
||||
"type": "ZHAConsumption",
|
||||
"state": {"consumption": 2, "power": 6},
|
||||
"config": {"reachable": True},
|
||||
"uniqueid": "00:00:00:00:00:00:00:06-00",
|
||||
"uniqueid": "00:0d:6f:00:0b:7a:64:29-01-0702",
|
||||
},
|
||||
"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",
|
||||
{
|
||||
"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",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
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)
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
light_level_sensor = hass.states.get("sensor.light_level_sensor")
|
||||
assert light_level_sensor.state == "999.8"
|
||||
assert (
|
||||
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
|
||||
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}
|
||||
)
|
||||
|
||||
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")
|
||||
# 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()
|
||||
|
||||
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
|
||||
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 (
|
||||
ent_reg.async_get("sensor.switch_2_battery_level").entity_category
|
||||
== EntityCategory.DIAGNOSTIC
|
||||
ent_reg.async_get(expected["entity_id"]).entity_category
|
||||
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")
|
||||
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 = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user