diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index b794b60b33d..ba71fb0def1 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -390,6 +390,27 @@ class APIDomainServicesView(HomeAssistantView): ) context = self.context(request) + if not hass.services.has_service(domain, service): + raise HTTPBadRequest from ServiceNotFound(domain, service) + + if response_requested := "return_response" in request.query: + if ( + hass.services.supports_response(domain, service) + is ha.SupportsResponse.NONE + ): + return self.json_message( + "Service does not support responses. Remove return_response from request.", + HTTPStatus.BAD_REQUEST, + ) + elif ( + hass.services.supports_response(domain, service) is ha.SupportsResponse.ONLY + ): + return self.json_message( + "Service call requires responses but caller did not ask for responses. " + "Add ?return_response to query parameters.", + HTTPStatus.BAD_REQUEST, + ) + changed_states: list[json_fragment] = [] @ha.callback @@ -406,13 +427,14 @@ class APIDomainServicesView(HomeAssistantView): try: # shield the service call from cancellation on connection drop - await shield( + response = await shield( hass.services.async_call( domain, service, data, # type: ignore[arg-type] blocking=True, context=context, + return_response=response_requested, ) ) except (vol.Invalid, ServiceNotFound) as ex: @@ -420,6 +442,11 @@ class APIDomainServicesView(HomeAssistantView): finally: cancel_listen() + if response_requested: + return self.json( + {"changed_states": changed_states, "service_response": response} + ) + return self.json(changed_states) diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index c283aeb718e..abce262fd12 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -3,6 +3,7 @@ import asyncio from http import HTTPStatus import json +from typing import Any from unittest.mock import patch from aiohttp import ServerDisconnectedError, web @@ -355,6 +356,67 @@ async def test_api_call_service_with_data( assert state["attributes"] == {"data": 1} +SERVICE_DICT = {"changed_states": [], "service_response": {"foo": "bar"}} +RESP_REQUIRED = { + "message": ( + "Service call requires responses but caller did not ask for " + "responses. Add ?return_response to query parameters." + ) +} +RESP_UNSUPPORTED = { + "message": "Service does not support responses. Remove return_response from request." +} + + +@pytest.mark.parametrize( + ( + "supports_response", + "requested_response", + "expected_number_of_service_calls", + "expected_status", + "expected_response", + ), + [ + (ha.SupportsResponse.ONLY, True, 1, HTTPStatus.OK, SERVICE_DICT), + (ha.SupportsResponse.ONLY, False, 0, HTTPStatus.BAD_REQUEST, RESP_REQUIRED), + (ha.SupportsResponse.OPTIONAL, True, 1, HTTPStatus.OK, SERVICE_DICT), + (ha.SupportsResponse.OPTIONAL, False, 1, HTTPStatus.OK, []), + (ha.SupportsResponse.NONE, True, 0, HTTPStatus.BAD_REQUEST, RESP_UNSUPPORTED), + (ha.SupportsResponse.NONE, False, 1, HTTPStatus.OK, []), + ], +) +async def test_api_call_service_returns_response_requested_response( + hass: HomeAssistant, + mock_api_client: TestClient, + supports_response: ha.SupportsResponse, + requested_response: bool, + expected_number_of_service_calls: int, + expected_status: int, + expected_response: Any, +) -> None: + """Test if the API allows us to call a service.""" + test_value = [] + + @ha.callback + def listener(service_call): + """Record that our service got called.""" + test_value.append(1) + return {"foo": "bar"} + + hass.services.async_register( + "test_domain", "test_service", listener, supports_response=supports_response + ) + + resp = await mock_api_client.post( + "/api/services/test_domain/test_service" + + ("?return_response" if requested_response else "") + ) + assert resp.status == expected_status + await hass.async_block_till_done() + assert len(test_value) == expected_number_of_service_calls + assert await resp.json() == expected_response + + async def test_api_call_service_client_closed( hass: HomeAssistant, mock_api_client: TestClient ) -> None: