Skip to content

Commit

Permalink
Add binary characteristics, add deprecation warning for optional stat…
Browse files Browse the repository at this point in the history
…e_characteristic parameter (home-assistant#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
  • Loading branch information
ThomDietrich authored and pull[bot] committed Jan 4, 2024
1 parent daebf0e commit 1753456
Show file tree
Hide file tree
Showing 3 changed files with 329 additions and 74 deletions.
96 changes: 82 additions & 14 deletions homeassistant/components/statistics/sensor.py
Expand Up @@ -60,12 +60,30 @@
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"
Expand All @@ -80,11 +98,24 @@
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,
Expand All @@ -107,6 +138,7 @@
STAT_VALUE_MAX,
STAT_VALUE_MIN,
STAT_VARIANCE,
STAT_DEFAULT,
]
),
vol.Optional(
Expand All @@ -122,6 +154,10 @@
),
}
)
PLATFORM_SCHEMA = vol.All(
_PLATFORM_SCHEMA_BASE,
valid_binary_characteristic_configuration,
)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
Expand Down Expand Up @@ -166,6 +202,9 @@ def __init__(
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
Expand All @@ -181,9 +220,15 @@ def __init__(
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

Expand Down Expand Up @@ -246,9 +291,13 @@ def _add_state_to_queue(self, new_state):

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,
Expand Down Expand Up @@ -290,8 +339,6 @@ def name(self):
@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
Expand Down Expand Up @@ -450,10 +497,6 @@ def _update_value(self):
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:
Expand All @@ -463,6 +506,8 @@ def _update_value(self):
value = int(value)
self._value = value

# Statistics for numeric sensor

def _stat_average_linear(self):
if len(self.states) >= 2:
area = 0
Expand Down Expand Up @@ -590,3 +635,26 @@ def _stat_variance(self):
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
1 change: 1 addition & 0 deletions tests/components/statistics/fixtures/configuration.yaml
Expand Up @@ -2,3 +2,4 @@ sensor:
- platform: statistics
entity_id: sensor.cpu
name: cputest
state_characteristic: mean

0 comments on commit 1753456

Please sign in to comment.