Allow returning a script variable from a script (#95346)

* Allow returning a script variable from a script

* Don't allow returning a template result

* Raise if response variable is undefined

* Add test

* Update homeassistant/helpers/script.py

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Format code

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Erik Montnemery 2023-06-27 17:13:53 +02:00 committed by GitHub
parent e19b29d6ae
commit cb22fb16f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 100 additions and 19 deletions

View File

@ -224,7 +224,6 @@ CONF_RESOURCE: Final = "resource"
CONF_RESOURCE_TEMPLATE: Final = "resource_template" CONF_RESOURCE_TEMPLATE: Final = "resource_template"
CONF_RESOURCES: Final = "resources" CONF_RESOURCES: Final = "resources"
CONF_RESPONSE_VARIABLE: Final = "response_variable" CONF_RESPONSE_VARIABLE: Final = "response_variable"
CONF_RESPONSE: Final = "response"
CONF_RGB: Final = "rgb" CONF_RGB: Final = "rgb"
CONF_ROOM: Final = "room" CONF_ROOM: Final = "room"
CONF_SCAN_INTERVAL: Final = "scan_interval" CONF_SCAN_INTERVAL: Final = "scan_interval"

View File

@ -59,7 +59,6 @@ from homeassistant.const import (
CONF_PARALLEL, CONF_PARALLEL,
CONF_PLATFORM, CONF_PLATFORM,
CONF_REPEAT, CONF_REPEAT,
CONF_RESPONSE,
CONF_RESPONSE_VARIABLE, CONF_RESPONSE_VARIABLE,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_SCENE, CONF_SCENE,
@ -1691,10 +1690,7 @@ _SCRIPT_STOP_SCHEMA = vol.Schema(
**SCRIPT_ACTION_BASE_SCHEMA, **SCRIPT_ACTION_BASE_SCHEMA,
vol.Required(CONF_STOP): vol.Any(None, string), vol.Required(CONF_STOP): vol.Any(None, string),
vol.Exclusive(CONF_ERROR, "error_or_response"): boolean, vol.Exclusive(CONF_ERROR, "error_or_response"): boolean,
vol.Exclusive(CONF_RESPONSE, "error_or_response"): vol.Any( vol.Exclusive(CONF_RESPONSE_VARIABLE, "error_or_response"): str,
vol.All(dict, template_complex),
vol.All(str, template),
),
} }
) )

View File

@ -46,7 +46,6 @@ from homeassistant.const import (
CONF_MODE, CONF_MODE,
CONF_PARALLEL, CONF_PARALLEL,
CONF_REPEAT, CONF_REPEAT,
CONF_RESPONSE,
CONF_RESPONSE_VARIABLE, CONF_RESPONSE_VARIABLE,
CONF_SCENE, CONF_SCENE,
CONF_SEQUENCE, CONF_SEQUENCE,
@ -1031,10 +1030,14 @@ class _ScriptRun:
raise _AbortScript(stop) raise _AbortScript(stop)
self._log("Stop script sequence: %s", stop) self._log("Stop script sequence: %s", stop)
if CONF_RESPONSE in self._action: if CONF_RESPONSE_VARIABLE in self._action:
response = template.render_complex( try:
self._action[CONF_RESPONSE], self._variables response = self._variables[self._action[CONF_RESPONSE_VARIABLE]]
) except KeyError as ex:
raise _AbortScript(
f"Response variable '{self._action[CONF_RESPONSE_VARIABLE]}' "
"is not defined"
) from ex
else: else:
response = None response = None
raise _StopScript(stop, response) raise _StopScript(stop, response)

View File

@ -25,7 +25,7 @@ from homeassistant.core import (
callback, callback,
split_entity_id, split_entity_id,
) )
from homeassistant.exceptions import ServiceNotFound from homeassistant.exceptions import HomeAssistantError, ServiceNotFound
from homeassistant.helpers import entity_registry as er, template from homeassistant.helpers import entity_registry as er, template
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.script import ( from homeassistant.helpers.script import (
@ -1515,10 +1515,15 @@ async def test_responses(hass: HomeAssistant, response: Any) -> None:
{ {
"script": { "script": {
"test": { "test": {
"sequence": { "sequence": [
"stop": "done", {
"response": response, "variables": {"test_var": {"response": response}},
} },
{
"stop": "done",
"response_variable": "test_var",
},
]
} }
} }
}, },
@ -1526,7 +1531,40 @@ async def test_responses(hass: HomeAssistant, response: Any) -> None:
assert await hass.services.async_call( assert await hass.services.async_call(
DOMAIN, "test", {"greeting": "world"}, blocking=True, return_response=True DOMAIN, "test", {"greeting": "world"}, blocking=True, return_response=True
) == {"value": 5} ) == {"response": response}
# Validate we can also call it without return_response
assert (
await hass.services.async_call(
DOMAIN, "test", {"greeting": "world"}, blocking=True, return_response=False
)
is None
)
async def test_responses_error(hass: HomeAssistant) -> None:
"""Test response variable not set."""
mock_restore_cache(hass, ())
assert await async_setup_component(
hass,
"script",
{
"script": {
"test": {
"sequence": [
{
"stop": "done",
"response_variable": "test_var",
},
]
}
}
},
)
with pytest.raises(HomeAssistantError):
assert await hass.services.async_call(
DOMAIN, "test", {"greeting": "world"}, blocking=True, return_response=True
)
# Validate we can also call it without return_response # Validate we can also call it without return_response
assert ( assert (
await hass.services.async_call( await hass.services.async_call(

View File

@ -24,7 +24,10 @@ from homeassistant.setup import DATA_SETUP_TIME, async_setup_component
from homeassistant.util.json import json_loads from homeassistant.util.json import json_loads
from tests.common import MockEntity, MockEntityPlatform, MockUser, async_mock_service from tests.common import MockEntity, MockEntityPlatform, MockUser, async_mock_service
from tests.typing import ClientSessionGenerator, WebSocketGenerator from tests.typing import (
ClientSessionGenerator,
WebSocketGenerator,
)
STATE_KEY_SHORT_NAMES = { STATE_KEY_SHORT_NAMES = {
"entity_id": "e", "entity_id": "e",
@ -1686,7 +1689,7 @@ async def test_execute_script(hass: HomeAssistant, websocket_client) -> None:
"data": {"hello": "world"}, "data": {"hello": "world"},
"response_variable": "service_result", "response_variable": "service_result",
}, },
{"stop": "done", "response": "{{ service_result }}"}, {"stop": "done", "response_variable": "service_result"},
], ],
} }
) )
@ -1732,6 +1735,48 @@ async def test_execute_script(hass: HomeAssistant, websocket_client) -> None:
assert call.context.as_dict() == msg_var["result"]["context"] assert call.context.as_dict() == msg_var["result"]["context"]
async def test_execute_script_complex_response(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test testing a condition."""
await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}})
await hass.async_block_till_done()
ws_client = await hass_ws_client(hass)
await ws_client.send_json_auto_id(
{
"type": "execute_script",
"sequence": [
{
"service": "calendar.list_events",
"data": {"duration": {"hours": 24, "minutes": 0, "seconds": 0}},
"target": {"entity_id": "calendar.calendar_1"},
"response_variable": "service_result",
},
{"stop": "done", "response_variable": "service_result"},
],
}
)
msg_no_var = await ws_client.receive_json()
assert msg_no_var["type"] == const.TYPE_RESULT
assert msg_no_var["success"]
assert msg_no_var["result"]["response"] == {
"events": [
{
"start": ANY,
"end": ANY,
"summary": "Future Event",
"description": "Future Description",
"location": "Future Location",
"uid": None,
"recurrence_id": None,
"rrule": None,
}
]
}
async def test_subscribe_unsubscribe_bootstrap_integrations( async def test_subscribe_unsubscribe_bootstrap_integrations(
hass: HomeAssistant, websocket_client, hass_admin_user: MockUser hass: HomeAssistant, websocket_client, hass_admin_user: MockUser
) -> None: ) -> None: