Correct validation of repeats in scripts and automations (#60318)
* Correct validation of repeats in scripts and automations * Improve validation test
This commit is contained in:
parent
ad9c3a47cf
commit
ea102f71a6
2 changed files with 43 additions and 12 deletions
|
@ -270,7 +270,7 @@ async def async_validate_action_config(
|
||||||
)
|
)
|
||||||
|
|
||||||
elif action_type == cv.SCRIPT_ACTION_REPEAT:
|
elif action_type == cv.SCRIPT_ACTION_REPEAT:
|
||||||
config[CONF_SEQUENCE] = await async_validate_actions_config(
|
config[CONF_REPEAT][CONF_SEQUENCE] = await async_validate_actions_config(
|
||||||
hass, config[CONF_REPEAT][CONF_SEQUENCE]
|
hass, config[CONF_REPEAT][CONF_SEQUENCE]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from functools import reduce
|
||||||
import logging
|
import logging
|
||||||
|
import operator
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
@ -23,7 +25,7 @@ from homeassistant.const import (
|
||||||
)
|
)
|
||||||
from homeassistant.core import SERVICE_CALL_LIMIT, Context, CoreState, callback
|
from homeassistant.core import SERVICE_CALL_LIMIT, Context, CoreState, callback
|
||||||
from homeassistant.exceptions import ConditionError, ServiceNotFound
|
from homeassistant.exceptions import ConditionError, ServiceNotFound
|
||||||
from homeassistant.helpers import config_validation as cv, script, trace
|
from homeassistant.helpers import config_validation as cv, script, template, trace
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
@ -3021,6 +3023,15 @@ async def test_set_redefines_variable(hass, caplog):
|
||||||
|
|
||||||
async def test_validate_action_config(hass):
|
async def test_validate_action_config(hass):
|
||||||
"""Validate action config."""
|
"""Validate action config."""
|
||||||
|
|
||||||
|
def templated_device_action(message):
|
||||||
|
return {
|
||||||
|
"device_id": "abcd",
|
||||||
|
"domain": "mobile_app",
|
||||||
|
"message": f"{message} {{{{ 5 + 5}}}}",
|
||||||
|
"type": "notify",
|
||||||
|
}
|
||||||
|
|
||||||
configs = {
|
configs = {
|
||||||
cv.SCRIPT_ACTION_CALL_SERVICE: {"service": "light.turn_on"},
|
cv.SCRIPT_ACTION_CALL_SERVICE: {"service": "light.turn_on"},
|
||||||
cv.SCRIPT_ACTION_DELAY: {"delay": 5},
|
cv.SCRIPT_ACTION_DELAY: {"delay": 5},
|
||||||
|
@ -3031,24 +3042,22 @@ async def test_validate_action_config(hass):
|
||||||
cv.SCRIPT_ACTION_CHECK_CONDITION: {
|
cv.SCRIPT_ACTION_CHECK_CONDITION: {
|
||||||
"condition": "{{ states.light.kitchen.state == 'on' }}"
|
"condition": "{{ states.light.kitchen.state == 'on' }}"
|
||||||
},
|
},
|
||||||
cv.SCRIPT_ACTION_DEVICE_AUTOMATION: {
|
cv.SCRIPT_ACTION_DEVICE_AUTOMATION: templated_device_action("device"),
|
||||||
"domain": "light",
|
|
||||||
"entity_id": "light.kitchen",
|
|
||||||
"device_id": "abcd",
|
|
||||||
"type": "turn_on",
|
|
||||||
},
|
|
||||||
cv.SCRIPT_ACTION_ACTIVATE_SCENE: {"scene": "scene.relax"},
|
cv.SCRIPT_ACTION_ACTIVATE_SCENE: {"scene": "scene.relax"},
|
||||||
cv.SCRIPT_ACTION_REPEAT: {
|
cv.SCRIPT_ACTION_REPEAT: {
|
||||||
"repeat": {"count": 3, "sequence": [{"event": "repeat_event"}]}
|
"repeat": {
|
||||||
|
"count": 3,
|
||||||
|
"sequence": [templated_device_action("repeat_event")],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cv.SCRIPT_ACTION_CHOOSE: {
|
cv.SCRIPT_ACTION_CHOOSE: {
|
||||||
"choose": [
|
"choose": [
|
||||||
{
|
{
|
||||||
"condition": "{{ states.light.kitchen.state == 'on' }}",
|
"condition": "{{ states.light.kitchen.state == 'on' }}",
|
||||||
"sequence": [{"event": "choose_event"}],
|
"sequence": [templated_device_action("choose_event")],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"default": [{"event": "choose_default_event"}],
|
"default": [templated_device_action("choose_default_event")],
|
||||||
},
|
},
|
||||||
cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER: {
|
cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER: {
|
||||||
"wait_for_trigger": [
|
"wait_for_trigger": [
|
||||||
|
@ -3057,9 +3066,17 @@ async def test_validate_action_config(hass):
|
||||||
},
|
},
|
||||||
cv.SCRIPT_ACTION_VARIABLES: {"variables": {"hello": "world"}},
|
cv.SCRIPT_ACTION_VARIABLES: {"variables": {"hello": "world"}},
|
||||||
}
|
}
|
||||||
|
expected_templates = {
|
||||||
|
cv.SCRIPT_ACTION_CHECK_CONDITION: None,
|
||||||
|
cv.SCRIPT_ACTION_DEVICE_AUTOMATION: [[]],
|
||||||
|
cv.SCRIPT_ACTION_REPEAT: [["repeat", "sequence", 0]],
|
||||||
|
cv.SCRIPT_ACTION_CHOOSE: [["choose", 0, "sequence", 0], ["default", 0]],
|
||||||
|
cv.SCRIPT_ACTION_WAIT_FOR_TRIGGER: None,
|
||||||
|
}
|
||||||
|
|
||||||
for key in cv.ACTION_TYPE_SCHEMAS:
|
for key in cv.ACTION_TYPE_SCHEMAS:
|
||||||
assert key in configs, f"No validate config test found for {key}"
|
assert key in configs, f"No validate config test found for {key}"
|
||||||
|
assert key in expected_templates or key in script.STATIC_VALIDATION_ACTION_TYPES
|
||||||
|
|
||||||
# Verify we raise if we don't know the action type
|
# Verify we raise if we don't know the action type
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -3068,13 +3085,27 @@ async def test_validate_action_config(hass):
|
||||||
), pytest.raises(ValueError):
|
), pytest.raises(ValueError):
|
||||||
await script.async_validate_action_config(hass, {})
|
await script.async_validate_action_config(hass, {})
|
||||||
|
|
||||||
|
# Verify each action can validate
|
||||||
|
validated_config = {}
|
||||||
for action_type, config in configs.items():
|
for action_type, config in configs.items():
|
||||||
assert cv.determine_script_action(config) == action_type
|
assert cv.determine_script_action(config) == action_type
|
||||||
try:
|
try:
|
||||||
await script.async_validate_action_config(hass, config)
|
validated_config[action_type] = await script.async_validate_action_config(
|
||||||
|
hass, config
|
||||||
|
)
|
||||||
except vol.Invalid as err:
|
except vol.Invalid as err:
|
||||||
assert False, f"{action_type} config invalid: {err}"
|
assert False, f"{action_type} config invalid: {err}"
|
||||||
|
|
||||||
|
# Verify non-static actions have validated
|
||||||
|
for action_type, paths_to_templates in expected_templates.items():
|
||||||
|
if paths_to_templates is None:
|
||||||
|
continue
|
||||||
|
for path_to_template in paths_to_templates:
|
||||||
|
device_action = reduce(
|
||||||
|
operator.getitem, path_to_template, validated_config[action_type]
|
||||||
|
)
|
||||||
|
assert isinstance(device_action["message"], template.Template)
|
||||||
|
|
||||||
|
|
||||||
async def test_embedded_wait_for_trigger_in_automation(hass):
|
async def test_embedded_wait_for_trigger_in_automation(hass):
|
||||||
"""Test an embedded wait for trigger."""
|
"""Test an embedded wait for trigger."""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue