Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Allow to set up simple automation rules via the config file."""
2 
3 from __future__ import annotations
4 
5 from abc import ABC, abstractmethod
6 import asyncio
7 from collections.abc import Callable, Mapping
8 from dataclasses import dataclass
9 from functools import partial
10 import logging
11 from typing import Any, Protocol, cast
12 
13 from propcache import cached_property
14 import voluptuous as vol
15 
16 from homeassistant.components import websocket_api
17 from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
18 from homeassistant.const import (
19  ATTR_ENTITY_ID,
20  ATTR_MODE,
21  ATTR_NAME,
22  CONF_ALIAS,
23  CONF_CONDITIONS,
24  CONF_DEVICE_ID,
25  CONF_ENTITY_ID,
26  CONF_EVENT_DATA,
27  CONF_ID,
28  CONF_MODE,
29  CONF_PATH,
30  CONF_PLATFORM,
31  CONF_VARIABLES,
32  CONF_ZONE,
33  EVENT_HOMEASSISTANT_STARTED,
34  SERVICE_RELOAD,
35  SERVICE_TOGGLE,
36  SERVICE_TURN_OFF,
37  SERVICE_TURN_ON,
38  STATE_ON,
39 )
40 from homeassistant.core import (
41  CALLBACK_TYPE,
42  Context,
43  CoreState,
44  Event,
45  HomeAssistant,
46  ServiceCall,
47  callback,
48  split_entity_id,
49  valid_entity_id,
50 )
51 from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError
52 from homeassistant.helpers import condition
55  DeprecatedConstant,
56  all_with_deprecated_constants,
57  check_if_deprecated_constant,
58  dir_with_deprecated_constants,
59 )
60 from homeassistant.helpers.entity import ToggleEntity
61 from homeassistant.helpers.entity_component import EntityComponent
63  IssueSeverity,
64  async_create_issue,
65  async_delete_issue,
66 )
67 from homeassistant.helpers.restore_state import RestoreEntity
68 from homeassistant.helpers.script import (
69  ATTR_CUR,
70  ATTR_MAX,
71  CONF_MAX,
72  CONF_MAX_EXCEEDED,
73  Script,
74  ScriptRunResult,
75  script_stack_cv,
76 )
77 from homeassistant.helpers.script_variables import ScriptVariables
79  ReloadServiceHelper,
80  async_register_admin_service,
81 )
82 from homeassistant.helpers.trace import (
83  TraceElement,
84  script_execution_set,
85  trace_append_element,
86  trace_get,
87  trace_path,
88 )
90  TriggerActionType,
91  TriggerData,
92  TriggerInfo,
93  async_initialize_triggers,
94 )
95 from homeassistant.helpers.typing import ConfigType
96 from homeassistant.loader import bind_hass
97 from homeassistant.util.dt import parse_datetime
98 from homeassistant.util.hass_dict import HassKey
99 
100 from .config import AutomationConfig, ValidationStatus
101 from .const import (
102  CONF_ACTIONS,
103  CONF_INITIAL_STATE,
104  CONF_TRACE,
105  CONF_TRIGGER_VARIABLES,
106  CONF_TRIGGERS,
107  DEFAULT_INITIAL_STATE,
108  DOMAIN,
109  LOGGER,
110 )
111 from .helpers import async_get_blueprints
112 from .trace import trace_automation
113 
114 DATA_COMPONENT: HassKey[EntityComponent[BaseAutomationEntity]] = HassKey(DOMAIN)
115 ENTITY_ID_FORMAT = DOMAIN + ".{}"
116 
117 
118 CONF_SKIP_CONDITION = "skip_condition"
119 CONF_STOP_ACTIONS = "stop_actions"
120 DEFAULT_STOP_ACTIONS = True
121 
122 EVENT_AUTOMATION_RELOADED = "automation_reloaded"
123 EVENT_AUTOMATION_TRIGGERED = "automation_triggered"
124 
125 ATTR_LAST_TRIGGERED = "last_triggered"
126 ATTR_SOURCE = "source"
127 ATTR_VARIABLES = "variables"
128 SERVICE_TRIGGER = "trigger"
129 
130 
131 class IfAction(Protocol):
132  """Define the format of if_action."""
133 
134  config: list[ConfigType]
135 
136  def __call__(self, variables: Mapping[str, Any] | None = None) -> bool:
137  """AND all conditions."""
138 
139 
140 # AutomationActionType, AutomationTriggerData,
141 # and AutomationTriggerInfo are deprecated as of 2022.9.
142 # Can be removed in 2025.1
143 _DEPRECATED_AutomationActionType = DeprecatedConstant(
144  TriggerActionType, "TriggerActionType", "2025.1"
145 )
146 _DEPRECATED_AutomationTriggerData = DeprecatedConstant(
147  TriggerData, "TriggerData", "2025.1"
148 )
149 _DEPRECATED_AutomationTriggerInfo = DeprecatedConstant(
150  TriggerInfo, "TriggerInfo", "2025.1"
151 )
152 
153 
154 @bind_hass
155 def is_on(hass: HomeAssistant, entity_id: str) -> bool:
156  """Return true if specified automation entity_id is on.
157 
158  Async friendly.
159  """
160  return hass.states.is_state(entity_id, STATE_ON)
161 
162 
164  hass: HomeAssistant, referenced_id: str, property_name: str
165 ) -> list[str]:
166  """Return all automations that reference the x."""
167  if DATA_COMPONENT not in hass.data:
168  return []
169 
170  return [
171  automation_entity.entity_id
172  for automation_entity in hass.data[DATA_COMPONENT].entities
173  if referenced_id in getattr(automation_entity, property_name)
174  ]
175 
176 
178  hass: HomeAssistant, entity_id: str, property_name: str
179 ) -> list[str]:
180  """Return all x in an automation."""
181  if DATA_COMPONENT not in hass.data:
182  return []
183 
184  if (automation_entity := hass.data[DATA_COMPONENT].get_entity(entity_id)) is None:
185  return []
186 
187  return list(getattr(automation_entity, property_name))
188 
189 
190 @callback
191 def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]:
192  """Return all automations that reference the entity."""
193  return _automations_with_x(hass, entity_id, "referenced_entities")
194 
195 
196 @callback
197 def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
198  """Return all entities in an automation."""
199  return _x_in_automation(hass, entity_id, "referenced_entities")
200 
201 
202 @callback
203 def automations_with_device(hass: HomeAssistant, device_id: str) -> list[str]:
204  """Return all automations that reference the device."""
205  return _automations_with_x(hass, device_id, "referenced_devices")
206 
207 
208 @callback
209 def devices_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
210  """Return all devices in an automation."""
211  return _x_in_automation(hass, entity_id, "referenced_devices")
212 
213 
214 @callback
215 def automations_with_area(hass: HomeAssistant, area_id: str) -> list[str]:
216  """Return all automations that reference the area."""
217  return _automations_with_x(hass, area_id, "referenced_areas")
218 
219 
220 @callback
221 def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
222  """Return all areas in an automation."""
223  return _x_in_automation(hass, entity_id, "referenced_areas")
224 
225 
226 @callback
227 def automations_with_floor(hass: HomeAssistant, floor_id: str) -> list[str]:
228  """Return all automations that reference the floor."""
229  return _automations_with_x(hass, floor_id, "referenced_floors")
230 
231 
232 @callback
233 def floors_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
234  """Return all floors in an automation."""
235  return _x_in_automation(hass, entity_id, "referenced_floors")
236 
237 
238 @callback
239 def automations_with_label(hass: HomeAssistant, label_id: str) -> list[str]:
240  """Return all automations that reference the label."""
241  return _automations_with_x(hass, label_id, "referenced_labels")
242 
243 
244 @callback
245 def labels_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
246  """Return all labels in an automation."""
247  return _x_in_automation(hass, entity_id, "referenced_labels")
248 
249 
250 @callback
251 def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]:
252  """Return all automations that reference the blueprint."""
253  if DOMAIN not in hass.data:
254  return []
255 
256  return [
257  automation_entity.entity_id
258  for automation_entity in hass.data[DATA_COMPONENT].entities
259  if automation_entity.referenced_blueprint == blueprint_path
260  ]
261 
262 
263 @callback
264 def blueprint_in_automation(hass: HomeAssistant, entity_id: str) -> str | None:
265  """Return the blueprint the automation is based on or None."""
266  if DATA_COMPONENT not in hass.data:
267  return None
268 
269  if (automation_entity := hass.data[DATA_COMPONENT].get_entity(entity_id)) is None:
270  return None
271 
272  return automation_entity.referenced_blueprint
273 
274 
275 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
276  """Set up all automations."""
277  hass.data[DATA_COMPONENT] = component = EntityComponent[BaseAutomationEntity](
278  LOGGER, DOMAIN, hass
279  )
280 
281  # Register automation as valid domain for Blueprint
283 
284  await _async_process_config(hass, config, component)
285 
286  # Add some default blueprints to blueprints/automation, does nothing
287  # if blueprints/automation already exists but still has to create
288  # an executor job to check if the folder exists so we run it in a
289  # separate task to avoid waiting for it to finish setting up
290  # since a tracked task will be waited at the end of startup
291  hass.async_create_task(
292  async_get_blueprints(hass).async_populate(), eager_start=True
293  )
294 
295  async def trigger_service_handler(
296  entity: BaseAutomationEntity, service_call: ServiceCall
297  ) -> None:
298  """Handle forced automation trigger, e.g. from frontend."""
299  await entity.async_trigger(
300  {**service_call.data[ATTR_VARIABLES], "trigger": {"platform": None}},
301  skip_condition=service_call.data[CONF_SKIP_CONDITION],
302  context=service_call.context,
303  )
304 
305  component.async_register_entity_service(
306  SERVICE_TRIGGER,
307  {
308  vol.Optional(ATTR_VARIABLES, default={}): dict,
309  vol.Optional(CONF_SKIP_CONDITION, default=True): bool,
310  },
311  trigger_service_handler,
312  )
313  component.async_register_entity_service(SERVICE_TOGGLE, None, "async_toggle")
314  component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on")
315  component.async_register_entity_service(
316  SERVICE_TURN_OFF,
317  {vol.Optional(CONF_STOP_ACTIONS, default=DEFAULT_STOP_ACTIONS): cv.boolean},
318  "async_turn_off",
319  )
320 
321  async def reload_service_handler(service_call: ServiceCall) -> None:
322  """Remove all automations and load new ones from config."""
323  await async_get_blueprints(hass).async_reset_cache()
324  if (conf := await component.async_prepare_reload(skip_reset=True)) is None:
325  return
326  if automation_id := service_call.data.get(CONF_ID):
327  await _async_process_single_config(hass, conf, component, automation_id)
328  else:
329  await _async_process_config(hass, conf, component)
330  hass.bus.async_fire(EVENT_AUTOMATION_RELOADED, context=service_call.context)
331 
332  def reload_targets(service_call: ServiceCall) -> set[str | None]:
333  if automation_id := service_call.data.get(CONF_ID):
334  return {automation_id}
335  return {automation.unique_id for automation in component.entities}
336 
337  reload_helper = ReloadServiceHelper(reload_service_handler, reload_targets)
338 
340  hass,
341  DOMAIN,
342  SERVICE_RELOAD,
343  reload_helper.execute_service,
344  schema=vol.Schema({vol.Optional(CONF_ID): str}),
345  )
346 
347  websocket_api.async_register_command(hass, websocket_config)
348 
349  return True
350 
351 
353  """Base class for automation entities."""
354 
355  _entity_component_unrecorded_attributes = frozenset(
356  (ATTR_LAST_TRIGGERED, ATTR_MODE, ATTR_CUR, ATTR_MAX, CONF_ID)
357  )
358  raw_config: ConfigType | None
359 
360  @property
361  def capability_attributes(self) -> dict[str, Any] | None:
362  """Return capability attributes."""
363  if self.unique_idunique_id is not None:
364  return {CONF_ID: self.unique_idunique_id}
365  return None
366 
367  @cached_property
368  @abstractmethod
369  def referenced_labels(self) -> set[str]:
370  """Return a set of referenced labels."""
371 
372  @cached_property
373  @abstractmethod
374  def referenced_floors(self) -> set[str]:
375  """Return a set of referenced floors."""
376 
377  @cached_property
378  @abstractmethod
379  def referenced_areas(self) -> set[str]:
380  """Return a set of referenced areas."""
381 
382  @property
383  @abstractmethod
384  def referenced_blueprint(self) -> str | None:
385  """Return referenced blueprint or None."""
386 
387  @cached_property
388  @abstractmethod
389  def referenced_devices(self) -> set[str]:
390  """Return a set of referenced devices."""
391 
392  @cached_property
393  @abstractmethod
394  def referenced_entities(self) -> set[str]:
395  """Return a set of referenced entities."""
396 
397  @abstractmethod
398  async def async_trigger(
399  self,
400  run_variables: dict[str, Any],
401  context: Context | None = None,
402  skip_condition: bool = False,
403  ) -> ScriptRunResult | None:
404  """Trigger automation."""
405 
406 
408  """A non-functional automation entity with its state set to unavailable.
409 
410  This class is instantiated when an automation fails to validate.
411  """
412 
413  _attr_should_poll = False
414  _attr_available = False
415 
416  def __init__(
417  self,
418  automation_id: str | None,
419  name: str,
420  raw_config: ConfigType | None,
421  validation_error: str,
422  validation_status: ValidationStatus,
423  ) -> None:
424  """Initialize an automation entity."""
425  self._attr_name_attr_name = name
426  self._attr_unique_id_attr_unique_id = automation_id
427  self.raw_configraw_config = raw_config
428  self._validation_error_validation_error = validation_error
429  self._validation_status_validation_status = validation_status
430 
431  @cached_property
432  def referenced_labels(self) -> set[str]:
433  """Return a set of referenced labels."""
434  return set()
435 
436  @cached_property
437  def referenced_floors(self) -> set[str]:
438  """Return a set of referenced floors."""
439  return set()
440 
441  @cached_property
442  def referenced_areas(self) -> set[str]:
443  """Return a set of referenced areas."""
444  return set()
445 
446  @property
447  def referenced_blueprint(self) -> str | None:
448  """Return referenced blueprint or None."""
449  return None
450 
451  @cached_property
452  def referenced_devices(self) -> set[str]:
453  """Return a set of referenced devices."""
454  return set()
455 
456  @cached_property
457  def referenced_entities(self) -> set[str]:
458  """Return a set of referenced entities."""
459  return set()
460 
461  async def async_added_to_hass(self) -> None:
462  """Create a repair issue to notify the user the automation has errors."""
463  await super().async_added_to_hass()
465  self.hasshass,
466  DOMAIN,
467  f"{self.entity_id}_validation_{self._validation_status}",
468  is_fixable=False,
469  severity=IssueSeverity.ERROR,
470  translation_key=f"validation_{self._validation_status}",
471  translation_placeholders={
472  "edit": f"/config/automation/edit/{self.unique_id}",
473  "entity_id": self.entity_identity_id,
474  "error": self._validation_error_validation_error,
475  "name": self._attr_name_attr_name or self.entity_identity_id,
476  },
477  )
478 
479  async def async_will_remove_from_hass(self) -> None:
480  await super().async_will_remove_from_hass()
482  self.hasshass, DOMAIN, f"{self.entity_id}_validation_{self._validation_status}"
483  )
484 
485  async def async_trigger(
486  self,
487  run_variables: dict[str, Any],
488  context: Context | None = None,
489  skip_condition: bool = False,
490  ) -> None:
491  """Trigger automation."""
492 
493 
495  """Entity to show status of entity."""
496 
497  _attr_should_poll = False
498 
499  def __init__(
500  self,
501  automation_id: str | None,
502  name: str,
503  trigger_config: list[ConfigType],
504  cond_func: IfAction | None,
505  action_script: Script,
506  initial_state: bool | None,
507  variables: ScriptVariables | None,
508  trigger_variables: ScriptVariables | None,
509  raw_config: ConfigType | None,
510  blueprint_inputs: ConfigType | None,
511  trace_config: ConfigType,
512  ) -> None:
513  """Initialize an automation entity."""
514  self._attr_name_attr_name = name
515  self._trigger_config_trigger_config = trigger_config
516  self._async_detach_triggers_async_detach_triggers: CALLBACK_TYPE | None = None
517  self._cond_func_cond_func = cond_func
518  self.action_scriptaction_script = action_script
519  self.action_scriptaction_script.change_listener = self.async_write_ha_stateasync_write_ha_state
520  self._initial_state_initial_state = initial_state
521  self._is_enabled_is_enabled = False
522  self._logger_logger = LOGGER
523  self._variables_variables = variables
524  self._trigger_variables_trigger_variables = trigger_variables
525  self.raw_configraw_config = raw_config
526  self._blueprint_inputs_blueprint_inputs = blueprint_inputs
527  self._trace_config_trace_config = trace_config
528  self._attr_unique_id_attr_unique_id = automation_id
529 
530  @property
531  def extra_state_attributes(self) -> dict[str, Any]:
532  """Return the entity state attributes."""
533  attrs = {
534  ATTR_LAST_TRIGGERED: self.action_scriptaction_script.last_triggered,
535  ATTR_MODE: self.action_scriptaction_script.script_mode,
536  ATTR_CUR: self.action_scriptaction_script.runs,
537  }
538  if self.action_scriptaction_script.supports_max:
539  attrs[ATTR_MAX] = self.action_scriptaction_script.max_runs
540  return attrs
541 
542  @property
543  def is_on(self) -> bool:
544  """Return True if entity is on."""
545  return self._async_detach_triggers_async_detach_triggers is not None or self._is_enabled_is_enabled
546 
547  @property
548  def referenced_labels(self) -> set[str]:
549  """Return a set of referenced labels."""
550  return self.action_scriptaction_script.referenced_labels
551 
552  @property
553  def referenced_floors(self) -> set[str]:
554  """Return a set of referenced floors."""
555  return self.action_scriptaction_script.referenced_floors
556 
557  @cached_property
558  def referenced_areas(self) -> set[str]:
559  """Return a set of referenced areas."""
560  return self.action_scriptaction_script.referenced_areas
561 
562  @property
563  def referenced_blueprint(self) -> str | None:
564  """Return referenced blueprint or None."""
565  if self._blueprint_inputs_blueprint_inputs is None:
566  return None
567  return cast(str, self._blueprint_inputs_blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH])
568 
569  @cached_property
570  def referenced_devices(self) -> set[str]:
571  """Return a set of referenced devices."""
572  referenced = self.action_scriptaction_script.referenced_devices
573 
574  if self._cond_func_cond_func is not None:
575  for conf in self._cond_func_cond_func.config:
576  referenced |= condition.async_extract_devices(conf)
577 
578  for conf in self._trigger_config_trigger_config:
579  referenced |= set(_trigger_extract_devices(conf))
580 
581  return referenced
582 
583  @cached_property
584  def referenced_entities(self) -> set[str]:
585  """Return a set of referenced entities."""
586  referenced = self.action_scriptaction_script.referenced_entities
587 
588  if self._cond_func_cond_func is not None:
589  for conf in self._cond_func_cond_func.config:
590  referenced |= condition.async_extract_entities(conf)
591 
592  for conf in self._trigger_config_trigger_config:
593  for entity_id in _trigger_extract_entities(conf):
594  referenced.add(entity_id)
595 
596  return referenced
597 
598  async def async_added_to_hass(self) -> None:
599  """Startup with initial state or previous state."""
600  await super().async_added_to_hass()
601 
602  self._logger_logger = logging.getLogger(
603  f"{__name__}.{split_entity_id(self.entity_id)[1]}"
604  )
605  self.action_scriptaction_script.update_logger(self._logger_logger)
606 
607  if state := await self.async_get_last_stateasync_get_last_state():
608  enable_automation = state.state == STATE_ON
609  last_triggered = state.attributes.get("last_triggered")
610  if last_triggered is not None:
611  self.action_scriptaction_script.last_triggered = parse_datetime(last_triggered)
612  self._logger_logger.debug(
613  "Loaded automation %s with state %s from state storage last state %s",
614  self.entity_identity_id,
615  enable_automation,
616  state,
617  )
618  else:
619  enable_automation = DEFAULT_INITIAL_STATE
620  self._logger_logger.debug(
621  "Automation %s not in state storage, state %s from default is used",
622  self.entity_identity_id,
623  enable_automation,
624  )
625 
626  if self._initial_state_initial_state is not None:
627  enable_automation = self._initial_state_initial_state
628  self._logger_logger.debug(
629  "Automation %s initial state %s overridden from config initial_state",
630  self.entity_identity_id,
631  enable_automation,
632  )
633 
634  if enable_automation:
635  await self._async_enable_async_enable()
636 
637  async def async_turn_on(self, **kwargs: Any) -> None:
638  """Turn the entity on and update the state."""
639  await self._async_enable_async_enable()
640  self.async_write_ha_stateasync_write_ha_state()
641 
642  async def async_turn_off(self, **kwargs: Any) -> None:
643  """Turn the entity off."""
644  if CONF_STOP_ACTIONS in kwargs:
645  await self._async_disable_async_disable(kwargs[CONF_STOP_ACTIONS])
646  else:
647  await self._async_disable_async_disable()
648  self.async_write_ha_stateasync_write_ha_state()
649 
650  async def async_trigger(
651  self,
652  run_variables: dict[str, Any],
653  context: Context | None = None,
654  skip_condition: bool = False,
655  ) -> ScriptRunResult | None:
656  """Trigger automation.
657 
658  This method is a coroutine.
659  """
660  reason = ""
661  alias = ""
662  if "trigger" in run_variables:
663  if "description" in run_variables["trigger"]:
664  reason = f' by {run_variables["trigger"]["description"]}'
665  if "alias" in run_variables["trigger"]:
666  alias = f' trigger \'{run_variables["trigger"]["alias"]}\''
667  self._logger_logger.debug("Automation%s triggered%s", alias, reason)
668 
669  # Create a new context referring to the old context.
670  parent_id = None if context is None else context.id
671  trigger_context = Context(parent_id=parent_id)
672 
673  with trace_automation(
674  self.hasshass,
675  self.unique_idunique_id,
676  self.raw_configraw_config,
677  self._blueprint_inputs_blueprint_inputs,
678  trigger_context,
679  self._trace_config_trace_config,
680  ) as automation_trace:
681  this = None
682  if state := self.hasshass.states.get(self.entity_identity_id):
683  this = state.as_dict()
684  variables: dict[str, Any] = {"this": this, **(run_variables or {})}
685  if self._variables_variables:
686  try:
687  variables = self._variables_variables.async_render(self.hasshass, variables)
688  except TemplateError as err:
689  self._logger_logger.error("Error rendering variables: %s", err)
690  automation_trace.set_error(err)
691  return None
692 
693  # Prepare tracing the automation
694  automation_trace.set_trace(trace_get())
695 
696  # Set trigger reason
697  trigger_description = variables.get("trigger", {}).get("description")
698  automation_trace.set_trigger_description(trigger_description)
699 
700  # Add initial variables as the trigger step
701  if "trigger" in variables and "idx" in variables["trigger"]:
702  trigger_path = f"trigger/{variables['trigger']['idx']}"
703  else:
704  trigger_path = "trigger"
705  trace_element = TraceElement(variables, trigger_path)
706  trace_append_element(trace_element)
707 
708  if (
709  not skip_condition
710  and self._cond_func_cond_func is not None
711  and not self._cond_func_cond_func(variables)
712  ):
713  self._logger_logger.debug(
714  "Conditions not met, aborting automation. Condition summary: %s",
715  trace_get(clear=False),
716  )
717  script_execution_set("failed_conditions")
718  return None
719 
720  self.async_set_contextasync_set_context(trigger_context)
721  event_data = {
722  ATTR_NAME: self.namename,
723  ATTR_ENTITY_ID: self.entity_identity_id,
724  }
725  if "trigger" in variables and "description" in variables["trigger"]:
726  event_data[ATTR_SOURCE] = variables["trigger"]["description"]
727 
728  @callback
729  def started_action() -> None:
730  # This is always a callback from a coro so there is no
731  # risk of this running in a thread which allows us to use
732  # async_fire_internal
733  self.hasshass.bus.async_fire_internal(
734  EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context
735  )
736 
737  # Make a new empty script stack; automations are allowed
738  # to recursively trigger themselves
739  script_stack_cv.set([])
740 
741  try:
742  with trace_path("action"):
743  return await self.action_scriptaction_script.async_run(
744  variables, trigger_context, started_action
745  )
746  except ServiceNotFound as err:
748  self.hasshass,
749  DOMAIN,
750  f"{self.entity_id}_service_not_found_{err.domain}.{err.service}",
751  is_fixable=True,
752  is_persistent=True,
753  severity=IssueSeverity.ERROR,
754  translation_key="service_not_found",
755  translation_placeholders={
756  "service": f"{err.domain}.{err.service}",
757  "entity_id": self.entity_identity_id,
758  "name": self._attr_name_attr_name or self.entity_identity_id,
759  "edit": f"/config/automation/edit/{self.unique_id}",
760  },
761  )
762  automation_trace.set_error(err)
763  except (vol.Invalid, HomeAssistantError) as err:
764  self._logger_logger.error(
765  "Error while executing automation %s: %s",
766  self.entity_identity_id,
767  err,
768  )
769  automation_trace.set_error(err)
770  except Exception as err:
771  self._logger_logger.exception("While executing automation %s", self.entity_identity_id)
772  automation_trace.set_error(err)
773 
774  return None
775 
776  async def async_will_remove_from_hass(self) -> None:
777  """Remove listeners when removing automation from Home Assistant."""
778  await super().async_will_remove_from_hass()
779  await self._async_disable_async_disable()
780 
781  async def _async_enable_automation(self, event: Event) -> None:
782  """Start automation on startup."""
783  # Don't do anything if no longer enabled or already attached
784  if not self._is_enabled_is_enabled or self._async_detach_triggers_async_detach_triggers is not None:
785  return
786 
787  self._async_detach_triggers_async_detach_triggers = await self._async_attach_triggers_async_attach_triggers(True)
788  self.async_write_ha_stateasync_write_ha_state()
789 
790  async def _async_enable(self) -> None:
791  """Enable this automation entity.
792 
793  This method is not expected to write state to the
794  state machine.
795  """
796  if self._is_enabled_is_enabled:
797  return
798 
799  self._is_enabled_is_enabled = True
800  # HomeAssistant is starting up
801  if self.hasshass.state is not CoreState.not_running:
802  self._async_detach_triggers_async_detach_triggers = await self._async_attach_triggers_async_attach_triggers(False)
803  return
804 
805  self.hasshass.bus.async_listen_once(
806  EVENT_HOMEASSISTANT_STARTED,
807  self._async_enable_automation_async_enable_automation,
808  )
809 
810  async def _async_disable(self, stop_actions: bool = DEFAULT_STOP_ACTIONS) -> None:
811  """Disable the automation entity.
812 
813  This method is not expected to write state to the
814  state machine.
815  """
816  if not self._is_enabled_is_enabled and not self.action_scriptaction_script.runs:
817  return
818 
819  self._is_enabled_is_enabled = False
820 
821  if self._async_detach_triggers_async_detach_triggers is not None:
822  self._async_detach_triggers_async_detach_triggers()
823  self._async_detach_triggers_async_detach_triggers = None
824 
825  if stop_actions:
826  await self.action_scriptaction_script.async_stop()
827 
828  def _log_callback(self, level: int, msg: str, **kwargs: Any) -> None:
829  """Log helper callback."""
830  self._logger_logger.log(level, "%s %s", msg, self.namename, **kwargs)
831 
833  self,
834  run_variables: dict[str, Any],
835  context: Context | None = None,
836  skip_condition: bool = False,
837  ) -> ScriptRunResult | None:
838  """Trigger automation if enabled.
839 
840  If the trigger starts but has a delay, the automation will be triggered
841  when the delay has passed so we need to make sure its still enabled before
842  executing the action.
843  """
844  if not self._is_enabled_is_enabled:
845  return None
846  return await self.async_triggerasync_triggerasync_trigger(run_variables, context, skip_condition)
847 
849  self, home_assistant_start: bool
850  ) -> Callable[[], None] | None:
851  """Set up the triggers."""
852  this = None
853  if state := self.hasshass.states.get(self.entity_identity_id):
854  this = state.as_dict()
855  variables = {"this": this}
856  if self._trigger_variables_trigger_variables:
857  try:
858  variables = self._trigger_variables_trigger_variables.async_render(
859  self.hasshass,
860  variables,
861  limited=True,
862  )
863  except TemplateError as err:
864  self._logger_logger.error("Error rendering trigger variables: %s", err)
865  return None
866 
867  return await async_initialize_triggers(
868  self.hasshass,
869  self._trigger_config_trigger_config,
870  self._async_trigger_if_enabled_async_trigger_if_enabled,
871  DOMAIN,
872  str(self.namename),
873  self._log_callback_log_callback,
874  home_assistant_start,
875  variables,
876  )
877 
878 
879 @dataclass(slots=True)
881  """Container for prepared automation entity configuration."""
882 
883  config_block: ConfigType
884  list_no: int
885  raw_blueprint_inputs: ConfigType | None
886  raw_config: ConfigType | None
887  validation_error: str | None
888  validation_status: ValidationStatus
889 
890 
892  hass: HomeAssistant,
893  config: ConfigType,
894  wanted_automation_id: str | None,
895 ) -> list[AutomationEntityConfig]:
896  """Parse configuration and prepare automation entity configuration."""
897  automation_configs: list[AutomationEntityConfig] = []
898 
899  conf: list[ConfigType] = config[DOMAIN]
900 
901  for list_no, config_block in enumerate(conf):
902  automation_id: str | None = config_block.get(CONF_ID)
903  if wanted_automation_id is not None and automation_id != wanted_automation_id:
904  continue
905 
906  raw_config = cast(AutomationConfig, config_block).raw_config
907  raw_blueprint_inputs = cast(AutomationConfig, config_block).raw_blueprint_inputs
908  validation_error = cast(AutomationConfig, config_block).validation_error
909  validation_status = cast(AutomationConfig, config_block).validation_status
910  automation_configs.append(
912  config_block,
913  list_no,
914  raw_blueprint_inputs,
915  raw_config,
916  validation_error,
917  validation_status,
918  )
919  )
920 
921  return automation_configs
922 
923 
924 def _automation_name(automation_config: AutomationEntityConfig) -> str:
925  """Return the configured name of an automation."""
926  config_block = automation_config.config_block
927  list_no = automation_config.list_no
928  return config_block.get(CONF_ALIAS) or f"{DOMAIN} {list_no}"
929 
930 
932  hass: HomeAssistant, automation_configs: list[AutomationEntityConfig]
933 ) -> list[BaseAutomationEntity]:
934  """Create automation entities from prepared configuration."""
935  entities: list[BaseAutomationEntity] = []
936 
937  for automation_config in automation_configs:
938  config_block = automation_config.config_block
939 
940  automation_id: str | None = config_block.get(CONF_ID)
941  name = _automation_name(automation_config)
942 
943  if automation_config.validation_status != ValidationStatus.OK:
944  entities.append(
946  automation_id,
947  name,
948  automation_config.raw_config,
949  cast(str, automation_config.validation_error),
950  automation_config.validation_status,
951  )
952  )
953  continue
954 
955  initial_state: bool | None = config_block.get(CONF_INITIAL_STATE)
956 
957  action_script = Script(
958  hass,
959  config_block[CONF_ACTIONS],
960  name,
961  DOMAIN,
962  running_description="automation actions",
963  script_mode=config_block[CONF_MODE],
964  max_runs=config_block[CONF_MAX],
965  max_exceeded=config_block[CONF_MAX_EXCEEDED],
966  logger=LOGGER,
967  # We don't pass variables here
968  # Automation will already render them to use them in the condition
969  # and so will pass them on to the script.
970  )
971 
972  if CONF_CONDITIONS in config_block:
973  cond_func = await _async_process_if(hass, name, config_block)
974 
975  if cond_func is None:
976  continue
977  else:
978  cond_func = None
979 
980  # Add trigger variables to variables
981  variables = None
982  if CONF_TRIGGER_VARIABLES in config_block and CONF_VARIABLES in config_block:
983  variables = ScriptVariables(
984  dict(config_block[CONF_TRIGGER_VARIABLES].as_dict())
985  )
986  variables.variables.update(config_block[CONF_VARIABLES].as_dict())
987  elif CONF_TRIGGER_VARIABLES in config_block:
988  variables = config_block[CONF_TRIGGER_VARIABLES]
989  elif CONF_VARIABLES in config_block:
990  variables = config_block[CONF_VARIABLES]
991 
992  entity = AutomationEntity(
993  automation_id,
994  name,
995  config_block[CONF_TRIGGERS],
996  cond_func,
997  action_script,
998  initial_state,
999  variables,
1000  config_block.get(CONF_TRIGGER_VARIABLES),
1001  automation_config.raw_config,
1002  automation_config.raw_blueprint_inputs,
1003  config_block[CONF_TRACE],
1004  )
1005  entities.append(entity)
1006 
1007  return entities
1008 
1009 
1011  hass: HomeAssistant,
1012  config: dict[str, Any],
1013  component: EntityComponent[BaseAutomationEntity],
1014 ) -> None:
1015  """Process config and add automations."""
1016 
1017  def automation_matches_config(
1018  automation: BaseAutomationEntity, config: AutomationEntityConfig
1019  ) -> bool:
1020  name = _automation_name(config)
1021  return automation.name == name and automation.raw_config == config.raw_config
1022 
1023  def find_matches(
1024  automations: list[BaseAutomationEntity],
1025  automation_configs: list[AutomationEntityConfig],
1026  ) -> tuple[set[int], set[int]]:
1027  """Find matches between a list of automation entities and a list of configurations.
1028 
1029  An automation or configuration is only allowed to match at most once to handle
1030  the case of multiple automations with identical configuration.
1031 
1032  Returns a tuple of sets of indices: ({automation_matches}, {config_matches})
1033  """
1034  automation_matches: set[int] = set()
1035  config_matches: set[int] = set()
1036  automation_configs_with_id: dict[str, tuple[int, AutomationEntityConfig]] = {}
1037  automation_configs_without_id: list[tuple[int, AutomationEntityConfig]] = []
1038 
1039  for config_idx, automation_config in enumerate(automation_configs):
1040  if automation_id := automation_config.config_block.get(CONF_ID):
1041  automation_configs_with_id[automation_id] = (
1042  config_idx,
1043  automation_config,
1044  )
1045  continue
1046  automation_configs_without_id.append((config_idx, automation_config))
1047 
1048  for automation_idx, automation in enumerate(automations):
1049  if automation.unique_id:
1050  if automation.unique_id not in automation_configs_with_id:
1051  continue
1052  config_idx, automation_config = automation_configs_with_id.pop(
1053  automation.unique_id
1054  )
1055  if automation_matches_config(automation, automation_config):
1056  automation_matches.add(automation_idx)
1057  config_matches.add(config_idx)
1058  continue
1059 
1060  for config_idx, automation_config in automation_configs_without_id:
1061  if config_idx in config_matches:
1062  # Only allow an automation config to match at most once
1063  continue
1064  if automation_matches_config(automation, automation_config):
1065  automation_matches.add(automation_idx)
1066  config_matches.add(config_idx)
1067  # Only allow an automation to match at most once
1068  break
1069 
1070  return automation_matches, config_matches
1071 
1072  automation_configs = await _prepare_automation_config(hass, config, None)
1073  automations: list[BaseAutomationEntity] = list(component.entities)
1074 
1075  # Find automations and configurations which have matches
1076  automation_matches, config_matches = find_matches(automations, automation_configs)
1077 
1078  # Remove automations which have changed config or no longer exist
1079  tasks = [
1080  automation.async_remove()
1081  for idx, automation in enumerate(automations)
1082  if idx not in automation_matches
1083  ]
1084  await asyncio.gather(*tasks)
1085 
1086  # Create automations which have changed config or have been added
1087  updated_automation_configs = [
1088  config
1089  for idx, config in enumerate(automation_configs)
1090  if idx not in config_matches
1091  ]
1092  entities = await _create_automation_entities(hass, updated_automation_configs)
1093  await component.async_add_entities(entities)
1094 
1095 
1097  automation: BaseAutomationEntity | None, config: AutomationEntityConfig | None
1098 ) -> bool:
1099  """Return False if an automation's config has been changed."""
1100  if not automation:
1101  return False
1102  if not config:
1103  return False
1104  name = _automation_name(config)
1105  return automation.name == name and automation.raw_config == config.raw_config
1106 
1107 
1109  hass: HomeAssistant,
1110  config: dict[str, Any],
1111  component: EntityComponent[BaseAutomationEntity],
1112  automation_id: str,
1113 ) -> None:
1114  """Process config and add a single automation."""
1115 
1116  automation_configs = await _prepare_automation_config(hass, config, automation_id)
1117  automation = next(
1118  (x for x in component.entities if x.unique_id == automation_id), None
1119  )
1120  automation_config = automation_configs[0] if automation_configs else None
1121 
1122  if _automation_matches_config(automation, automation_config):
1123  return
1124 
1125  if automation:
1126  await automation.async_remove()
1127  entities = await _create_automation_entities(hass, automation_configs)
1128  await component.async_add_entities(entities)
1129 
1130 
1132  hass: HomeAssistant, name: str, config: dict[str, Any]
1133 ) -> IfAction | None:
1134  """Process if checks."""
1135  if_configs = config[CONF_CONDITIONS]
1136 
1137  try:
1138  if_action = await condition.async_conditions_from_config(
1139  hass, if_configs, LOGGER, name
1140  )
1141  except HomeAssistantError as ex:
1142  LOGGER.warning("Invalid condition: %s", ex)
1143  return None
1144 
1145  result: IfAction = if_action # type: ignore[assignment]
1146  result.config = if_configs
1147 
1148  return result
1149 
1150 
1151 @callback
1152 def _trigger_extract_devices(trigger_conf: dict) -> list[str]:
1153  """Extract devices from a trigger config."""
1154  if trigger_conf[CONF_PLATFORM] == "device":
1155  return [trigger_conf[CONF_DEVICE_ID]]
1156 
1157  if (
1158  trigger_conf[CONF_PLATFORM] == "event"
1159  and CONF_EVENT_DATA in trigger_conf
1160  and CONF_DEVICE_ID in trigger_conf[CONF_EVENT_DATA]
1161  and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID], str)
1162  ):
1163  return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]]
1164 
1165  if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf:
1166  return trigger_conf[CONF_DEVICE_ID] # type: ignore[no-any-return]
1167 
1168  return []
1169 
1170 
1171 @callback
1172 def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
1173  """Extract entities from a trigger config."""
1174  if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"):
1175  return trigger_conf[CONF_ENTITY_ID] # type: ignore[no-any-return]
1176 
1177  if trigger_conf[CONF_PLATFORM] == "calendar":
1178  return [trigger_conf[CONF_ENTITY_ID]]
1179 
1180  if trigger_conf[CONF_PLATFORM] == "zone":
1181  return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] # type: ignore[no-any-return]
1182 
1183  if trigger_conf[CONF_PLATFORM] == "geo_location":
1184  return [trigger_conf[CONF_ZONE]]
1185 
1186  if trigger_conf[CONF_PLATFORM] == "sun":
1187  return ["sun.sun"]
1188 
1189  if (
1190  trigger_conf[CONF_PLATFORM] == "event"
1191  and CONF_EVENT_DATA in trigger_conf
1192  and CONF_ENTITY_ID in trigger_conf[CONF_EVENT_DATA]
1193  and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID], str)
1194  and valid_entity_id(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID])
1195  ):
1196  return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]]
1197 
1198  return []
1199 
1200 
1201 @websocket_api.websocket_command({"type": "automation/config", "entity_id": str})
1203  hass: HomeAssistant,
1204  connection: websocket_api.ActiveConnection,
1205  msg: dict[str, Any],
1206 ) -> None:
1207  """Get automation config."""
1208  automation = hass.data[DATA_COMPONENT].get_entity(msg["entity_id"])
1209 
1210  if automation is None:
1211  connection.send_error(
1212  msg["id"], websocket_api.ERR_NOT_FOUND, "Entity not found"
1213  )
1214  return
1215 
1216  connection.send_result(
1217  msg["id"],
1218  {
1219  "config": automation.raw_config,
1220  },
1221  )
1222 
1223 
1224 # These can be removed if no deprecated constant are in this module anymore
1225 __getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
1226 __dir__ = partial(
1227  dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
1228 )
1229 __all__ = all_with_deprecated_constants(globals())
None _log_callback(self, int level, str msg, **Any kwargs)
Definition: __init__.py:828
ScriptRunResult|None async_trigger(self, dict[str, Any] run_variables, Context|None context=None, bool skip_condition=False)
Definition: __init__.py:655
None _async_disable(self, bool stop_actions=DEFAULT_STOP_ACTIONS)
Definition: __init__.py:810
Callable[[], None]|None _async_attach_triggers(self, bool home_assistant_start)
Definition: __init__.py:850
ScriptRunResult|None _async_trigger_if_enabled(self, dict[str, Any] run_variables, Context|None context=None, bool skip_condition=False)
Definition: __init__.py:837
None __init__(self, str|None automation_id, str name, list[ConfigType] trigger_config, IfAction|None cond_func, Script action_script, bool|None initial_state, ScriptVariables|None variables, ScriptVariables|None trigger_variables, ConfigType|None raw_config, ConfigType|None blueprint_inputs, ConfigType trace_config)
Definition: __init__.py:512
ScriptRunResult|None async_trigger(self, dict[str, Any] run_variables, Context|None context=None, bool skip_condition=False)
Definition: __init__.py:403
bool __call__(self, Mapping[str, Any]|None variables=None)
Definition: __init__.py:136
None __init__(self, str|None automation_id, str name, ConfigType|None raw_config, str validation_error, ValidationStatus validation_status)
Definition: __init__.py:423
None async_trigger(self, dict[str, Any] run_variables, Context|None context=None, bool skip_condition=False)
Definition: __init__.py:490
str|UndefinedType|None name(self)
Definition: entity.py:738
None async_set_context(self, Context context)
Definition: entity.py:937
blueprint.DomainBlueprints async_get_blueprints(HomeAssistant hass)
Definition: helpers.py:29
Generator[AutomationTrace] trace_automation(HomeAssistant hass, str|None automation_id, ConfigType|None config, ConfigType|None blueprint_inputs, Context context, ConfigType trace_config)
Definition: trace.py:58
list[str] devices_in_automation(HomeAssistant hass, str entity_id)
Definition: __init__.py:209
list[str] automations_with_area(HomeAssistant hass, str area_id)
Definition: __init__.py:215
list[str] automations_with_label(HomeAssistant hass, str label_id)
Definition: __init__.py:239
bool is_on(HomeAssistant hass, str entity_id)
Definition: __init__.py:155
IfAction|None _async_process_if(HomeAssistant hass, str name, dict[str, Any] config)
Definition: __init__.py:1133
None _async_process_config(HomeAssistant hass, dict[str, Any] config, EntityComponent[BaseAutomationEntity] component)
Definition: __init__.py:1014
list[str] _trigger_extract_devices(dict trigger_conf)
Definition: __init__.py:1152
list[str] _trigger_extract_entities(dict trigger_conf)
Definition: __init__.py:1172
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:275
str _automation_name(AutomationEntityConfig automation_config)
Definition: __init__.py:924
list[str] floors_in_automation(HomeAssistant hass, str entity_id)
Definition: __init__.py:233
list[str] automations_with_floor(HomeAssistant hass, str floor_id)
Definition: __init__.py:227
list[BaseAutomationEntity] _create_automation_entities(HomeAssistant hass, list[AutomationEntityConfig] automation_configs)
Definition: __init__.py:933
list[str] labels_in_automation(HomeAssistant hass, str entity_id)
Definition: __init__.py:245
list[str] automations_with_entity(HomeAssistant hass, str entity_id)
Definition: __init__.py:191
None _async_process_single_config(HomeAssistant hass, dict[str, Any] config, EntityComponent[BaseAutomationEntity] component, str automation_id)
Definition: __init__.py:1113
list[str] entities_in_automation(HomeAssistant hass, str entity_id)
Definition: __init__.py:197
list[str] areas_in_automation(HomeAssistant hass, str entity_id)
Definition: __init__.py:221
None websocket_config(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
Definition: __init__.py:1206
str|None blueprint_in_automation(HomeAssistant hass, str entity_id)
Definition: __init__.py:264
list[str] _x_in_automation(HomeAssistant hass, str entity_id, str property_name)
Definition: __init__.py:179
list[str] _automations_with_x(HomeAssistant hass, str referenced_id, str property_name)
Definition: __init__.py:165
list[str] automations_with_blueprint(HomeAssistant hass, str blueprint_path)
Definition: __init__.py:251
list[str] automations_with_device(HomeAssistant hass, str device_id)
Definition: __init__.py:203
bool _automation_matches_config(BaseAutomationEntity|None automation, AutomationEntityConfig|None config)
Definition: __init__.py:1098
list[AutomationEntityConfig] _prepare_automation_config(HomeAssistant hass, ConfigType config, str|None wanted_automation_id)
Definition: __init__.py:895
datetime|None parse_datetime(str|None value)
Definition: sensor.py:138
CalendarEntity get_entity(HomeAssistant hass, str entity_id)
Definition: trigger.py:96
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_stop(HomeAssistant hass)
Definition: discovery.py:694
None async_create_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:69
None async_delete_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:85
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)
Definition: deprecation.py:356
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))
Definition: service.py:1121
None script_execution_set(str reason, ServiceResponse response=None)
Definition: trace.py:235
Generator[None] trace_path(str|list[str] suffix)
Definition: trace.py:251
dict[str, deque[TraceElement]]|None trace_get(bool clear=True)
Definition: trace.py:194
None trace_append_element(TraceElement trace_element, int|None maxlen=None)
Definition: trace.py:184
CALLBACK_TYPE|None async_initialize_triggers(HomeAssistant hass, list[ConfigType] trigger_config, Callable action, str domain, str name, Callable log_cb, bool home_assistant_start=False, TemplateVarsType variables=None)
Definition: trigger.py:311