From 076faaa4a4f231eb5b7b7c72fa20c239c7cc391c Mon Sep 17 00:00:00 2001 From: w35l3y Date: Mon, 31 Jan 2022 15:23:26 -0300 Subject: [PATCH] Add support to reprompt user (#65256) --- homeassistant/components/alexa/intent.py | 12 ++--- .../components/intent_script/__init__.py | 12 +++++ homeassistant/helpers/intent.py | 16 ++++++- tests/components/alexa/test_intent.py | 43 ++++++++++++++++++ tests/components/intent_script/test_init.py | 45 +++++++++++++++++++ 5 files changed, 121 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index fede7d96810..0b8bf55fcda 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -166,7 +166,10 @@ async def async_handle_intent(hass, message): alexa_response.add_speech( alexa_speech, intent_response.speech[intent_speech]["speech"] ) - break + if intent_speech in intent_response.reprompt: + alexa_response.add_reprompt( + alexa_speech, intent_response.reprompt[intent_speech]["reprompt"] + ) if "simple" in intent_response.card: alexa_response.add_card( @@ -267,10 +270,9 @@ class AlexaResponse: key = "ssml" if speech_type == SpeechType.ssml else "text" - self.reprompt = { - "type": speech_type.value, - key: text.async_render(self.variables, parse_result=False), - } + self.should_end_session = False + + self.reprompt = {"type": speech_type.value, key: text} def as_dict(self): """Return response in an Alexa valid dict.""" diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 2deca297f34..d14aaf5a68b 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -12,6 +12,7 @@ DOMAIN = "intent_script" CONF_INTENTS = "intents" CONF_SPEECH = "speech" +CONF_REPROMPT = "reprompt" CONF_ACTION = "action" CONF_CARD = "card" @@ -39,6 +40,10 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_TYPE, default="plain"): cv.string, vol.Required(CONF_TEXT): cv.template, }, + vol.Optional(CONF_REPROMPT): { + vol.Optional(CONF_TYPE, default="plain"): cv.string, + vol.Required(CONF_TEXT): cv.template, + }, } } }, @@ -72,6 +77,7 @@ class ScriptIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj): """Handle the intent.""" speech = self.config.get(CONF_SPEECH) + reprompt = self.config.get(CONF_REPROMPT) card = self.config.get(CONF_CARD) action = self.config.get(CONF_ACTION) is_async_action = self.config.get(CONF_ASYNC_ACTION) @@ -93,6 +99,12 @@ class ScriptIntentHandler(intent.IntentHandler): speech[CONF_TYPE], ) + if reprompt is not None and reprompt[CONF_TEXT].template: + response.async_set_reprompt( + reprompt[CONF_TEXT].async_render(slots, parse_result=False), + reprompt[CONF_TYPE], + ) + if card is not None: response.async_set_card( card[CONF_TITLE].async_render(slots, parse_result=False), diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index ca154d20b75..13cc32a35b6 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -249,6 +249,7 @@ class IntentResponse: """Initialize an IntentResponse.""" self.intent = intent self.speech: dict[str, dict[str, Any]] = {} + self.reprompt: dict[str, dict[str, Any]] = {} self.card: dict[str, dict[str, str]] = {} @callback @@ -258,14 +259,25 @@ class IntentResponse: """Set speech response.""" self.speech[speech_type] = {"speech": speech, "extra_data": extra_data} + @callback + def async_set_reprompt( + self, speech: str, speech_type: str = "plain", extra_data: Any | None = None + ) -> None: + """Set reprompt response.""" + self.reprompt[speech_type] = {"reprompt": speech, "extra_data": extra_data} + @callback def async_set_card( self, title: str, content: str, card_type: str = "simple" ) -> None: - """Set speech response.""" + """Set card response.""" self.card[card_type] = {"title": title, "content": content} @callback def as_dict(self) -> dict[str, dict[str, dict[str, Any]]]: """Return a dictionary representation of an intent response.""" - return {"speech": self.speech, "card": self.card} + return ( + {"speech": self.speech, "reprompt": self.reprompt, "card": self.card} + if self.reprompt + else {"speech": self.speech, "card": self.card} + ) diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index d6c32996330..f15fa860c7b 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -13,6 +13,9 @@ from homeassistant.setup import async_setup_component SESSION_ID = "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000" APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" +APPLICATION_ID_SESSION_OPEN = ( + "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebf" +) REQUEST_ID = "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000" AUTHORITY_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.ZODIAC" BUILTIN_AUTH_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.TEST" @@ -102,6 +105,16 @@ def alexa_client(loop, hass, hass_client): "text": "LaunchRequest has been received.", } }, + APPLICATION_ID_SESSION_OPEN: { + "speech": { + "type": "plain", + "text": "LaunchRequest has been received.", + }, + "reprompt": { + "type": "plain", + "text": "LaunchRequest has been received.", + }, + }, } }, ) @@ -139,6 +152,36 @@ async def test_intent_launch_request(alexa_client): data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "LaunchRequest has been received." + assert data.get("response", {}).get("shouldEndSession") + + +async def test_intent_launch_request_with_session_open(alexa_client): + """Test the launch of a request.""" + data = { + "version": "1.0", + "session": { + "new": True, + "sessionId": SESSION_ID, + "application": {"applicationId": APPLICATION_ID_SESSION_OPEN}, + "attributes": {}, + "user": {"userId": "amzn1.account.AM3B00000000000000000000000"}, + }, + "request": { + "type": "LaunchRequest", + "requestId": REQUEST_ID, + "timestamp": "2015-05-13T12:34:56Z", + }, + } + req = await _intent_req(alexa_client, data) + assert req.status == HTTPStatus.OK + data = await req.json() + text = data.get("response", {}).get("outputSpeech", {}).get("text") + assert text == "LaunchRequest has been received." + text = ( + data.get("response", {}).get("reprompt", {}).get("outputSpeech", {}).get("text") + ) + assert text == "LaunchRequest has been received." + assert not data.get("response", {}).get("shouldEndSession") async def test_intent_launch_request_not_configured(alexa_client): diff --git a/tests/components/intent_script/test_init.py b/tests/components/intent_script/test_init.py index 95b167caba6..6f345522e63 100644 --- a/tests/components/intent_script/test_init.py +++ b/tests/components/intent_script/test_init.py @@ -40,3 +40,48 @@ async def test_intent_script(hass): assert response.card["simple"]["title"] == "Hello Paulus" assert response.card["simple"]["content"] == "Content for Paulus" + + +async def test_intent_script_wait_response(hass): + """Test intent scripts work.""" + calls = async_mock_service(hass, "test", "service") + + await async_setup_component( + hass, + "intent_script", + { + "intent_script": { + "HelloWorldWaitResponse": { + "action": { + "service": "test.service", + "data_template": {"hello": "{{ name }}"}, + }, + "card": { + "title": "Hello {{ name }}", + "content": "Content for {{ name }}", + }, + "speech": {"text": "Good morning {{ name }}"}, + "reprompt": { + "text": "I didn't hear you, {{ name }}... I said good morning!" + }, + } + } + }, + ) + + response = await intent.async_handle( + hass, "test", "HelloWorldWaitResponse", {"name": {"value": "Paulus"}} + ) + + assert len(calls) == 1 + assert calls[0].data["hello"] == "Paulus" + + assert response.speech["plain"]["speech"] == "Good morning Paulus" + + assert ( + response.reprompt["plain"]["reprompt"] + == "I didn't hear you, Paulus... I said good morning!" + ) + + assert response.card["simple"]["title"] == "Hello Paulus" + assert response.card["simple"]["content"] == "Content for Paulus"