mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Provide statistics device_class based on source entity and characteristic (#69710)
This commit is contained in:
parent
c973e5d0d2
commit
9fdec407e0
@ -20,6 +20,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
@ -88,7 +89,7 @@ DEPRECATION_WARNING_CHARACTERISTIC = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Statistics supported by a sensor source (numeric)
|
# Statistics supported by a sensor source (numeric)
|
||||||
STATS_NUMERIC_SUPPORT = (
|
STATS_NUMERIC_SUPPORT = {
|
||||||
STAT_AVERAGE_LINEAR,
|
STAT_AVERAGE_LINEAR,
|
||||||
STAT_AVERAGE_STEP,
|
STAT_AVERAGE_STEP,
|
||||||
STAT_AVERAGE_TIMELESS,
|
STAT_AVERAGE_TIMELESS,
|
||||||
@ -110,26 +111,51 @@ STATS_NUMERIC_SUPPORT = (
|
|||||||
STAT_VALUE_MAX,
|
STAT_VALUE_MAX,
|
||||||
STAT_VALUE_MIN,
|
STAT_VALUE_MIN,
|
||||||
STAT_VARIANCE,
|
STAT_VARIANCE,
|
||||||
)
|
}
|
||||||
|
|
||||||
# Statistics supported by a binary_sensor source
|
# Statistics supported by a binary_sensor source
|
||||||
STATS_BINARY_SUPPORT = (
|
STATS_BINARY_SUPPORT = {
|
||||||
STAT_AVERAGE_STEP,
|
STAT_AVERAGE_STEP,
|
||||||
STAT_AVERAGE_TIMELESS,
|
STAT_AVERAGE_TIMELESS,
|
||||||
STAT_COUNT,
|
STAT_COUNT,
|
||||||
STAT_MEAN,
|
STAT_MEAN,
|
||||||
)
|
}
|
||||||
|
|
||||||
STATS_NOT_A_NUMBER = (
|
STATS_NOT_A_NUMBER = {
|
||||||
STAT_DATETIME_NEWEST,
|
STAT_DATETIME_NEWEST,
|
||||||
STAT_DATETIME_OLDEST,
|
STAT_DATETIME_OLDEST,
|
||||||
STAT_QUANTILES,
|
STAT_QUANTILES,
|
||||||
)
|
}
|
||||||
|
|
||||||
STATS_DATETIME = (
|
STATS_DATETIME = {
|
||||||
STAT_DATETIME_NEWEST,
|
STAT_DATETIME_NEWEST,
|
||||||
STAT_DATETIME_OLDEST,
|
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_STATE_CHARACTERISTIC = "state_characteristic"
|
||||||
CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size"
|
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:
|
def _derive_unit_of_measurement(self, new_state: State) -> str | None:
|
||||||
base_unit: str | None = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
base_unit: str | None = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
unit: str | None
|
unit: str | None
|
||||||
if self.is_binary and self._state_characteristic in (
|
if self.is_binary and self._state_characteristic in STAT_BINARY_PERCENTAGE:
|
||||||
STAT_AVERAGE_STEP,
|
|
||||||
STAT_AVERAGE_TIMELESS,
|
|
||||||
STAT_MEAN,
|
|
||||||
):
|
|
||||||
unit = "%"
|
unit = "%"
|
||||||
elif not base_unit:
|
elif not base_unit:
|
||||||
unit = None
|
unit = None
|
||||||
elif self._state_characteristic in (
|
elif self._state_characteristic in 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,
|
|
||||||
):
|
|
||||||
unit = base_unit
|
unit = base_unit
|
||||||
elif self._state_characteristic in STATS_NOT_A_NUMBER:
|
elif self._state_characteristic in STATS_NOT_A_NUMBER:
|
||||||
unit = None
|
unit = None
|
||||||
@ -374,8 +381,11 @@ class StatisticsSensor(SensorEntity):
|
|||||||
return unit
|
return unit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self) -> Literal[SensorDeviceClass.TIMESTAMP] | None:
|
def device_class(self) -> SensorDeviceClass | None:
|
||||||
"""Return the class of this device."""
|
"""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:
|
if self._state_characteristic in STATS_DATETIME:
|
||||||
return SensorDeviceClass.TIMESTAMP
|
return SensorDeviceClass.TIMESTAMP
|
||||||
return None
|
return None
|
||||||
|
@ -8,10 +8,15 @@ from typing import Any
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from homeassistant import config as hass_config
|
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 import DOMAIN as STATISTICS_DOMAIN
|
||||||
from homeassistant.components.statistics.sensor import StatisticsSensor
|
from homeassistant.components.statistics.sensor import StatisticsSensor
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_DEVICE_CLASS,
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
SERVICE_RELOAD,
|
SERVICE_RELOAD,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
@ -428,6 +433,61 @@ async def test_precision(hass: HomeAssistant):
|
|||||||
assert state.state == str(round(mean, 3))
|
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):
|
async def test_state_class(hass: HomeAssistant):
|
||||||
"""Test state class, which depends on the characteristic configured."""
|
"""Test state class, which depends on the characteristic configured."""
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user