From e6ff8d6839cd08ef786ef5bd07cbcba404330961 Mon Sep 17 00:00:00 2001 From: Thorjan Knudsvik Date: Sun, 19 Jul 2020 01:18:31 +0200 Subject: [PATCH] Adds median to min_max component (#36686) --- homeassistant/components/min_max/sensor.py | 42 ++++++++++++++++------ tests/components/min_max/test_sensor.py | 37 +++++++++++++++++++ 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 6c99a8db60c..a5e31829d97 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -1,4 +1,4 @@ -"""Support for displaying the minimal and the maximal value.""" +"""Support for displaying minimal, maximal, mean or median values.""" import logging import voluptuous as vol @@ -24,6 +24,7 @@ ATTR_MAX_VALUE = "max_value" ATTR_MAX_ENTITY_ID = "max_entity_id" ATTR_COUNT_SENSORS = "count_sensors" ATTR_MEAN = "mean" +ATTR_MEDIAN = "median" ATTR_LAST = "last" ATTR_LAST_ENTITY_ID = "last_entity_id" @@ -32,6 +33,7 @@ ATTR_TO_PROPERTY = [ ATTR_MAX_VALUE, ATTR_MAX_ENTITY_ID, ATTR_MEAN, + ATTR_MEDIAN, ATTR_MIN_VALUE, ATTR_MIN_ENTITY_ID, ATTR_LAST, @@ -47,6 +49,7 @@ SENSOR_TYPES = { ATTR_MIN_VALUE: "min", ATTR_MAX_VALUE: "max", ATTR_MEAN: "mean", + ATTR_MEDIAN: "median", ATTR_LAST: "last", } @@ -80,7 +83,7 @@ def calc_min(sensor_values): val = None entity_id = None for sensor_id, sensor_value in sensor_values: - if sensor_value != STATE_UNKNOWN: + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]: if val is None or val > sensor_value: entity_id, val = sensor_id, sensor_value return entity_id, val @@ -91,7 +94,7 @@ def calc_max(sensor_values): val = None entity_id = None for sensor_id, sensor_value in sensor_values: - if sensor_value != STATE_UNKNOWN: + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]: if val is None or val < sensor_value: entity_id, val = sensor_id, sensor_value return entity_id, val @@ -99,15 +102,31 @@ def calc_max(sensor_values): def calc_mean(sensor_values, round_digits): """Calculate mean value, honoring unknown states.""" - sensor_value_sum = 0 - count = 0 + result = [] for _, sensor_value in sensor_values: - if sensor_value != STATE_UNKNOWN: - sensor_value_sum += sensor_value - count += 1 - if count == 0: + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + result.append(sensor_value) + if len(result) == 0: return None - return round(sensor_value_sum / count, round_digits) + return round(sum(result) / len(result), round_digits) + + +def calc_median(sensor_values, round_digits): + """Calculate median value, honoring unknown states.""" + result = [] + for _, sensor_value in sensor_values: + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + result.append(sensor_value) + if len(result) == 0: + return None + result.sort() + if len(result) % 2 == 0: + median1 = result[len(result) // 2] + median2 = result[len(result) // 2 - 1] + median = (median1 + median2) / 2 + else: + median = result[len(result) // 2] + return round(median, round_digits) class MinMaxSensor(Entity): @@ -126,7 +145,7 @@ class MinMaxSensor(Entity): self._name = f"{next(v for k, v in SENSOR_TYPES.items() if self._sensor_type == v)} sensor".capitalize() self._unit_of_measurement = None self._unit_of_measurement_mismatch = False - self.min_value = self.max_value = self.mean = self.last = None + self.min_value = self.max_value = self.mean = self.last = self.median = None self.min_entity_id = self.max_entity_id = self.last_entity_id = None self.count_sensors = len(self._entity_ids) self.states = {} @@ -224,3 +243,4 @@ class MinMaxSensor(Entity): self.min_entity_id, self.min_value = calc_min(sensor_values) 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) diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 57ac39f8ee4..bece386a89e 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -1,4 +1,5 @@ """The test for the min/max sensor platform.""" +import statistics import unittest from homeassistant.const import ( @@ -27,6 +28,7 @@ class TestMinMaxSensor(unittest.TestCase): self.mean = round(sum(self.values) / self.count, 2) self.mean_1_digit = round(sum(self.values) / self.count, 1) self.mean_4_digits = round(sum(self.values) / self.count, 4) + self.median = round(statistics.median(self.values), 2) def teardown_method(self, method): """Stop everything that was started.""" @@ -58,6 +60,7 @@ class TestMinMaxSensor(unittest.TestCase): assert self.max == state.attributes.get("max_value") assert entity_ids[1] == state.attributes.get("max_entity_id") assert self.mean == state.attributes.get("mean") + assert self.median == state.attributes.get("median") def test_max_sensor(self): """Test the max sensor.""" @@ -85,6 +88,7 @@ class TestMinMaxSensor(unittest.TestCase): assert self.min == state.attributes.get("min_value") assert entity_ids[1] == state.attributes.get("max_entity_id") assert self.mean == state.attributes.get("mean") + assert self.median == state.attributes.get("median") def test_mean_sensor(self): """Test the mean sensor.""" @@ -112,6 +116,7 @@ class TestMinMaxSensor(unittest.TestCase): assert entity_ids[2] == state.attributes.get("min_entity_id") assert self.max == state.attributes.get("max_value") assert entity_ids[1] == state.attributes.get("max_entity_id") + assert self.median == state.attributes.get("median") def test_mean_1_digit_sensor(self): """Test the mean with 1-digit precision sensor.""" @@ -140,6 +145,7 @@ class TestMinMaxSensor(unittest.TestCase): assert entity_ids[2] == state.attributes.get("min_entity_id") assert self.max == state.attributes.get("max_value") assert entity_ids[1] == state.attributes.get("max_entity_id") + assert self.median == state.attributes.get("median") def test_mean_4_digit_sensor(self): """Test the mean with 1-digit precision sensor.""" @@ -168,6 +174,35 @@ class TestMinMaxSensor(unittest.TestCase): assert entity_ids[2] == state.attributes.get("min_entity_id") assert self.max == state.attributes.get("max_value") assert entity_ids[1] == state.attributes.get("max_entity_id") + assert self.median == state.attributes.get("median") + + def test_median_sensor(self): + """Test the median sensor.""" + config = { + "sensor": { + "platform": "min_max", + "name": "test_median", + "type": "median", + "entity_ids": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + } + } + + assert setup_component(self.hass, "sensor", config) + + entity_ids = config["sensor"]["entity_ids"] + + for entity_id, value in dict(zip(entity_ids, self.values)).items(): + self.hass.states.set(entity_id, value) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test_median") + + assert str(float(self.median)) == state.state + assert self.min == state.attributes.get("min_value") + assert entity_ids[2] == state.attributes.get("min_entity_id") + assert self.max == state.attributes.get("max_value") + assert entity_ids[1] == state.attributes.get("max_entity_id") + assert self.mean == state.attributes.get("mean") def test_not_enough_sensor_value(self): """Test that there is nothing done if not enough values available.""" @@ -193,6 +228,7 @@ class TestMinMaxSensor(unittest.TestCase): assert state.attributes.get("min_value") is None assert state.attributes.get("max_entity_id") is None assert state.attributes.get("max_value") is None + assert state.attributes.get("median") is None self.hass.states.set(entity_ids[1], self.values[1]) self.hass.block_till_done() @@ -295,3 +331,4 @@ class TestMinMaxSensor(unittest.TestCase): assert self.min == state.attributes.get("min_value") assert self.max == state.attributes.get("max_value") assert self.mean == state.attributes.get("mean") + assert self.median == state.attributes.get("median")