Skip to content

Commit

Permalink
Handle negative numbers in sensor long term statistics (home-assistan…
Browse files Browse the repository at this point in the history
…t#55708)

* Handle negative numbers in sensor long term statistics

* Use negative states in tests
  • Loading branch information
emontnemery authored Sep 4, 2021
1 parent b3181a0 commit 38d42de
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 56 deletions.
44 changes: 20 additions & 24 deletions homeassistant/components/sensor/recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,6 @@ def _get_entities(hass: HomeAssistant) -> list[tuple[str, str, str | None]]:
return entity_ids


# Faster than try/except
# From https://stackoverflow.com/a/23639915
def _is_number(s: str) -> bool: # pylint: disable=invalid-name
"""Return True if string is a number."""
return s.replace(".", "", 1).isdigit()


def _time_weighted_average(
fstates: list[tuple[float, State]], start: datetime.datetime, end: datetime.datetime
) -> float:
Expand Down Expand Up @@ -190,9 +183,13 @@ def _normalize_states(

if device_class not in UNIT_CONVERSIONS:
# We're not normalizing this device class, return the state as they are
fstates = [
(float(el.state), el) for el in entity_history if _is_number(el.state)
]
fstates = []
for state in entity_history:
try:
fstates.append((float(state.state), state))
except ValueError:
continue

if fstates:
all_units = _get_units(fstates)
if len(all_units) > 1:
Expand Down Expand Up @@ -220,23 +217,22 @@ def _normalize_states(
fstates = []

for state in entity_history:
# Exclude non numerical states from statistics
if not _is_number(state.state):
continue
try:
fstate = float(state.state)
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
# Exclude unsupported units from statistics
if unit not in UNIT_CONVERSIONS[device_class]:
if WARN_UNSUPPORTED_UNIT not in hass.data:
hass.data[WARN_UNSUPPORTED_UNIT] = set()
if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]:
hass.data[WARN_UNSUPPORTED_UNIT].add(entity_id)
_LOGGER.warning("%s has unknown unit %s", entity_id, unit)
continue

fstate = float(state.state)
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
# Exclude unsupported units from statistics
if unit not in UNIT_CONVERSIONS[device_class]:
if WARN_UNSUPPORTED_UNIT not in hass.data:
hass.data[WARN_UNSUPPORTED_UNIT] = set()
if entity_id not in hass.data[WARN_UNSUPPORTED_UNIT]:
hass.data[WARN_UNSUPPORTED_UNIT].add(entity_id)
_LOGGER.warning("%s has unknown unit %s", entity_id, unit)
fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state))
except ValueError:
continue

fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state))

return DEVICE_CLASS_UNITS[device_class], fstates


Expand Down
64 changes: 32 additions & 32 deletions tests/components/sensor/test_recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,18 @@
@pytest.mark.parametrize(
"device_class,unit,native_unit,mean,min,max",
[
(None, "%", "%", 16.440677, 10, 30),
("battery", "%", "%", 16.440677, 10, 30),
("battery", None, None, 16.440677, 10, 30),
("humidity", "%", "%", 16.440677, 10, 30),
("humidity", None, None, 16.440677, 10, 30),
("pressure", "Pa", "Pa", 16.440677, 10, 30),
("pressure", "hPa", "Pa", 1644.0677, 1000, 3000),
("pressure", "mbar", "Pa", 1644.0677, 1000, 3000),
("pressure", "inHg", "Pa", 55674.53, 33863.89, 101591.67),
("pressure", "psi", "Pa", 113354.48, 68947.57, 206842.71),
("temperature", "°C", "°C", 16.440677, 10, 30),
("temperature", "°F", "°C", -8.644068, -12.22222, -1.111111),
(None, "%", "%", 13.050847, -10, 30),
("battery", "%", "%", 13.050847, -10, 30),
("battery", None, None, 13.050847, -10, 30),
("humidity", "%", "%", 13.050847, -10, 30),
("humidity", None, None, 13.050847, -10, 30),
("pressure", "Pa", "Pa", 13.050847, -10, 30),
("pressure", "hPa", "Pa", 1305.0847, -1000, 3000),
("pressure", "mbar", "Pa", 1305.0847, -1000, 3000),
("pressure", "inHg", "Pa", 44195.25, -33863.89, 101591.67),
("pressure", "psi", "Pa", 89982.42, -68947.57, 206842.71),
("temperature", "°C", "°C", 13.050847, -10, 30),
("temperature", "°F", "°C", -10.52731, -23.33333, -1.111111),
],
)
def test_compile_hourly_statistics(
Expand Down Expand Up @@ -155,8 +155,8 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes
{
"statistic_id": "sensor.test1",
"start": process_timestamp_to_utc_isoformat(zero),
"mean": approx(16.440677966101696),
"min": approx(10.0),
"mean": approx(13.050847),
"min": approx(-10.0),
"max": approx(30.0),
"last_reset": None,
"state": None,
Expand All @@ -167,8 +167,8 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes
{
"statistic_id": "sensor.test6",
"start": process_timestamp_to_utc_isoformat(zero),
"mean": approx(16.440677966101696),
"min": approx(10.0),
"mean": approx(13.050847),
"min": approx(-10.0),
"max": approx(30.0),
"last_reset": None,
"state": None,
Expand All @@ -179,8 +179,8 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes
{
"statistic_id": "sensor.test7",
"start": process_timestamp_to_utc_isoformat(zero),
"mean": approx(16.440677966101696),
"min": approx(10.0),
"mean": approx(13.050847),
"min": approx(-10.0),
"max": approx(30.0),
"last_reset": None,
"state": None,
Expand Down Expand Up @@ -988,10 +988,10 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes):
@pytest.mark.parametrize(
"device_class,unit,native_unit,mean,min,max",
[
(None, None, None, 16.440677, 10, 30),
(None, "%", "%", 16.440677, 10, 30),
("battery", "%", "%", 16.440677, 10, 30),
("battery", None, None, 16.440677, 10, 30),
(None, None, None, 13.050847, -10, 30),
(None, "%", "%", 13.050847, -10, 30),
("battery", "%", "%", 13.050847, -10, 30),
("battery", None, None, 13.050847, -10, 30),
],
)
def test_compile_hourly_statistics_changing_units_1(
Expand Down Expand Up @@ -1074,10 +1074,10 @@ def test_compile_hourly_statistics_changing_units_1(
@pytest.mark.parametrize(
"device_class,unit,native_unit,mean,min,max",
[
(None, None, None, 16.440677, 10, 30),
(None, "%", "%", 16.440677, 10, 30),
("battery", "%", "%", 16.440677, 10, 30),
("battery", None, None, 16.440677, 10, 30),
(None, None, None, 13.050847, -10, 30),
(None, "%", "%", 13.050847, -10, 30),
("battery", "%", "%", 13.050847, -10, 30),
("battery", None, None, 13.050847, -10, 30),
],
)
def test_compile_hourly_statistics_changing_units_2(
Expand Down Expand Up @@ -1119,10 +1119,10 @@ def test_compile_hourly_statistics_changing_units_2(
@pytest.mark.parametrize(
"device_class,unit,native_unit,mean,min,max",
[
(None, None, None, 16.440677, 10, 30),
(None, "%", "%", 16.440677, 10, 30),
("battery", "%", "%", 16.440677, 10, 30),
("battery", None, None, 16.440677, 10, 30),
(None, None, None, 13.050847, -10, 30),
(None, "%", "%", 13.050847, -10, 30),
("battery", "%", "%", 13.050847, -10, 30),
("battery", None, None, 13.050847, -10, 30),
],
)
def test_compile_hourly_statistics_changing_units_3(
Expand Down Expand Up @@ -1203,7 +1203,7 @@ def test_compile_hourly_statistics_changing_units_3(
@pytest.mark.parametrize(
"device_class,unit,native_unit,mean,min,max",
[
(None, None, None, 16.440677, 10, 30),
(None, None, None, 13.050847, -10, 30),
],
)
def test_compile_hourly_statistics_changing_statistics(
Expand Down Expand Up @@ -1309,7 +1309,7 @@ def set_state(entity_id, state, **kwargs):

states = {entity_id: []}
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one):
states[entity_id].append(set_state(entity_id, "10", attributes=attributes))
states[entity_id].append(set_state(entity_id, "-10", attributes=attributes))

with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two):
states[entity_id].append(set_state(entity_id, "15", attributes=attributes))
Expand Down

0 comments on commit 38d42de

Please sign in to comment.