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

@ -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