mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
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:
parent
638020f168
commit
d31124d5d4
@ -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
|
||||
]
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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"),
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user