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:
parent
e62c9d338e
commit
7452998081
2 changed files with 148 additions and 9 deletions
|
@ -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:
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Reference in a new issue