diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 67ad10dd865..bc6acb2732a 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -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 diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 30ea0925993..dbcb3b1b8e7 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -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(