From f26fadbdfc45c4fb147997d4efc7e4929d2b27ed Mon Sep 17 00:00:00 2001 From: Justin Sherman Date: Sun, 25 Sep 2022 20:08:31 -0700 Subject: [PATCH] Add range to min_max (#78282) --- .../components/min_max/config_flow.py | 1 + homeassistant/components/min_max/sensor.py | 17 ++++++ tests/components/min_max/test_sensor.py | 56 ++++++++++++++++++- 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py index 2114a5406d0..0fed67f15b9 100644 --- a/homeassistant/components/min_max/config_flow.py +++ b/homeassistant/components/min_max/config_flow.py @@ -22,6 +22,7 @@ _STATISTIC_MEASURES = [ selector.SelectOptionDict(value="mean", label="Arithmetic mean"), selector.SelectOptionDict(value="median", label="Median"), selector.SelectOptionDict(value="last", label="Most recently updated"), + selector.SelectOptionDict(value="range", label="Statistical range"), ] diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 615aebc8e39..0f53875861d 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -39,6 +39,7 @@ ATTR_MEAN = "mean" ATTR_MEDIAN = "median" ATTR_LAST = "last" ATTR_LAST_ENTITY_ID = "last_entity_id" +ATTR_RANGE = "range" ICON = "mdi:calculator" @@ -48,6 +49,7 @@ SENSOR_TYPES = { ATTR_MEAN: "mean", ATTR_MEDIAN: "median", ATTR_LAST: "last", + ATTR_RANGE: "range", } SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()} @@ -158,6 +160,19 @@ def calc_median(sensor_values, round_digits): return round(statistics.median(result), round_digits) +def calc_range(sensor_values, round_digits): + """Calculate range value, honoring unknown states.""" + result = [ + sensor_value + for _, sensor_value in sensor_values + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] + ] + + if not result: + return None + return round(max(result) - min(result), round_digits) + + class MinMaxSensor(SensorEntity): """Representation of a min/max sensor.""" @@ -180,6 +195,7 @@ class MinMaxSensor(SensorEntity): self._unit_of_measurement = None self._unit_of_measurement_mismatch = False self.min_value = self.max_value = self.mean = self.last = self.median = None + self.range = None self.min_entity_id = self.max_entity_id = self.last_entity_id = None self.count_sensors = len(self._entity_ids) self.states = {} @@ -288,3 +304,4 @@ class MinMaxSensor(SensorEntity): self.max_entity_id, self.max_value = calc_max(sensor_values) self.mean = calc_mean(sensor_values, self._round_digits) self.median = calc_median(sensor_values, self._round_digits) + self.range = calc_range(sensor_values, self._round_digits) diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 72728ac20b6..47435dfbaf3 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -26,6 +26,8 @@ MEAN = round(sum(VALUES) / COUNT, 2) MEAN_1_DIGIT = round(sum(VALUES) / COUNT, 1) MEAN_4_DIGITS = round(sum(VALUES) / COUNT, 4) MEDIAN = round(statistics.median(VALUES), 2) +RANGE_1_DIGIT = round(max(VALUES) - min(VALUES), 1) +RANGE_4_DIGITS = round(max(VALUES) - min(VALUES), 4) async def test_default_name_sensor(hass): @@ -160,7 +162,7 @@ async def test_mean_1_digit_sensor(hass): async def test_mean_4_digit_sensor(hass): - """Test the mean with 1-digit precision sensor.""" + """Test the mean with 4-digit precision sensor.""" config = { "sensor": { "platform": "min_max", @@ -211,6 +213,58 @@ async def test_median_sensor(hass): assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT +async def test_range_4_digit_sensor(hass): + """Test the range with 4-digit precision sensor.""" + config = { + "sensor": { + "platform": "min_max", + "name": "test_range", + "type": "range", + "round_digits": 4, + "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entity_ids"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_range") + + assert str(float(RANGE_4_DIGITS)) == state.state + + +async def test_range_1_digit_sensor(hass): + """Test the range with 1-digit precision sensor.""" + config = { + "sensor": { + "platform": "min_max", + "name": "test_range", + "type": "range", + "round_digits": 1, + "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entity_ids"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_range") + + assert str(float(RANGE_1_DIGIT)) == state.state + + async def test_not_enough_sensor_value(hass): """Test that there is nothing done if not enough values available.""" config = {