Home Assistant Unofficial Reference 2024.12.1
event.py
Go to the documentation of this file.
1 """Offer event listening automation rules."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import ItemsView, Mapping
6 from typing import Any
7 
8 import voluptuous as vol
9 
10 from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM, EVENT_STATE_REPORTED
11 from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback
12 from homeassistant.exceptions import HomeAssistantError
13 from homeassistant.helpers import config_validation as cv, template
14 from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
15 from homeassistant.helpers.typing import ConfigType
16 
17 CONF_EVENT_TYPE = "event_type"
18 CONF_EVENT_CONTEXT = "context"
19 
20 
21 def _validate_event_types(value: Any) -> Any:
22  """Validate the event types.
23 
24  If the event types are templated, we check when attaching the trigger.
25  """
26  templates: list[template.Template] = value
27  if any(tpl.is_static and tpl.template == EVENT_STATE_REPORTED for tpl in templates):
28  raise vol.Invalid(f"Can't listen to {EVENT_STATE_REPORTED} in event trigger")
29  return value
30 
31 
32 TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
33  {
34  vol.Required(CONF_PLATFORM): "event",
35  vol.Required(CONF_EVENT_TYPE): vol.All(
36  cv.ensure_list, [cv.template], _validate_event_types
37  ),
38  vol.Optional(CONF_EVENT_DATA): vol.All(dict, cv.template_complex),
39  vol.Optional(CONF_EVENT_CONTEXT): vol.All(dict, cv.template_complex),
40  }
41 )
42 
43 
44 def _schema_value(value: Any) -> Any:
45  if isinstance(value, list):
46  return vol.In(value)
47 
48  return value
49 
50 
52  hass: HomeAssistant,
53  config: ConfigType,
54  action: TriggerActionType,
55  trigger_info: TriggerInfo,
56  *,
57  platform_type: str = "event",
58 ) -> CALLBACK_TYPE:
59  """Listen for events based on configuration."""
60  trigger_data = trigger_info["trigger_data"]
61  variables = trigger_info["variables"]
62 
63  event_types = template.render_complex(
64  config[CONF_EVENT_TYPE], variables, limited=True
65  )
66  if EVENT_STATE_REPORTED in event_types:
67  raise HomeAssistantError(
68  f"Can't listen to {EVENT_STATE_REPORTED} in event trigger"
69  )
70  event_data_schema: vol.Schema | None = None
71  event_data_items: ItemsView | None = None
72  if CONF_EVENT_DATA in config:
73  # Render the schema input
74  event_data = {}
75  event_data.update(
76  template.render_complex(config[CONF_EVENT_DATA], variables, limited=True)
77  )
78  # Build the schema or a an items view if the schema is simple
79  # and does not contain sub-dicts. We explicitly do not check for
80  # list like the context data below since lists are a special case
81  # only for context data. (see test test_event_data_with_list)
82  if any(isinstance(value, dict) for value in event_data.values()):
83  event_data_schema = vol.Schema(
84  {vol.Required(key): value for key, value in event_data.items()},
85  extra=vol.ALLOW_EXTRA,
86  )
87  else:
88  # Use a simple items comparison if possible
89  event_data_items = event_data.items()
90 
91  event_context_schema: vol.Schema | None = None
92  event_context_items: ItemsView | None = None
93  if CONF_EVENT_CONTEXT in config:
94  # Render the schema input
95  event_context = {}
96  event_context.update(
97  template.render_complex(config[CONF_EVENT_CONTEXT], variables, limited=True)
98  )
99  # Build the schema or a an items view if the schema is simple
100  # and does not contain lists. Lists are a special case to support
101  # matching events by user_id. (see test test_if_fires_on_multiple_user_ids)
102  # This can likely be optimized further in the future to handle the
103  # multiple user_id case without requiring expensive schema
104  # validation.
105  if any(isinstance(value, list) for value in event_context.values()):
106  event_context_schema = vol.Schema(
107  {
108  vol.Required(key): _schema_value(value)
109  for key, value in event_context.items()
110  },
111  extra=vol.ALLOW_EXTRA,
112  )
113  else:
114  # Use a simple items comparison if possible
115  event_context_items = event_context.items()
116 
117  job = HassJob(action, f"event trigger {trigger_info}")
118 
119  @callback
120  def filter_event(event_data: Mapping[str, Any]) -> bool:
121  """Filter events."""
122  try:
123  # Check that the event data and context match the configured
124  # schema if one was provided
125  if event_data_items:
126  # Fast path for simple items comparison
127  if not (event_data.items() >= event_data_items):
128  return False
129  elif event_data_schema:
130  # Slow path for schema validation
131  event_data_schema(event_data)
132  except vol.Invalid:
133  # If event doesn't match, skip event
134  return False
135  return True
136 
137  @callback
138  def handle_event(event: Event) -> None:
139  """Listen for events and calls the action when data matches."""
140  if event_context_items:
141  # Fast path for simple items comparison
142  # This is safe because we do not mutate the event context
143  if not (event.context._as_dict.items() >= event_context_items): # noqa: SLF001
144  return
145  elif event_context_schema:
146  try:
147  # Slow path for schema validation
148  # This is safe because we make a copy of the event context
149  event_context_schema(dict(event.context._as_dict)) # noqa: SLF001
150  except vol.Invalid:
151  # If event doesn't match, skip event
152  return
153 
154  hass.loop.call_soon(
155  hass.async_run_hass_job,
156  job,
157  {
158  "trigger": {
159  **trigger_data,
160  "platform": platform_type,
161  "event": event,
162  "description": f"event '{event.event_type}'",
163  }
164  },
165  event.context,
166  )
167 
168  event_filter = filter_event if event_data_items or event_data_schema else None
169  removes = [
170  hass.bus.async_listen(event_type, handle_event, event_filter=event_filter)
171  for event_type in event_types
172  ]
173 
174  @callback
175  def remove_listen_events() -> None:
176  """Remove event listeners."""
177  for remove in removes:
178  remove()
179 
180  return remove_listen_events
bool remove(self, _T matcher)
Definition: match.py:214
CALLBACK_TYPE async_attach_trigger(HomeAssistant hass, ConfigType config, TriggerActionType action, TriggerInfo trigger_info, *str platform_type="event")
Definition: event.py:58