Home Assistant Unofficial Reference 2024.12.1
script.py
Go to the documentation of this file.
1 """Helpers to execute scripts."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import AsyncGenerator, Callable, Mapping, Sequence
7 from contextlib import asynccontextmanager
8 from contextvars import ContextVar
9 from copy import copy
10 from dataclasses import dataclass
11 from datetime import datetime, timedelta
12 from functools import partial
13 import itertools
14 import logging
15 from types import MappingProxyType
16 from typing import Any, Literal, TypedDict, cast, overload
17 
18 import async_interrupt
19 from propcache import cached_property
20 import voluptuous as vol
21 
22 from homeassistant import exceptions
23 from homeassistant.components import scene
24 from homeassistant.components.device_automation import action as device_action
25 from homeassistant.components.logger import LOGSEVERITY
26 from homeassistant.const import (
27  ATTR_AREA_ID,
28  ATTR_DEVICE_ID,
29  ATTR_ENTITY_ID,
30  ATTR_FLOOR_ID,
31  ATTR_LABEL_ID,
32  CONF_ALIAS,
33  CONF_CHOOSE,
34  CONF_CONDITION,
35  CONF_CONDITIONS,
36  CONF_CONTINUE_ON_ERROR,
37  CONF_CONTINUE_ON_TIMEOUT,
38  CONF_COUNT,
39  CONF_DEFAULT,
40  CONF_DELAY,
41  CONF_DEVICE_ID,
42  CONF_DOMAIN,
43  CONF_ELSE,
44  CONF_ENABLED,
45  CONF_ERROR,
46  CONF_EVENT,
47  CONF_EVENT_DATA,
48  CONF_EVENT_DATA_TEMPLATE,
49  CONF_FOR_EACH,
50  CONF_IF,
51  CONF_MODE,
52  CONF_PARALLEL,
53  CONF_REPEAT,
54  CONF_RESPONSE_VARIABLE,
55  CONF_SCENE,
56  CONF_SEQUENCE,
57  CONF_SERVICE,
58  CONF_SERVICE_DATA,
59  CONF_SERVICE_DATA_TEMPLATE,
60  CONF_SET_CONVERSATION_RESPONSE,
61  CONF_STOP,
62  CONF_TARGET,
63  CONF_THEN,
64  CONF_TIMEOUT,
65  CONF_UNTIL,
66  CONF_VARIABLES,
67  CONF_WAIT_FOR_TRIGGER,
68  CONF_WAIT_TEMPLATE,
69  CONF_WHILE,
70  EVENT_HOMEASSISTANT_STOP,
71  SERVICE_TURN_ON,
72 )
73 from homeassistant.core import (
74  Context,
75  Event,
76  HassJob,
77  HomeAssistant,
78  ServiceResponse,
79  State,
80  SupportsResponse,
81  callback,
82 )
83 from homeassistant.util import slugify
84 from homeassistant.util.async_ import create_eager_task
85 from homeassistant.util.dt import utcnow
86 from homeassistant.util.hass_dict import HassKey
87 from homeassistant.util.signal_type import SignalType, SignalTypeFormat
88 
89 from . import condition, config_validation as cv, service, template
90 from .condition import ConditionCheckerType, trace_condition_function
91 from .dispatcher import async_dispatcher_connect, async_dispatcher_send_internal
92 from .event import async_call_later, async_track_template
93 from .script_variables import ScriptVariables
94 from .template import Template
95 from .trace import (
96  TraceElement,
97  async_trace_path,
98  script_execution_set,
99  trace_append_element,
100  trace_id_get,
101  trace_path,
102  trace_path_get,
103  trace_path_stack_cv,
104  trace_set_result,
105  trace_stack_cv,
106  trace_stack_pop,
107  trace_stack_push,
108  trace_stack_top,
109  trace_update_result,
110 )
111 from .trigger import async_initialize_triggers, async_validate_trigger_config
112 from .typing import UNDEFINED, ConfigType, TemplateVarsType, UndefinedType
113 
114 SCRIPT_MODE_PARALLEL = "parallel"
115 SCRIPT_MODE_QUEUED = "queued"
116 SCRIPT_MODE_RESTART = "restart"
117 SCRIPT_MODE_SINGLE = "single"
118 SCRIPT_MODE_CHOICES = [
119  SCRIPT_MODE_PARALLEL,
120  SCRIPT_MODE_QUEUED,
121  SCRIPT_MODE_RESTART,
122  SCRIPT_MODE_SINGLE,
123 ]
124 DEFAULT_SCRIPT_MODE = SCRIPT_MODE_SINGLE
125 
126 CONF_MAX = "max"
127 DEFAULT_MAX = 10
128 
129 CONF_MAX_EXCEEDED = "max_exceeded"
130 _MAX_EXCEEDED_CHOICES = [*LOGSEVERITY, "SILENT"]
131 DEFAULT_MAX_EXCEEDED = "WARNING"
132 
133 ATTR_CUR = "current"
134 ATTR_MAX = "max"
135 
136 DATA_SCRIPTS: HassKey[list[ScriptData]] = HassKey("helpers.script")
137 DATA_SCRIPT_BREAKPOINTS: HassKey[dict[str, dict[str, set[str]]]] = HassKey(
138  "helpers.script_breakpoints"
139 )
140 DATA_NEW_SCRIPT_RUNS_NOT_ALLOWED: HassKey[None] = HassKey("helpers.script_not_allowed")
141 RUN_ID_ANY = "*"
142 NODE_ANY = "*"
143 
144 _LOGGER = logging.getLogger(__name__)
145 
146 _LOG_EXCEPTION = logging.ERROR + 1
147 _TIMEOUT_MSG = "Timeout reached, abort script."
148 
149 _SHUTDOWN_MAX_WAIT = 60
150 
151 
152 ACTION_TRACE_NODE_MAX_LEN = 20 # Max length of a trace node for repeated actions
153 
154 SCRIPT_BREAKPOINT_HIT = SignalType[str, str, str]("script_breakpoint_hit")
155 SCRIPT_DEBUG_CONTINUE_STOP: SignalTypeFormat[Literal["continue", "stop"]] = (
156  SignalTypeFormat("script_debug_continue_stop_{}_{}")
157 )
158 SCRIPT_DEBUG_CONTINUE_ALL = "script_debug_continue_all"
159 
160 script_stack_cv: ContextVar[list[str] | None] = ContextVar("script_stack", default=None)
161 
162 
163 class ScriptData(TypedDict):
164  """Store data related to script instance."""
165 
166  instance: Script
167  started_before_shutdown: bool
168 
169 
170 class ScriptStoppedError(Exception):
171  """Error to indicate that the script has been stopped."""
172 
173 
174 def _set_result_unless_done(future: asyncio.Future[None]) -> None:
175  """Set result of future unless it is done."""
176  if not future.done():
177  future.set_result(None)
178 
179 
180 def action_trace_append(variables: dict[str, Any], path: str) -> TraceElement:
181  """Append a TraceElement to trace[path]."""
182  trace_element = TraceElement(variables, path)
183  trace_append_element(trace_element, ACTION_TRACE_NODE_MAX_LEN)
184  return trace_element
185 
186 
187 @asynccontextmanager
188 async def trace_action(
189  hass: HomeAssistant,
190  script_run: _ScriptRun,
191  stop: asyncio.Future[None],
192  variables: dict[str, Any],
193 ) -> AsyncGenerator[TraceElement]:
194  """Trace action execution."""
195  path = trace_path_get()
196  trace_element = action_trace_append(variables, path)
197  trace_stack_push(trace_stack_cv, trace_element)
198 
199  trace_id = trace_id_get()
200  if trace_id:
201  key = trace_id[0]
202  run_id = trace_id[1]
203  breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS]
204  if key in breakpoints and (
205  (
206  run_id in breakpoints[key]
207  and (
208  path in breakpoints[key][run_id]
209  or NODE_ANY in breakpoints[key][run_id]
210  )
211  )
212  or (
213  RUN_ID_ANY in breakpoints[key]
214  and (
215  path in breakpoints[key][RUN_ID_ANY]
216  or NODE_ANY in breakpoints[key][RUN_ID_ANY]
217  )
218  )
219  ):
220  async_dispatcher_send_internal(
221  hass, SCRIPT_BREAKPOINT_HIT, key, run_id, path
222  )
223 
224  done = hass.loop.create_future()
225 
226  @callback
227  def async_continue_stop(
228  command: Literal["continue", "stop"] | None = None,
229  ) -> None:
230  if command == "stop":
233 
234  signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id)
235  remove_signal1 = async_dispatcher_connect(hass, signal, async_continue_stop)
236  remove_signal2 = async_dispatcher_connect(
237  hass, SCRIPT_DEBUG_CONTINUE_ALL, async_continue_stop
238  )
239 
240  await asyncio.wait([stop, done], return_when=asyncio.FIRST_COMPLETED)
241  remove_signal1()
242  remove_signal2()
243 
244  try:
245  yield trace_element
246  except _AbortScript as ex:
247  trace_element.set_error(ex.__cause__ or ex)
248  raise
249  except _ConditionFail:
250  # Clear errors which may have been set when evaluating the condition
251  trace_element.set_error(None)
252  raise
253  except _StopScript:
254  raise
255  except Exception as ex:
256  trace_element.set_error(ex)
257  raise
258  finally:
259  trace_stack_pop(trace_stack_cv)
260 
261 
263  schema: Mapping[Any, Any], default_script_mode: str, extra: int = vol.PREVENT_EXTRA
264 ) -> vol.Schema:
265  """Make a schema for a component that uses the script helper."""
266  return vol.Schema(
267  {
268  **schema,
269  vol.Optional(CONF_MODE, default=default_script_mode): vol.In(
270  SCRIPT_MODE_CHOICES
271  ),
272  vol.Optional(CONF_MAX, default=DEFAULT_MAX): vol.All(
273  vol.Coerce(int), vol.Range(min=2)
274  ),
275  vol.Optional(CONF_MAX_EXCEEDED, default=DEFAULT_MAX_EXCEEDED): vol.All(
276  vol.Upper, vol.In(_MAX_EXCEEDED_CHOICES)
277  ),
278  },
279  extra=extra,
280  )
281 
282 
283 STATIC_VALIDATION_ACTION_TYPES = (
284  cv.SCRIPT_ACTION_ACTIVATE_SCENE,
285  cv.SCRIPT_ACTION_CALL_SERVICE,
286  cv.SCRIPT_ACTION_DELAY,
287  cv.SCRIPT_ACTION_FIRE_EVENT,
288  cv.SCRIPT_ACTION_SET_CONVERSATION_RESPONSE,
289  cv.SCRIPT_ACTION_STOP,
290  cv.SCRIPT_ACTION_VARIABLES,
291  cv.SCRIPT_ACTION_WAIT_TEMPLATE,
292 )
293 
294 REPEAT_WARN_ITERATIONS = 5000
295 REPEAT_TERMINATE_ITERATIONS = 10000
296 
297 
299  hass: HomeAssistant, actions: list[ConfigType]
300 ) -> list[ConfigType]:
301  """Validate a list of actions."""
302  # No gather here because async_validate_action_config is unlikely
303  # to suspend and the overhead of creating many tasks is not worth it
304  return [await async_validate_action_config(hass, action) for action in actions]
305 
306 
308  hass: HomeAssistant, config: ConfigType
309 ) -> ConfigType:
310  """Validate config."""
311  action_type = cv.determine_script_action(config)
312 
313  if action_type in STATIC_VALIDATION_ACTION_TYPES:
314  pass
315 
316  elif action_type == cv.SCRIPT_ACTION_DEVICE_AUTOMATION:
317  config = await device_action.async_validate_action_config(hass, config)
318 
319  elif action_type == cv.SCRIPT_ACTION_CHECK_CONDITION:
320  config = await condition.async_validate_condition_config(hass, config)
321 
322  elif action_type == cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER:
323  config[CONF_WAIT_FOR_TRIGGER] = await async_validate_trigger_config(
324  hass, config[CONF_WAIT_FOR_TRIGGER]
325  )
326 
327  elif action_type == cv.SCRIPT_ACTION_REPEAT:
328  if CONF_UNTIL in config[CONF_REPEAT]:
329  conditions = await condition.async_validate_conditions_config(
330  hass, config[CONF_REPEAT][CONF_UNTIL]
331  )
332  config[CONF_REPEAT][CONF_UNTIL] = conditions
333  if CONF_WHILE in config[CONF_REPEAT]:
334  conditions = await condition.async_validate_conditions_config(
335  hass, config[CONF_REPEAT][CONF_WHILE]
336  )
337  config[CONF_REPEAT][CONF_WHILE] = conditions
338  config[CONF_REPEAT][CONF_SEQUENCE] = await async_validate_actions_config(
339  hass, config[CONF_REPEAT][CONF_SEQUENCE]
340  )
341 
342  elif action_type == cv.SCRIPT_ACTION_CHOOSE:
343  if CONF_DEFAULT in config:
344  config[CONF_DEFAULT] = await async_validate_actions_config(
345  hass, config[CONF_DEFAULT]
346  )
347 
348  for choose_conf in config[CONF_CHOOSE]:
349  conditions = await condition.async_validate_conditions_config(
350  hass, choose_conf[CONF_CONDITIONS]
351  )
352  choose_conf[CONF_CONDITIONS] = conditions
353  choose_conf[CONF_SEQUENCE] = await async_validate_actions_config(
354  hass, choose_conf[CONF_SEQUENCE]
355  )
356 
357  elif action_type == cv.SCRIPT_ACTION_IF:
358  config[CONF_IF] = await condition.async_validate_conditions_config(
359  hass, config[CONF_IF]
360  )
361  config[CONF_THEN] = await async_validate_actions_config(hass, config[CONF_THEN])
362  if CONF_ELSE in config:
363  config[CONF_ELSE] = await async_validate_actions_config(
364  hass, config[CONF_ELSE]
365  )
366 
367  elif action_type == cv.SCRIPT_ACTION_PARALLEL:
368  for parallel_conf in config[CONF_PARALLEL]:
369  parallel_conf[CONF_SEQUENCE] = await async_validate_actions_config(
370  hass, parallel_conf[CONF_SEQUENCE]
371  )
372 
373  elif action_type == cv.SCRIPT_ACTION_SEQUENCE:
374  config[CONF_SEQUENCE] = await async_validate_actions_config(
375  hass, config[CONF_SEQUENCE]
376  )
377 
378  else:
379  raise ValueError(f"No validation for {action_type}")
380 
381  return config
382 
383 
384 class _HaltScript(Exception):
385  """Throw if script needs to stop executing."""
386 
387 
389  """Throw if script needs to abort because of an unexpected error."""
390 
391 
393  """Throw if script needs to stop because a condition evaluated to False."""
394 
395 
397  """Throw if script needs to stop."""
398 
399  def __init__(self, message: str, response: Any) -> None:
400  """Initialize a halt exception."""
401  super().__init__(message)
402  self.responseresponse = response
403 
404 
406  """Manage Script sequence run."""
407 
408  _action: dict[str, Any]
409 
410  def __init__(
411  self,
412  hass: HomeAssistant,
413  script: Script,
414  variables: dict[str, Any],
415  context: Context | None,
416  log_exceptions: bool,
417  ) -> None:
418  self._hass_hass = hass
419  self._script_script = script
420  self._variables_variables = variables
421  self._context_context = context
422  self._log_exceptions_log_exceptions = log_exceptions
423  self._step_step = -1
424  self._started_started = False
425  self._stop_stop = hass.loop.create_future()
426  self._stopped_stopped = asyncio.Event()
427  self._conversation_response_conversation_response: str | None | UndefinedType = UNDEFINED
428 
429  def _changed(self) -> None:
430  if not self._stop_stop.done():
431  self._script_script._changed() # noqa: SLF001
432 
433  async def _async_get_condition(self, config: ConfigType) -> ConditionCheckerType:
434  return await self._script_script._async_get_condition(config) # noqa: SLF001
435 
436  def _log(
437  self, msg: str, *args: Any, level: int = logging.INFO, **kwargs: Any
438  ) -> None:
439  self._script_script._log(msg, *args, level=level, **kwargs) # noqa: SLF001
440 
441  def _step_log(self, default_message: str, timeout: float | None = None) -> None:
442  self._script_script.last_action = self._action.get(CONF_ALIAS, default_message)
443  _timeout = (
444  "" if timeout is None else f" (timeout: {timedelta(seconds=timeout)})"
445  )
446  self._log_log("Executing step %s%s", self._script_script.last_action, _timeout)
447 
448  async def async_run(self) -> ScriptRunResult | None:
449  """Run script."""
450  self._started_started = True
451  # Push the script to the script execution stack
452  if (script_stack := script_stack_cv.get()) is None:
453  script_stack = []
454  script_stack_cv.set(script_stack)
455  script_stack.append(self._script_script.unique_id)
456  response = None
457 
458  try:
459  self._log_log("Running %s", self._script_script.running_description)
460  for self._step_step, self._action in enumerate(self._script_script.sequence):
461  if self._stop_stop.done():
462  script_execution_set("cancelled")
463  break
464  await self._async_step_async_step(log_exceptions=False)
465  else:
466  script_execution_set("finished")
467  except _AbortScript:
468  script_execution_set("aborted")
469  # Let the _AbortScript bubble up if this is a sub-script
470  if not self._script_script.top_level:
471  raise
472  except _ConditionFail:
473  script_execution_set("aborted")
474  except _StopScript as err:
475  script_execution_set("finished", err.response)
476 
477  # Let the _StopScript bubble up if this is a sub-script
478  if not self._script_script.top_level:
479  raise
480 
481  response = err.response
482 
483  except Exception:
484  script_execution_set("error")
485  raise
486  finally:
487  # Pop the script from the script execution stack
488  script_stack.pop()
489  self._finish_finish()
490 
491  return ScriptRunResult(self._conversation_response_conversation_response, response, self._variables_variables)
492 
493  async def _async_step(self, log_exceptions: bool) -> None:
494  continue_on_error = self._action.get(CONF_CONTINUE_ON_ERROR, False)
495 
496  with trace_path(str(self._step_step)):
497  async with trace_action(
498  self._hass_hass, self, self._stop_stop, self._variables_variables
499  ) as trace_element:
500  if self._stop_stop.done():
501  return
502 
503  action = cv.determine_script_action(self._action)
504 
505  if CONF_ENABLED in self._action:
506  enabled = self._action[CONF_ENABLED]
507  if isinstance(enabled, Template):
508  try:
509  enabled = enabled.async_render(limited=True)
510  except exceptions.TemplateError as ex:
511  self._handle_exception_handle_exception(
512  ex,
513  continue_on_error,
514  self._log_exceptions_log_exceptions or log_exceptions,
515  )
516  if not enabled:
517  self._log_log(
518  "Skipped disabled step %s",
519  self._action.get(CONF_ALIAS, action),
520  )
521  trace_set_result(enabled=False)
522  return
523 
524  handler = f"_async_{action}_step"
525  try:
526  await getattr(self, handler)()
527  except Exception as ex: # noqa: BLE001
528  self._handle_exception_handle_exception(
529  ex, continue_on_error, self._log_exceptions_log_exceptions or log_exceptions
530  )
531  finally:
532  trace_element.update_variables(self._variables_variables)
533 
534  def _finish(self) -> None:
535  self._script_script._runs.remove(self) # noqa: SLF001
536  if not self._script_script.is_running:
537  self._script_script.last_action = None
538  self._changed_changed()
539  self._stopped_stopped.set()
540 
541  async def async_stop(self) -> None:
542  """Stop script run."""
543  _set_result_unless_done(self._stop_stop)
544  # If the script was never started
545  # the stopped event will never be
546  # set because the script will never
547  # start running
548  if self._started_started:
549  await self._stopped_stopped.wait()
550 
552  self, exception: Exception, continue_on_error: bool, log_exceptions: bool
553  ) -> None:
554  if not isinstance(exception, _HaltScript) and log_exceptions:
555  self._log_exception_log_exception(exception)
556 
557  if not continue_on_error:
558  raise exception
559 
560  # An explicit request to stop the script has been raised.
561  if isinstance(exception, _StopScript):
562  raise exception
563 
564  # These are incorrect scripts, and not runtime errors that need to
565  # be handled and thus cannot be stopped by `continue_on_error`.
566  if isinstance(
567  exception,
568  (
569  vol.Invalid,
575  ),
576  ):
577  raise exception
578 
579  # Only Home Assistant errors can be ignored.
580  if not isinstance(exception, exceptions.HomeAssistantError):
581  raise exception
582 
583  def _log_exception(self, exception: Exception) -> None:
584  action_type = cv.determine_script_action(self._action)
585 
586  error = str(exception)
587  level = logging.ERROR
588 
589  if isinstance(exception, vol.Invalid):
590  error_desc = "Invalid data"
591 
592  elif isinstance(exception, exceptions.TemplateError):
593  error_desc = "Error rendering template"
594 
595  elif isinstance(exception, exceptions.Unauthorized):
596  error_desc = "Unauthorized"
597 
598  elif isinstance(exception, exceptions.ServiceNotFound):
599  error_desc = "Service not found"
600 
601  elif isinstance(exception, exceptions.HomeAssistantError):
602  error_desc = "Error"
603 
604  else:
605  error_desc = "Unexpected error"
606  level = _LOG_EXCEPTION
607 
608  self._log_log(
609  "Error executing script. %s for %s at pos %s: %s",
610  error_desc,
611  action_type,
612  self._step_step + 1,
613  error,
614  level=level,
615  )
616 
617  def _get_pos_time_period_template(self, key: str) -> timedelta:
618  try:
619  return cv.positive_time_period( # type: ignore[no-any-return]
620  template.render_complex(self._action[key], self._variables_variables)
621  )
622  except (exceptions.TemplateError, vol.Invalid) as ex:
623  self._log_log(
624  "Error rendering %s %s template: %s",
625  self._script_script.name,
626  key,
627  ex,
628  level=logging.ERROR,
629  )
630  raise _AbortScript from ex
631 
632  async def _async_delay_step(self) -> None:
633  """Handle delay."""
634  delay_delta = self._get_pos_time_period_template_get_pos_time_period_template(CONF_DELAY)
635 
636  self._step_log_step_log(f"delay {delay_delta}")
637 
638  delay = delay_delta.total_seconds()
639  self._changed_changed()
640  if not delay:
641  # Handle an empty delay
642  trace_set_result(delay=delay, done=True)
643  return
644 
645  trace_set_result(delay=delay, done=False)
646  futures, timeout_handle, timeout_future = self._async_futures_with_timeout_async_futures_with_timeout_async_futures_with_timeout_async_futures_with_timeout(
647  delay
648  )
649 
650  try:
651  await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED)
652  finally:
653  if timeout_future.done():
654  trace_set_result(delay=delay, done=True)
655  else:
656  timeout_handle.cancel()
657 
658  def _get_timeout_seconds_from_action(self) -> float | None:
659  """Get the timeout from the action."""
660  if CONF_TIMEOUT in self._action:
661  return self._get_pos_time_period_template_get_pos_time_period_template(CONF_TIMEOUT).total_seconds()
662  return None
663 
664  async def _async_wait_template_step(self) -> None:
665  """Handle a wait template."""
666  timeout = self._get_timeout_seconds_from_action_get_timeout_seconds_from_action()
667  self._step_log_step_log("wait template", timeout)
668 
669  self._variables_variables["wait"] = {"remaining": timeout, "completed": False}
670  trace_set_result(wait=self._variables_variables["wait"])
671 
672  wait_template = self._action[CONF_WAIT_TEMPLATE]
673 
674  # check if condition already okay
675  if condition.async_template(self._hass_hass, wait_template, self._variables_variables, False):
676  self._variables_variables["wait"]["completed"] = True
677  self._changed_changed()
678  return
679 
680  if timeout == 0:
681  self._changed_changed()
682  self._async_handle_timeout_async_handle_timeout()
683  return
684 
685  futures, timeout_handle, timeout_future = self._async_futures_with_timeout_async_futures_with_timeout_async_futures_with_timeout_async_futures_with_timeout(
686  timeout
687  )
688  done = self._hass_hass.loop.create_future()
689  futures.append(done)
690 
691  @callback
692  def async_script_wait(
693  entity_id: str, from_s: State | None, to_s: State | None
694  ) -> None:
695  """Handle script after template condition is true."""
696  self._async_set_remaining_time_var_async_set_remaining_time_var(timeout_handle)
697  self._variables_variables["wait"]["completed"] = True
699 
700  unsub = async_track_template(
701  self._hass_hass, wait_template, async_script_wait, self._variables_variables
702  )
703  self._changed_changed()
704  await self._async_wait_with_optional_timeout_async_wait_with_optional_timeout(
705  futures, timeout_handle, timeout_future, unsub
706  )
707 
709  self, timeout_handle: asyncio.TimerHandle | None
710  ) -> None:
711  """Set the remaining time variable for a wait step."""
712  wait_var = self._variables_variables["wait"]
713  if timeout_handle:
714  wait_var["remaining"] = timeout_handle.when() - self._hass_hass.loop.time()
715  else:
716  wait_var["remaining"] = None
717 
718  async def _async_run_long_action[_T](
719  self, long_task: asyncio.Task[_T]
720  ) -> _T | None:
721  """Run a long task while monitoring for stop request."""
722  try:
723  async with async_interrupt.interrupt(self._stop_stop, ScriptStoppedError, None):
724  # if stop is set, interrupt will cancel inside the context
725  # manager which will cancel long_task, and raise
726  # ScriptStoppedError outside the context manager
727  return await long_task
728  except ScriptStoppedError as ex:
729  raise asyncio.CancelledError from ex
730 
731  async def _async_call_service_step(self) -> None:
732  """Call the service specified in the action."""
733  self._step_log_step_log("call service")
734 
735  params = service.async_prepare_call_from_config(
736  self._hass_hass, self._action, self._variables_variables
737  )
738 
739  # Validate response data parameters. This check ignores services that do
740  # not exist which will raise an appropriate error in the service call below.
741  response_variable = self._action.get(CONF_RESPONSE_VARIABLE)
742  return_response = response_variable is not None
743  if self._hass_hass.services.has_service(params[CONF_DOMAIN], params[CONF_SERVICE]):
744  supports_response = self._hass_hass.services.supports_response(
745  params[CONF_DOMAIN], params[CONF_SERVICE]
746  )
747  if supports_response == SupportsResponse.ONLY and not return_response:
748  raise vol.Invalid(
749  f"Script requires '{CONF_RESPONSE_VARIABLE}' for response data "
750  f"for service call {params[CONF_DOMAIN]}.{params[CONF_SERVICE]}"
751  )
752  if supports_response == SupportsResponse.NONE and return_response:
753  raise vol.Invalid(
754  f"Script does not support '{CONF_RESPONSE_VARIABLE}' for service "
755  f"'{CONF_RESPONSE_VARIABLE}' which does not support response data."
756  )
757 
758  running_script = (
759  params[CONF_DOMAIN] == "automation"
760  and params[CONF_SERVICE] == "trigger"
761  or params[CONF_DOMAIN] in ("python_script", "script")
762  )
763  trace_set_result(params=params, running_script=running_script)
764  response_data = await self._async_run_long_action(
765  self._hass_hass.async_create_task_internal(
766  self._hass_hass.services.async_call(
767  **params,
768  blocking=True,
769  context=self._context_context,
770  return_response=return_response,
771  ),
772  eager_start=True,
773  )
774  )
775  if response_variable:
776  self._variables_variables[response_variable] = response_data
777 
778  async def _async_device_step(self) -> None:
779  """Perform the device automation specified in the action."""
780  self._step_log_step_log("device automation")
781  await device_action.async_call_action_from_config(
782  self._hass_hass, self._action, self._variables_variables, self._context_context
783  )
784 
785  async def _async_scene_step(self) -> None:
786  """Activate the scene specified in the action."""
787  self._step_log_step_log("activate scene")
788  trace_set_result(scene=self._action[CONF_SCENE])
789  await self._hass_hass.services.async_call(
790  scene.DOMAIN,
791  SERVICE_TURN_ON,
792  {ATTR_ENTITY_ID: self._action[CONF_SCENE]},
793  blocking=True,
794  context=self._context_context,
795  )
796 
797  async def _async_event_step(self) -> None:
798  """Fire an event."""
799  self._step_log_step_log(self._action.get(CONF_ALIAS, self._action[CONF_EVENT]))
800  event_data = {}
801  for conf in (CONF_EVENT_DATA, CONF_EVENT_DATA_TEMPLATE):
802  if conf not in self._action:
803  continue
804 
805  try:
806  event_data.update(
807  template.render_complex(self._action[conf], self._variables_variables)
808  )
809  except exceptions.TemplateError as ex:
810  self._log_log(
811  "Error rendering event data template: %s", ex, level=logging.ERROR
812  )
813 
814  trace_set_result(event=self._action[CONF_EVENT], event_data=event_data)
815  self._hass_hass.bus.async_fire_internal(
816  self._action[CONF_EVENT], event_data, context=self._context_context
817  )
818 
819  async def _async_condition_step(self) -> None:
820  """Test if condition is matching."""
821  self._script_script.last_action = self._action.get(
822  CONF_ALIAS, self._action[CONF_CONDITION]
823  )
824  cond = await self._async_get_condition_async_get_condition(self._action)
825  try:
826  trace_element = trace_stack_top(trace_stack_cv)
827  if trace_element:
828  trace_element.reuse_by_child = True
829  check = cond(self._hass_hass, self._variables_variables)
830  except exceptions.ConditionError as ex:
831  _LOGGER.warning("Error in 'condition' evaluation:\n%s", ex)
832  check = False
833 
834  self._log_log("Test condition %s: %s", self._script_script.last_action, check)
835  trace_update_result(result=check)
836  if not check:
837  raise _ConditionFail
838 
840  self,
841  conditions: list[ConditionCheckerType],
842  name: str,
843  condition_path: str | None = None,
844  ) -> bool | None:
845  if condition_path is None:
846  condition_path = name
847 
848  @trace_condition_function
849  def traced_test_conditions(
850  hass: HomeAssistant, variables: TemplateVarsType
851  ) -> bool | None:
852  try:
853  with trace_path(condition_path):
854  for idx, cond in enumerate(conditions):
855  with trace_path(str(idx)):
856  if cond(hass, variables) is False:
857  return False
858  except exceptions.ConditionError as ex:
859  _LOGGER.warning("Error in '%s[%s]' evaluation: %s", name, idx, ex)
860  return None
861 
862  return True
863 
864  return traced_test_conditions(self._hass_hass, self._variables_variables)
865 
866  @async_trace_path("repeat")
867  async def _async_repeat_step(self) -> None: # noqa: C901
868  """Repeat a sequence."""
869  description = self._action.get(CONF_ALIAS, "sequence")
870  repeat = self._action[CONF_REPEAT]
871 
872  saved_repeat_vars = self._variables_variables.get("repeat")
873 
874  def set_repeat_var(
875  iteration: int, count: int | None = None, item: Any = None
876  ) -> None:
877  repeat_vars = {"first": iteration == 1, "index": iteration}
878  if count:
879  repeat_vars["last"] = iteration == count
880  if item is not None:
881  repeat_vars["item"] = item
882  self._variables_variables["repeat"] = repeat_vars
883 
884  script = self._script_script._get_repeat_script(self._step_step) # noqa: SLF001
885  warned_too_many_loops = False
886 
887  async def async_run_sequence(iteration: int, extra_msg: str = "") -> None:
888  self._log_log("Repeating %s: Iteration %i%s", description, iteration, extra_msg)
889  with trace_path("sequence"):
890  await self._async_run_script_async_run_script(script)
891 
892  if CONF_COUNT in repeat:
893  count = repeat[CONF_COUNT]
894  if isinstance(count, template.Template):
895  try:
896  count = int(count.async_render(self._variables_variables))
897  except (exceptions.TemplateError, ValueError) as ex:
898  self._log_log(
899  "Error rendering %s repeat count template: %s",
900  self._script_script.name,
901  ex,
902  level=logging.ERROR,
903  )
904  raise _AbortScript from ex
905  extra_msg = f" of {count}"
906  for iteration in range(1, count + 1):
907  set_repeat_var(iteration, count)
908  await async_run_sequence(iteration, extra_msg)
909  if self._stop_stop.done():
910  break
911 
912  elif CONF_FOR_EACH in repeat:
913  try:
914  items = template.render_complex(repeat[CONF_FOR_EACH], self._variables_variables)
915  except (exceptions.TemplateError, ValueError) as ex:
916  self._log_log(
917  "Error rendering %s repeat for each items template: %s",
918  self._script_script.name,
919  ex,
920  level=logging.ERROR,
921  )
922  raise _AbortScript from ex
923 
924  if not isinstance(items, list):
925  self._log_log(
926  "Repeat 'for_each' must be a list of items in %s, got: %s",
927  self._script_script.name,
928  items,
929  level=logging.ERROR,
930  )
931  raise _AbortScript("Repeat 'for_each' must be a list of items")
932 
933  count = len(items)
934  for iteration, item in enumerate(items, 1):
935  set_repeat_var(iteration, count, item)
936  extra_msg = f" of {count} with item: {item!r}"
937  if self._stop_stop.done():
938  break
939  await async_run_sequence(iteration, extra_msg)
940 
941  elif CONF_WHILE in repeat:
942  conditions = [
943  await self._async_get_condition_async_get_condition(config) for config in repeat[CONF_WHILE]
944  ]
945  for iteration in itertools.count(1):
946  set_repeat_var(iteration)
947  try:
948  if self._stop_stop.done():
949  break
950  if not self._test_conditions_test_conditions(conditions, "while"):
951  break
952  except exceptions.ConditionError as ex:
953  _LOGGER.warning("Error in 'while' evaluation:\n%s", ex)
954  break
955 
956  if iteration > 1:
957  if iteration > REPEAT_WARN_ITERATIONS:
958  if not warned_too_many_loops:
959  warned_too_many_loops = True
960  _LOGGER.warning(
961  "While condition %s in script `%s` looped %s times",
962  repeat[CONF_WHILE],
963  self._script_script.name,
964  REPEAT_WARN_ITERATIONS,
965  )
966 
967  if iteration > REPEAT_TERMINATE_ITERATIONS:
968  _LOGGER.critical(
969  "While condition %s in script `%s` "
970  "terminated because it looped %s times",
971  repeat[CONF_WHILE],
972  self._script_script.name,
973  REPEAT_TERMINATE_ITERATIONS,
974  )
975  raise _AbortScript(
976  f"While condition {repeat[CONF_WHILE]} "
977  "terminated because it looped "
978  f" {REPEAT_TERMINATE_ITERATIONS} times"
979  )
980 
981  # If the user creates a script with a tight loop,
982  # yield to the event loop so the system stays
983  # responsive while all the cpu time is consumed.
984  await asyncio.sleep(0)
985 
986  await async_run_sequence(iteration)
987 
988  elif CONF_UNTIL in repeat:
989  conditions = [
990  await self._async_get_condition_async_get_condition(config) for config in repeat[CONF_UNTIL]
991  ]
992  for iteration in itertools.count(1):
993  set_repeat_var(iteration)
994  await async_run_sequence(iteration)
995  try:
996  if self._stop_stop.done():
997  break
998  if self._test_conditions_test_conditions(conditions, "until") in [True, None]:
999  break
1000  except exceptions.ConditionError as ex:
1001  _LOGGER.warning("Error in 'until' evaluation:\n%s", ex)
1002  break
1003 
1004  if iteration >= REPEAT_WARN_ITERATIONS:
1005  if not warned_too_many_loops:
1006  warned_too_many_loops = True
1007  _LOGGER.warning(
1008  "Until condition %s in script `%s` looped %s times",
1009  repeat[CONF_UNTIL],
1010  self._script_script.name,
1011  REPEAT_WARN_ITERATIONS,
1012  )
1013 
1014  if iteration >= REPEAT_TERMINATE_ITERATIONS:
1015  _LOGGER.critical(
1016  "Until condition %s in script `%s` "
1017  "terminated because it looped %s times",
1018  repeat[CONF_UNTIL],
1019  self._script_script.name,
1020  REPEAT_TERMINATE_ITERATIONS,
1021  )
1022  raise _AbortScript(
1023  f"Until condition {repeat[CONF_UNTIL]} "
1024  "terminated because it looped "
1025  f"{REPEAT_TERMINATE_ITERATIONS} times"
1026  )
1027 
1028  # If the user creates a script with a tight loop,
1029  # yield to the event loop so the system stays responsive
1030  # while all the cpu time is consumed.
1031  await asyncio.sleep(0)
1032 
1033  if saved_repeat_vars:
1034  self._variables_variables["repeat"] = saved_repeat_vars
1035  else:
1036  self._variables_variables.pop("repeat", None) # Not set if count = 0
1037 
1038  async def _async_choose_step(self) -> None:
1039  """Choose a sequence."""
1040  choose_data = await self._script_script._async_get_choose_data(self._step_step) # noqa: SLF001
1041 
1042  with trace_path("choose"):
1043  for idx, (conditions, script) in enumerate(choose_data["choices"]):
1044  with trace_path(str(idx)):
1045  try:
1046  if self._test_conditions_test_conditions(conditions, "choose", "conditions"):
1047  trace_set_result(choice=idx)
1048  with trace_path("sequence"):
1049  await self._async_run_script_async_run_script(script)
1050  return
1051  except exceptions.ConditionError as ex:
1052  _LOGGER.warning("Error in 'choose' evaluation:\n%s", ex)
1053 
1054  if choose_data["default"] is not None:
1055  trace_set_result(choice="default")
1056  with trace_path(["default"]):
1057  await self._async_run_script_async_run_script(choose_data["default"])
1058 
1059  async def _async_if_step(self) -> None:
1060  """If sequence."""
1061  if_data = await self._script_script._async_get_if_data(self._step_step) # noqa: SLF001
1062 
1063  test_conditions: bool | None = False
1064  try:
1065  with trace_path("if"):
1066  test_conditions = self._test_conditions_test_conditions(
1067  if_data["if_conditions"], "if", "condition"
1068  )
1069  except exceptions.ConditionError as ex:
1070  _LOGGER.warning("Error in 'if' evaluation:\n%s", ex)
1071 
1072  if test_conditions:
1073  trace_set_result(choice="then")
1074  with trace_path("then"):
1075  await self._async_run_script_async_run_script(if_data["if_then"])
1076  return
1077 
1078  if if_data["if_else"] is not None:
1079  trace_set_result(choice="else")
1080  with trace_path("else"):
1081  await self._async_run_script_async_run_script(if_data["if_else"])
1082 
1083  @overload
1085  self,
1086  timeout: float,
1087  ) -> tuple[
1088  list[asyncio.Future[None]],
1089  asyncio.TimerHandle,
1090  asyncio.Future[None],
1091  ]: ...
1092 
1093  @overload
1095  self,
1096  timeout: None,
1097  ) -> tuple[
1098  list[asyncio.Future[None]],
1099  None,
1100  None,
1101  ]: ...
1102 
1104  self,
1105  timeout: float | None,
1106  ) -> tuple[
1107  list[asyncio.Future[None]],
1108  asyncio.TimerHandle | None,
1109  asyncio.Future[None] | None,
1110  ]:
1111  """Return a list of futures to wait for.
1112 
1113  The list will contain the stop future.
1114 
1115  If timeout is set, a timeout future and handle will be created
1116  and will be added to the list of futures.
1117  """
1118  timeout_handle: asyncio.TimerHandle | None = None
1119  timeout_future: asyncio.Future[None] | None = None
1120  futures: list[asyncio.Future[None]] = [self._stop_stop]
1121  if timeout:
1122  timeout_future = self._hass_hass.loop.create_future()
1123  timeout_handle = self._hass_hass.loop.call_later(
1124  timeout, _set_result_unless_done, timeout_future
1125  )
1126  futures.append(timeout_future)
1127  return futures, timeout_handle, timeout_future
1128 
1129  async def _async_wait_for_trigger_step(self) -> None:
1130  """Wait for a trigger event."""
1131  timeout = self._get_timeout_seconds_from_action_get_timeout_seconds_from_action()
1132 
1133  self._step_log_step_log("wait for trigger", timeout)
1134 
1135  variables = {**self._variables_variables}
1136  self._variables_variables["wait"] = {
1137  "remaining": timeout,
1138  "completed": False,
1139  "trigger": None,
1140  }
1141  trace_set_result(wait=self._variables_variables["wait"])
1142 
1143  if timeout == 0:
1144  self._changed_changed()
1145  self._async_handle_timeout_async_handle_timeout()
1146  return
1147 
1148  futures, timeout_handle, timeout_future = self._async_futures_with_timeout_async_futures_with_timeout_async_futures_with_timeout_async_futures_with_timeout(
1149  timeout
1150  )
1151  done = self._hass_hass.loop.create_future()
1152  futures.append(done)
1153 
1154  async def async_done(
1155  variables: dict[str, Any], context: Context | None = None
1156  ) -> None:
1157  self._async_set_remaining_time_var_async_set_remaining_time_var(timeout_handle)
1158  self._variables_variables["wait"]["completed"] = True
1159  self._variables_variables["wait"]["trigger"] = variables["trigger"]
1161 
1162  def log_cb(level: int, msg: str, **kwargs: Any) -> None:
1163  self._log_log(msg, level=level, **kwargs)
1164 
1165  remove_triggers = await async_initialize_triggers(
1166  self._hass_hass,
1167  self._action[CONF_WAIT_FOR_TRIGGER],
1168  async_done,
1169  self._script_script.domain,
1170  self._script_script.name,
1171  log_cb,
1172  variables=variables,
1173  )
1174  if not remove_triggers:
1175  return
1176  self._changed_changed()
1177  await self._async_wait_with_optional_timeout_async_wait_with_optional_timeout(
1178  futures, timeout_handle, timeout_future, remove_triggers
1179  )
1180 
1181  def _async_handle_timeout(self) -> None:
1182  """Handle timeout."""
1183  self._variables_variables["wait"]["remaining"] = 0.0
1184  if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
1185  self._log_log(_TIMEOUT_MSG)
1186  trace_set_result(wait=self._variables_variables["wait"], timeout=True)
1187  raise _AbortScript from TimeoutError()
1188 
1190  self,
1191  futures: list[asyncio.Future[None]],
1192  timeout_handle: asyncio.TimerHandle | None,
1193  timeout_future: asyncio.Future[None] | None,
1194  unsub: Callable[[], None],
1195  ) -> None:
1196  try:
1197  await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED)
1198  if timeout_future and timeout_future.done():
1199  self._async_handle_timeout_async_handle_timeout()
1200  finally:
1201  if timeout_future and not timeout_future.done() and timeout_handle:
1202  timeout_handle.cancel()
1203 
1204  unsub()
1205 
1206  async def _async_variables_step(self) -> None:
1207  """Set a variable value."""
1208  self._step_log_step_log("setting variables")
1209  self._variables_variables = self._action[CONF_VARIABLES].async_render(
1210  self._hass_hass, self._variables_variables, render_as_defaults=False
1211  )
1212 
1213  async def _async_set_conversation_response_step(self) -> None:
1214  """Set conversation response."""
1215  self._step_log_step_log("setting conversation response")
1216  resp: template.Template | None = self._action[CONF_SET_CONVERSATION_RESPONSE]
1217  if resp is None:
1218  self._conversation_response_conversation_response = None
1219  else:
1220  self._conversation_response_conversation_response = resp.async_render(
1221  variables=self._variables_variables, parse_result=False
1222  )
1223  trace_set_result(conversation_response=self._conversation_response_conversation_response)
1224 
1225  async def _async_stop_step(self) -> None:
1226  """Stop script execution."""
1227  stop = self._action[CONF_STOP]
1228  error = self._action.get(CONF_ERROR, False)
1229  trace_set_result(stop=stop, error=error)
1230  if error:
1231  self._log_log("Error script sequence: %s", stop)
1232  raise _AbortScript(stop)
1233 
1234  self._log_log("Stop script sequence: %s", stop)
1235  if CONF_RESPONSE_VARIABLE in self._action:
1236  try:
1237  response = self._variables_variables[self._action[CONF_RESPONSE_VARIABLE]]
1238  except KeyError as ex:
1239  raise _AbortScript(
1240  f"Response variable '{self._action[CONF_RESPONSE_VARIABLE]}' "
1241  "is not defined"
1242  ) from ex
1243  else:
1244  response = None
1245  raise _StopScript(stop, response)
1246 
1247  @async_trace_path("sequence")
1248  async def _async_sequence_step(self) -> None:
1249  """Run a sequence."""
1250  sequence = await self._script_script._async_get_sequence_script(self._step_step) # noqa: SLF001
1251  await self._async_run_script_async_run_script(sequence)
1252 
1253  @async_trace_path("parallel")
1254  async def _async_parallel_step(self) -> None:
1255  """Run a sequence in parallel."""
1256  scripts = await self._script_script._async_get_parallel_scripts(self._step_step) # noqa: SLF001
1257 
1258  async def async_run_with_trace(idx: int, script: Script) -> None:
1259  """Run a script with a trace path."""
1260  trace_path_stack_cv.set(copy(trace_path_stack_cv.get()))
1261  with trace_path([str(idx), "sequence"]):
1262  await self._async_run_script_async_run_script(script)
1263 
1264  results = await asyncio.gather(
1265  *(async_run_with_trace(idx, script) for idx, script in enumerate(scripts)),
1266  return_exceptions=True,
1267  )
1268  for result in results:
1269  if isinstance(result, Exception):
1270  raise result
1271 
1272  async def _async_run_script(self, script: Script) -> None:
1273  """Execute a script."""
1274  result = await self._async_run_long_action(
1275  self._hass_hass.async_create_task_internal(
1276  script.async_run(self._variables_variables, self._context_context), eager_start=True
1277  )
1278  )
1279  if result and result.conversation_response is not UNDEFINED:
1280  self._conversation_response_conversation_response = result.conversation_response
1281 
1282 
1284  """Manage queued Script sequence run."""
1285 
1286  lock_acquired = False
1287 
1288  async def async_run(self) -> None:
1289  """Run script."""
1290  # Wait for previous run, if any, to finish by attempting to acquire the script's
1291  # shared lock. At the same time monitor if we've been told to stop.
1292  try:
1293  async with async_interrupt.interrupt(self._stop_stop, ScriptStoppedError, None):
1294  await self._script_script._queue_lck.acquire() # noqa: SLF001
1295  except ScriptStoppedError as ex:
1296  # If we've been told to stop, then just finish up.
1297  self._finish_finish_finish()
1298  raise asyncio.CancelledError from ex
1299 
1300  self.lock_acquiredlock_acquiredlock_acquired = True
1301  # We've acquired the lock so we can go ahead and start the run.
1302  await super().async_run()
1303 
1304  def _finish(self) -> None:
1305  if self.lock_acquiredlock_acquiredlock_acquired:
1306  self._script_script._queue_lck.release() # noqa: SLF001
1307  self.lock_acquiredlock_acquiredlock_acquired = False
1308  super()._finish()
1309 
1310 
1311 @callback
1312 def _schedule_stop_scripts_after_shutdown(hass: HomeAssistant) -> None:
1313  """Stop running Script objects started after shutdown."""
1315  hass, _SHUTDOWN_MAX_WAIT, partial(_async_stop_scripts_after_shutdown, hass)
1316  )
1317 
1318 
1320  hass: HomeAssistant, point_in_time: datetime
1321 ) -> None:
1322  """Stop running Script objects started after shutdown."""
1323  hass.data[DATA_NEW_SCRIPT_RUNS_NOT_ALLOWED] = None
1324  running_scripts = [
1325  script for script in hass.data[DATA_SCRIPTS] if script["instance"].is_running
1326  ]
1327  if running_scripts:
1328  names = ", ".join([script["instance"].name for script in running_scripts])
1329  _LOGGER.warning("Stopping scripts running too long after shutdown: %s", names)
1330  await asyncio.gather(
1331  *(
1332  create_eager_task(script["instance"].async_stop(update_state=False))
1333  for script in running_scripts
1334  )
1335  )
1336 
1337 
1338 async def _async_stop_scripts_at_shutdown(hass: HomeAssistant, event: Event) -> None:
1339  """Stop running Script objects started before shutdown."""
1341 
1342  running_scripts = [
1343  script
1344  for script in hass.data[DATA_SCRIPTS]
1345  if script["instance"].is_running and script["started_before_shutdown"]
1346  ]
1347  if running_scripts:
1348  names = ", ".join([script["instance"].name for script in running_scripts])
1349  _LOGGER.debug("Stopping scripts running at shutdown: %s", names)
1350  await asyncio.gather(
1351  *(
1352  create_eager_task(script["instance"].async_stop())
1353  for script in running_scripts
1354  )
1355  )
1356 
1357 
1358 type _VarsType = dict[str, Any] | Mapping[str, Any] | MappingProxyType[str, Any]
1359 
1360 
1361 def _referenced_extract_ids(data: Any, key: str, found: set[str]) -> None:
1362  """Extract referenced IDs."""
1363  # Data may not exist, or be a template
1364  if not isinstance(data, dict):
1365  return
1366 
1367  item_ids = data.get(key)
1368 
1369  if item_ids is None or isinstance(item_ids, template.Template):
1370  return
1371 
1372  if isinstance(item_ids, str):
1373  found.add(item_ids)
1374  else:
1375  for item_id in item_ids:
1376  found.add(item_id)
1377 
1378 
1379 class _ChooseData(TypedDict):
1380  choices: list[tuple[list[ConditionCheckerType], Script]]
1381  default: Script | None
1382 
1383 
1384 class _IfData(TypedDict):
1385  if_conditions: list[ConditionCheckerType]
1386  if_then: Script
1387  if_else: Script | None
1388 
1389 
1390 @dataclass
1392  """Container with the result of a script run."""
1393 
1394  conversation_response: str | None | UndefinedType
1395  service_response: ServiceResponse
1396  variables: dict[str, Any]
1397 
1398 
1399 class Script:
1400  """Representation of a script."""
1401 
1403  self,
1404  hass: HomeAssistant,
1405  sequence: Sequence[dict[str, Any]],
1406  name: str,
1407  domain: str,
1408  *,
1409  # Used in "Running <running_description>" log message
1410  change_listener: Callable[[], Any] | None = None,
1411  copy_variables: bool = False,
1412  log_exceptions: bool = True,
1413  logger: logging.Logger | None = None,
1414  max_exceeded: str = DEFAULT_MAX_EXCEEDED,
1415  max_runs: int = DEFAULT_MAX,
1416  running_description: str | None = None,
1417  script_mode: str = DEFAULT_SCRIPT_MODE,
1418  top_level: bool = True,
1419  variables: ScriptVariables | None = None,
1420  ) -> None:
1421  """Initialize the script."""
1422  if not (all_scripts := hass.data.get(DATA_SCRIPTS)):
1423  all_scripts = hass.data[DATA_SCRIPTS] = []
1424  hass.bus.async_listen_once(
1425  EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass)
1426  )
1427  self.top_leveltop_level = top_level
1428  if top_level:
1429  all_scripts.append(
1430  {"instance": self, "started_before_shutdown": not hass.is_stopping}
1431  )
1432  if DATA_SCRIPT_BREAKPOINTS not in hass.data:
1433  hass.data[DATA_SCRIPT_BREAKPOINTS] = {}
1434 
1435  self._hass_hass = hass
1436  self.sequencesequence = sequence
1437  self.namename = name
1438  self.unique_idunique_id = f"{domain}.{name}-{id(self)}"
1439  self.domaindomain = domain
1440  self.running_descriptionrunning_description = running_description or f"{domain} script"
1441  self._change_listener_change_listener = change_listener
1442  self._change_listener_job_change_listener_job = (
1443  None if change_listener is None else HassJob(change_listener)
1444  )
1445 
1446  self.script_modescript_mode = script_mode
1447  self._set_logger_set_logger(logger)
1448  self._log_exceptions_log_exceptions = log_exceptions
1449 
1450  self.last_actionlast_action: str | None = None
1451  self.last_triggeredlast_triggered: datetime | None = None
1452 
1453  self._runs: list[_ScriptRun] = []
1454  self.max_runsmax_runs = max_runs
1455  self._max_exceeded_max_exceeded = max_exceeded
1456  if script_mode == SCRIPT_MODE_QUEUED:
1457  self._queue_lck_queue_lck = asyncio.Lock()
1458  self._config_cache: dict[frozenset[tuple[str, str]], ConditionCheckerType] = {}
1459  self._repeat_script: dict[int, Script] = {}
1460  self._choose_data: dict[int, _ChooseData] = {}
1461  self._if_data: dict[int, _IfData] = {}
1462  self._parallel_scripts: dict[int, list[Script]] = {}
1463  self._sequence_scripts: dict[int, Script] = {}
1464  self.variablesvariables = variables
1465  self._variables_dynamic_variables_dynamic = template.is_complex(variables)
1466  self._copy_variables_on_run_copy_variables_on_run = copy_variables
1467 
1468  @property
1469  def change_listener(self) -> Callable[..., Any] | None:
1470  """Return the change_listener."""
1471  return self._change_listener_change_listener
1472 
1473  @change_listener.setter
1474  def change_listener(self, change_listener: Callable[[], Any]) -> None:
1475  """Update the change_listener."""
1476  self._change_listener_change_listener = change_listener
1477  if (
1478  self._change_listener_job_change_listener_job is None
1479  or change_listener != self._change_listener_job_change_listener_job.target
1480  ):
1481  self._change_listener_job_change_listener_job = HassJob(change_listener)
1482 
1483  def _set_logger(self, logger: logging.Logger | None = None) -> None:
1484  if logger:
1485  self._logger_logger = logger
1486  else:
1487  self._logger_logger = logging.getLogger(f"{__name__}.{slugify(self.name)}")
1488 
1489  def update_logger(self, logger: logging.Logger | None = None) -> None:
1490  """Update logger."""
1491  self._set_logger_set_logger(logger)
1492  for script in self._repeat_script.values():
1493  script.update_logger(self._logger_logger)
1494  for parallel_scripts in self._parallel_scripts.values():
1495  for parallel_script in parallel_scripts:
1496  parallel_script.update_logger(self._logger_logger)
1497  for choose_data in self._choose_data.values():
1498  for _, script in choose_data["choices"]:
1499  script.update_logger(self._logger_logger)
1500  if choose_data["default"] is not None:
1501  choose_data["default"].update_logger(self._logger_logger)
1502  for if_data in self._if_data.values():
1503  if_data["if_then"].update_logger(self._logger_logger)
1504  if if_data["if_else"] is not None:
1505  if_data["if_else"].update_logger(self._logger_logger)
1506 
1507  def _changed(self) -> None:
1508  if self._change_listener_job_change_listener_job:
1509  self._hass_hass.async_run_hass_job(self._change_listener_job_change_listener_job)
1510 
1511  @callback
1512  def _chain_change_listener(self, sub_script: Script) -> None:
1513  if sub_script.is_running:
1514  self.last_actionlast_action = sub_script.last_action
1515  self._changed_changed()
1516 
1517  @property
1518  def is_running(self) -> bool:
1519  """Return true if script is on."""
1520  return len(self._runs) > 0
1521 
1522  @property
1523  def runs(self) -> int:
1524  """Return the number of current runs."""
1525  return len(self._runs)
1526 
1527  @property
1528  def supports_max(self) -> bool:
1529  """Return true if the current mode support max."""
1530  return self.script_modescript_mode in (SCRIPT_MODE_PARALLEL, SCRIPT_MODE_QUEUED)
1531 
1532  @cached_property
1533  def referenced_labels(self) -> set[str]:
1534  """Return a set of referenced labels."""
1535  referenced_labels: set[str] = set()
1536  Script._find_referenced_target(ATTR_LABEL_ID, referenced_labels, self.sequencesequence)
1537  return referenced_labels
1538 
1539  @cached_property
1540  def referenced_floors(self) -> set[str]:
1541  """Return a set of referenced fooors."""
1542  referenced_floors: set[str] = set()
1543  Script._find_referenced_target(ATTR_FLOOR_ID, referenced_floors, self.sequencesequence)
1544  return referenced_floors
1545 
1546  @cached_property
1547  def referenced_areas(self) -> set[str]:
1548  """Return a set of referenced areas."""
1549  referenced_areas: set[str] = set()
1550  Script._find_referenced_target(ATTR_AREA_ID, referenced_areas, self.sequencesequence)
1551  return referenced_areas
1552 
1553  @staticmethod
1555  target: Literal["area_id", "floor_id", "label_id"],
1556  referenced: set[str],
1557  sequence: Sequence[dict[str, Any]],
1558  ) -> None:
1559  """Find referenced target in a sequence."""
1560  for step in sequence:
1561  action = cv.determine_script_action(step)
1562 
1563  if action == cv.SCRIPT_ACTION_CALL_SERVICE:
1564  for data in (
1565  step.get(CONF_TARGET),
1566  step.get(CONF_SERVICE_DATA),
1567  step.get(CONF_SERVICE_DATA_TEMPLATE),
1568  ):
1569  _referenced_extract_ids(data, target, referenced)
1570 
1571  elif action == cv.SCRIPT_ACTION_CHOOSE:
1572  for choice in step[CONF_CHOOSE]:
1573  Script._find_referenced_target(
1574  target, referenced, choice[CONF_SEQUENCE]
1575  )
1576  if CONF_DEFAULT in step:
1577  Script._find_referenced_target(
1578  target, referenced, step[CONF_DEFAULT]
1579  )
1580 
1581  elif action == cv.SCRIPT_ACTION_IF:
1582  Script._find_referenced_target(target, referenced, step[CONF_THEN])
1583  if CONF_ELSE in step:
1584  Script._find_referenced_target(target, referenced, step[CONF_ELSE])
1585 
1586  elif action == cv.SCRIPT_ACTION_PARALLEL:
1587  for script in step[CONF_PARALLEL]:
1588  Script._find_referenced_target(
1589  target, referenced, script[CONF_SEQUENCE]
1590  )
1591 
1592  @cached_property
1593  def referenced_devices(self) -> set[str]:
1594  """Return a set of referenced devices."""
1595  referenced_devices: set[str] = set()
1596  Script._find_referenced_devices(referenced_devices, self.sequencesequence)
1597  return referenced_devices
1598 
1599  @staticmethod
1601  referenced: set[str], sequence: Sequence[dict[str, Any]]
1602  ) -> None:
1603  for step in sequence:
1604  action = cv.determine_script_action(step)
1605 
1606  if action == cv.SCRIPT_ACTION_CALL_SERVICE:
1607  for data in (
1608  step.get(CONF_TARGET),
1609  step.get(CONF_SERVICE_DATA),
1610  step.get(CONF_SERVICE_DATA_TEMPLATE),
1611  ):
1612  _referenced_extract_ids(data, ATTR_DEVICE_ID, referenced)
1613 
1614  elif action == cv.SCRIPT_ACTION_CHECK_CONDITION:
1615  referenced |= condition.async_extract_devices(step)
1616 
1617  elif action == cv.SCRIPT_ACTION_DEVICE_AUTOMATION:
1618  referenced.add(step[CONF_DEVICE_ID])
1619 
1620  elif action == cv.SCRIPT_ACTION_CHOOSE:
1621  for choice in step[CONF_CHOOSE]:
1622  for cond in choice[CONF_CONDITIONS]:
1623  referenced |= condition.async_extract_devices(cond)
1624  Script._find_referenced_devices(referenced, choice[CONF_SEQUENCE])
1625  if CONF_DEFAULT in step:
1626  Script._find_referenced_devices(referenced, step[CONF_DEFAULT])
1627 
1628  elif action == cv.SCRIPT_ACTION_IF:
1629  for cond in step[CONF_IF]:
1630  referenced |= condition.async_extract_devices(cond)
1631  Script._find_referenced_devices(referenced, step[CONF_THEN])
1632  if CONF_ELSE in step:
1633  Script._find_referenced_devices(referenced, step[CONF_ELSE])
1634 
1635  elif action == cv.SCRIPT_ACTION_PARALLEL:
1636  for script in step[CONF_PARALLEL]:
1637  Script._find_referenced_devices(referenced, script[CONF_SEQUENCE])
1638 
1639  @cached_property
1640  def referenced_entities(self) -> set[str]:
1641  """Return a set of referenced entities."""
1642  referenced_entities: set[str] = set()
1643  Script._find_referenced_entities(referenced_entities, self.sequencesequence)
1644  return referenced_entities
1645 
1646  @staticmethod
1648  referenced: set[str], sequence: Sequence[dict[str, Any]]
1649  ) -> None:
1650  for step in sequence:
1651  action = cv.determine_script_action(step)
1652 
1653  if action == cv.SCRIPT_ACTION_CALL_SERVICE:
1654  for data in (
1655  step,
1656  step.get(CONF_TARGET),
1657  step.get(CONF_SERVICE_DATA),
1658  step.get(CONF_SERVICE_DATA_TEMPLATE),
1659  ):
1660  _referenced_extract_ids(data, ATTR_ENTITY_ID, referenced)
1661 
1662  elif action == cv.SCRIPT_ACTION_CHECK_CONDITION:
1663  referenced |= condition.async_extract_entities(step)
1664 
1665  elif action == cv.SCRIPT_ACTION_ACTIVATE_SCENE:
1666  referenced.add(step[CONF_SCENE])
1667 
1668  elif action == cv.SCRIPT_ACTION_CHOOSE:
1669  for choice in step[CONF_CHOOSE]:
1670  for cond in choice[CONF_CONDITIONS]:
1671  referenced |= condition.async_extract_entities(cond)
1672  Script._find_referenced_entities(referenced, choice[CONF_SEQUENCE])
1673  if CONF_DEFAULT in step:
1674  Script._find_referenced_entities(referenced, step[CONF_DEFAULT])
1675 
1676  elif action == cv.SCRIPT_ACTION_IF:
1677  for cond in step[CONF_IF]:
1678  referenced |= condition.async_extract_entities(cond)
1679  Script._find_referenced_entities(referenced, step[CONF_THEN])
1680  if CONF_ELSE in step:
1681  Script._find_referenced_entities(referenced, step[CONF_ELSE])
1682 
1683  elif action == cv.SCRIPT_ACTION_PARALLEL:
1684  for script in step[CONF_PARALLEL]:
1685  Script._find_referenced_entities(referenced, script[CONF_SEQUENCE])
1686 
1687  def run(
1688  self, variables: _VarsType | None = None, context: Context | None = None
1689  ) -> None:
1690  """Run script."""
1691  asyncio.run_coroutine_threadsafe(
1692  self.async_runasync_run(variables, context), self._hass_hass.loop
1693  ).result()
1694 
1695  async def async_run(
1696  self,
1697  run_variables: _VarsType | None = None,
1698  context: Context | None = None,
1699  started_action: Callable[..., Any] | None = None,
1700  ) -> ScriptRunResult | None:
1701  """Run script."""
1702  if context is None:
1703  self._log_log(
1704  "Running script requires passing in a context", level=logging.WARNING
1705  )
1706  context = Context()
1707 
1708  # Prevent spawning new script runs when Home Assistant is shutting down
1709  if DATA_NEW_SCRIPT_RUNS_NOT_ALLOWED in self._hass_hass.data:
1710  self._log_log("Home Assistant is shutting down, starting script blocked")
1711  return None
1712 
1713  # Prevent spawning new script runs if not allowed by script mode
1714  if self.is_runningis_running:
1715  if self.script_modescript_mode == SCRIPT_MODE_SINGLE:
1716  if self._max_exceeded_max_exceeded != "SILENT":
1717  self._log_log("Already running", level=LOGSEVERITY[self._max_exceeded_max_exceeded])
1718  script_execution_set("failed_single")
1719  return None
1720  if self.script_modescript_mode != SCRIPT_MODE_RESTART and self.runsrunsruns == self.max_runsmax_runs:
1721  if self._max_exceeded_max_exceeded != "SILENT":
1722  self._log_log(
1723  "Maximum number of runs exceeded",
1724  level=LOGSEVERITY[self._max_exceeded_max_exceeded],
1725  )
1726  script_execution_set("failed_max_runs")
1727  return None
1728 
1729  # If this is a top level Script then make a copy of the variables in case they
1730  # are read-only, but more importantly, so as not to leak any variables created
1731  # during the run back to the caller.
1732  if self.top_leveltop_level:
1733  if self.variablesvariables:
1734  try:
1735  variables = self.variablesvariables.async_render(
1736  self._hass_hass,
1737  run_variables,
1738  )
1739  except exceptions.TemplateError as err:
1740  self._log_log("Error rendering variables: %s", err, level=logging.ERROR)
1741  raise
1742  elif run_variables:
1743  variables = dict(run_variables)
1744  else:
1745  variables = {}
1746 
1747  variables["context"] = context
1748  elif self._copy_variables_on_run_copy_variables_on_run:
1749  # This is not the top level script, variables have been turned to a dict
1750  variables = cast(dict[str, Any], copy(run_variables))
1751  else:
1752  # This is not the top level script, variables have been turned to a dict
1753  variables = cast(dict[str, Any], run_variables)
1754 
1755  # Prevent non-allowed recursive calls which will cause deadlocks when we try to
1756  # stop (restart) or wait for (queued) our own script run.
1757  script_stack = script_stack_cv.get()
1758  if (
1759  self.script_modescript_mode in (SCRIPT_MODE_RESTART, SCRIPT_MODE_QUEUED)
1760  and script_stack is not None
1761  and self.unique_idunique_id in script_stack
1762  ):
1763  script_execution_set("disallowed_recursion_detected")
1764  formatted_stack = [
1765  f"- {name_id.partition('-')[0]}" for name_id in script_stack
1766  ]
1767  self._log_log(
1768  "Disallowed recursion detected, "
1769  f"{script_stack[-1].partition('-')[0]} tried to start "
1770  f"{self.domain}.{self.name} which is already running "
1771  "in the current execution path; "
1772  "Traceback (most recent call last):\n"
1773  f"{"\n".join(formatted_stack)}",
1774  level=logging.WARNING,
1775  )
1776  return None
1777 
1778  if self.script_modescript_mode != SCRIPT_MODE_QUEUED:
1779  cls = _ScriptRun
1780  else:
1781  cls = _QueuedScriptRun
1782  run = cls(self._hass_hass, self, variables, context, self._log_exceptions_log_exceptions)
1783  has_existing_runs = bool(self._runs)
1784  self._runs.append(run)
1785  if self.script_modescript_mode == SCRIPT_MODE_RESTART and has_existing_runs:
1786  # When script mode is SCRIPT_MODE_RESTART, first add the new run and then
1787  # stop any other runs. If we stop other runs first, self.is_running will
1788  # return false after the other script runs were stopped until our task
1789  # resumes running. Its important that we check if there are existing
1790  # runs before sleeping as otherwise if two runs are started at the exact
1791  # same time they will cancel each other out.
1792  self._log_log("Restarting")
1793  await self.async_stopasync_stop(update_state=False, spare=run)
1794 
1795  if started_action:
1796  started_action()
1797  self.last_triggeredlast_triggered = utcnow()
1798  self._changed_changed()
1799 
1800  try:
1801  return await asyncio.shield(create_eager_task(run.async_run()))
1802  except asyncio.CancelledError:
1803  await run.async_stop()
1804  self._changed_changed()
1805  raise
1806 
1807  async def _async_stop(
1808  self, aws: list[asyncio.Task[None]], update_state: bool
1809  ) -> None:
1810  await asyncio.wait(aws)
1811  if update_state:
1812  self._changed_changed()
1813 
1814  async def async_stop(
1815  self, update_state: bool = True, spare: _ScriptRun | None = None
1816  ) -> None:
1817  """Stop running script."""
1818  # Collect a list of script runs to stop. This must be done before calling
1819  # asyncio.shield as asyncio.shield yields to the event loop, which would cause
1820  # us to wait for script runs added after the call to async_stop.
1821  aws = [
1822  create_eager_task(run.async_stop()) for run in self._runs if run != spare
1823  ]
1824  if not aws:
1825  return
1826  await asyncio.shield(create_eager_task(self._async_stop_async_stop(aws, update_state)))
1827 
1828  async def _async_get_condition(self, config: ConfigType) -> ConditionCheckerType:
1829  config_cache_key = frozenset((k, str(v)) for k, v in config.items())
1830  if not (cond := self._config_cache.get(config_cache_key)):
1831  cond = await condition.async_from_config(self._hass_hass, config)
1832  self._config_cache[config_cache_key] = cond
1833  return cond
1834 
1835  def _prep_repeat_script(self, step: int) -> Script:
1836  action = self.sequencesequence[step]
1837  step_name = action.get(CONF_ALIAS, f"Repeat at step {step+1}")
1838  sub_script = Script(
1839  self._hass_hass,
1840  action[CONF_REPEAT][CONF_SEQUENCE],
1841  f"{self.name}: {step_name}",
1842  self.domaindomain,
1843  running_description=self.running_descriptionrunning_description,
1844  script_mode=SCRIPT_MODE_PARALLEL,
1845  max_runs=self.max_runsmax_runs,
1846  logger=self._logger_logger,
1847  top_level=False,
1848  )
1849  sub_script.change_listener = partial(self._chain_change_listener_chain_change_listener, sub_script)
1850  return sub_script
1851 
1852  def _get_repeat_script(self, step: int) -> Script:
1853  if not (sub_script := self._repeat_script.get(step)):
1854  sub_script = self._prep_repeat_script_prep_repeat_script(step)
1855  self._repeat_script[step] = sub_script
1856  return sub_script
1857 
1858  async def _async_prep_choose_data(self, step: int) -> _ChooseData:
1859  action = self.sequencesequence[step]
1860  step_name = action.get(CONF_ALIAS, f"Choose at step {step+1}")
1861  choices = []
1862  for idx, choice in enumerate(action[CONF_CHOOSE], start=1):
1863  conditions = [
1864  await self._async_get_condition_async_get_condition(config)
1865  for config in choice.get(CONF_CONDITIONS, [])
1866  ]
1867  choice_name = choice.get(CONF_ALIAS, f"choice {idx}")
1868  sub_script = Script(
1869  self._hass_hass,
1870  choice[CONF_SEQUENCE],
1871  f"{self.name}: {step_name}: {choice_name}",
1872  self.domaindomain,
1873  running_description=self.running_descriptionrunning_description,
1874  script_mode=SCRIPT_MODE_PARALLEL,
1875  max_runs=self.max_runsmax_runs,
1876  logger=self._logger_logger,
1877  top_level=False,
1878  )
1879  sub_script.change_listener = partial(
1880  self._chain_change_listener_chain_change_listener, sub_script
1881  )
1882  choices.append((conditions, sub_script))
1883 
1884  default_script: Script | None
1885  if CONF_DEFAULT in action:
1886  default_script = Script(
1887  self._hass_hass,
1888  action[CONF_DEFAULT],
1889  f"{self.name}: {step_name}: default",
1890  self.domaindomain,
1891  running_description=self.running_descriptionrunning_description,
1892  script_mode=SCRIPT_MODE_PARALLEL,
1893  max_runs=self.max_runsmax_runs,
1894  logger=self._logger_logger,
1895  top_level=False,
1896  )
1897  default_script.change_listener = partial(
1898  self._chain_change_listener_chain_change_listener, default_script
1899  )
1900  else:
1901  default_script = None
1902 
1903  return {"choices": choices, "default": default_script}
1904 
1905  async def _async_get_choose_data(self, step: int) -> _ChooseData:
1906  if not (choose_data := self._choose_data.get(step)):
1907  choose_data = await self._async_prep_choose_data_async_prep_choose_data(step)
1908  self._choose_data[step] = choose_data
1909  return choose_data
1910 
1911  async def _async_prep_if_data(self, step: int) -> _IfData:
1912  """Prepare data for an if statement."""
1913  action = self.sequencesequence[step]
1914  step_name = action.get(CONF_ALIAS, f"If at step {step+1}")
1915 
1916  conditions = [
1917  await self._async_get_condition_async_get_condition(config) for config in action[CONF_IF]
1918  ]
1919 
1920  then_script = Script(
1921  self._hass_hass,
1922  action[CONF_THEN],
1923  f"{self.name}: {step_name}",
1924  self.domaindomain,
1925  running_description=self.running_descriptionrunning_description,
1926  script_mode=SCRIPT_MODE_PARALLEL,
1927  max_runs=self.max_runsmax_runs,
1928  logger=self._logger_logger,
1929  top_level=False,
1930  )
1931  then_script.change_listener = partial(self._chain_change_listener_chain_change_listener, then_script)
1932 
1933  if CONF_ELSE in action:
1934  else_script = Script(
1935  self._hass_hass,
1936  action[CONF_ELSE],
1937  f"{self.name}: {step_name}",
1938  self.domaindomain,
1939  running_description=self.running_descriptionrunning_description,
1940  script_mode=SCRIPT_MODE_PARALLEL,
1941  max_runs=self.max_runsmax_runs,
1942  logger=self._logger_logger,
1943  top_level=False,
1944  )
1945  else_script.change_listener = partial(
1946  self._chain_change_listener_chain_change_listener, else_script
1947  )
1948  else:
1949  else_script = None
1950 
1951  return _IfData(
1952  if_conditions=conditions,
1953  if_then=then_script,
1954  if_else=else_script,
1955  )
1956 
1957  async def _async_get_if_data(self, step: int) -> _IfData:
1958  if not (if_data := self._if_data.get(step)):
1959  if_data = await self._async_prep_if_data_async_prep_if_data(step)
1960  self._if_data[step] = if_data
1961  return if_data
1962 
1963  async def _async_prep_parallel_scripts(self, step: int) -> list[Script]:
1964  action = self.sequencesequence[step]
1965  step_name = action.get(CONF_ALIAS, f"Parallel action at step {step+1}")
1966  parallel_scripts: list[Script] = []
1967  for idx, parallel_script in enumerate(action[CONF_PARALLEL], start=1):
1968  parallel_name = parallel_script.get(CONF_ALIAS, f"parallel {idx}")
1969  parallel_script = Script(
1970  self._hass_hass,
1971  parallel_script[CONF_SEQUENCE],
1972  f"{self.name}: {step_name}: {parallel_name}",
1973  self.domaindomain,
1974  running_description=self.running_descriptionrunning_description,
1975  script_mode=SCRIPT_MODE_PARALLEL,
1976  max_runs=self.max_runsmax_runs,
1977  logger=self._logger_logger,
1978  top_level=False,
1979  copy_variables=True,
1980  )
1981  parallel_script.change_listener = partial(
1982  self._chain_change_listener_chain_change_listener, parallel_script
1983  )
1984  parallel_scripts.append(parallel_script)
1985 
1986  return parallel_scripts
1987 
1988  async def _async_get_parallel_scripts(self, step: int) -> list[Script]:
1989  if not (parallel_scripts := self._parallel_scripts.get(step)):
1990  parallel_scripts = await self._async_prep_parallel_scripts_async_prep_parallel_scripts(step)
1991  self._parallel_scripts[step] = parallel_scripts
1992  return parallel_scripts
1993 
1994  async def _async_prep_sequence_script(self, step: int) -> Script:
1995  """Prepare a sequence script."""
1996  action = self.sequencesequence[step]
1997  step_name = action.get(CONF_ALIAS, f"Sequence action at step {step+1}")
1998 
1999  sequence_script = Script(
2000  self._hass_hass,
2001  action[CONF_SEQUENCE],
2002  f"{self.name}: {step_name}",
2003  self.domaindomain,
2004  running_description=self.running_descriptionrunning_description,
2005  script_mode=SCRIPT_MODE_PARALLEL,
2006  max_runs=self.max_runsmax_runs,
2007  logger=self._logger_logger,
2008  top_level=False,
2009  )
2010  sequence_script.change_listener = partial(
2011  self._chain_change_listener_chain_change_listener, sequence_script
2012  )
2013 
2014  return sequence_script
2015 
2016  async def _async_get_sequence_script(self, step: int) -> Script:
2017  """Get a (cached) sequence script."""
2018  if not (sequence_script := self._sequence_scripts.get(step)):
2019  sequence_script = await self._async_prep_sequence_script_async_prep_sequence_script(step)
2020  self._sequence_scripts[step] = sequence_script
2021  return sequence_script
2022 
2023  def _log(
2024  self, msg: str, *args: Any, level: int = logging.INFO, **kwargs: Any
2025  ) -> None:
2026  msg = f"%s: {msg}"
2027  args = (self.namename, *args)
2028 
2029  if level == _LOG_EXCEPTION:
2030  self._logger_logger.exception(msg, *args, **kwargs)
2031  else:
2032  self._logger_logger.log(level, msg, *args, **kwargs)
2033 
2034 
2035 @callback
2037  hass: HomeAssistant, key: str, run_id: str | None, node: str
2038 ) -> None:
2039  """Clear a breakpoint."""
2040  run_id = run_id or RUN_ID_ANY
2041  breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS]
2042  if key not in breakpoints or run_id not in breakpoints[key]:
2043  return
2044  breakpoints[key][run_id].discard(node)
2045 
2046 
2047 @callback
2048 def breakpoint_clear_all(hass: HomeAssistant) -> None:
2049  """Clear all breakpoints."""
2050  hass.data[DATA_SCRIPT_BREAKPOINTS] = {}
2051 
2052 
2053 @callback
2055  hass: HomeAssistant, key: str, run_id: str | None, node: str
2056 ) -> None:
2057  """Set a breakpoint."""
2058  run_id = run_id or RUN_ID_ANY
2059  breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS]
2060  if key not in breakpoints:
2061  breakpoints[key] = {}
2062  if run_id not in breakpoints[key]:
2063  breakpoints[key][run_id] = set()
2064  breakpoints[key][run_id].add(node)
2065 
2066 
2067 @callback
2068 def breakpoint_list(hass: HomeAssistant) -> list[dict[str, Any]]:
2069  """List breakpoints."""
2070  breakpoints = hass.data[DATA_SCRIPT_BREAKPOINTS]
2071 
2072  return [
2073  {"key": key, "run_id": run_id, "node": node}
2074  for key in breakpoints
2075  for run_id in breakpoints[key]
2076  for node in breakpoints[key][run_id]
2077  ]
2078 
2079 
2080 @callback
2081 def debug_continue(hass: HomeAssistant, key: str, run_id: str) -> None:
2082  """Continue execution of a halted script."""
2083  # Clear any wildcard breakpoint
2084  breakpoint_clear(hass, key, run_id, NODE_ANY)
2085 
2086  signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id)
2087  async_dispatcher_send_internal(hass, signal, "continue")
2088 
2089 
2090 @callback
2091 def debug_step(hass: HomeAssistant, key: str, run_id: str) -> None:
2092  """Single step a halted script."""
2093  # Set a wildcard breakpoint
2094  breakpoint_set(hass, key, run_id, NODE_ANY)
2095 
2096  signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id)
2097  async_dispatcher_send_internal(hass, signal, "continue")
2098 
2099 
2100 @callback
2101 def debug_stop(hass: HomeAssistant, key: str, run_id: str) -> None:
2102  """Stop execution of a running or halted script."""
2103  signal = SCRIPT_DEBUG_CONTINUE_STOP.format(key, run_id)
2104  async_dispatcher_send_internal(hass, signal, "stop")
None _chain_change_listener(self, Script sub_script)
Definition: script.py:1512
Callable[..., Any]|None change_listener(self)
Definition: script.py:1469
set[str] referenced_entities(self)
Definition: script.py:1640
list[Script] _async_prep_parallel_scripts(self, int step)
Definition: script.py:1963
None update_logger(self, logging.Logger|None logger=None)
Definition: script.py:1489
list[Script] _async_get_parallel_scripts(self, int step)
Definition: script.py:1988
None _find_referenced_entities(set[str] referenced, Sequence[dict[str, Any]] sequence)
Definition: script.py:1649
None __init__(self, HomeAssistant hass, Sequence[dict[str, Any]] sequence, str name, str domain, *Callable[[], Any]|None change_listener=None, bool copy_variables=False, bool log_exceptions=True, logging.Logger|None logger=None, str max_exceeded=DEFAULT_MAX_EXCEEDED, int max_runs=DEFAULT_MAX, str|None running_description=None, str script_mode=DEFAULT_SCRIPT_MODE, bool top_level=True, ScriptVariables|None variables=None)
Definition: script.py:1420
_ChooseData _async_get_choose_data(self, int step)
Definition: script.py:1905
_IfData _async_prep_if_data(self, int step)
Definition: script.py:1911
None _set_logger(self, logging.Logger|None logger=None)
Definition: script.py:1483
Script _get_repeat_script(self, int step)
Definition: script.py:1852
None async_stop(self, bool update_state=True, _ScriptRun|None spare=None)
Definition: script.py:1816
None _log(self, str msg, *Any args, int level=logging.INFO, **Any kwargs)
Definition: script.py:2025
Script _async_get_sequence_script(self, int step)
Definition: script.py:2016
None _async_stop(self, list[asyncio.Task[None]] aws, bool update_state)
Definition: script.py:1809
Script _async_prep_sequence_script(self, int step)
Definition: script.py:1994
None run(self, _VarsType|None variables=None, Context|None context=None)
Definition: script.py:1689
ConditionCheckerType _async_get_condition(self, ConfigType config)
Definition: script.py:1828
ScriptRunResult|None async_run(self, _VarsType|None run_variables=None, Context|None context=None, Callable[..., Any]|None started_action=None)
Definition: script.py:1700
_IfData _async_get_if_data(self, int step)
Definition: script.py:1957
_ChooseData _async_prep_choose_data(self, int step)
Definition: script.py:1858
None _find_referenced_target(Literal["area_id", "floor_id", "label_id"] target, set[str] referenced, Sequence[dict[str, Any]] sequence)
Definition: script.py:1558
Script _prep_repeat_script(self, int step)
Definition: script.py:1835
None _find_referenced_devices(set[str] referenced, Sequence[dict[str, Any]] sequence)
Definition: script.py:1602
tuple[ list[asyncio.Future[None]], asyncio.TimerHandle, asyncio.Future[None],] _async_futures_with_timeout(self, float timeout)
Definition: script.py:1091
None _async_step(self, bool log_exceptions)
Definition: script.py:493
None _step_log(self, str default_message, float|None timeout=None)
Definition: script.py:441
None _handle_exception(self, Exception exception, bool continue_on_error, bool log_exceptions)
Definition: script.py:553
ConditionCheckerType _async_get_condition(self, ConfigType config)
Definition: script.py:433
None _log(self, str msg, *Any args, int level=logging.INFO, **Any kwargs)
Definition: script.py:438
tuple[ list[asyncio.Future[None]], asyncio.TimerHandle|None, asyncio.Future[None]|None,] _async_futures_with_timeout(self, float|None timeout)
Definition: script.py:1110
None _async_wait_with_optional_timeout(self, list[asyncio.Future[None]] futures, asyncio.TimerHandle|None timeout_handle, asyncio.Future[None]|None timeout_future, Callable[[], None] unsub)
Definition: script.py:1195
None _async_run_script(self, Script script)
Definition: script.py:1272
ScriptRunResult|None async_run(self)
Definition: script.py:448
timedelta _get_pos_time_period_template(self, str key)
Definition: script.py:617
None _async_set_remaining_time_var(self, asyncio.TimerHandle|None timeout_handle)
Definition: script.py:710
None __init__(self, HomeAssistant hass, Script script, dict[str, Any] variables, Context|None context, bool log_exceptions)
Definition: script.py:417
bool|None _test_conditions(self, list[ConditionCheckerType] conditions, str name, str|None condition_path=None)
Definition: script.py:844
tuple[ list[asyncio.Future[None]], None, None,] _async_futures_with_timeout(self, None timeout)
Definition: script.py:1101
None _log_exception(self, Exception exception)
Definition: script.py:583
float|None _get_timeout_seconds_from_action(self)
Definition: script.py:658
None __init__(self, str message, Any response)
Definition: script.py:399
bool add(self, _T matcher)
Definition: match.py:185
ConfigType async_validate_trigger_config(HomeAssistant hass, ConfigType config)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_stop(HomeAssistant hass)
Definition: discovery.py:694
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
CALLBACK_TYPE async_track_template(HomeAssistant hass, Template template, Callable[[str, State|None, State|None], Coroutine[Any, Any, None]|None] action, TemplateVarsType|None variables=None)
Definition: event.py:893
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597
ConfigType async_validate_action_config(HomeAssistant hass, ConfigType config)
Definition: script.py:309
None _referenced_extract_ids(Any data, str key, set[str] found)
Definition: script.py:1361
list[ConfigType] async_validate_actions_config(HomeAssistant hass, list[ConfigType] actions)
Definition: script.py:300
vol.Schema make_script_schema(Mapping[Any, Any] schema, str default_script_mode, int extra=vol.PREVENT_EXTRA)
Definition: script.py:264
AsyncGenerator[TraceElement] trace_action(HomeAssistant hass, _ScriptRun script_run, asyncio.Future[None] stop, dict[str, Any] variables)
Definition: script.py:193
None debug_continue(HomeAssistant hass, str key, str run_id)
Definition: script.py:2081
TraceElement action_trace_append(dict[str, Any] variables, str path)
Definition: script.py:180
None _set_result_unless_done(asyncio.Future[None] future)
Definition: script.py:174
None _async_stop_scripts_at_shutdown(HomeAssistant hass, Event event)
Definition: script.py:1338
None breakpoint_set(HomeAssistant hass, str key, str|None run_id, str node)
Definition: script.py:2056
None _schedule_stop_scripts_after_shutdown(HomeAssistant hass)
Definition: script.py:1312
list[dict[str, Any]] breakpoint_list(HomeAssistant hass)
Definition: script.py:2068
None breakpoint_clear_all(HomeAssistant hass)
Definition: script.py:2048
None breakpoint_clear(HomeAssistant hass, str key, str|None run_id, str node)
Definition: script.py:2038
None debug_step(HomeAssistant hass, str key, str run_id)
Definition: script.py:2091
None _async_stop_scripts_after_shutdown(HomeAssistant hass, datetime point_in_time)
Definition: script.py:1321
None debug_stop(HomeAssistant hass, str key, str run_id)
Definition: script.py:2101
None script_execution_set(str reason, ServiceResponse response=None)
Definition: trace.py:235
None trace_stack_pop(ContextVar[list[Any]|None] trace_stack_var)
Definition: trace.py:146
Generator[None] trace_path(str|list[str] suffix)
Definition: trace.py:251
None trace_update_result(**Any kwargs)
Definition: trace.py:222
tuple[str, str]|None trace_id_get()
Definition: trace.py:130
None trace_set_result(**Any kwargs)
Definition: trace.py:216
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