Convert last_reset timestamps to UTC (#56561)

* Convert last_reset timestamps to UTC

* Add test

* Apply suggestion from code review
This commit is contained in:
Erik Montnemery 2021-09-24 09:16:50 +02:00 committed by GitHub
parent e62c9d338e
commit 7452998081
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 148 additions and 9 deletions

View file

@ -312,6 +312,21 @@ def _wanted_statistics(
return wanted_statistics
def _last_reset_as_utc_isoformat(
last_reset_s: str | None, entity_id: str
) -> str | None:
"""Parse last_reset and convert it to UTC."""
if last_reset_s is None:
return None
last_reset = dt_util.parse_datetime(last_reset_s)
if last_reset is None:
_LOGGER.warning(
"Ignoring invalid last reset '%s' for %s", last_reset_s, entity_id
)
return None
return dt_util.as_utc(last_reset).isoformat()
def compile_statistics( # noqa: C901
hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
) -> list[StatisticResult]:
@ -424,7 +439,11 @@ def compile_statistics( # noqa: C901
reset = False
if (
state_class != STATE_CLASS_TOTAL_INCREASING
and (last_reset := state.attributes.get("last_reset"))
and (
last_reset := _last_reset_as_utc_isoformat(
state.attributes.get("last_reset"), entity_id
)
)
!= old_last_reset
):
if old_state is None:

View file

@ -50,6 +50,16 @@ GAS_SENSOR_ATTRIBUTES = {
}
@pytest.fixture(autouse=True)
def set_time_zone():
"""Set the time zone for the tests."""
# Set our timezone to CST/Regina so we can check calculations
# This keeps UTC-6 all year round
dt_util.set_default_time_zone(dt_util.get_time_zone("America/Regina"))
yield
dt_util.set_default_time_zone(dt_util.get_time_zone("UTC"))
@pytest.mark.parametrize(
"device_class,unit,native_unit,mean,min,max",
[
@ -338,14 +348,121 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change(
"unit_of_measurement": unit,
"last_reset": None,
}
seq = [10, 15, 15, 15, 20, 20, 20, 10]
seq = [10, 15, 15, 15, 20, 20, 20, 25]
# Make sure the sequence has consecutive equal states
assert seq[1] == seq[2] == seq[3]
# Make sure the first and last state differ
assert seq[0] != seq[-1]
states = {"sensor.test1": []}
# Insert states for a 1st statistics period
one = zero
for i in range(len(seq)):
one = one + timedelta(seconds=5)
attributes = dict(attributes)
attributes["last_reset"] = dt_util.as_local(one).isoformat()
_states = record_meter_state(
hass, one, "sensor.test1", attributes, seq[i : i + 1]
)
states["sensor.test1"].extend(_states["sensor.test1"])
# Insert states for a 2nd statistics period
two = zero + timedelta(minutes=5)
for i in range(len(seq)):
two = two + timedelta(seconds=5)
attributes = dict(attributes)
attributes["last_reset"] = dt_util.as_local(two).isoformat()
_states = record_meter_state(
hass, two, "sensor.test1", attributes, seq[i : i + 1]
)
states["sensor.test1"].extend(_states["sensor.test1"])
hist = history.get_significant_states(
hass,
zero - timedelta.resolution,
two + timedelta.resolution,
significant_changes_only=False,
)
assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"]
recorder.do_adhoc_statistics(start=zero)
recorder.do_adhoc_statistics(start=zero + timedelta(minutes=5))
wait_recording_done(hass)
statistic_ids = list_statistic_ids(hass)
assert statistic_ids == [
{"statistic_id": "sensor.test1", "unit_of_measurement": native_unit}
]
stats = statistics_during_period(hass, zero, period="5minute")
assert stats == {
"sensor.test1": [
{
"statistic_id": "sensor.test1",
"start": process_timestamp_to_utc_isoformat(zero),
"end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)),
"max": None,
"mean": None,
"min": None,
"last_reset": process_timestamp_to_utc_isoformat(dt_util.as_local(one)),
"state": approx(factor * seq[7]),
"sum": approx(factor * (sum(seq) - seq[0])),
"sum_decrease": approx(factor * 0.0),
"sum_increase": approx(factor * (sum(seq) - seq[0])),
},
{
"statistic_id": "sensor.test1",
"start": process_timestamp_to_utc_isoformat(
zero + timedelta(minutes=5)
),
"end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=10)),
"max": None,
"mean": None,
"min": None,
"last_reset": process_timestamp_to_utc_isoformat(dt_util.as_local(two)),
"state": approx(factor * seq[7]),
"sum": approx(factor * (2 * sum(seq) - seq[0])),
"sum_decrease": approx(factor * 0.0),
"sum_increase": approx(factor * (2 * sum(seq) - seq[0])),
},
]
}
assert "Error while processing event StatisticsTask" not in caplog.text
@pytest.mark.parametrize("state_class", ["measurement"])
@pytest.mark.parametrize(
"device_class,unit,native_unit,factor",
[
("energy", "kWh", "kWh", 1),
],
)
def test_compile_hourly_sum_statistics_amount_invalid_last_reset(
hass_recorder, caplog, state_class, device_class, unit, native_unit, factor
):
"""Test compiling hourly statistics."""
zero = dt_util.utcnow()
hass = hass_recorder()
recorder = hass.data[DATA_INSTANCE]
setup_component(hass, "sensor", {})
attributes = {
"device_class": device_class,
"state_class": state_class,
"unit_of_measurement": unit,
"last_reset": None,
}
seq = [10, 15, 15, 15, 20, 20, 20, 25]
states = {"sensor.test1": []}
# Insert states
one = zero
for i in range(len(seq)):
one = one + timedelta(seconds=5)
attributes = dict(attributes)
attributes["last_reset"] = dt_util.as_local(one).isoformat()
if i == 3:
attributes["last_reset"] = "festivus" # not a valid time
_states = record_meter_state(
hass, one, "sensor.test1", attributes, seq[i : i + 1]
)
@ -375,7 +492,7 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change(
"max": None,
"mean": None,
"min": None,
"last_reset": process_timestamp_to_utc_isoformat(one),
"last_reset": process_timestamp_to_utc_isoformat(dt_util.as_local(one)),
"state": approx(factor * seq[7]),
"sum": approx(factor * (sum(seq) - seq[0])),
"sum_decrease": approx(factor * 0.0),
@ -384,6 +501,7 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change(
]
}
assert "Error while processing event StatisticsTask" not in caplog.text
assert "Ignoring invalid last reset 'festivus' for sensor.test1" in caplog.text
@pytest.mark.parametrize("state_class", ["measurement"])
@ -413,6 +531,8 @@ def test_compile_hourly_sum_statistics_nan_inf_state(
one = zero
for i in range(len(seq)):
one = one + timedelta(seconds=5)
attributes = dict(attributes)
attributes["last_reset"] = dt_util.as_local(one).isoformat()
_states = record_meter_state(
hass, one, "sensor.test1", attributes, seq[i : i + 1]
)
@ -1685,7 +1805,11 @@ def test_compile_statistics_hourly_summary(hass_recorder, caplog):
start_meter = start
for j in range(len(seq)):
_states = record_meter_state(
hass, start_meter, "sensor.test4", sum_attributes, seq[j : j + 1]
hass,
start_meter,
"sensor.test4",
sum_attributes,
seq[j : j + 1],
)
start_meter = start + timedelta(minutes=1)
states["sensor.test4"] += _states["sensor.test4"]
@ -1955,7 +2079,7 @@ def record_meter_states(hass, zero, entity_id, _attributes, seq):
return four, eight, states
def record_meter_state(hass, zero, entity_id, _attributes, seq):
def record_meter_state(hass, zero, entity_id, attributes, seq):
"""Record test state.
We inject a state update for meter sensor.
@ -1967,10 +2091,6 @@ def record_meter_state(hass, zero, entity_id, _attributes, seq):
wait_recording_done(hass)
return hass.states.get(entity_id)
attributes = dict(_attributes)
if "last_reset" in _attributes:
attributes["last_reset"] = zero.isoformat()
states = {entity_id: []}
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=zero):
states[entity_id].append(set_state(entity_id, seq[0], attributes=attributes))