Skip to content

Commit 1743766

Browse files
authored
Add last_reported to state reported event data (home-assistant#148932)
1 parent 277241c commit 1743766

File tree

4 files changed

+110
-50
lines changed

4 files changed

+110
-50
lines changed

homeassistant/components/derivative/sensor.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,12 @@ def on_state_reported(event: Event[EventStateReportedData]) -> None:
320320
# changed state, then we know it will still be zero.
321321
return
322322
schedule_max_sub_interval_exceeded(new_state)
323-
calc_derivative(new_state, new_state.state, event.data["old_last_reported"])
323+
calc_derivative(
324+
new_state,
325+
new_state.state,
326+
event.data["last_reported"],
327+
event.data["old_last_reported"],
328+
)
324329

325330
@callback
326331
def on_state_changed(event: Event[EventStateChangedData]) -> None:
@@ -334,19 +339,27 @@ def on_state_changed(event: Event[EventStateChangedData]) -> None:
334339
schedule_max_sub_interval_exceeded(new_state)
335340
old_state = event.data["old_state"]
336341
if old_state is not None:
337-
calc_derivative(new_state, old_state.state, old_state.last_reported)
342+
calc_derivative(
343+
new_state,
344+
old_state.state,
345+
new_state.last_updated,
346+
old_state.last_reported,
347+
)
338348
else:
339349
# On first state change from none, update availability
340350
self.async_write_ha_state()
341351

342352
def calc_derivative(
343-
new_state: State, old_value: str, old_last_reported: datetime
353+
new_state: State,
354+
old_value: str,
355+
new_timestamp: datetime,
356+
old_timestamp: datetime,
344357
) -> None:
345358
"""Handle the sensor state changes."""
346359
if not _is_decimal_state(old_value):
347360
if self._last_valid_state_time:
348361
old_value = self._last_valid_state_time[0]
349-
old_last_reported = self._last_valid_state_time[1]
362+
old_timestamp = self._last_valid_state_time[1]
350363
else:
351364
# Sensor becomes valid for the first time, just keep the restored value
352365
self.async_write_ha_state()
@@ -358,12 +371,10 @@ def calc_derivative(
358371
"" if unit is None else unit
359372
)
360373

361-
self._prune_state_list(new_state.last_reported)
374+
self._prune_state_list(new_timestamp)
362375

363376
try:
364-
elapsed_time = (
365-
new_state.last_reported - old_last_reported
366-
).total_seconds()
377+
elapsed_time = (new_timestamp - old_timestamp).total_seconds()
367378
delta_value = Decimal(new_state.state) - Decimal(old_value)
368379
new_derivative = (
369380
delta_value
@@ -392,22 +403,18 @@ def calc_derivative(
392403
return
393404

394405
# add latest derivative to the window list
395-
self._state_list.append(
396-
(old_last_reported, new_state.last_reported, new_derivative)
397-
)
406+
self._state_list.append((old_timestamp, new_timestamp, new_derivative))
398407
self._last_valid_state_time = (
399408
new_state.state,
400-
new_state.last_reported,
409+
new_timestamp,
401410
)
402411

403412
# If outside of time window just report derivative (is the same as modeling it in the window),
404413
# otherwise take the weighted average with the previous derivatives
405414
if elapsed_time > self._time_window:
406415
derivative = new_derivative
407416
else:
408-
derivative = self._calc_derivative_from_state_list(
409-
new_state.last_reported
410-
)
417+
derivative = self._calc_derivative_from_state_list(new_timestamp)
411418
self._write_native_value(derivative)
412419

413420
source_state = self.hass.states.get(self._sensor_source_id)

homeassistant/components/integration/sensor.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ def _integrate_on_state_change_with_max_sub_interval(
463463
) -> None:
464464
"""Handle sensor state update when sub interval is configured."""
465465
self._integrate_on_state_update_with_max_sub_interval(
466-
None, event.data["old_state"], event.data["new_state"]
466+
None, None, event.data["old_state"], event.data["new_state"]
467467
)
468468

469469
@callback
@@ -472,13 +472,17 @@ def _integrate_on_state_report_with_max_sub_interval(
472472
) -> None:
473473
"""Handle sensor state report when sub interval is configured."""
474474
self._integrate_on_state_update_with_max_sub_interval(
475-
event.data["old_last_reported"], None, event.data["new_state"]
475+
event.data["old_last_reported"],
476+
event.data["last_reported"],
477+
None,
478+
event.data["new_state"],
476479
)
477480

478481
@callback
479482
def _integrate_on_state_update_with_max_sub_interval(
480483
self,
481-
old_last_reported: datetime | None,
484+
old_timestamp: datetime | None,
485+
new_timestamp: datetime | None,
482486
old_state: State | None,
483487
new_state: State | None,
484488
) -> None:
@@ -489,7 +493,9 @@ def _integrate_on_state_update_with_max_sub_interval(
489493
"""
490494
self._cancel_max_sub_interval_exceeded_callback()
491495
try:
492-
self._integrate_on_state_change(old_last_reported, old_state, new_state)
496+
self._integrate_on_state_change(
497+
old_timestamp, new_timestamp, old_state, new_state
498+
)
493499
self._last_integration_trigger = _IntegrationTrigger.StateEvent
494500
self._last_integration_time = datetime.now(tz=UTC)
495501
finally:
@@ -503,7 +509,7 @@ def _integrate_on_state_change_callback(
503509
) -> None:
504510
"""Handle sensor state change."""
505511
return self._integrate_on_state_change(
506-
None, event.data["old_state"], event.data["new_state"]
512+
None, None, event.data["old_state"], event.data["new_state"]
507513
)
508514

509515
@callback
@@ -512,12 +518,16 @@ def _integrate_on_state_report_callback(
512518
) -> None:
513519
"""Handle sensor state report."""
514520
return self._integrate_on_state_change(
515-
event.data["old_last_reported"], None, event.data["new_state"]
521+
event.data["old_last_reported"],
522+
event.data["last_reported"],
523+
None,
524+
event.data["new_state"],
516525
)
517526

518527
def _integrate_on_state_change(
519528
self,
520-
old_last_reported: datetime | None,
529+
old_timestamp: datetime | None,
530+
new_timestamp: datetime | None,
521531
old_state: State | None,
522532
new_state: State | None,
523533
) -> None:
@@ -531,16 +541,17 @@ def _integrate_on_state_change(
531541

532542
if old_state:
533543
# state has changed, we recover old_state from the event
544+
new_timestamp = new_state.last_updated
534545
old_state_state = old_state.state
535-
old_last_reported = old_state.last_reported
546+
old_timestamp = old_state.last_reported
536547
else:
537-
# event state reported without any state change
548+
# first state or event state reported without any state change
538549
old_state_state = new_state.state
539550

540551
self._attr_available = True
541552
self._derive_and_set_attributes_from_state(new_state)
542553

543-
if old_last_reported is None and old_state is None:
554+
if old_timestamp is None and old_state is None:
544555
self.async_write_ha_state()
545556
return
546557

@@ -551,11 +562,12 @@ def _integrate_on_state_change(
551562
return
552563

553564
if TYPE_CHECKING:
554-
assert old_last_reported is not None
565+
assert new_timestamp is not None
566+
assert old_timestamp is not None
555567
elapsed_seconds = Decimal(
556-
(new_state.last_reported - old_last_reported).total_seconds()
568+
(new_timestamp - old_timestamp).total_seconds()
557569
if self._last_integration_trigger == _IntegrationTrigger.StateEvent
558-
else (new_state.last_reported - self._last_integration_time).total_seconds()
570+
else (new_timestamp - self._last_integration_time).total_seconds()
559571
)
560572

561573
area = self._method.calculate_area_with_two_states(elapsed_seconds, *states)

homeassistant/components/statistics/sensor.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -727,12 +727,11 @@ async def async_start_preview(
727727

728728
def _async_handle_new_state(
729729
self,
730-
reported_state: State | None,
730+
reported_state: State,
731+
timestamp: float,
731732
) -> None:
732733
"""Handle the sensor state changes."""
733-
if (new_state := reported_state) is None:
734-
return
735-
self._add_state_to_queue(new_state)
734+
self._add_state_to_queue(reported_state, timestamp)
736735
self._async_purge_update_and_schedule()
737736

738737
if self._preview_callback:
@@ -747,14 +746,18 @@ def _async_stats_sensor_state_change_listener(
747746
self,
748747
event: Event[EventStateChangedData],
749748
) -> None:
750-
self._async_handle_new_state(event.data["new_state"])
749+
if (new_state := event.data["new_state"]) is None:
750+
return
751+
self._async_handle_new_state(new_state, new_state.last_updated_timestamp)
751752

752753
@callback
753754
def _async_stats_sensor_state_report_listener(
754755
self,
755756
event: Event[EventStateReportedData],
756757
) -> None:
757-
self._async_handle_new_state(event.data["new_state"])
758+
self._async_handle_new_state(
759+
event.data["new_state"], event.data["last_reported"].timestamp()
760+
)
758761

759762
async def _async_stats_sensor_startup(self) -> None:
760763
"""Add listener and get recorded state.
@@ -785,7 +788,9 @@ async def async_added_to_hass(self) -> None:
785788
"""Register callbacks."""
786789
await self._async_stats_sensor_startup()
787790

788-
def _add_state_to_queue(self, new_state: State) -> None:
791+
def _add_state_to_queue(
792+
self, new_state: State, last_reported_timestamp: float
793+
) -> None:
789794
"""Add the state to the queue."""
790795

791796
# Attention: it is not safe to store the new_state object,
@@ -805,7 +810,7 @@ def _add_state_to_queue(self, new_state: State) -> None:
805810
self.states.append(new_state.state == "on")
806811
else:
807812
self.states.append(float(new_state.state))
808-
self.ages.append(new_state.last_reported_timestamp)
813+
self.ages.append(last_reported_timestamp)
809814
self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = True
810815
except ValueError:
811816
self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = False
@@ -1062,7 +1067,7 @@ async def _initialize_from_database(self) -> None:
10621067
self._fetch_states_from_database
10631068
):
10641069
for state in reversed(states):
1065-
self._add_state_to_queue(state)
1070+
self._add_state_to_queue(state, state.last_reported_timestamp)
10661071
self._calculate_state_attributes(state)
10671072
self._async_purge_update_and_schedule()
10681073

homeassistant/core.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ class EventStateEventData(TypedDict):
157157
"""Base class for EVENT_STATE_CHANGED and EVENT_STATE_REPORTED data."""
158158

159159
entity_id: str
160-
new_state: State | None
161160

162161

163162
class EventStateChangedData(EventStateEventData):
@@ -166,6 +165,7 @@ class EventStateChangedData(EventStateEventData):
166165
A state changed event is fired when on state write the state is changed.
167166
"""
168167

168+
new_state: State | None
169169
old_state: State | None
170170

171171

@@ -175,6 +175,8 @@ class EventStateReportedData(EventStateEventData):
175175
A state reported event is fired when on state write the state is unchanged.
176176
"""
177177

178+
last_reported: datetime.datetime
179+
new_state: State
178180
old_last_reported: datetime.datetime
179181

180182

@@ -1749,18 +1751,38 @@ class CompressedState(TypedDict):
17491751

17501752

17511753
class State:
1752-
"""Object to represent a state within the state machine.
1753-
1754-
entity_id: the entity that is represented.
1755-
state: the state of the entity
1756-
attributes: extra information on entity and state
1757-
last_changed: last time the state was changed.
1758-
last_reported: last time the state was reported.
1759-
last_updated: last time the state or attributes were changed.
1760-
context: Context in which it was created
1761-
domain: Domain of this state.
1762-
object_id: Object id of this state.
1754+
"""Object to represent a state within the state machine."""
1755+
1756+
entity_id: str
1757+
"""The entity that is represented by the state."""
1758+
domain: str
1759+
"""Domain of the entity that is represented by the state."""
1760+
object_id: str
1761+
"""object_id: Object id of this state."""
1762+
state: str
1763+
"""The state of the entity."""
1764+
attributes: ReadOnlyDict[str, Any]
1765+
"""Extra information on entity and state"""
1766+
last_changed: datetime.datetime
1767+
"""Last time the state was changed."""
1768+
last_reported: datetime.datetime
1769+
"""Last time the state was reported.
1770+
1771+
Note: When the state is set and neither the state nor attributes are
1772+
changed, the existing state will be mutated with an updated last_reported.
1773+
1774+
When handling a state change event, the last_reported attribute of the old
1775+
state will not be modified and can safely be used. The last_reported attribute
1776+
of the new state may be modified and the last_updated attribute should be used
1777+
instead.
1778+
1779+
When handling a state report event, the last_reported attribute may be
1780+
modified and last_reported from the event data should be used instead.
17631781
"""
1782+
last_updated: datetime.datetime
1783+
"""Last time the state or attributes were changed."""
1784+
context: Context
1785+
"""Context in which the state was created."""
17641786

17651787
__slots__ = (
17661788
"_cache",
@@ -1841,7 +1863,20 @@ def last_changed_timestamp(self) -> float:
18411863

18421864
@under_cached_property
18431865
def last_reported_timestamp(self) -> float:
1844-
"""Timestamp of last report."""
1866+
"""Timestamp of last report.
1867+
1868+
Note: When the state is set and neither the state nor attributes are
1869+
changed, the existing state will be mutated with an updated last_reported.
1870+
1871+
When handling a state change event, the last_reported_timestamp attribute
1872+
of the old state will not be modified and can safely be used. The
1873+
last_reported_timestamp attribute of the new state may be modified and the
1874+
last_updated_timestamp attribute should be used instead.
1875+
1876+
When handling a state report event, the last_reported_timestamp attribute may
1877+
be modified and last_reported from the event data should be used instead.
1878+
"""
1879+
18451880
return self.last_reported.timestamp()
18461881

18471882
@under_cached_property
@@ -2340,6 +2375,7 @@ def async_set_internal(
23402375
EVENT_STATE_REPORTED,
23412376
{
23422377
"entity_id": entity_id,
2378+
"last_reported": now,
23432379
"old_last_reported": old_last_reported,
23442380
"new_state": old_state,
23452381
},

0 commit comments

Comments
 (0)