Provide statistics device_class based on source entity and characteristic (#69710)

pull/70835/head
Thomas Dietrich 2022-04-27 00:12:47 +02:00 committed by GitHub
parent c973e5d0d2
commit 9fdec407e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 101 additions and 31 deletions

View File

@ -20,6 +20,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONF_ENTITY_ID,
CONF_NAME,
@ -88,7 +89,7 @@ DEPRECATION_WARNING_CHARACTERISTIC = (
)
# Statistics supported by a sensor source (numeric)
STATS_NUMERIC_SUPPORT = (
STATS_NUMERIC_SUPPORT = {
STAT_AVERAGE_LINEAR,
STAT_AVERAGE_STEP,
STAT_AVERAGE_TIMELESS,
@ -110,26 +111,51 @@ STATS_NUMERIC_SUPPORT = (
STAT_VALUE_MAX,
STAT_VALUE_MIN,
STAT_VARIANCE,
)
}
# Statistics supported by a binary_sensor source
STATS_BINARY_SUPPORT = (
STATS_BINARY_SUPPORT = {
STAT_AVERAGE_STEP,
STAT_AVERAGE_TIMELESS,
STAT_COUNT,
STAT_MEAN,
)
}
STATS_NOT_A_NUMBER = (
STATS_NOT_A_NUMBER = {
STAT_DATETIME_NEWEST,
STAT_DATETIME_OLDEST,
STAT_QUANTILES,
)
}
STATS_DATETIME = (
STATS_DATETIME = {
STAT_DATETIME_NEWEST,
STAT_DATETIME_OLDEST,
)
}
# Statistics which retain the unit of the source entity
STAT_NUMERIC_RETAIN_UNIT = {
STAT_AVERAGE_LINEAR,
STAT_AVERAGE_STEP,
STAT_AVERAGE_TIMELESS,
STAT_CHANGE,
STAT_DISTANCE_95P,
STAT_DISTANCE_99P,
STAT_DISTANCE_ABSOLUTE,
STAT_MEAN,
STAT_MEDIAN,
STAT_NOISINESS,
STAT_STANDARD_DEVIATION,
STAT_TOTAL,
STAT_VALUE_MAX,
STAT_VALUE_MIN,
}
# Statistics which produce percentage ratio from binary_sensor source entity
STAT_BINARY_PERCENTAGE = {
STAT_AVERAGE_STEP,
STAT_AVERAGE_TIMELESS,
STAT_MEAN,
}
CONF_STATE_CHARACTERISTIC = "state_characteristic"
CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size"
@ -336,30 +362,11 @@ class StatisticsSensor(SensorEntity):
def _derive_unit_of_measurement(self, new_state: State) -> str | None:
base_unit: str | None = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
unit: str | None
if self.is_binary and self._state_characteristic in (
STAT_AVERAGE_STEP,
STAT_AVERAGE_TIMELESS,
STAT_MEAN,
):
if self.is_binary and self._state_characteristic in STAT_BINARY_PERCENTAGE:
unit = "%"
elif not base_unit:
unit = None
elif self._state_characteristic in (
STAT_AVERAGE_LINEAR,
STAT_AVERAGE_STEP,
STAT_AVERAGE_TIMELESS,
STAT_CHANGE,
STAT_DISTANCE_95P,
STAT_DISTANCE_99P,
STAT_DISTANCE_ABSOLUTE,
STAT_MEAN,
STAT_MEDIAN,
STAT_NOISINESS,
STAT_STANDARD_DEVIATION,
STAT_TOTAL,
STAT_VALUE_MAX,
STAT_VALUE_MIN,
):
elif self._state_characteristic in STAT_NUMERIC_RETAIN_UNIT:
unit = base_unit
elif self._state_characteristic in STATS_NOT_A_NUMBER:
unit = None
@ -374,8 +381,11 @@ class StatisticsSensor(SensorEntity):
return unit
@property
def device_class(self) -> Literal[SensorDeviceClass.TIMESTAMP] | None:
def device_class(self) -> SensorDeviceClass | None:
"""Return the class of this device."""
if self._state_characteristic in STAT_NUMERIC_RETAIN_UNIT:
_state = self.hass.states.get(self._source_entity_id)
return None if _state is None else _state.attributes.get(ATTR_DEVICE_CLASS)
if self._state_characteristic in STATS_DATETIME:
return SensorDeviceClass.TIMESTAMP
return None

View File

@ -8,10 +8,15 @@ from typing import Any
from unittest.mock import patch
from homeassistant import config as hass_config
from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass
from homeassistant.components.sensor import (
ATTR_STATE_CLASS,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.components.statistics import DOMAIN as STATISTICS_DOMAIN
from homeassistant.components.statistics.sensor import StatisticsSensor
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
SERVICE_RELOAD,
STATE_UNAVAILABLE,
@ -428,6 +433,61 @@ async def test_precision(hass: HomeAssistant):
assert state.state == str(round(mean, 3))
async def test_device_class(hass: HomeAssistant):
"""Test device class, which depends on the source entity."""
assert await async_setup_component(
hass,
"sensor",
{
"sensor": [
{
# Device class is carried over from source sensor for characteristics with same unit
"platform": "statistics",
"name": "test_source_class",
"entity_id": "sensor.test_monitored",
"state_characteristic": "mean",
},
{
# Device class is set to None for characteristics with special meaning
"platform": "statistics",
"name": "test_none",
"entity_id": "sensor.test_monitored",
"state_characteristic": "count",
},
{
# Device class is set to timestamp for datetime characteristics
"platform": "statistics",
"name": "test_timestamp",
"entity_id": "sensor.test_monitored",
"state_characteristic": "datetime_oldest",
},
]
},
)
await hass.async_block_till_done()
for value in VALUES_NUMERIC:
hass.states.async_set(
"sensor.test_monitored",
str(value),
{
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
},
)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_source_class")
assert state is not None
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE
state = hass.states.get("sensor.test_none")
assert state is not None
assert state.attributes.get(ATTR_DEVICE_CLASS) is None
state = hass.states.get("sensor.test_timestamp")
assert state is not None
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP
async def test_state_class(hass: HomeAssistant):
"""Test state class, which depends on the characteristic configured."""
assert await async_setup_component(