Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions homeassistant/components/statistics/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@
STAT_CHANGE_SAMPLE = "change_sample"
STAT_CHANGE_SECOND = "change_second"
STAT_COUNT = "count"
STAT_COUNT_BINARY_ON = "count_on"
STAT_COUNT_BINARY_OFF = "count_off"
STAT_DATETIME_NEWEST = "datetime_newest"
STAT_DATETIME_OLDEST = "datetime_oldest"
STAT_DATETIME_VALUE_MAX = "datetime_value_max"
STAT_DATETIME_VALUE_MIN = "datetime_value_min"
STAT_DISTANCE_95P = "distance_95_percent_of_values"
STAT_DISTANCE_99P = "distance_99_percent_of_values"
STAT_DISTANCE_ABSOLUTE = "distance_absolute"
Expand Down Expand Up @@ -99,6 +103,8 @@
STAT_COUNT,
STAT_DATETIME_NEWEST,
STAT_DATETIME_OLDEST,
STAT_DATETIME_VALUE_MAX,
STAT_DATETIME_VALUE_MIN,
STAT_DISTANCE_95P,
STAT_DISTANCE_99P,
STAT_DISTANCE_ABSOLUTE,
Expand All @@ -118,18 +124,26 @@
STAT_AVERAGE_STEP,
STAT_AVERAGE_TIMELESS,
STAT_COUNT,
STAT_COUNT_BINARY_ON,
STAT_COUNT_BINARY_OFF,
STAT_DATETIME_NEWEST,
STAT_DATETIME_OLDEST,
STAT_MEAN,
}

STATS_NOT_A_NUMBER = {
STAT_DATETIME_NEWEST,
STAT_DATETIME_OLDEST,
STAT_DATETIME_VALUE_MAX,
STAT_DATETIME_VALUE_MIN,
STAT_QUANTILES,
}

STATS_DATETIME = {
STAT_DATETIME_NEWEST,
STAT_DATETIME_OLDEST,
STAT_DATETIME_VALUE_MAX,
STAT_DATETIME_VALUE_MIN,
}

# Statistics which retain the unit of the source entity
Expand Down Expand Up @@ -351,7 +365,7 @@ def _add_state_to_queue(self, new_state: State) -> None:
except ValueError:
self.attributes[STAT_SOURCE_VALUE_VALID] = False
_LOGGER.error(
"%s: parsing error, expected number and received %s",
"%s: parsing error. Expected number or binary state, but received '%s'",
self.entity_id,
new_state.state,
)
Expand All @@ -370,7 +384,11 @@ def _derive_unit_of_measurement(self, new_state: State) -> str | None:
unit = base_unit
elif self._state_characteristic in STATS_NOT_A_NUMBER:
unit = None
elif self._state_characteristic == STAT_COUNT:
elif self._state_characteristic in (
STAT_COUNT,
STAT_COUNT_BINARY_ON,
STAT_COUNT_BINARY_OFF,
):
unit = None
elif self._state_characteristic == STAT_VARIANCE:
unit = base_unit + "²"
Expand Down Expand Up @@ -614,6 +632,16 @@ def _stat_datetime_oldest(self) -> datetime | None:
return self.ages[0]
return None

def _stat_datetime_value_max(self) -> datetime | None:
if len(self.states) > 0:
return self.ages[self.states.index(max(self.states))]
return None

def _stat_datetime_value_min(self) -> datetime | None:
if len(self.states) > 0:
return self.ages[self.states.index(min(self.states))]
return None

def _stat_distance_95_percent_of_values(self) -> StateType:
if len(self.states) >= 2:
return 2 * 1.96 * cast(float, self._stat_standard_deviation())
Expand Down Expand Up @@ -704,6 +732,18 @@ def _stat_binary_average_timeless(self) -> StateType:
def _stat_binary_count(self) -> StateType:
return len(self.states)

def _stat_binary_count_on(self) -> StateType:
return self.states.count(True)

def _stat_binary_count_off(self) -> StateType:
return self.states.count(False)

def _stat_binary_datetime_newest(self) -> datetime | None:
return self._stat_datetime_newest()

def _stat_binary_datetime_oldest(self) -> datetime | None:
return self._stat_datetime_oldest()

def _stat_binary_mean(self) -> StateType:
if len(self.states) > 0:
return 100.0 / len(self.states) * self.states.count(True)
Expand Down
48 changes: 48 additions & 0 deletions tests/components/statistics/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,22 @@ def mock_now():
"value_9": (start_datetime + timedelta(minutes=1)).isoformat(),
"unit": None,
},
{
"source_sensor_domain": "sensor",
"name": "datetime_value_max",
"value_0": STATE_UNKNOWN,
"value_1": (start_datetime + timedelta(minutes=9)).isoformat(),
"value_9": (start_datetime + timedelta(minutes=2)).isoformat(),
"unit": None,
},
{
"source_sensor_domain": "sensor",
"name": "datetime_value_min",
"value_0": STATE_UNKNOWN,
"value_1": (start_datetime + timedelta(minutes=9)).isoformat(),
"value_9": (start_datetime + timedelta(minutes=5)).isoformat(),
"unit": None,
},
{
"source_sensor_domain": "sensor",
"name": "distance_95_percent_of_values",
Expand Down Expand Up @@ -811,6 +827,38 @@ def mock_now():
"value_9": len(VALUES_BINARY),
"unit": None,
},
{
"source_sensor_domain": "binary_sensor",
"name": "count_on",
"value_0": 0,
"value_1": 1,
"value_9": VALUES_BINARY.count("on"),
"unit": None,
},
{
"source_sensor_domain": "binary_sensor",
"name": "count_off",
"value_0": 0,
"value_1": 0,
"value_9": VALUES_BINARY.count("off"),
"unit": None,
},
{
"source_sensor_domain": "binary_sensor",
"name": "datetime_newest",
"value_0": STATE_UNKNOWN,
"value_1": (start_datetime + timedelta(minutes=9)).isoformat(),
"value_9": (start_datetime + timedelta(minutes=9)).isoformat(),
"unit": None,
},
{
"source_sensor_domain": "binary_sensor",
"name": "datetime_oldest",
"value_0": STATE_UNKNOWN,
"value_1": (start_datetime + timedelta(minutes=9)).isoformat(),
"value_9": (start_datetime + timedelta(minutes=1)).isoformat(),
"unit": None,
},
{
"source_sensor_domain": "binary_sensor",
"name": "mean",
Expand Down