diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 4c872234746..06f76b8806e 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -1,6 +1,7 @@ """Support for Alexa skill service end point.""" import enum import logging +from typing import Any from homeassistant.components import http from homeassistant.core import callback @@ -180,12 +181,15 @@ async def async_handle_intent(hass, message): return alexa_response.as_dict() -def resolve_slot_synonyms(key, request): +def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]: """Check slot request for synonym resolutions.""" - # Default to the spoken slot value if more than one or none are found. For + # Default to the spoken slot value if more than one or none are found. Always + # passes the id and name of the nearest possible slot resolution. For # reference to the request object structure, see the Alexa docs: # https://tinyurl.com/ybvm7jhs - resolved_value = request["value"] + resolved_data = {} + resolved_data["value"] = request["value"] + resolved_data["id"] = "" if ( "resolutions" in request @@ -200,20 +204,26 @@ def resolve_slot_synonyms(key, request): if entry["status"]["code"] != SYN_RESOLUTION_MATCH: continue - possible_values.extend([item["value"]["name"] for item in entry["values"]]) + possible_values.extend([item["value"] for item in entry["values"]]) + + # Always set id if available, otherwise an empty string is used as id + if len(possible_values) >= 1: + # Set ID if available + if "id" in possible_values[0]: + resolved_data["id"] = possible_values[0]["id"] # If there is only one match use the resolved value, otherwise the - # resolution cannot be determined, so use the spoken slot value + # resolution cannot be determined, so use the spoken slot value and empty string as id if len(possible_values) == 1: - resolved_value = possible_values[0] + resolved_data["value"] = possible_values[0]["name"] else: _LOGGER.debug( "Found multiple synonym resolutions for slot value: {%s: %s}", key, - resolved_value, + resolved_data["value"], ) - return resolved_value + return resolved_data class AlexaResponse: @@ -237,8 +247,10 @@ class AlexaResponse: continue _key = key.replace(".", "_") + _slot_data = resolve_slot_data(key, value) - self.variables[_key] = resolve_slot_synonyms(key, value) + self.variables[_key] = _slot_data["value"] + self.variables[_key + "_Id"] = _slot_data["id"] def add_card(self, card_type, title, content): """Add a card to the response.""" diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index c3c0fdcbd41..03546c0ed22 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -77,6 +77,12 @@ def alexa_client(event_loop, hass, hass_client): "text": "You told us your sign is {{ ZodiacSign }}.", } }, + "GetZodiacHoroscopeIDIntent": { + "speech": { + "type": "plain", + "text": "You told us your sign is {{ ZodiacSign_Id }}.", + } + }, "AMAZON.PlaybackAction": { "speech": { "type": "plain", @@ -299,6 +305,113 @@ async def test_intent_request_with_slots_and_synonym_resolution(alexa_client) -> assert text == "You told us your sign is Virgo." +async def test_intent_request_with_slots_and_synonym_id_resolution( + alexa_client, +) -> None: + """Test a request with slots, id and a name synonym.""" + data = { + "version": "1.0", + "session": { + "new": False, + "sessionId": SESSION_ID, + "application": {"applicationId": APPLICATION_ID}, + "attributes": { + "supportedHoroscopePeriods": { + "daily": True, + "weekly": False, + "monthly": False, + } + }, + "user": {"userId": "amzn1.account.AM3B00000000000000000000000"}, + }, + "request": { + "type": "IntentRequest", + "requestId": REQUEST_ID, + "timestamp": "2015-05-13T12:34:56Z", + "intent": { + "name": "GetZodiacHoroscopeIDIntent", + "slots": { + "ZodiacSign": { + "name": "ZodiacSign", + "value": "V zodiac", + "resolutions": { + "resolutionsPerAuthority": [ + { + "authority": AUTHORITY_ID, + "status": {"code": "ER_SUCCESS_MATCH"}, + "values": [{"value": {"name": "Virgo", "id": "1"}}], + } + ] + }, + } + }, + }, + }, + } + 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 == "You told us your sign is 1." + + +async def test_intent_request_with_slots_and_multi_synonym_id_resolution( + alexa_client, +) -> None: + """Test a request with slots and multiple name synonyms (id).""" + data = { + "version": "1.0", + "session": { + "new": False, + "sessionId": SESSION_ID, + "application": {"applicationId": APPLICATION_ID}, + "attributes": { + "supportedHoroscopePeriods": { + "daily": True, + "weekly": False, + "monthly": False, + } + }, + "user": {"userId": "amzn1.account.AM3B00000000000000000000000"}, + }, + "request": { + "type": "IntentRequest", + "requestId": REQUEST_ID, + "timestamp": "2015-05-13T12:34:56Z", + "intent": { + "name": "GetZodiacHoroscopeIDIntent", + "slots": { + "ZodiacSign": { + "name": "ZodiacSign", + "value": "Virgio Test", + "resolutions": { + "resolutionsPerAuthority": [ + { + "authority": AUTHORITY_ID, + "status": {"code": "ER_SUCCESS_MATCH"}, + "values": [ + {"value": {"name": "Virgio Test", "id": "2"}} + ], + }, + { + "authority": AUTHORITY_ID, + "status": {"code": "ER_SUCCESS_MATCH"}, + "values": [{"value": {"name": "Virgo", "id": "1"}}], + }, + ] + }, + } + }, + }, + }, + } + 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 == "You told us your sign is 2." + + async def test_intent_request_with_slots_and_multi_synonym_resolution( alexa_client, ) -> None: