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
This commit is contained in:
Thomas Dietrich 2021-12-02 09:03:24 +01:00 committed by GitHub
parent 9128dc214c
commit bee3c9102c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 330 additions and 75 deletions

View file

@ -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,
},
]