Avoid creating unneeded Context and Event objects when firing events (#113798)

* Avoid creating unneeded Context and Event objects when firing events

* Add test

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Erik Montnemery 2024-03-20 09:40:06 +01:00 committed by GitHub
parent 638020f168
commit d31124d5d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 257 additions and 128 deletions

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from collections.abc import ItemsView
from collections.abc import ItemsView, Mapping
from typing import Any
import voluptuous as vol
@ -101,30 +101,18 @@ async def async_attach_trigger(
job = HassJob(action, f"event trigger {trigger_info}")
@callback
def filter_event(event: Event) -> bool:
def filter_event(event_data: Mapping[str, Any]) -> bool:
"""Filter events."""
try:
# Check that the event data and context match the configured
# schema if one was provided
if event_data_items:
# Fast path for simple items comparison
if not (event.data.items() >= event_data_items):
if not (event_data.items() >= event_data_items):
return False
elif event_data_schema:
# Slow path for schema validation
event_data_schema(event.data)
if event_context_items:
# Fast path for simple items comparison
# This is safe because we do not mutate the event context
# pylint: disable-next=protected-access
if not (event.context._as_dict.items() >= event_context_items):
return False
elif event_context_schema:
# Slow path for schema validation
# This is safe because we make a copy of the event context
# pylint: disable-next=protected-access
event_context_schema(dict(event.context._as_dict))
event_data_schema(event_data)
except vol.Invalid:
# If event doesn't match, skip event
return False
@ -133,6 +121,22 @@ async def async_attach_trigger(
@callback
def handle_event(event: Event) -> None:
"""Listen for events and calls the action when data matches."""
if event_context_items:
# Fast path for simple items comparison
# This is safe because we do not mutate the event context
# pylint: disable-next=protected-access
if not (event.context._as_dict.items() >= event_context_items):
return
elif event_context_schema:
try:
# Slow path for schema validation
# This is safe because we make a copy of the event context
# pylint: disable-next=protected-access
event_context_schema(dict(event.context._as_dict))
except vol.Invalid:
# If event doesn't match, skip event
return
hass.async_run_hass_job(
job,
{
@ -146,9 +150,10 @@ async def async_attach_trigger(
event.context,
)
event_filter = filter_event if event_data_items or event_data_schema else None
removes = [
hass.bus.async_listen(
event_type, handle_event, event_filter=filter_event, run_immediately=True
event_type, handle_event, event_filter=event_filter, run_immediately=True
)
for event_type in event_types
]

View File

@ -1,7 +1,9 @@
"""Publish simple item state changes via MQTT."""
from collections.abc import Mapping
import json
import logging
from typing import Any
import voluptuous as vol
@ -90,9 +92,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@callback
def _ha_started(hass: HomeAssistant) -> None:
@callback
def _event_filter(evt: Event) -> bool:
entity_id: str = evt.data["entity_id"]
new_state: State | None = evt.data["new_state"]
def _event_filter(event_data: Mapping[str, Any]) -> bool:
entity_id: str = event_data["entity_id"]
new_state: State | None = event_data["new_state"]
if new_state is None:
return False
if not publish_filter(entity_id):

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from collections.abc import Callable
from collections.abc import Callable, Mapping
import logging
from typing import Any, Self
@ -248,11 +248,11 @@ class PersonStorageCollection(collection.DictStorageCollection):
)
@callback
def _entity_registry_filter(self, event: Event) -> bool:
def _entity_registry_filter(self, event_data: Mapping[str, Any]) -> bool:
"""Filter entity registry events."""
return (
event.data["action"] == "remove"
and split_entity_id(event.data[ATTR_ENTITY_ID])[0] == "device_tracker"
event_data["action"] == "remove"
and split_entity_id(event_data[ATTR_ENTITY_ID])[0] == "device_tracker"
)
async def _entity_registry_updated(self, event: Event) -> None:

View File

@ -321,7 +321,7 @@ class Events(Base):
EventOrigin(self.origin)
if self.origin
else EVENT_ORIGIN_ORDER[self.origin_idx or 0],
dt_util.utc_from_timestamp(self.time_fired_ts or 0),
self.time_fired_ts or 0,
context=context,
)
except JSON_DECODE_EXCEPTIONS:

View File

@ -1,6 +1,8 @@
"""Recorder entity registry helper."""
from collections.abc import Mapping
import logging
from typing import Any
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
@ -29,9 +31,9 @@ def async_setup(hass: HomeAssistant) -> None:
)
@callback
def entity_registry_changed_filter(event: Event) -> bool:
def entity_registry_changed_filter(event_data: Mapping[str, Any]) -> bool:
"""Handle entity_id changed filter."""
return event.data["action"] == "update" and "old_entity_id" in event.data
return event_data["action"] == "update" and "old_entity_id" in event_data
@callback
def _setup_entity_registry_event_handler(hass: HomeAssistant) -> None:

View File

@ -1,5 +1,8 @@
"""Provides device automations for Tasmota."""
from collections.abc import Mapping
from typing import Any
from hatasmota.const import AUTOMATION_TYPE_TRIGGER
from hatasmota.models import DiscoveryHashType
from hatasmota.trigger import TasmotaTrigger
@ -27,9 +30,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> N
await async_remove_automations(hass, event.data["device_id"])
@callback
def _async_device_removed_filter(event: Event) -> bool:
def _async_device_removed_filter(event_data: Mapping[str, Any]) -> bool:
"""Filter device registry events."""
return event.data["action"] == "remove"
return event_data["action"] == "remove"
async def async_discover(
tasmota_automation: TasmotaTrigger, discovery_hash: DiscoveryHashType

View File

@ -97,7 +97,7 @@ class VoIPDevices:
self.hass.bus.async_listen(
dr.EVENT_DEVICE_REGISTRY_UPDATED,
async_device_removed,
callback(lambda ev: ev.data.get("action") == "remove"),
callback(lambda event_data: event_data.get("action") == "remove"),
)
)

View File

@ -2519,16 +2519,16 @@ class EntityRegistryDisabledHandler:
@callback
def _handle_entry_updated_filter(event: Event) -> bool:
def _handle_entry_updated_filter(event_data: Mapping[str, Any]) -> bool:
"""Handle entity registry entry update filter.
Only handle changes to "disabled_by".
If "disabled_by" was CONFIG_ENTRY, reload is not needed.
"""
if (
event.data["action"] != "update"
or "disabled_by" not in event.data["changes"]
or event.data["changes"]["disabled_by"]
event_data["action"] != "update"
or "disabled_by" not in event_data["changes"]
or event_data["changes"]["disabled_by"]
is entity_registry.RegistryEntryDisabler.CONFIG_ENTRY
):
return False

View File

@ -67,6 +67,7 @@ from .const import (
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
EVENT_LOGGING_CHANGED,
EVENT_SERVICE_REGISTERED,
EVENT_SERVICE_REMOVED,
EVENT_STATE_CHANGED,
@ -1215,24 +1216,24 @@ class Event(Generic[_DataT]):
event_type: str,
data: _DataT | None = None,
origin: EventOrigin = EventOrigin.local,
time_fired: datetime.datetime | None = None,
time_fired_timestamp: float | None = None,
context: Context | None = None,
) -> None:
"""Initialize a new event."""
self.event_type = event_type
self.data: _DataT = data or {} # type: ignore[assignment]
self.origin = origin
self.time_fired = time_fired or dt_util.utcnow()
self.time_fired_timestamp = time_fired_timestamp or time.time()
if not context:
context = Context(id=ulid_at_time(self.time_fired.timestamp()))
context = Context(id=ulid_at_time(self.time_fired_timestamp))
self.context = context
if not context.origin_event:
context.origin_event = self
@cached_property
def time_fired_timestamp(self) -> float:
def time_fired(self) -> datetime.datetime:
"""Return time fired as a timestamp."""
return self.time_fired.timestamp()
return dt_util.utc_from_timestamp(self.time_fired_timestamp)
@cached_property
def _as_dict(self) -> dict[str, Any]:
@ -1282,18 +1283,22 @@ class Event(Generic[_DataT]):
def __repr__(self) -> str:
"""Return the representation."""
if self.data:
return (
f"<Event {self.event_type}[{str(self.origin)[0]}]:"
f" {util.repr_helper(self.data)}>"
)
return _event_repr(self.event_type, self.origin, self.data)
return f"<Event {self.event_type}[{str(self.origin)[0]}]>"
def _event_repr(
event_type: str, origin: EventOrigin, data: Mapping[str, Any] | None
) -> str:
"""Return the representation."""
if data:
return f"<Event {event_type}[{str(origin)[0]}]: {util.repr_helper(data)}>"
return f"<Event {event_type}[{str(origin)[0]}]>"
_FilterableJobType = tuple[
HassJob[[Event[_DataT]], Coroutine[Any, Any, None] | None], # job
Callable[[Event[_DataT]], bool] | None, # event_filter
Callable[[_DataT], bool] | None, # event_filter
bool, # run_immediately
]
@ -1325,7 +1330,7 @@ class _OneTimeListener:
class EventBus:
"""Allow the firing of and listening for events."""
__slots__ = ("_listeners", "_match_all_listeners", "_hass")
__slots__ = ("_debug", "_hass", "_listeners", "_match_all_listeners")
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize a new event bus."""
@ -1333,6 +1338,15 @@ class EventBus:
self._match_all_listeners: list[_FilterableJobType[Any]] = []
self._listeners[MATCH_ALL] = self._match_all_listeners
self._hass = hass
self._async_logging_changed()
self.async_listen(
EVENT_LOGGING_CHANGED, self._async_logging_changed, run_immediately=True
)
@callback
def _async_logging_changed(self, event: Event | None = None) -> None:
"""Handle logging change."""
self._debug = _LOGGER.isEnabledFor(logging.DEBUG)
@callback
def async_listeners(self) -> dict[str, int]:
@ -1366,7 +1380,7 @@ class EventBus:
event_data: Mapping[str, Any] | None = None,
origin: EventOrigin = EventOrigin.local,
context: Context | None = None,
time_fired: datetime.datetime | None = None,
time_fired: float | None = None,
) -> None:
"""Fire an event.
@ -1376,30 +1390,57 @@ class EventBus:
raise MaxLengthExceeded(
event_type, "event_type", MAX_LENGTH_EVENT_EVENT_TYPE
)
return self._async_fire(event_type, event_data, origin, context, time_fired)
listeners = self._listeners.get(event_type, [])
match_all_listeners = self._match_all_listeners
@callback
def _async_fire(
self,
event_type: str,
event_data: Mapping[str, Any] | None = None,
origin: EventOrigin = EventOrigin.local,
context: Context | None = None,
time_fired: float | None = None,
) -> None:
"""Fire an event.
event = Event(event_type, event_data, origin, time_fired, context)
This method must be run in the event loop.
"""
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug("Bus:Handling %s", event)
if not listeners and not match_all_listeners:
return
if self._debug:
_LOGGER.debug(
"Bus:Handling %s", _event_repr(event_type, origin, event_data)
)
listeners = self._listeners.get(event_type)
# EVENT_HOMEASSISTANT_CLOSE should not be sent to MATCH_ALL listeners
if event_type != EVENT_HOMEASSISTANT_CLOSE:
listeners = match_all_listeners + listeners
if listeners:
listeners = self._match_all_listeners + listeners
else:
listeners = self._match_all_listeners.copy()
if not listeners:
return
event: Event | None = None
for job, event_filter, run_immediately in listeners:
if event_filter is not None:
try:
if not event_filter(event):
if event_data is None or not event_filter(event_data):
continue
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error in event filter")
continue
if not event:
event = Event(
event_type,
event_data,
origin,
time_fired,
context,
)
if run_immediately:
try:
self._hass.async_run_hass_job(job, event)
@ -1433,7 +1474,7 @@ class EventBus:
self,
event_type: str,
listener: Callable[[Event[_DataT]], Coroutine[Any, Any, None] | None],
event_filter: Callable[[Event[_DataT]], bool] | None = None,
event_filter: Callable[[_DataT], bool] | None = None,
run_immediately: bool = False,
) -> CALLBACK_TYPE:
"""Listen for all events or events of a specific type.
@ -1952,7 +1993,7 @@ class StateMachine:
return False
old_state.expire()
self._bus.async_fire(
self._bus._async_fire( # pylint: disable=protected-access
EVENT_STATE_CHANGED,
{"entity_id": entity_id, "old_state": old_state, "new_state": None},
context=context,
@ -2047,32 +2088,35 @@ class StateMachine:
same_attr = old_state.attributes == attributes
last_changed = old_state.last_changed if same_state else None
# It is much faster to convert a timestamp to a utc datetime object
# than converting a utc datetime object to a timestamp since cpython
# does not have a fast path for handling the UTC timezone and has to do
# multiple local timezone conversions.
#
# from_timestamp implementation:
# https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L2936
#
# timestamp implementation:
# https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6387
# https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6323
timestamp = time.time()
now = dt_util.utc_from_timestamp(timestamp)
if same_state and same_attr:
return
if context is None:
# It is much faster to convert a timestamp to a utc datetime object
# than converting a utc datetime object to a timestamp since cpython
# does not have a fast path for handling the UTC timezone and has to do
# multiple local timezone conversions.
#
# from_timestamp implementation:
# https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L2936
#
# timestamp implementation:
# https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6387
# https://github.com/python/cpython/blob/c90a862cdcf55dc1753c6466e5fa4a467a13ae24/Modules/_datetimemodule.c#L6323
timestamp = time.time()
now = dt_util.utc_from_timestamp(timestamp)
if TYPE_CHECKING:
assert timestamp is not None
context = Context(id=ulid_at_time(timestamp))
else:
now = dt_util.utcnow()
if same_attr:
if TYPE_CHECKING:
assert old_state is not None
attributes = old_state.attributes
# This is intentionally called with positional only arguments for performance
# reasons
state = State(
entity_id,
new_state,
@ -2086,11 +2130,11 @@ class StateMachine:
if old_state is not None:
old_state.expire()
self._states[entity_id] = state
self._bus.async_fire(
self._bus._async_fire( # pylint: disable=protected-access
EVENT_STATE_CHANGED,
{"entity_id": entity_id, "old_state": old_state, "new_state": state},
context=context,
time_fired=now,
time_fired=timestamp,
)
@ -2429,7 +2473,7 @@ class ServiceRegistry:
domain, service, processed_data, context, return_response
)
self._hass.bus.async_fire(
self._hass.bus._async_fire( # pylint: disable=protected-access
EVENT_CALL_SERVICE,
{
ATTR_DOMAIN: domain,

View File

@ -314,10 +314,11 @@ class AreaRegistry(BaseRegistry):
@callback
def _removed_from_registry_filter(
event: fr.EventFloorRegistryUpdated | lr.EventLabelRegistryUpdated,
event_data: fr.EventFloorRegistryUpdatedData
| lr.EventLabelRegistryUpdatedData,
) -> bool:
"""Filter all except for the item removed from registry events."""
return event.data["action"] == "remove"
return event_data["action"] == "remove"
@callback
def _handle_floor_registry_update(event: fr.EventFloorRegistryUpdated) -> None:

View File

@ -1145,10 +1145,10 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None:
@callback
def _label_removed_from_registry_filter(
event: lr.EventLabelRegistryUpdated,
event_data: lr.EventLabelRegistryUpdatedData,
) -> bool:
"""Filter all except for the remove action from label registry events."""
return event.data["action"] == "remove"
return event_data["action"] == "remove"
@callback
def _handle_label_registry_update(event: lr.EventLabelRegistryUpdated) -> None:
@ -1178,12 +1178,12 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None:
debounced_cleanup.async_schedule_call()
@callback
def entity_registry_changed_filter(event: Event) -> bool:
def entity_registry_changed_filter(event_data: Mapping[str, Any]) -> bool:
"""Handle entity updated or removed filter."""
if (
event.data["action"] == "update"
and "device_id" not in event.data["changes"]
) or event.data["action"] == "create":
event_data["action"] == "update"
and "device_id" not in event_data["changes"]
) or event_data["action"] == "create":
return False
return True

View File

@ -1431,10 +1431,11 @@ def _async_setup_cleanup(hass: HomeAssistant, registry: EntityRegistry) -> None:
@callback
def _removed_from_registry_filter(
event: lr.EventLabelRegistryUpdated | cr.EventCategoryRegistryUpdated,
event_data: lr.EventLabelRegistryUpdatedData
| cr.EventCategoryRegistryUpdatedData,
) -> bool:
"""Filter all except for the remove action from registry events."""
return event.data["action"] == "remove"
return event_data["action"] == "remove"
@callback
def _handle_label_registry_update(event: lr.EventLabelRegistryUpdated) -> None:
@ -1488,9 +1489,9 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) -
"""Set up the entity restore mechanism."""
@callback
def cleanup_restored_states_filter(event: Event) -> bool:
def cleanup_restored_states_filter(event_data: Mapping[str, Any]) -> bool:
"""Clean up restored states filter."""
return bool(event.data["action"] == "remove")
return bool(event_data["action"] == "remove")
@callback
def cleanup_restored_states(event: Event) -> None:

View File

@ -109,7 +109,7 @@ class _KeyedEventTracker(Generic[_TypedDictT]):
[
HomeAssistant,
dict[str, list[HassJob[[Event[_TypedDictT]], Any]]],
Event[_TypedDictT],
_TypedDictT,
],
bool,
]
@ -237,11 +237,11 @@ def async_track_state_change(
job = HassJob(action, f"track state change {entity_ids} {from_state} {to_state}")
@callback
def state_change_filter(event: Event[EventStateChangedData]) -> bool:
def state_change_filter(event_data: EventStateChangedData) -> bool:
"""Handle specific state changes."""
if from_state is not None:
old_state_str: str | None = None
if (old_state := event.data["old_state"]) is not None:
if (old_state := event_data["old_state"]) is not None:
old_state_str = old_state.state
if not match_from_state(old_state_str):
@ -249,7 +249,7 @@ def async_track_state_change(
if to_state is not None:
new_state_str: str | None = None
if (new_state := event.data["new_state"]) is not None:
if (new_state := event_data["new_state"]) is not None:
new_state_str = new_state.state
if not match_to_state(new_state_str):
@ -270,7 +270,7 @@ def async_track_state_change(
@callback
def state_change_listener(event: Event[EventStateChangedData]) -> None:
"""Handle specific state changes."""
if not state_change_filter(event):
if not state_change_filter(event.data):
return
state_change_dispatcher(event)
@ -341,10 +341,10 @@ def _async_dispatch_entity_id_event(
def _async_state_change_filter(
hass: HomeAssistant,
callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]],
event: Event[EventStateChangedData],
event_data: EventStateChangedData,
) -> bool:
"""Filter state changes by entity_id."""
return event.data["entity_id"] in callbacks
return event_data["entity_id"] in callbacks
_KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker(
@ -473,10 +473,10 @@ def _async_dispatch_old_entity_id_or_entity_id_event(
def _async_entity_registry_updated_filter(
hass: HomeAssistant,
callbacks: dict[str, list[HassJob[[Event[EventEntityRegistryUpdatedData]], Any]]],
event: Event[EventEntityRegistryUpdatedData],
event_data: EventEntityRegistryUpdatedData,
) -> bool:
"""Filter entity registry updates by entity_id."""
return event.data.get("old_entity_id", event.data["entity_id"]) in callbacks
return event_data.get("old_entity_id", event_data["entity_id"]) in callbacks
_KEYED_TRACK_ENTITY_REGISTRY_UPDATED = _KeyedEventTracker(
@ -512,10 +512,10 @@ def async_track_entity_registry_updated_event(
def _async_device_registry_updated_filter(
hass: HomeAssistant,
callbacks: dict[str, list[HassJob[[Event[EventDeviceRegistryUpdatedData]], Any]]],
event: Event[EventDeviceRegistryUpdatedData],
event_data: EventDeviceRegistryUpdatedData,
) -> bool:
"""Filter device registry updates by device_id."""
return event.data["device_id"] in callbacks
return event_data["device_id"] in callbacks
@callback
@ -585,12 +585,12 @@ def _async_dispatch_domain_event(
def _async_domain_added_filter(
hass: HomeAssistant,
callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]],
event: Event[EventStateChangedData],
event_data: EventStateChangedData,
) -> bool:
"""Filter state changes by entity_id."""
return event.data["old_state"] is None and (
return event_data["old_state"] is None and (
MATCH_ALL in callbacks
or split_entity_id(event.data["entity_id"])[0] in callbacks
or split_entity_id(event_data["entity_id"])[0] in callbacks
)
@ -634,12 +634,12 @@ def _async_track_state_added_domain(
def _async_domain_removed_filter(
hass: HomeAssistant,
callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]],
event: Event[EventStateChangedData],
event_data: EventStateChangedData,
) -> bool:
"""Filter state changes by entity_id."""
return event.data["new_state"] is None and (
return event_data["new_state"] is None and (
MATCH_ALL in callbacks
or split_entity_id(event.data["entity_id"])[0] in callbacks
or split_entity_id(event_data["entity_id"])[0] in callbacks
)

View File

@ -492,11 +492,11 @@ def async_setup(hass: HomeAssistant) -> None:
hass.data[TRANSLATION_FLATTEN_CACHE] = cache
@callback
def _async_load_translations_filter(event: Event) -> bool:
def _async_load_translations_filter(event_data: Mapping[str, Any]) -> bool:
"""Filter out unwanted events."""
nonlocal current_language
if (
new_language := event.data.get("language")
new_language := event_data.get("language")
) and new_language != current_language:
current_language = new_language
return True

View File

@ -97,7 +97,7 @@ async def fire_events_with_filter(hass):
events_to_fire = 10**6
@core.callback
def event_filter(event):
def event_filter(event_data):
"""Filter event."""
return False

View File

@ -603,9 +603,9 @@ def _async_when_setup(
await when_setup()
@callback
def _async_is_component_filter(event: Event[EventComponentLoaded]) -> bool:
def _async_is_component_filter(event_data: EventComponentLoaded) -> bool:
"""Check if the event is for the component."""
return event.data[ATTR_COMPONENT] == component
return event_data[ATTR_COMPONENT] == component
listeners.append(
hass.bus.async_listen(

View File

@ -98,7 +98,7 @@ def test_repr() -> None:
EVENT_STATE_CHANGED,
{"entity_id": "sensor.temperature", "old_state": None, "new_state": state},
context=state.context,
time_fired=fixed_time,
time_fired_timestamp=fixed_time.timestamp(),
)
assert "2016-07-09 11:00:00+00:00" in repr(States.from_event(event))
assert "2016-07-09 11:00:00+00:00" in repr(Events.from_event(event))
@ -164,7 +164,7 @@ def test_from_event_to_delete_state() -> None:
assert db_state.entity_id == "sensor.temperature"
assert db_state.state == ""
assert db_state.last_changed_ts is None
assert db_state.last_updated_ts == event.time_fired.timestamp()
assert db_state.last_updated_ts == pytest.approx(event.time_fired.timestamp())
def test_states_from_native_invalid_entity_id() -> None:
@ -247,7 +247,10 @@ async def test_process_timestamp_to_utc_isoformat() -> None:
async def test_event_to_db_model() -> None:
"""Test we can round trip Event conversion."""
event = ha.Event(
"state_changed", {"some": "attr"}, ha.EventOrigin.local, dt_util.utcnow()
"state_changed",
{"some": "attr"},
ha.EventOrigin.local,
dt_util.utcnow().timestamp(),
)
db_event = Events.from_event(event)
dialect = SupportedDialect.MYSQL

View File

@ -78,13 +78,13 @@ async def test_migrate_times(caplog: pytest.LogCaptureFixture, tmp_path: Path) -
"new_state": mock_state,
},
EventOrigin.local,
time_fired=now,
time_fired_timestamp=now.timestamp(),
)
custom_event = Event(
"custom_event",
{"entity_id": "sensor.custom"},
EventOrigin.local,
time_fired=now,
time_fired_timestamp=now.timestamp(),
)
number_of_migrations = 5
@ -242,13 +242,13 @@ async def test_migrate_can_resume_entity_id_post_migration(
"new_state": mock_state,
},
EventOrigin.local,
time_fired=now,
time_fired_timestamp=now.timestamp(),
)
custom_event = Event(
"custom_event",
{"entity_id": "sensor.custom"},
EventOrigin.local,
time_fired=now,
time_fired_timestamp=now.timestamp(),
)
number_of_migrations = 5

View File

@ -836,18 +836,23 @@ def test_event_eq() -> None:
data = {"some": "attr"}
context = ha.Context()
event1, event2 = (
ha.Event("some_type", data, time_fired=now, context=context) for _ in range(2)
ha.Event(
"some_type", data, time_fired_timestamp=now.timestamp(), context=context
)
for _ in range(2)
)
assert event1.as_dict() == event2.as_dict()
def test_event_time_fired_timestamp() -> None:
"""Test time_fired_timestamp."""
def test_event_time() -> None:
"""Test time_fired and time_fired_timestamp."""
now = dt_util.utcnow()
event = ha.Event("some_type", {"some": "attr"}, time_fired=now)
assert event.time_fired_timestamp == now.timestamp()
event = ha.Event(
"some_type", {"some": "attr"}, time_fired_timestamp=now.timestamp()
)
assert event.time_fired_timestamp == now.timestamp()
assert event.time_fired == now
def test_event_json_fragment() -> None:
@ -856,7 +861,10 @@ def test_event_json_fragment() -> None:
data = {"some": "attr"}
context = ha.Context()
event1, event2 = (
ha.Event("some_type", data, time_fired=now, context=context) for _ in range(2)
ha.Event(
"some_type", data, time_fired_timestamp=now.timestamp(), context=context
)
for _ in range(2)
)
# We are testing that the JSON fragments are the same when as_dict is called
@ -898,7 +906,7 @@ def test_event_as_dict() -> None:
now = dt_util.utcnow()
data = {"some": "attr"}
event = ha.Event(event_type, data, ha.EventOrigin.local, now)
event = ha.Event(event_type, data, ha.EventOrigin.local, now.timestamp())
expected = {
"event_type": event_type,
"data": data,
@ -1108,9 +1116,9 @@ async def test_eventbus_filtered_listener(hass: HomeAssistant) -> None:
calls.append(event)
@ha.callback
def filter(event):
def filter(event_data):
"""Mock filter."""
return not event.data["filtered"]
return not event_data["filtered"]
unsub = hass.bus.async_listen("test", listener, event_filter=filter)
@ -3152,3 +3160,63 @@ async def test_async_add_job_deprecated(
"https://developers.home-assistant.io/blog/2024/03/13/deprecate_add_run_job"
" for replacement options"
) in caplog.text
async def test_eventbus_lazy_object_creation(hass: HomeAssistant) -> None:
"""Test we don't create unneeded objects when firing events."""
calls = []
@ha.callback
def listener(event):
"""Mock listener."""
calls.append(event)
@ha.callback
def filter(event_data):
"""Mock filter."""
return not event_data["filtered"]
unsub = hass.bus.async_listen("test_1", listener, event_filter=filter)
# Test lazy creation of Event objects
with patch("homeassistant.core.Event") as mock_event:
# Fire an event which is filtered out by its listener
hass.bus.async_fire("test_1", {"filtered": True})
await hass.async_block_till_done()
mock_event.assert_not_called()
assert len(calls) == 0
# Fire an event which has no listener
hass.bus.async_fire("test_2")
await hass.async_block_till_done()
mock_event.assert_not_called()
assert len(calls) == 0
# Fire an event which is not filtered out by its listener
hass.bus.async_fire("test_1", {"filtered": False})
await hass.async_block_till_done()
mock_event.assert_called_once()
assert len(calls) == 1
calls = []
# Test lazy creation of Context objects
with patch("homeassistant.core.Context") as mock_context:
# Fire an event which is filtered out by its listener
hass.bus.async_fire("test_1", {"filtered": True})
await hass.async_block_till_done()
mock_context.assert_not_called()
assert len(calls) == 0
# Fire an event which has no listener
hass.bus.async_fire("test_2")
await hass.async_block_till_done()
mock_context.assert_not_called()
assert len(calls) == 0
# Fire an event which is not filtered out by its listener
hass.bus.async_fire("test_1", {"filtered": False})
await hass.async_block_till_done()
mock_context.assert_called_once()
assert len(calls) == 1
unsub()