Add support for responses to call_service
WS cmd (#98610)
* Add support for responses to call_service WS cmd * Revert ServiceNotFound removal and add a parameter for return_response * fix type * fix tests * remove exception handling that was added * Revert unnecessary modifications * Use kwargs
This commit is contained in:
parent
229944c21c
commit
618b666126
3 changed files with 102 additions and 13 deletions
|
@ -18,7 +18,14 @@ from homeassistant.const import (
|
||||||
MATCH_ALL,
|
MATCH_ALL,
|
||||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Context, Event, HomeAssistant, State, callback
|
from homeassistant.core import (
|
||||||
|
Context,
|
||||||
|
Event,
|
||||||
|
HomeAssistant,
|
||||||
|
ServiceResponse,
|
||||||
|
State,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
ServiceNotFound,
|
ServiceNotFound,
|
||||||
|
@ -213,6 +220,7 @@ def handle_unsubscribe_events(
|
||||||
vol.Required("service"): str,
|
vol.Required("service"): str,
|
||||||
vol.Optional("target"): cv.ENTITY_SERVICE_FIELDS,
|
vol.Optional("target"): cv.ENTITY_SERVICE_FIELDS,
|
||||||
vol.Optional("service_data"): dict,
|
vol.Optional("service_data"): dict,
|
||||||
|
vol.Optional("return_response", default=False): bool,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@decorators.async_response
|
@decorators.async_response
|
||||||
|
@ -220,7 +228,6 @@ async def handle_call_service(
|
||||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle call service command."""
|
"""Handle call service command."""
|
||||||
blocking = True
|
|
||||||
# We do not support templates.
|
# We do not support templates.
|
||||||
target = msg.get("target")
|
target = msg.get("target")
|
||||||
if template.is_complex(target):
|
if template.is_complex(target):
|
||||||
|
@ -228,15 +235,19 @@ async def handle_call_service(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
context = connection.context(msg)
|
context = connection.context(msg)
|
||||||
await hass.services.async_call(
|
response = await hass.services.async_call(
|
||||||
msg["domain"],
|
domain=msg["domain"],
|
||||||
msg["service"],
|
service=msg["service"],
|
||||||
msg.get("service_data"),
|
service_data=msg.get("service_data"),
|
||||||
blocking,
|
blocking=True,
|
||||||
context,
|
context=context,
|
||||||
target=target,
|
target=target,
|
||||||
|
return_response=msg["return_response"],
|
||||||
)
|
)
|
||||||
connection.send_result(msg["id"], {"context": context})
|
result: dict[str, Context | ServiceResponse] = {"context": context}
|
||||||
|
if msg["return_response"]:
|
||||||
|
result["response"] = response
|
||||||
|
connection.send_result(msg["id"], result)
|
||||||
except ServiceNotFound as err:
|
except ServiceNotFound as err:
|
||||||
if err.domain == msg["domain"] and err.service == msg["service"]:
|
if err.domain == msg["domain"] and err.service == msg["service"]:
|
||||||
connection.send_error(
|
connection.send_error(
|
||||||
|
|
|
@ -307,8 +307,11 @@ def async_mock_service(
|
||||||
calls.append(call)
|
calls.append(call)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
if supports_response is None and response is not None:
|
if supports_response is None:
|
||||||
supports_response = SupportsResponse.OPTIONAL
|
if response is not None:
|
||||||
|
supports_response = SupportsResponse.OPTIONAL
|
||||||
|
else:
|
||||||
|
supports_response = SupportsResponse.NONE
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
domain,
|
domain,
|
||||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.components.websocket_api.auth import (
|
||||||
)
|
)
|
||||||
from homeassistant.components.websocket_api.const import FEATURE_COALESCE_MESSAGES, URL
|
from homeassistant.components.websocket_api.const import FEATURE_COALESCE_MESSAGES, URL
|
||||||
from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS
|
from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS
|
||||||
from homeassistant.core import Context, HomeAssistant, State, callback
|
from homeassistant.core import Context, HomeAssistant, State, SupportsResponse, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
@ -183,14 +183,76 @@ async def test_call_service(
|
||||||
assert call.context.as_dict() == msg["result"]["context"]
|
assert call.context.as_dict() == msg["result"]["context"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_return_response_error(hass: HomeAssistant, websocket_client) -> None:
|
||||||
|
"""Test return_response=True errors when service has no response."""
|
||||||
|
hass.services.async_register(
|
||||||
|
"domain_test", "test_service_with_no_response", lambda x: None
|
||||||
|
)
|
||||||
|
await websocket_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"type": "call_service",
|
||||||
|
"domain": "domain_test",
|
||||||
|
"service": "test_service_with_no_response",
|
||||||
|
"service_data": {"hello": "world"},
|
||||||
|
"return_response": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
msg = await websocket_client.receive_json()
|
||||||
|
|
||||||
|
assert msg["id"] == 8
|
||||||
|
assert msg["type"] == const.TYPE_RESULT
|
||||||
|
assert not msg["success"]
|
||||||
|
assert msg["error"]["code"] == "unknown_error"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("command", ("call_service", "call_service_action"))
|
@pytest.mark.parametrize("command", ("call_service", "call_service_action"))
|
||||||
async def test_call_service_blocking(
|
async def test_call_service_blocking(
|
||||||
hass: HomeAssistant, websocket_client: MockHAClientWebSocket, command
|
hass: HomeAssistant, websocket_client: MockHAClientWebSocket, command
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test call service commands block, except for homeassistant restart / stop."""
|
"""Test call service commands block, except for homeassistant restart / stop."""
|
||||||
|
async_mock_service(
|
||||||
|
hass,
|
||||||
|
"domain_test",
|
||||||
|
"test_service",
|
||||||
|
response={"hello": "world"},
|
||||||
|
supports_response=SupportsResponse.OPTIONAL,
|
||||||
|
)
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.core.ServiceRegistry.async_call", autospec=True
|
"homeassistant.core.ServiceRegistry.async_call", autospec=True
|
||||||
) as mock_call:
|
) as mock_call:
|
||||||
|
mock_call.return_value = {"foo": "bar"}
|
||||||
|
await websocket_client.send_json(
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "call_service",
|
||||||
|
"domain": "domain_test",
|
||||||
|
"service": "test_service",
|
||||||
|
"service_data": {"hello": "world"},
|
||||||
|
"return_response": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
msg = await websocket_client.receive_json()
|
||||||
|
|
||||||
|
assert msg["id"] == 4
|
||||||
|
assert msg["type"] == const.TYPE_RESULT
|
||||||
|
assert msg["success"]
|
||||||
|
assert msg["result"]["response"] == {"foo": "bar"}
|
||||||
|
mock_call.assert_called_once_with(
|
||||||
|
ANY,
|
||||||
|
"domain_test",
|
||||||
|
"test_service",
|
||||||
|
{"hello": "world"},
|
||||||
|
blocking=True,
|
||||||
|
context=ANY,
|
||||||
|
target=ANY,
|
||||||
|
return_response=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.core.ServiceRegistry.async_call", autospec=True
|
||||||
|
) as mock_call:
|
||||||
|
mock_call.return_value = None
|
||||||
await websocket_client.send_json(
|
await websocket_client.send_json(
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
|
@ -213,11 +275,14 @@ async def test_call_service_blocking(
|
||||||
blocking=True,
|
blocking=True,
|
||||||
context=ANY,
|
context=ANY,
|
||||||
target=ANY,
|
target=ANY,
|
||||||
|
return_response=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async_mock_service(hass, "homeassistant", "test_service")
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.core.ServiceRegistry.async_call", autospec=True
|
"homeassistant.core.ServiceRegistry.async_call", autospec=True
|
||||||
) as mock_call:
|
) as mock_call:
|
||||||
|
mock_call.return_value = None
|
||||||
await websocket_client.send_json(
|
await websocket_client.send_json(
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
|
@ -239,11 +304,14 @@ async def test_call_service_blocking(
|
||||||
blocking=True,
|
blocking=True,
|
||||||
context=ANY,
|
context=ANY,
|
||||||
target=ANY,
|
target=ANY,
|
||||||
|
return_response=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async_mock_service(hass, "homeassistant", "restart")
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.core.ServiceRegistry.async_call", autospec=True
|
"homeassistant.core.ServiceRegistry.async_call", autospec=True
|
||||||
) as mock_call:
|
) as mock_call:
|
||||||
|
mock_call.return_value = None
|
||||||
await websocket_client.send_json(
|
await websocket_client.send_json(
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
|
@ -258,7 +326,14 @@ async def test_call_service_blocking(
|
||||||
assert msg["type"] == const.TYPE_RESULT
|
assert msg["type"] == const.TYPE_RESULT
|
||||||
assert msg["success"]
|
assert msg["success"]
|
||||||
mock_call.assert_called_once_with(
|
mock_call.assert_called_once_with(
|
||||||
ANY, "homeassistant", "restart", ANY, blocking=True, context=ANY, target=ANY
|
ANY,
|
||||||
|
"homeassistant",
|
||||||
|
"restart",
|
||||||
|
ANY,
|
||||||
|
blocking=True,
|
||||||
|
context=ANY,
|
||||||
|
target=ANY,
|
||||||
|
return_response=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue