Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Have statistics functions return a meaningful, non-none result even if only one value is available #127305

Merged
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
22 changes: 20 additions & 2 deletions homeassistant/components/statistics/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,8 @@ def _callable_characteristic_fn(
# Statistics for numeric sensor

def _stat_average_linear(self) -> StateType:
if len(self.states) == 1:
return self.states[0]
if len(self.states) >= 2:
area: float = 0
for i in range(1, len(self.states)):
Expand All @@ -748,6 +750,8 @@ def _stat_average_linear(self) -> StateType:
return None

def _stat_average_step(self) -> StateType:
if len(self.states) == 1:
return self.states[0]
if len(self.states) >= 2:
area: float = 0
for i in range(1, len(self.states)):
Expand Down Expand Up @@ -803,12 +807,12 @@ def _stat_datetime_value_min(self) -> datetime | None:
return None

def _stat_distance_95_percent_of_values(self) -> StateType:
if len(self.states) >= 2:
if len(self.states) >= 1:
return 2 * 1.96 * cast(float, self._stat_standard_deviation())
return None

def _stat_distance_99_percent_of_values(self) -> StateType:
if len(self.states) >= 2:
if len(self.states) >= 1:
return 2 * 2.58 * cast(float, self._stat_standard_deviation())
return None

Expand All @@ -835,17 +839,23 @@ def _stat_median(self) -> StateType:
return None

def _stat_noisiness(self) -> StateType:
if len(self.states) == 1:
return 0.0
if len(self.states) >= 2:
return cast(float, self._stat_sum_differences()) / (len(self.states) - 1)
return None

def _stat_percentile(self) -> StateType:
if len(self.states) == 1:
return self.states[0]
if len(self.states) >= 2:
percentiles = statistics.quantiles(self.states, n=100, method="exclusive")
return percentiles[self._percentile - 1]
return None

def _stat_standard_deviation(self) -> StateType:
if len(self.states) == 1:
return 0.0
if len(self.states) >= 2:
return statistics.stdev(self.states)
return None
Expand All @@ -856,6 +866,8 @@ def _stat_sum(self) -> StateType:
return None

def _stat_sum_differences(self) -> StateType:
if len(self.states) == 1:
return 0.0
if len(self.states) >= 2:
return sum(
abs(j - i)
Expand All @@ -864,6 +876,8 @@ def _stat_sum_differences(self) -> StateType:
return None

def _stat_sum_differences_nonnegative(self) -> StateType:
if len(self.states) == 1:
return 0.0
if len(self.states) >= 2:
return sum(
(j - i if j >= i else j - 0)
Expand All @@ -885,13 +899,17 @@ def _stat_value_min(self) -> StateType:
return None

def _stat_variance(self) -> StateType:
if len(self.states) == 1:
return 0.0
if len(self.states) >= 2:
return statistics.variance(self.states)
return None

# Statistics for binary sensor

def _stat_binary_average_step(self) -> StateType:
if len(self.states) == 1:
return 100.0 * int(self.states[0] is True)
if len(self.states) >= 2:
on_seconds: float = 0
for i in range(1, len(self.states)):
Expand Down
22 changes: 11 additions & 11 deletions tests/components/statistics/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1013,15 +1013,15 @@ async def test_state_characteristics(hass: HomeAssistant) -> None:
"source_sensor_domain": "sensor",
"name": "average_linear",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 6.0,
"value_9": 10.68,
"unit": "°C",
},
{
"source_sensor_domain": "sensor",
"name": "average_step",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 6.0,
"value_9": 11.36,
"unit": "°C",
},
Expand Down Expand Up @@ -1113,15 +1113,15 @@ async def test_state_characteristics(hass: HomeAssistant) -> None:
"source_sensor_domain": "sensor",
"name": "distance_95_percent_of_values",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 0.0,
"value_9": float(round(2 * 1.96 * statistics.stdev(VALUES_NUMERIC), 2)),
"unit": "°C",
},
{
"source_sensor_domain": "sensor",
"name": "distance_99_percent_of_values",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 0.0,
"value_9": float(round(2 * 2.58 * statistics.stdev(VALUES_NUMERIC), 2)),
"unit": "°C",
},
Expand Down Expand Up @@ -1161,23 +1161,23 @@ async def test_state_characteristics(hass: HomeAssistant) -> None:
"source_sensor_domain": "sensor",
"name": "noisiness",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 0.0,
"value_9": float(round(sum([3, 4.8, 10.2, 1.2, 5.4, 2.5, 7.3, 8]) / 8, 2)),
"unit": "°C",
},
{
"source_sensor_domain": "sensor",
"name": "percentile",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 6.0,
"value_9": 9.2,
"unit": "°C",
},
{
"source_sensor_domain": "sensor",
"name": "standard_deviation",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 0.0,
"value_9": float(round(statistics.stdev(VALUES_NUMERIC), 2)),
"unit": "°C",
},
Expand All @@ -1193,7 +1193,7 @@ async def test_state_characteristics(hass: HomeAssistant) -> None:
"source_sensor_domain": "sensor",
"name": "sum_differences",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 0.0,
"value_9": float(
sum(
[
Expand All @@ -1214,7 +1214,7 @@ async def test_state_characteristics(hass: HomeAssistant) -> None:
"source_sensor_domain": "sensor",
"name": "sum_differences_nonnegative",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 0.0,
"value_9": float(
sum(
[
Expand Down Expand Up @@ -1259,15 +1259,15 @@ async def test_state_characteristics(hass: HomeAssistant) -> None:
"source_sensor_domain": "sensor",
"name": "variance",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 0.0,
"value_9": float(round(statistics.variance(VALUES_NUMERIC), 2)),
"unit": "°C²",
},
{
"source_sensor_domain": "binary_sensor",
"name": "average_step",
"value_0": STATE_UNKNOWN,
"value_1": STATE_UNKNOWN,
"value_1": 100.0,
"value_9": 50.0,
"unit": "%",
},
Expand Down