diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index e0f733fbfa0..4756ac13ce3 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -65,7 +65,7 @@ class Events(Base): # type: ignore return Events( event_type=event.event_type, event_data=event_data or json.dumps(event.data, cls=JSONEncoder), - origin=str(event.origin), + origin=str(event.origin.value), time_fired=event.time_fired, context_id=event.context.id, context_user_id=event.context.user_id, diff --git a/homeassistant/core.py b/homeassistant/core.py index 82fbe1be2b6..9f598d46410 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -21,6 +21,7 @@ from typing import ( Any, Awaitable, Callable, + Collection, Coroutine, Dict, Iterable, @@ -561,8 +562,8 @@ class Event: return { "event_type": self.event_type, "data": dict(self.data), - "origin": str(self.origin), - "time_fired": self.time_fired, + "origin": str(self.origin.value), + "time_fired": self.time_fired.isoformat(), "context": self.context.as_dict(), } @@ -626,6 +627,7 @@ class EventBus: event_data: Optional[Dict] = None, origin: EventOrigin = EventOrigin.local, context: Optional[Context] = None, + time_fired: Optional[datetime.datetime] = None, ) -> None: """Fire an event. @@ -638,7 +640,7 @@ class EventBus: if match_all_listeners is not None and event_type != EVENT_HOMEASSISTANT_CLOSE: listeners = match_all_listeners + listeners - event = Event(event_type, event_data, origin, None, context) + event = Event(event_type, event_data, origin, time_fired, context) if event_type != EVENT_TIME_CHANGED: _LOGGER.debug("Bus:Handling %s", event) @@ -771,6 +773,7 @@ class State: "context", "domain", "object_id", + "_as_dict", ] def __init__( @@ -805,6 +808,7 @@ class State: self.last_changed = last_changed or self.last_updated self.context = context or Context() self.domain, self.object_id = split_entity_id(self.entity_id) + self._as_dict: Optional[Dict[str, Collection[Any]]] = None @property def name(self) -> str: @@ -821,14 +825,21 @@ class State: To be used for JSON serialization. Ensures: state == State.from_dict(state.as_dict()) """ - return { - "entity_id": self.entity_id, - "state": self.state, - "attributes": dict(self.attributes), - "last_changed": self.last_changed, - "last_updated": self.last_updated, - "context": self.context.as_dict(), - } + if not self._as_dict: + last_changed_isoformat = self.last_changed.isoformat() + if self.last_changed == self.last_updated: + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = self.last_updated.isoformat() + self._as_dict = { + "entity_id": self.entity_id, + "state": self.state, + "attributes": dict(self.attributes), + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + "context": self.context.as_dict(), + } + return self._as_dict @classmethod def from_dict(cls, json_dict: Dict) -> Any: @@ -1643,13 +1654,18 @@ def _async_create_timer(hass: HomeAssistant) -> None: """Fire next time event.""" now = dt_util.utcnow() - hass.bus.async_fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}, context=timer_context) + hass.bus.async_fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now}, time_fired=now, context=timer_context + ) # If we are more than a second late, a tick was missed late = monotonic() - target if late > 1: hass.bus.async_fire( - EVENT_TIMER_OUT_OF_SYNC, {ATTR_SECONDS: late}, context=timer_context + EVENT_TIMER_OUT_OF_SYNC, + {ATTR_SECONDS: late}, + time_fired=now, + context=timer_context, ) schedule_tick(now) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 3969ff90706..e05108c46bc 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -201,10 +201,7 @@ async def test_get_states(hass, websocket_client): states = [] for state in hass.states.async_all(): - state = state.as_dict() - state["last_changed"] = state["last_changed"].isoformat() - state["last_updated"] = state["last_updated"].isoformat() - states.append(state) + states.append(state.as_dict()) assert msg["result"] == states diff --git a/tests/test_core.py b/tests/test_core.py index 33cb0a37e23..1ae1f32a10a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -291,7 +291,7 @@ def test_event_repr(): def test_event_as_dict(): - """Test as Event as dictionary.""" + """Test an Event as dictionary.""" event_type = "some_type" now = dt_util.utcnow() data = {"some": "attr"} @@ -301,7 +301,7 @@ def test_event_as_dict(): "event_type": event_type, "data": data, "origin": "LOCAL", - "time_fired": now, + "time_fired": now.isoformat(), "context": { "id": event.context.id, "parent_id": None, @@ -309,6 +309,36 @@ def test_event_as_dict(): }, } assert event.as_dict() == expected + # 2nd time to verify cache + assert event.as_dict() == expected + + +def test_state_as_dict(): + """Test a State as dictionary.""" + last_time = datetime(1984, 12, 8, 12, 0, 0) + state = ha.State( + "happy.happy", + "on", + {"pig": "dog"}, + last_updated=last_time, + last_changed=last_time, + ) + expected = { + "context": { + "id": state.context.id, + "parent_id": None, + "user_id": state.context.user_id, + }, + "entity_id": "happy.happy", + "attributes": {"pig": "dog"}, + "last_changed": last_time.isoformat(), + "last_updated": last_time.isoformat(), + "state": "on", + } + assert state.as_dict() == expected + # 2nd time to verify cache + assert state.as_dict() == expected + assert state.as_dict() is state.as_dict() class TestEventBus(unittest.TestCase):