diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index cfc093c7762..55a225eebb8 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -78,7 +78,7 @@ STAT_DISTANCE_ABSOLUTE = "distance_absolute" STAT_MEAN = "mean" STAT_MEDIAN = "median" STAT_NOISINESS = "noisiness" -STAT_QUANTILES = "quantiles" +STAT_PERCENTILE = "percentile" STAT_STANDARD_DEVIATION = "standard_deviation" STAT_SUM = "sum" STAT_SUM_DIFFERENCES = "sum_differences" @@ -107,7 +107,7 @@ STATS_NUMERIC_SUPPORT = { STAT_MEAN, STAT_MEDIAN, STAT_NOISINESS, - STAT_QUANTILES, + STAT_PERCENTILE, STAT_STANDARD_DEVIATION, STAT_SUM, STAT_SUM_DIFFERENCES, @@ -135,7 +135,6 @@ STATS_NOT_A_NUMBER = { STAT_DATETIME_OLDEST, STAT_DATETIME_VALUE_MAX, STAT_DATETIME_VALUE_MIN, - STAT_QUANTILES, } STATS_DATETIME = { @@ -157,6 +156,7 @@ STAT_NUMERIC_RETAIN_UNIT = { STAT_MEAN, STAT_MEDIAN, STAT_NOISINESS, + STAT_PERCENTILE, STAT_STANDARD_DEVIATION, STAT_SUM, STAT_SUM_DIFFERENCES, @@ -177,13 +177,10 @@ CONF_STATE_CHARACTERISTIC = "state_characteristic" CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size" CONF_MAX_AGE = "max_age" CONF_PRECISION = "precision" -CONF_QUANTILE_INTERVALS = "quantile_intervals" -CONF_QUANTILE_METHOD = "quantile_method" +CONF_PERCENTILE = "percentile" DEFAULT_NAME = "Stats" DEFAULT_PRECISION = 2 -DEFAULT_QUANTILE_INTERVALS = 4 -DEFAULT_QUANTILE_METHOD = "exclusive" ICON = "mdi:calculator" @@ -248,11 +245,8 @@ _PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend( vol.Optional(CONF_SAMPLES_MAX_BUFFER_SIZE): vol.Coerce(int), vol.Optional(CONF_MAX_AGE): cv.time_period, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), - vol.Optional( - CONF_QUANTILE_INTERVALS, default=DEFAULT_QUANTILE_INTERVALS - ): vol.All(vol.Coerce(int), vol.Range(min=2)), - vol.Optional(CONF_QUANTILE_METHOD, default=DEFAULT_QUANTILE_METHOD): vol.In( - ["exclusive", "inclusive"] + vol.Optional(CONF_PERCENTILE, default=50): vol.All( + vol.Coerce(int), vol.Range(min=1, max=99) ), } ) @@ -283,8 +277,7 @@ async def async_setup_platform( samples_max_buffer_size=config[CONF_SAMPLES_MAX_BUFFER_SIZE], samples_max_age=config.get(CONF_MAX_AGE), precision=config[CONF_PRECISION], - quantile_intervals=config[CONF_QUANTILE_INTERVALS], - quantile_method=config[CONF_QUANTILE_METHOD], + percentile=config[CONF_PERCENTILE], ) ], update_before_add=True, @@ -303,8 +296,7 @@ class StatisticsSensor(SensorEntity): samples_max_buffer_size: int, samples_max_age: timedelta | None, precision: int, - quantile_intervals: int, - quantile_method: Literal["exclusive", "inclusive"], + percentile: int, ) -> None: """Initialize the Statistics sensor.""" self._attr_icon: str = ICON @@ -319,8 +311,7 @@ class StatisticsSensor(SensorEntity): self._samples_max_buffer_size: int = samples_max_buffer_size self._samples_max_age: timedelta | None = samples_max_age self._precision: int = precision - self._quantile_intervals: int = quantile_intervals - self._quantile_method: Literal["exclusive", "inclusive"] = quantile_method + self._percentile: int = percentile self._value: StateType | datetime = None self._unit_of_measurement: str | None = None self._available: bool = False @@ -700,18 +691,10 @@ class StatisticsSensor(SensorEntity): return cast(float, self._stat_sum_differences()) / (len(self.states) - 1) return None - def _stat_quantiles(self) -> StateType: - if len(self.states) > self._quantile_intervals: - return str( - [ - round(quantile, self._precision) - for quantile in statistics.quantiles( - self.states, - n=self._quantile_intervals, - method=self._quantile_method, - ) - ] - ) + def _stat_percentile(self) -> StateType: + if len(self.states) >= 2: + percentiles = statistics.quantiles(self.states, n=100, method="exclusive") + return percentiles[self._percentile - 1] return None def _stat_standard_deviation(self) -> StateType: diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 691159fe2fc..be771d8be88 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -391,7 +391,7 @@ async def test_age_limit_expiry(hass: HomeAssistant): async def test_precision(hass: HomeAssistant): - """Test correct result with precision set.""" + """Test correct results with precision set.""" assert await async_setup_component( hass, "sensor", @@ -433,6 +433,60 @@ async def test_precision(hass: HomeAssistant): assert state.state == str(round(mean, 3)) +async def test_percentile(hass: HomeAssistant): + """Test correct results for percentile characteristic.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_percentile_omitted", + "entity_id": "sensor.test_monitored", + "state_characteristic": "percentile", + "sampling_size": 20, + }, + { + "platform": "statistics", + "name": "test_percentile_default", + "entity_id": "sensor.test_monitored", + "state_characteristic": "percentile", + "sampling_size": 20, + "percentile": 50, + }, + { + "platform": "statistics", + "name": "test_percentile_min", + "entity_id": "sensor.test_monitored", + "state_characteristic": "percentile", + "sampling_size": 20, + "percentile": 1, + }, + ] + }, + ) + 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}, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_percentile_omitted") + assert state is not None + assert state.state == str(9.2) + state = hass.states.get("sensor.test_percentile_default") + assert state is not None + assert state.state == str(9.2) + state = hass.states.get("sensor.test_percentile_min") + assert state is not None + assert state.state == str(2.72) + + async def test_device_class(hass: HomeAssistant): """Test device class, which depends on the source entity.""" assert await async_setup_component( @@ -753,13 +807,11 @@ async def test_state_characteristics(hass: HomeAssistant): }, { "source_sensor_domain": "sensor", - "name": "quantiles", + "name": "percentile", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, - "value_9": [ - round(quantile, 2) for quantile in statistics.quantiles(VALUES_NUMERIC) - ], - "unit": None, + "value_9": 9.2, + "unit": "°C", }, { "source_sensor_domain": "sensor",