Add stop/error script/automation action (#67340)
This commit is contained in:
parent
af737f2be8
commit
81d90b1bc7
4 changed files with 136 additions and 11 deletions
|
@ -146,6 +146,7 @@ CONF_ENTITY_CATEGORY: Final = "entity_category"
|
||||||
CONF_ENTITY_ID: Final = "entity_id"
|
CONF_ENTITY_ID: Final = "entity_id"
|
||||||
CONF_ENTITY_NAMESPACE: Final = "entity_namespace"
|
CONF_ENTITY_NAMESPACE: Final = "entity_namespace"
|
||||||
CONF_ENTITY_PICTURE_TEMPLATE: Final = "entity_picture_template"
|
CONF_ENTITY_PICTURE_TEMPLATE: Final = "entity_picture_template"
|
||||||
|
CONF_ERROR: Final = "error"
|
||||||
CONF_EVENT: Final = "event"
|
CONF_EVENT: Final = "event"
|
||||||
CONF_EVENT_DATA: Final = "event_data"
|
CONF_EVENT_DATA: Final = "event_data"
|
||||||
CONF_EVENT_DATA_TEMPLATE: Final = "event_data_template"
|
CONF_EVENT_DATA_TEMPLATE: Final = "event_data_template"
|
||||||
|
@ -226,6 +227,7 @@ CONF_SOURCE: Final = "source"
|
||||||
CONF_SSL: Final = "ssl"
|
CONF_SSL: Final = "ssl"
|
||||||
CONF_STATE: Final = "state"
|
CONF_STATE: Final = "state"
|
||||||
CONF_STATE_TEMPLATE: Final = "state_template"
|
CONF_STATE_TEMPLATE: Final = "state_template"
|
||||||
|
CONF_STOP: Final = "stop"
|
||||||
CONF_STRUCTURE: Final = "structure"
|
CONF_STRUCTURE: Final = "structure"
|
||||||
CONF_SWITCHES: Final = "switches"
|
CONF_SWITCHES: Final = "switches"
|
||||||
CONF_TARGET: Final = "target"
|
CONF_TARGET: Final = "target"
|
||||||
|
|
|
@ -44,6 +44,7 @@ from homeassistant.const import (
|
||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
CONF_ENTITY_NAMESPACE,
|
CONF_ENTITY_NAMESPACE,
|
||||||
|
CONF_ERROR,
|
||||||
CONF_EVENT,
|
CONF_EVENT,
|
||||||
CONF_EVENT_DATA,
|
CONF_EVENT_DATA,
|
||||||
CONF_EVENT_DATA_TEMPLATE,
|
CONF_EVENT_DATA_TEMPLATE,
|
||||||
|
@ -58,6 +59,7 @@ from homeassistant.const import (
|
||||||
CONF_SERVICE,
|
CONF_SERVICE,
|
||||||
CONF_SERVICE_TEMPLATE,
|
CONF_SERVICE_TEMPLATE,
|
||||||
CONF_STATE,
|
CONF_STATE,
|
||||||
|
CONF_STOP,
|
||||||
CONF_TARGET,
|
CONF_TARGET,
|
||||||
CONF_TIMEOUT,
|
CONF_TIMEOUT,
|
||||||
CONF_UNIT_SYSTEM_IMPERIAL,
|
CONF_UNIT_SYSTEM_IMPERIAL,
|
||||||
|
@ -1425,6 +1427,20 @@ _SCRIPT_SET_SCHEMA = vol.Schema(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_SCRIPT_STOP_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
**SCRIPT_ACTION_BASE_SCHEMA,
|
||||||
|
vol.Required(CONF_STOP): vol.Any(None, string),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
_SCRIPT_ERROR_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
**SCRIPT_ACTION_BASE_SCHEMA,
|
||||||
|
vol.Optional(CONF_ERROR): vol.Any(None, string),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
SCRIPT_ACTION_DELAY = "delay"
|
SCRIPT_ACTION_DELAY = "delay"
|
||||||
SCRIPT_ACTION_WAIT_TEMPLATE = "wait_template"
|
SCRIPT_ACTION_WAIT_TEMPLATE = "wait_template"
|
||||||
SCRIPT_ACTION_CHECK_CONDITION = "condition"
|
SCRIPT_ACTION_CHECK_CONDITION = "condition"
|
||||||
|
@ -1436,6 +1452,8 @@ SCRIPT_ACTION_REPEAT = "repeat"
|
||||||
SCRIPT_ACTION_CHOOSE = "choose"
|
SCRIPT_ACTION_CHOOSE = "choose"
|
||||||
SCRIPT_ACTION_WAIT_FOR_TRIGGER = "wait_for_trigger"
|
SCRIPT_ACTION_WAIT_FOR_TRIGGER = "wait_for_trigger"
|
||||||
SCRIPT_ACTION_VARIABLES = "variables"
|
SCRIPT_ACTION_VARIABLES = "variables"
|
||||||
|
SCRIPT_ACTION_STOP = "stop"
|
||||||
|
SCRIPT_ACTION_ERROR = "error"
|
||||||
|
|
||||||
|
|
||||||
def determine_script_action(action: dict[str, Any]) -> str:
|
def determine_script_action(action: dict[str, Any]) -> str:
|
||||||
|
@ -1473,6 +1491,12 @@ def determine_script_action(action: dict[str, Any]) -> str:
|
||||||
if CONF_SERVICE in action or CONF_SERVICE_TEMPLATE in action:
|
if CONF_SERVICE in action or CONF_SERVICE_TEMPLATE in action:
|
||||||
return SCRIPT_ACTION_CALL_SERVICE
|
return SCRIPT_ACTION_CALL_SERVICE
|
||||||
|
|
||||||
|
if CONF_STOP in action:
|
||||||
|
return SCRIPT_ACTION_STOP
|
||||||
|
|
||||||
|
if CONF_ERROR in action:
|
||||||
|
return SCRIPT_ACTION_ERROR
|
||||||
|
|
||||||
raise ValueError("Unable to determine action")
|
raise ValueError("Unable to determine action")
|
||||||
|
|
||||||
|
|
||||||
|
@ -1488,6 +1512,8 @@ ACTION_TYPE_SCHEMAS: dict[str, Callable[[Any], dict]] = {
|
||||||
SCRIPT_ACTION_CHOOSE: _SCRIPT_CHOOSE_SCHEMA,
|
SCRIPT_ACTION_CHOOSE: _SCRIPT_CHOOSE_SCHEMA,
|
||||||
SCRIPT_ACTION_WAIT_FOR_TRIGGER: _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA,
|
SCRIPT_ACTION_WAIT_FOR_TRIGGER: _SCRIPT_WAIT_FOR_TRIGGER_SCHEMA,
|
||||||
SCRIPT_ACTION_VARIABLES: _SCRIPT_SET_SCHEMA,
|
SCRIPT_ACTION_VARIABLES: _SCRIPT_SET_SCHEMA,
|
||||||
|
SCRIPT_ACTION_STOP: _SCRIPT_STOP_SCHEMA,
|
||||||
|
SCRIPT_ACTION_ERROR: _SCRIPT_ERROR_SCHEMA,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ from homeassistant.const import (
|
||||||
CONF_DELAY,
|
CONF_DELAY,
|
||||||
CONF_DEVICE_ID,
|
CONF_DEVICE_ID,
|
||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
|
CONF_ERROR,
|
||||||
CONF_EVENT,
|
CONF_EVENT,
|
||||||
CONF_EVENT_DATA,
|
CONF_EVENT_DATA,
|
||||||
CONF_EVENT_DATA_TEMPLATE,
|
CONF_EVENT_DATA_TEMPLATE,
|
||||||
|
@ -41,6 +42,7 @@ from homeassistant.const import (
|
||||||
CONF_SCENE,
|
CONF_SCENE,
|
||||||
CONF_SEQUENCE,
|
CONF_SEQUENCE,
|
||||||
CONF_SERVICE,
|
CONF_SERVICE,
|
||||||
|
CONF_STOP,
|
||||||
CONF_TARGET,
|
CONF_TARGET,
|
||||||
CONF_TIMEOUT,
|
CONF_TIMEOUT,
|
||||||
CONF_UNTIL,
|
CONF_UNTIL,
|
||||||
|
@ -191,9 +193,11 @@ async def trace_action(hass, script_run, stop, variables):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield trace_element
|
yield trace_element
|
||||||
except _StopScript as ex:
|
except _AbortScript as ex:
|
||||||
trace_element.set_error(ex.__cause__ or ex)
|
trace_element.set_error(ex.__cause__ or ex)
|
||||||
raise ex
|
raise ex
|
||||||
|
except _StopScript as ex:
|
||||||
|
raise ex
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
trace_element.set_error(ex)
|
trace_element.set_error(ex)
|
||||||
raise ex
|
raise ex
|
||||||
|
@ -227,6 +231,8 @@ STATIC_VALIDATION_ACTION_TYPES = (
|
||||||
cv.SCRIPT_ACTION_FIRE_EVENT,
|
cv.SCRIPT_ACTION_FIRE_EVENT,
|
||||||
cv.SCRIPT_ACTION_ACTIVATE_SCENE,
|
cv.SCRIPT_ACTION_ACTIVATE_SCENE,
|
||||||
cv.SCRIPT_ACTION_VARIABLES,
|
cv.SCRIPT_ACTION_VARIABLES,
|
||||||
|
cv.SCRIPT_ACTION_ERROR,
|
||||||
|
cv.SCRIPT_ACTION_STOP,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -295,6 +301,10 @@ async def async_validate_action_config(
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
class _AbortScript(Exception):
|
||||||
|
"""Throw if script needs to abort because of an unexpected error."""
|
||||||
|
|
||||||
|
|
||||||
class _StopScript(Exception):
|
class _StopScript(Exception):
|
||||||
"""Throw if script needs to stop."""
|
"""Throw if script needs to stop."""
|
||||||
|
|
||||||
|
@ -360,6 +370,8 @@ class _ScriptRun:
|
||||||
else:
|
else:
|
||||||
script_execution_set("finished")
|
script_execution_set("finished")
|
||||||
except _StopScript:
|
except _StopScript:
|
||||||
|
script_execution_set("finished")
|
||||||
|
except _AbortScript:
|
||||||
script_execution_set("aborted")
|
script_execution_set("aborted")
|
||||||
except Exception:
|
except Exception:
|
||||||
script_execution_set("error")
|
script_execution_set("error")
|
||||||
|
@ -378,7 +390,7 @@ class _ScriptRun:
|
||||||
handler = f"_async_{cv.determine_script_action(self._action)}_step"
|
handler = f"_async_{cv.determine_script_action(self._action)}_step"
|
||||||
await getattr(self, handler)()
|
await getattr(self, handler)()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if not isinstance(ex, _StopScript) and (
|
if not isinstance(ex, (_AbortScript, _StopScript)) and (
|
||||||
self._log_exceptions or log_exceptions
|
self._log_exceptions or log_exceptions
|
||||||
):
|
):
|
||||||
self._log_exception(ex)
|
self._log_exception(ex)
|
||||||
|
@ -443,7 +455,7 @@ class _ScriptRun:
|
||||||
ex,
|
ex,
|
||||||
level=logging.ERROR,
|
level=logging.ERROR,
|
||||||
)
|
)
|
||||||
raise _StopScript from ex
|
raise _AbortScript from ex
|
||||||
|
|
||||||
async def _async_delay_step(self):
|
async def _async_delay_step(self):
|
||||||
"""Handle delay."""
|
"""Handle delay."""
|
||||||
|
@ -509,7 +521,7 @@ class _ScriptRun:
|
||||||
if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
|
if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
|
||||||
self._log(_TIMEOUT_MSG)
|
self._log(_TIMEOUT_MSG)
|
||||||
trace_set_result(wait=self._variables["wait"], timeout=True)
|
trace_set_result(wait=self._variables["wait"], timeout=True)
|
||||||
raise _StopScript from ex
|
raise _AbortScript from ex
|
||||||
finally:
|
finally:
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
@ -643,7 +655,7 @@ class _ScriptRun:
|
||||||
self._log("Test condition %s: %s", self._script.last_action, check)
|
self._log("Test condition %s: %s", self._script.last_action, check)
|
||||||
trace_update_result(result=check)
|
trace_update_result(result=check)
|
||||||
if not check:
|
if not check:
|
||||||
raise _StopScript
|
raise _AbortScript
|
||||||
|
|
||||||
def _test_conditions(self, conditions, name, condition_path=None):
|
def _test_conditions(self, conditions, name, condition_path=None):
|
||||||
if condition_path is None:
|
if condition_path is None:
|
||||||
|
@ -700,7 +712,7 @@ class _ScriptRun:
|
||||||
ex,
|
ex,
|
||||||
level=logging.ERROR,
|
level=logging.ERROR,
|
||||||
)
|
)
|
||||||
raise _StopScript from ex
|
raise _AbortScript from ex
|
||||||
extra_msg = f" of {count}"
|
extra_msg = f" of {count}"
|
||||||
for iteration in range(1, count + 1):
|
for iteration in range(1, count + 1):
|
||||||
set_repeat_var(iteration, count)
|
set_repeat_var(iteration, count)
|
||||||
|
@ -820,7 +832,7 @@ class _ScriptRun:
|
||||||
if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
|
if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True):
|
||||||
self._log(_TIMEOUT_MSG)
|
self._log(_TIMEOUT_MSG)
|
||||||
trace_set_result(wait=self._variables["wait"], timeout=True)
|
trace_set_result(wait=self._variables["wait"], timeout=True)
|
||||||
raise _StopScript from ex
|
raise _AbortScript from ex
|
||||||
finally:
|
finally:
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
@ -833,6 +845,20 @@ class _ScriptRun:
|
||||||
self._hass, self._variables, render_as_defaults=False
|
self._hass, self._variables, render_as_defaults=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _async_stop_step(self):
|
||||||
|
"""Stop script execution."""
|
||||||
|
stop = self._action[CONF_STOP]
|
||||||
|
self._log("Stop script sequence: %s", stop)
|
||||||
|
trace_set_result(stop=stop)
|
||||||
|
raise _StopScript(stop)
|
||||||
|
|
||||||
|
async def _async_error_step(self):
|
||||||
|
"""Abort and error script execution."""
|
||||||
|
error = self._action[CONF_ERROR]
|
||||||
|
self._log("Error script sequence: %s", error)
|
||||||
|
trace_set_result(error=error)
|
||||||
|
raise _AbortScript(error)
|
||||||
|
|
||||||
async def _async_run_script(self, script: Script) -> None:
|
async def _async_run_script(self, script: Script) -> None:
|
||||||
"""Execute a script."""
|
"""Execute a script."""
|
||||||
await self._async_run_long_action(
|
await self._async_run_long_action(
|
||||||
|
|
|
@ -1438,7 +1438,7 @@ async def test_condition_warning(hass, caplog):
|
||||||
assert_action_trace(
|
assert_action_trace(
|
||||||
{
|
{
|
||||||
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
||||||
"1": [{"error_type": script._StopScript, "result": {"result": False}}],
|
"1": [{"error_type": script._AbortScript, "result": {"result": False}}],
|
||||||
"1/entity_id/0": [{"error_type": ConditionError}],
|
"1/entity_id/0": [{"error_type": ConditionError}],
|
||||||
},
|
},
|
||||||
expected_script_execution="aborted",
|
expected_script_execution="aborted",
|
||||||
|
@ -1492,7 +1492,7 @@ async def test_condition_basic(hass, caplog):
|
||||||
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
||||||
"1": [
|
"1": [
|
||||||
{
|
{
|
||||||
"error_type": script._StopScript,
|
"error_type": script._AbortScript,
|
||||||
"result": {"entities": ["test.entity"], "result": False},
|
"result": {"entities": ["test.entity"], "result": False},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -1547,7 +1547,7 @@ async def test_shorthand_template_condition(hass, caplog):
|
||||||
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
||||||
"1": [
|
"1": [
|
||||||
{
|
{
|
||||||
"error_type": script._StopScript,
|
"error_type": script._AbortScript,
|
||||||
"result": {"entities": ["test.entity"], "result": False},
|
"result": {"entities": ["test.entity"], "result": False},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -1613,7 +1613,7 @@ async def test_condition_validation(hass, caplog):
|
||||||
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
||||||
"1": [
|
"1": [
|
||||||
{
|
{
|
||||||
"error_type": script._StopScript,
|
"error_type": script._AbortScript,
|
||||||
"result": {"result": False},
|
"result": {"result": False},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -3508,6 +3508,8 @@ async def test_validate_action_config(hass):
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
cv.SCRIPT_ACTION_VARIABLES: {"variables": {"hello": "world"}},
|
cv.SCRIPT_ACTION_VARIABLES: {"variables": {"hello": "world"}},
|
||||||
|
cv.SCRIPT_ACTION_STOP: {"stop": "Stop it right there buddy..."},
|
||||||
|
cv.SCRIPT_ACTION_ERROR: {"error": "Stand up, and try again!"},
|
||||||
}
|
}
|
||||||
expected_templates = {
|
expected_templates = {
|
||||||
cv.SCRIPT_ACTION_CHECK_CONDITION: None,
|
cv.SCRIPT_ACTION_CHECK_CONDITION: None,
|
||||||
|
@ -3778,3 +3780,72 @@ async def test_platform_async_validate_action_config(hass):
|
||||||
platform.async_validate_action_config.return_value = config
|
platform.async_validate_action_config.return_value = config
|
||||||
await script.async_validate_action_config(hass, config)
|
await script.async_validate_action_config(hass, config)
|
||||||
platform.async_validate_action_config.assert_awaited()
|
platform.async_validate_action_config.assert_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_stop_action(hass, caplog):
|
||||||
|
"""Test if automation stops on calling the stop action."""
|
||||||
|
event = "test_event"
|
||||||
|
events = async_capture_events(hass, event)
|
||||||
|
|
||||||
|
alias = "stop step"
|
||||||
|
sequence = cv.SCRIPT_SCHEMA(
|
||||||
|
[
|
||||||
|
{"event": event},
|
||||||
|
{
|
||||||
|
"alias": alias,
|
||||||
|
"stop": "In the name of love",
|
||||||
|
},
|
||||||
|
{"event": event},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
|
||||||
|
|
||||||
|
await script_obj.async_run(context=Context())
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "Stop script sequence: In the name of love" in caplog.text
|
||||||
|
caplog.clear()
|
||||||
|
assert len(events) == 1
|
||||||
|
|
||||||
|
assert_action_trace(
|
||||||
|
{
|
||||||
|
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
||||||
|
"1": [{"result": {"stop": "In the name of love"}}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_error_action(hass, caplog):
|
||||||
|
"""Test if automation fails on calling the error action."""
|
||||||
|
event = "test_event"
|
||||||
|
events = async_capture_events(hass, event)
|
||||||
|
|
||||||
|
alias = "stop step"
|
||||||
|
sequence = cv.SCRIPT_SCHEMA(
|
||||||
|
[
|
||||||
|
{"event": event},
|
||||||
|
{
|
||||||
|
"alias": alias,
|
||||||
|
"error": "Epic one...",
|
||||||
|
},
|
||||||
|
{"event": event},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
|
||||||
|
|
||||||
|
await script_obj.async_run(context=Context())
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert "Test Name: Error script sequence: Epic one..." in caplog.text
|
||||||
|
caplog.clear()
|
||||||
|
assert len(events) == 1
|
||||||
|
|
||||||
|
assert_action_trace(
|
||||||
|
{
|
||||||
|
"0": [{"result": {"event": "test_event", "event_data": {}}}],
|
||||||
|
"1": [
|
||||||
|
{"error_type": script._AbortScript, "result": {"error": "Epic one..."}}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
expected_script_execution="aborted",
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue