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
|
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
|
def compile_statistics( # noqa: C901
|
||||||
hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
|
hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
|
||||||
) -> list[StatisticResult]:
|
) -> list[StatisticResult]:
|
||||||
|
@ -424,7 +439,11 @@ def compile_statistics( # noqa: C901
|
||||||
reset = False
|
reset = False
|
||||||
if (
|
if (
|
||||||
state_class != STATE_CLASS_TOTAL_INCREASING
|
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
|
!= old_last_reset
|
||||||
):
|
):
|
||||||
if old_state is None:
|
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(
|
@pytest.mark.parametrize(
|
||||||
"device_class,unit,native_unit,mean,min,max",
|
"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,
|
"unit_of_measurement": unit,
|
||||||
"last_reset": None,
|
"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
|
# Make sure the sequence has consecutive equal states
|
||||||
assert seq[1] == seq[2] == seq[3]
|
assert seq[1] == seq[2] == seq[3]
|
||||||
|
|
||||||
|
# Make sure the first and last state differ
|
||||||
|
assert seq[0] != seq[-1]
|
||||||
|
|
||||||
states = {"sensor.test1": []}
|
states = {"sensor.test1": []}
|
||||||
|
|
||||||
|
# Insert states for a 1st statistics period
|
||||||
one = zero
|
one = zero
|
||||||
for i in range(len(seq)):
|
for i in range(len(seq)):
|
||||||
one = one + timedelta(seconds=5)
|
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(
|
_states = record_meter_state(
|
||||||
hass, one, "sensor.test1", attributes, seq[i : i + 1]
|
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,
|
"max": None,
|
||||||
"mean": None,
|
"mean": None,
|
||||||
"min": 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]),
|
"state": approx(factor * seq[7]),
|
||||||
"sum": approx(factor * (sum(seq) - seq[0])),
|
"sum": approx(factor * (sum(seq) - seq[0])),
|
||||||
"sum_decrease": approx(factor * 0.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 "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"])
|
@pytest.mark.parametrize("state_class", ["measurement"])
|
||||||
|
@ -413,6 +531,8 @@ def test_compile_hourly_sum_statistics_nan_inf_state(
|
||||||
one = zero
|
one = zero
|
||||||
for i in range(len(seq)):
|
for i in range(len(seq)):
|
||||||
one = one + timedelta(seconds=5)
|
one = one + timedelta(seconds=5)
|
||||||
|
attributes = dict(attributes)
|
||||||
|
attributes["last_reset"] = dt_util.as_local(one).isoformat()
|
||||||
_states = record_meter_state(
|
_states = record_meter_state(
|
||||||
hass, one, "sensor.test1", attributes, seq[i : i + 1]
|
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
|
start_meter = start
|
||||||
for j in range(len(seq)):
|
for j in range(len(seq)):
|
||||||
_states = record_meter_state(
|
_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)
|
start_meter = start + timedelta(minutes=1)
|
||||||
states["sensor.test4"] += _states["sensor.test4"]
|
states["sensor.test4"] += _states["sensor.test4"]
|
||||||
|
@ -1955,7 +2079,7 @@ def record_meter_states(hass, zero, entity_id, _attributes, seq):
|
||||||
return four, eight, states
|
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.
|
"""Record test state.
|
||||||
|
|
||||||
We inject a state update for meter sensor.
|
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)
|
wait_recording_done(hass)
|
||||||
return hass.states.get(entity_id)
|
return hass.states.get(entity_id)
|
||||||
|
|
||||||
attributes = dict(_attributes)
|
|
||||||
if "last_reset" in _attributes:
|
|
||||||
attributes["last_reset"] = zero.isoformat()
|
|
||||||
|
|
||||||
states = {entity_id: []}
|
states = {entity_id: []}
|
||||||
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=zero):
|
with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=zero):
|
||||||
states[entity_id].append(set_state(entity_id, seq[0], attributes=attributes))
|
states[entity_id].append(set_state(entity_id, seq[0], attributes=attributes))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue