From bee3c9102c01d945686be088d9c04c45f7af5b06 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Thu, 2 Dec 2021 09:03:24 +0100 Subject: [PATCH] Add binary characteristics, add deprecation warning for optional state_characteristic parameter (#60402) * Add binary source sensor statistics * Make state_characteristic a required parameter * Move binary unitless testcase * Add testcases for binary characteristics * Revert charact. to optional with deprecation warning * Correctly check for binary supported characteristic --- homeassistant/components/statistics/sensor.py | 96 +++++- .../statistics/fixtures/configuration.yaml | 1 + tests/components/statistics/test_sensor.py | 308 ++++++++++++++---- 3 files changed, 330 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index e32ce804cb2..23a4a31d936 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -60,12 +60,30 @@ STAT_VALUE_MAX = "value_max" STAT_VALUE_MIN = "value_min" STAT_VARIANCE = "variance" +STAT_DEFAULT = "default" +DEPRECATION_WARNING = ( + "The configuration parameter 'state_characteristics' will become " + "mandatory in a future release of the statistics integration. " + "Please add 'state_characteristics: %s' to the configuration of " + 'sensor "%s" to keep the current behavior. Read the documentation ' + "for further details: " + "https://www.home-assistant.io/integrations/statistics/" +) + STATS_NOT_A_NUMBER = ( STAT_DATETIME_OLDEST, STAT_DATETIME_NEWEST, STAT_QUANTILES, ) +STATS_BINARY_SUPPORT = ( + STAT_AVERAGE_STEP, + STAT_AVERAGE_TIMELESS, + STAT_COUNT, + STAT_MEAN, + STAT_DEFAULT, +) + CONF_STATE_CHARACTERISTIC = "state_characteristic" CONF_SAMPLES_MAX_BUFFER_SIZE = "sampling_size" CONF_MAX_AGE = "max_age" @@ -80,11 +98,24 @@ DEFAULT_QUANTILE_INTERVALS = 4 DEFAULT_QUANTILE_METHOD = "exclusive" ICON = "mdi:calculator" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + +def valid_binary_characteristic_configuration(config): + """Validate that the characteristic selected is valid for the source sensor type, throw if it isn't.""" + if config.get(CONF_ENTITY_ID).split(".")[0] == "binary_sensor": + if config.get(CONF_STATE_CHARACTERISTIC) not in STATS_BINARY_SUPPORT: + raise ValueError( + "The configured characteristic '" + + config.get(CONF_STATE_CHARACTERISTIC) + + "' is not supported for a binary source sensor." + ) + return config + + +_PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_CHARACTERISTIC, default=STAT_MEAN): vol.In( + vol.Optional(CONF_STATE_CHARACTERISTIC, default=STAT_DEFAULT): vol.In( [ STAT_AVERAGE_LINEAR, STAT_AVERAGE_STEP, @@ -107,6 +138,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( STAT_VALUE_MAX, STAT_VALUE_MIN, STAT_VARIANCE, + STAT_DEFAULT, ] ), vol.Optional( @@ -122,6 +154,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ), } ) +PLATFORM_SCHEMA = vol.All( + _PLATFORM_SCHEMA_BASE, + valid_binary_characteristic_configuration, +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -166,6 +202,9 @@ class StatisticsSensor(SensorEntity): self.is_binary = self._source_entity_id.split(".")[0] == "binary_sensor" self._name = name self._state_characteristic = state_characteristic + if self._state_characteristic == STAT_DEFAULT: + self._state_characteristic = STAT_COUNT if self.is_binary else STAT_MEAN + _LOGGER.warning(DEPRECATION_WARNING, self._state_characteristic, name) self._samples_max_buffer_size = samples_max_buffer_size self._samples_max_age = samples_max_age self._precision = precision @@ -181,9 +220,15 @@ class StatisticsSensor(SensorEntity): STAT_BUFFER_USAGE_RATIO: None, STAT_SOURCE_VALUE_VALID: None, } - self._state_characteristic_fn = getattr( - self, f"_stat_{self._state_characteristic}" - ) + + if self.is_binary: + self._state_characteristic_fn = getattr( + self, f"_stat_binary_{self._state_characteristic}" + ) + else: + self._state_characteristic_fn = getattr( + self, f"_stat_{self._state_characteristic}" + ) self._update_listener = None @@ -246,9 +291,13 @@ class StatisticsSensor(SensorEntity): def _derive_unit_of_measurement(self, new_state): base_unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if not base_unit: - unit = None - elif self.is_binary: + if self.is_binary and self._state_characteristic in ( + STAT_AVERAGE_STEP, + STAT_AVERAGE_TIMELESS, + STAT_MEAN, + ): + unit = "%" + elif not base_unit: unit = None elif self._state_characteristic in ( STAT_AVERAGE_LINEAR, @@ -290,8 +339,6 @@ class StatisticsSensor(SensorEntity): @property def state_class(self): """Return the state class of this entity.""" - if self.is_binary: - return STATE_CLASS_MEASUREMENT if self._state_characteristic in STATS_NOT_A_NUMBER: return None return STATE_CLASS_MEASUREMENT @@ -450,10 +497,6 @@ class StatisticsSensor(SensorEntity): One of the _stat_*() functions is represented by self._state_characteristic_fn(). """ - if self.is_binary: - self._value = len(self.states) - return - value = self._state_characteristic_fn() if self._state_characteristic not in STATS_NOT_A_NUMBER: @@ -463,6 +506,8 @@ class StatisticsSensor(SensorEntity): value = int(value) self._value = value + # Statistics for numeric sensor + def _stat_average_linear(self): if len(self.states) >= 2: area = 0 @@ -590,3 +635,26 @@ class StatisticsSensor(SensorEntity): if len(self.states) >= 2: return statistics.variance(self.states) return None + + # Statistics for binary sensor + + def _stat_binary_average_step(self): + if len(self.states) >= 2: + on_seconds = 0 + for i in range(1, len(self.states)): + if self.states[i - 1] == "on": + on_seconds += (self.ages[i] - self.ages[i - 1]).total_seconds() + age_range_seconds = (self.ages[-1] - self.ages[0]).total_seconds() + return 100 / age_range_seconds * on_seconds + return None + + def _stat_binary_average_timeless(self): + return self._stat_binary_mean() + + def _stat_binary_count(self): + return len(self.states) + + def _stat_binary_mean(self): + if len(self.states) > 0: + return 100.0 / len(self.states) * self.states.count("on") + return None diff --git a/tests/components/statistics/fixtures/configuration.yaml b/tests/components/statistics/fixtures/configuration.yaml index a6ce34377a0..4708910b53e 100644 --- a/tests/components/statistics/fixtures/configuration.yaml +++ b/tests/components/statistics/fixtures/configuration.yaml @@ -2,3 +2,4 @@ sensor: - platform: statistics entity_id: sensor.cpu name: cputest + state_characteristic: mean diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index f1f895b2b1f..658d6a089e7 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -41,56 +41,14 @@ class TestStatisticsSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.values_binary = ["on", "off", "on", "off", "on", "off", "on"] + self.values_binary = ["on", "off", "on", "off", "on", "off", "on", "off", "on"] + self.mean_binary = round( + 100 / len(self.values_binary) * self.values_binary.count("on"), 2 + ) self.values = [17, 20, 15.2, 5, 3.8, 9.2, 6.7, 14, 6] self.mean = round(sum(self.values) / len(self.values), 2) self.addCleanup(self.hass.stop) - def test_sensor_defaults_binary(self): - """Test the general behavior of the sensor, with binary source sensor.""" - assert setup_component( - self.hass, - "sensor", - { - "sensor": [ - { - "platform": "statistics", - "name": "test", - "entity_id": "binary_sensor.test_monitored", - }, - { - "platform": "statistics", - "name": "test_unitless", - "entity_id": "binary_sensor.test_monitored_unitless", - }, - ] - }, - ) - - self.hass.block_till_done() - self.hass.start() - self.hass.block_till_done() - - for value in self.values_binary: - self.hass.states.set( - "binary_sensor.test_monitored", - value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, - ) - self.hass.states.set("binary_sensor.test_monitored_unitless", value) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - assert state.state == str(len(self.values_binary)) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT - assert state.attributes.get("buffer_usage_ratio") == round(7 / 20, 2) - assert state.attributes.get("source_value_valid") is True - assert "age_coverage_ratio" not in state.attributes - - state = self.hass.states.get("sensor.test_unitless") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - def test_sensor_defaults_numeric(self): """Test the general behavior of the sensor, with numeric source sensor.""" assert setup_component( @@ -178,6 +136,90 @@ class TestStatisticsSensor(unittest.TestCase): assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert new_state.attributes.get("source_value_valid") is False + def test_sensor_defaults_binary(self): + """Test the general behavior of the sensor, with binary source sensor.""" + assert setup_component( + self.hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test", + "entity_id": "binary_sensor.test_monitored", + }, + ] + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + for value in self.values_binary: + self.hass.states.set( + "binary_sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test") + assert state.state == str(len(self.values_binary)) + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) + assert state.attributes.get("source_value_valid") is True + assert "age_coverage_ratio" not in state.attributes + + def test_sensor_source_with_force_update(self): + """Test the behavior of the sensor when the source sensor force-updates with same value.""" + repeating_values = [18, 0, 0, 0, 0, 0, 0, 0, 9] + assert setup_component( + self.hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_normal", + "entity_id": "sensor.test_monitored_normal", + "state_characteristic": "mean", + }, + { + "platform": "statistics", + "name": "test_force", + "entity_id": "sensor.test_monitored_force", + "state_characteristic": "mean", + }, + ] + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + + for value in repeating_values: + self.hass.states.set( + "sensor.test_monitored_normal", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.states.set( + "sensor.test_monitored_force", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + force_update=True, + ) + self.hass.block_till_done() + + state_normal = self.hass.states.get("sensor.test_normal") + state_force = self.hass.states.get("sensor.test_force") + assert state_normal.state == str(round(sum(repeating_values) / 3, 2)) + assert state_force.state == str(round(sum(repeating_values) / 9, 2)) + assert state_normal.attributes.get("buffer_usage_ratio") == round(3 / 20, 2) + assert state_force.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) + def test_sampling_size_non_default(self): """Test rotation.""" assert setup_component( @@ -189,6 +231,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "sampling_size": 5, }, ] @@ -223,6 +266,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "sampling_size": 1, }, ] @@ -268,6 +312,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "max_age": {"minutes": 4}, }, ] @@ -341,6 +386,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "precision": 0, }, ] @@ -373,6 +419,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "precision": 1, }, ] @@ -459,6 +506,17 @@ class TestStatisticsSensor(unittest.TestCase): "entity_id": "sensor.test_monitored_unitless", "state_characteristic": "change_second", }, + { + "platform": "statistics", + "name": "test_unitless_4", + "entity_id": "binary_sensor.test_monitored_unitless", + }, + { + "platform": "statistics", + "name": "test_unitless_5", + "entity_id": "binary_sensor.test_monitored_unitless", + "state_characteristic": "mean", + }, ] }, ) @@ -473,6 +531,12 @@ class TestStatisticsSensor(unittest.TestCase): value, ) self.hass.block_till_done() + for value in self.values_binary: + self.hass.states.set( + "binary_sensor.test_monitored_unitless", + value, + ) + self.hass.block_till_done() state = self.hass.states.get("sensor.test_unitless_1") assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None @@ -480,6 +544,10 @@ class TestStatisticsSensor(unittest.TestCase): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None state = self.hass.states.get("sensor.test_unitless_3") assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = self.hass.states.get("sensor.test_unitless_4") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + state = self.hass.states.get("sensor.test_unitless_5") + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "%" def test_state_characteristics(self): """Test configured state characteristic for value and unit.""" @@ -495,6 +563,7 @@ class TestStatisticsSensor(unittest.TestCase): characteristics = ( { + "source_sensor_domain": "sensor", "name": "average_linear", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -502,6 +571,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "average_step", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -509,6 +579,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "average_timeless", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -516,6 +587,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "change", "value_0": STATE_UNKNOWN, "value_1": float(0), @@ -523,6 +595,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "change_sample", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -534,6 +607,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C/sample", }, { + "source_sensor_domain": "sensor", "name": "change_second", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -547,6 +621,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C/s", }, { + "source_sensor_domain": "sensor", "name": "count", "value_0": 0, "value_1": 1, @@ -554,6 +629,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": None, }, { + "source_sensor_domain": "sensor", "name": "datetime_newest", "value_0": STATE_UNKNOWN, "value_1": datetime( @@ -577,6 +653,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": None, }, { + "source_sensor_domain": "sensor", "name": "datetime_oldest", "value_0": STATE_UNKNOWN, "value_1": datetime( @@ -592,6 +669,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": None, }, { + "source_sensor_domain": "sensor", "name": "distance_95_percent_of_values", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -599,6 +677,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "distance_99_percent_of_values", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -606,6 +685,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "distance_absolute", "value_0": STATE_UNKNOWN, "value_1": float(0), @@ -613,6 +693,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "mean", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -620,6 +701,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "median", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -627,6 +709,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "noisiness", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -636,6 +719,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "quantiles", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -645,6 +729,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": None, }, { + "source_sensor_domain": "sensor", "name": "standard_deviation", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, @@ -652,6 +737,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "total", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -659,6 +745,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "value_max", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -666,6 +753,7 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "value_min", "value_0": STATE_UNKNOWN, "value_1": float(self.values[0]), @@ -673,20 +761,53 @@ class TestStatisticsSensor(unittest.TestCase): "unit": "°C", }, { + "source_sensor_domain": "sensor", "name": "variance", "value_0": STATE_UNKNOWN, "value_1": STATE_UNKNOWN, "value_9": float(round(statistics.variance(self.values), 2)), "unit": "°C²", }, + { + "source_sensor_domain": "binary_sensor", + "name": "average_step", + "value_0": STATE_UNKNOWN, + "value_1": STATE_UNKNOWN, + "value_9": 50.0, + "unit": "%", + }, + { + "source_sensor_domain": "binary_sensor", + "name": "average_timeless", + "value_0": STATE_UNKNOWN, + "value_1": 100.0, + "value_9": float(self.mean_binary), + "unit": "%", + }, + { + "source_sensor_domain": "binary_sensor", + "name": "count", + "value_0": 0, + "value_1": 1, + "value_9": len(self.values_binary), + "unit": None, + }, + { + "source_sensor_domain": "binary_sensor", + "name": "mean", + "value_0": STATE_UNKNOWN, + "value_1": 100.0, + "value_9": float(self.mean_binary), + "unit": "%", + }, ) sensors_config = [] for characteristic in characteristics: sensors_config.append( { "platform": "statistics", - "name": "test_" + characteristic["name"], - "entity_id": "sensor.test_monitored", + "name": f"test_{characteristic['source_sensor_domain']}_{characteristic['name']}", + "entity_id": f"{characteristic['source_sensor_domain']}.test_monitored", "state_characteristic": characteristic["name"], "max_age": {"minutes": 10}, } @@ -707,20 +828,29 @@ class TestStatisticsSensor(unittest.TestCase): # With all values in buffer - for value in self.values: + for i in range(len(self.values)): self.hass.states.set( "sensor.test_monitored", - value, + self.values[i], + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.states.set( + "binary_sensor.test_monitored", + self.values_binary[i], {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) self.hass.block_till_done() mock_data["return_time"] += timedelta(minutes=value_spacing_minutes) for characteristic in characteristics: - state = self.hass.states.get("sensor.test_" + characteristic["name"]) + state = self.hass.states.get( + f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" + ) assert state.state == str(characteristic["value_9"]), ( - f"value mismatch for characteristic '{characteristic['name']}' (buffer filled) " - f"- assert {state.state} == {str(characteristic['value_9'])}" + f"value mismatch for characteristic " + f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " + f"(buffer filled) - " + f"assert {state.state} == {str(characteristic['value_9'])}" ) assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -734,10 +864,14 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() for characteristic in characteristics: - state = self.hass.states.get("sensor.test_" + characteristic["name"]) + state = self.hass.states.get( + f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" + ) assert state.state == str(characteristic["value_0"]), ( - f"value mismatch for characteristic '{characteristic['name']}' (buffer empty) " - f"- assert {state.state} == {str(characteristic['value_0'])}" + f"value mismatch for characteristic " + f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " + f"(buffer empty) - " + f"assert {state.state} == {str(characteristic['value_0'])}" ) # With single value in buffer @@ -747,15 +881,65 @@ class TestStatisticsSensor(unittest.TestCase): self.values[0], {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, ) - self.hass.block_till_done() + self.hass.states.set( + "binary_sensor.test_monitored", + self.values_binary[0], + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + force_update=True, + ) mock_data["return_time"] += timedelta(minutes=1) + fire_time_changed(self.hass, mock_data["return_time"]) + self.hass.block_till_done() for characteristic in characteristics: - state = self.hass.states.get("sensor.test_" + characteristic["name"]) - assert state.state == str(characteristic["value_1"]), ( - f"value mismatch for characteristic '{characteristic['name']}' (one stored value) " - f"- assert {state.state} == {str(characteristic['value_1'])}" + state = self.hass.states.get( + f"sensor.test_{characteristic['source_sensor_domain']}_{characteristic['name']}" ) + assert state.state == str(characteristic["value_1"]), ( + f"value mismatch for characteristic " + f"'{characteristic['source_sensor_domain']}/{characteristic['name']}' " + f"(one stored value) - " + f"assert {state.state} == {str(characteristic['value_1'])}" + ) + + def test_invalid_state_characteristic(self): + """Test the detection of wrong state_characteristics selected.""" + assert setup_component( + self.hass, + "sensor", + { + "sensor": [ + { + "platform": "statistics", + "name": "test_numeric", + "entity_id": "sensor.test_monitored", + "state_characteristic": "invalid", + }, + { + "platform": "statistics", + "name": "test_binary", + "entity_id": "binary_sensor.test_monitored", + "state_characteristic": "variance", + }, + ] + }, + ) + + self.hass.block_till_done() + self.hass.start() + self.hass.block_till_done() + + self.hass.states.set( + "sensor.test_monitored", + self.values[0], + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test_numeric") + assert state is None + state = self.hass.states.get("sensor.test_binary") + assert state is None def test_initialize_from_database(self): """Test initializing the statistics from the database.""" @@ -784,6 +968,7 @@ class TestStatisticsSensor(unittest.TestCase): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "sampling_size": 100, }, ] @@ -886,6 +1071,7 @@ async def test_reload(hass): "platform": "statistics", "name": "test", "entity_id": "sensor.test_monitored", + "state_characteristic": "mean", "sampling_size": 100, }, ]