mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add sequence action for automations & scripts (#117690)
Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
parent
e70d8aec96
commit
9224997411
@ -1783,7 +1783,7 @@ _SCRIPT_STOP_SCHEMA = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
_SCRIPT_PARALLEL_SEQUENCE = vol.Schema(
|
_SCRIPT_SEQUENCE_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
**SCRIPT_ACTION_BASE_SCHEMA,
|
**SCRIPT_ACTION_BASE_SCHEMA,
|
||||||
vol.Required(CONF_SEQUENCE): SCRIPT_SCHEMA,
|
vol.Required(CONF_SEQUENCE): SCRIPT_SCHEMA,
|
||||||
@ -1802,7 +1802,7 @@ _SCRIPT_PARALLEL_SCHEMA = vol.Schema(
|
|||||||
{
|
{
|
||||||
**SCRIPT_ACTION_BASE_SCHEMA,
|
**SCRIPT_ACTION_BASE_SCHEMA,
|
||||||
vol.Required(CONF_PARALLEL): vol.All(
|
vol.Required(CONF_PARALLEL): vol.All(
|
||||||
ensure_list, [vol.Any(_SCRIPT_PARALLEL_SEQUENCE, _parallel_sequence_action)]
|
ensure_list, [vol.Any(_SCRIPT_SEQUENCE_SCHEMA, _parallel_sequence_action)]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -1818,6 +1818,7 @@ SCRIPT_ACTION_FIRE_EVENT = "event"
|
|||||||
SCRIPT_ACTION_IF = "if"
|
SCRIPT_ACTION_IF = "if"
|
||||||
SCRIPT_ACTION_PARALLEL = "parallel"
|
SCRIPT_ACTION_PARALLEL = "parallel"
|
||||||
SCRIPT_ACTION_REPEAT = "repeat"
|
SCRIPT_ACTION_REPEAT = "repeat"
|
||||||
|
SCRIPT_ACTION_SEQUENCE = "sequence"
|
||||||
SCRIPT_ACTION_SET_CONVERSATION_RESPONSE = "set_conversation_response"
|
SCRIPT_ACTION_SET_CONVERSATION_RESPONSE = "set_conversation_response"
|
||||||
SCRIPT_ACTION_STOP = "stop"
|
SCRIPT_ACTION_STOP = "stop"
|
||||||
SCRIPT_ACTION_VARIABLES = "variables"
|
SCRIPT_ACTION_VARIABLES = "variables"
|
||||||
@ -1844,6 +1845,7 @@ ACTIONS_MAP = {
|
|||||||
CONF_SERVICE_TEMPLATE: SCRIPT_ACTION_CALL_SERVICE,
|
CONF_SERVICE_TEMPLATE: SCRIPT_ACTION_CALL_SERVICE,
|
||||||
CONF_STOP: SCRIPT_ACTION_STOP,
|
CONF_STOP: SCRIPT_ACTION_STOP,
|
||||||
CONF_PARALLEL: SCRIPT_ACTION_PARALLEL,
|
CONF_PARALLEL: SCRIPT_ACTION_PARALLEL,
|
||||||
|
CONF_SEQUENCE: SCRIPT_ACTION_SEQUENCE,
|
||||||
CONF_SET_CONVERSATION_RESPONSE: SCRIPT_ACTION_SET_CONVERSATION_RESPONSE,
|
CONF_SET_CONVERSATION_RESPONSE: SCRIPT_ACTION_SET_CONVERSATION_RESPONSE,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1874,6 +1876,7 @@ ACTION_TYPE_SCHEMAS: dict[str, Callable[[Any], dict]] = {
|
|||||||
SCRIPT_ACTION_IF: _SCRIPT_IF_SCHEMA,
|
SCRIPT_ACTION_IF: _SCRIPT_IF_SCHEMA,
|
||||||
SCRIPT_ACTION_PARALLEL: _SCRIPT_PARALLEL_SCHEMA,
|
SCRIPT_ACTION_PARALLEL: _SCRIPT_PARALLEL_SCHEMA,
|
||||||
SCRIPT_ACTION_REPEAT: _SCRIPT_REPEAT_SCHEMA,
|
SCRIPT_ACTION_REPEAT: _SCRIPT_REPEAT_SCHEMA,
|
||||||
|
SCRIPT_ACTION_SEQUENCE: _SCRIPT_SEQUENCE_SCHEMA,
|
||||||
SCRIPT_ACTION_SET_CONVERSATION_RESPONSE: _SCRIPT_SET_CONVERSATION_RESPONSE_SCHEMA,
|
SCRIPT_ACTION_SET_CONVERSATION_RESPONSE: _SCRIPT_SET_CONVERSATION_RESPONSE_SCHEMA,
|
||||||
SCRIPT_ACTION_STOP: _SCRIPT_STOP_SCHEMA,
|
SCRIPT_ACTION_STOP: _SCRIPT_STOP_SCHEMA,
|
||||||
SCRIPT_ACTION_VARIABLES: _SCRIPT_SET_SCHEMA,
|
SCRIPT_ACTION_VARIABLES: _SCRIPT_SET_SCHEMA,
|
||||||
|
@ -370,6 +370,11 @@ async def async_validate_action_config(
|
|||||||
hass, parallel_conf[CONF_SEQUENCE]
|
hass, parallel_conf[CONF_SEQUENCE]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif action_type == cv.SCRIPT_ACTION_SEQUENCE:
|
||||||
|
config[CONF_SEQUENCE] = await async_validate_actions_config(
|
||||||
|
hass, config[CONF_SEQUENCE]
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No validation for {action_type}")
|
raise ValueError(f"No validation for {action_type}")
|
||||||
|
|
||||||
@ -431,9 +436,7 @@ class _ScriptRun:
|
|||||||
def _log(
|
def _log(
|
||||||
self, msg: str, *args: Any, level: int = logging.INFO, **kwargs: Any
|
self, msg: str, *args: Any, level: int = logging.INFO, **kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
self._script._log( # noqa: SLF001
|
self._script._log(msg, *args, level=level, **kwargs) # noqa: SLF001
|
||||||
msg, *args, level=level, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def _step_log(self, default_message, timeout=None):
|
def _step_log(self, default_message, timeout=None):
|
||||||
self._script.last_action = self._action.get(CONF_ALIAS, default_message)
|
self._script.last_action = self._action.get(CONF_ALIAS, default_message)
|
||||||
@ -1206,6 +1209,12 @@ class _ScriptRun:
|
|||||||
response = None
|
response = None
|
||||||
raise _StopScript(stop, response)
|
raise _StopScript(stop, response)
|
||||||
|
|
||||||
|
@async_trace_path("sequence")
|
||||||
|
async def _async_sequence_step(self) -> None:
|
||||||
|
"""Run a sequence."""
|
||||||
|
sequence = await self._script._async_get_sequence_script(self._step) # noqa: SLF001
|
||||||
|
await self._async_run_script(sequence)
|
||||||
|
|
||||||
@async_trace_path("parallel")
|
@async_trace_path("parallel")
|
||||||
async def _async_parallel_step(self) -> None:
|
async def _async_parallel_step(self) -> None:
|
||||||
"""Run a sequence in parallel."""
|
"""Run a sequence in parallel."""
|
||||||
@ -1416,6 +1425,7 @@ class Script:
|
|||||||
self._choose_data: dict[int, _ChooseData] = {}
|
self._choose_data: dict[int, _ChooseData] = {}
|
||||||
self._if_data: dict[int, _IfData] = {}
|
self._if_data: dict[int, _IfData] = {}
|
||||||
self._parallel_scripts: dict[int, list[Script]] = {}
|
self._parallel_scripts: dict[int, list[Script]] = {}
|
||||||
|
self._sequence_scripts: dict[int, Script] = {}
|
||||||
self.variables = variables
|
self.variables = variables
|
||||||
self._variables_dynamic = template.is_complex(variables)
|
self._variables_dynamic = template.is_complex(variables)
|
||||||
if self._variables_dynamic:
|
if self._variables_dynamic:
|
||||||
@ -1942,6 +1952,35 @@ class Script:
|
|||||||
self._parallel_scripts[step] = parallel_scripts
|
self._parallel_scripts[step] = parallel_scripts
|
||||||
return parallel_scripts
|
return parallel_scripts
|
||||||
|
|
||||||
|
async def _async_prep_sequence_script(self, step: int) -> Script:
|
||||||
|
"""Prepare a sequence script."""
|
||||||
|
action = self.sequence[step]
|
||||||
|
step_name = action.get(CONF_ALIAS, f"Sequence action at step {step+1}")
|
||||||
|
|
||||||
|
sequence_script = Script(
|
||||||
|
self._hass,
|
||||||
|
action[CONF_SEQUENCE],
|
||||||
|
f"{self.name}: {step_name}",
|
||||||
|
self.domain,
|
||||||
|
running_description=self.running_description,
|
||||||
|
script_mode=SCRIPT_MODE_PARALLEL,
|
||||||
|
max_runs=self.max_runs,
|
||||||
|
logger=self._logger,
|
||||||
|
top_level=False,
|
||||||
|
)
|
||||||
|
sequence_script.change_listener = partial(
|
||||||
|
self._chain_change_listener, sequence_script
|
||||||
|
)
|
||||||
|
|
||||||
|
return sequence_script
|
||||||
|
|
||||||
|
async def _async_get_sequence_script(self, step: int) -> Script:
|
||||||
|
"""Get a (cached) sequence script."""
|
||||||
|
if not (sequence_script := self._sequence_scripts.get(step)):
|
||||||
|
sequence_script = await self._async_prep_sequence_script(step)
|
||||||
|
self._sequence_scripts[step] = sequence_script
|
||||||
|
return sequence_script
|
||||||
|
|
||||||
def _log(
|
def _log(
|
||||||
self, msg: str, *args: Any, level: int = logging.INFO, **kwargs: Any
|
self, msg: str, *args: Any, level: int = logging.INFO, **kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -3538,6 +3538,103 @@ async def test_if_condition_validation(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sequence(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None:
|
||||||
|
"""Test sequence action."""
|
||||||
|
events = async_capture_events(hass, "test_event")
|
||||||
|
|
||||||
|
sequence = cv.SCRIPT_SCHEMA(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"alias": "Sequential group",
|
||||||
|
"sequence": [
|
||||||
|
{
|
||||||
|
"alias": "sequence group, action 1",
|
||||||
|
"event": "test_event",
|
||||||
|
"event_data": {
|
||||||
|
"sequence": "group",
|
||||||
|
"action": "1",
|
||||||
|
"what": "{{ what }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "sequence group, action 2",
|
||||||
|
"event": "test_event",
|
||||||
|
"event_data": {
|
||||||
|
"sequence": "group",
|
||||||
|
"action": "2",
|
||||||
|
"what": "{{ what }}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": "action 2",
|
||||||
|
"event": "test_event",
|
||||||
|
"event_data": {"action": "2", "what": "{{ what }}"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
|
||||||
|
|
||||||
|
await script_obj.async_run(MappingProxyType({"what": "world"}), Context())
|
||||||
|
|
||||||
|
assert len(events) == 3
|
||||||
|
assert events[0].data == {
|
||||||
|
"sequence": "group",
|
||||||
|
"action": "1",
|
||||||
|
"what": "world",
|
||||||
|
}
|
||||||
|
assert events[1].data == {
|
||||||
|
"sequence": "group",
|
||||||
|
"action": "2",
|
||||||
|
"what": "world",
|
||||||
|
}
|
||||||
|
assert events[2].data == {
|
||||||
|
"action": "2",
|
||||||
|
"what": "world",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert (
|
||||||
|
"Test Name: Sequential group: Executing step sequence group, action 1"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"Test Name: Sequential group: Executing step sequence group, action 2"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
assert "Test Name: Executing step action 2" in caplog.text
|
||||||
|
|
||||||
|
expected_trace = {
|
||||||
|
"0": [{"variables": {"what": "world"}}],
|
||||||
|
"0/sequence/0": [
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"event": "test_event",
|
||||||
|
"event_data": {"sequence": "group", "action": "1", "what": "world"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"0/sequence/1": [
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"event": "test_event",
|
||||||
|
"event_data": {"sequence": "group", "action": "2", "what": "world"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"1": [
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"event": "test_event",
|
||||||
|
"event_data": {"action": "2", "what": "world"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
assert_action_trace(expected_trace)
|
||||||
|
|
||||||
|
|
||||||
async def test_parallel(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None:
|
async def test_parallel(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None:
|
||||||
"""Test parallel action."""
|
"""Test parallel action."""
|
||||||
events = async_capture_events(hass, "test_event")
|
events = async_capture_events(hass, "test_event")
|
||||||
@ -5167,6 +5264,9 @@ async def test_validate_action_config(
|
|||||||
cv.SCRIPT_ACTION_PARALLEL: {
|
cv.SCRIPT_ACTION_PARALLEL: {
|
||||||
"parallel": [templated_device_action("parallel_event")],
|
"parallel": [templated_device_action("parallel_event")],
|
||||||
},
|
},
|
||||||
|
cv.SCRIPT_ACTION_SEQUENCE: {
|
||||||
|
"sequence": [templated_device_action("sequence_event")],
|
||||||
|
},
|
||||||
cv.SCRIPT_ACTION_SET_CONVERSATION_RESPONSE: {
|
cv.SCRIPT_ACTION_SET_CONVERSATION_RESPONSE: {
|
||||||
"set_conversation_response": "Hello world"
|
"set_conversation_response": "Hello world"
|
||||||
},
|
},
|
||||||
@ -5179,6 +5279,7 @@ async def test_validate_action_config(
|
|||||||
cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER: None,
|
cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER: None,
|
||||||
cv.SCRIPT_ACTION_IF: None,
|
cv.SCRIPT_ACTION_IF: None,
|
||||||
cv.SCRIPT_ACTION_PARALLEL: None,
|
cv.SCRIPT_ACTION_PARALLEL: None,
|
||||||
|
cv.SCRIPT_ACTION_SEQUENCE: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
for key in cv.ACTION_TYPE_SCHEMAS:
|
for key in cv.ACTION_TYPE_SCHEMAS:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user