diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 9550b6dbade60..c243fc12d5ee0 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -15,4 +15,6 @@ ATTR_MAIN_TEXT = 'mainText' ATTR_REDIRECTION_URL = 'redirectionURL' +SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH' + DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z' diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index c3a0155e3125b..3ade199aabbac 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -3,6 +3,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ + """ import asyncio import enum @@ -13,7 +14,7 @@ from homeassistant.helpers import intent from homeassistant.components import http -from .const import DOMAIN +from .const import DOMAIN, SYN_RESOLUTION_MATCH INTENTS_API_ENDPOINT = '/api/alexa' @@ -123,6 +124,43 @@ def post(self, request): return self.json(alexa_response) +def resolve_slot_synonyms(key, request): + """Check slot request for synonym resolutions.""" + # Default to the spoken slot value if more than one or none are found. For + # reference to the request object structure, see the Alexa docs: + # https://tinyurl.com/ybvm7jhs + resolved_value = request['value'] + + if ('resolutions' in request and + 'resolutionsPerAuthority' in request['resolutions'] and + len(request['resolutions']['resolutionsPerAuthority']) >= 1): + + # Extract all of the possible values from each authority with a + # successful match + possible_values = [] + + for entry in request['resolutions']['resolutionsPerAuthority']: + if entry['status']['code'] != SYN_RESOLUTION_MATCH: + continue + + possible_values.extend([item['value']['name'] + for item + in entry['values']]) + + # If there is only one match use the resolved value, otherwise the + # resolution cannot be determined, so use the spoken slot value + if len(possible_values) == 1: + resolved_value = possible_values[0] + else: + _LOGGER.debug( + 'Found multiple synonym resolutions for slot value: {%s: %s}', + key, + request['value'] + ) + + return resolved_value + + class AlexaResponse(object): """Help generating the response for Alexa.""" @@ -135,28 +173,17 @@ def __init__(self, hass, intent_info): self.session_attributes = {} self.should_end_session = True self.variables = {} + # Intent is None if request was a LaunchRequest or SessionEndedRequest if intent_info is not None: for key, value in intent_info.get('slots', {}).items(): - underscored_key = key.replace('.', '_') - - if 'value' in value: - self.variables[underscored_key] = value['value'] - - if 'resolutions' in value: - self._populate_resolved_values(underscored_key, value) - - def _populate_resolved_values(self, underscored_key, value): - for resolution in value['resolutions']['resolutionsPerAuthority']: - if 'values' not in resolution: - continue - - for resolved in resolution['values']: - if 'value' not in resolved: + # Only include slots with values + if 'value' not in value: continue - if 'name' in resolved['value']: - self.variables[underscored_key] = resolved['value']['name'] + _key = key.replace('.', '_') + + self.variables[_key] = resolve_slot_synonyms(key, value) 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 097c91ded79b5..a3587622b3dec 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -14,6 +14,7 @@ APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" 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" # pylint: disable=invalid-name calls = [] @@ -209,7 +210,7 @@ def test_intent_request_with_slots(alexa_client): @asyncio.coroutine -def test_intent_request_with_slots_and_name_resolution(alexa_client): +def test_intent_request_with_slots_and_synonym_resolution(alexa_client): """Test a request with slots and a name synonym.""" data = { "version": "1.0", @@ -239,7 +240,7 @@ def test_intent_request_with_slots_and_name_resolution(alexa_client): "slots": { "ZodiacSign": { "name": "ZodiacSign", - "value": "virgo", + "value": "V zodiac", "resolutions": { "resolutionsPerAuthority": [ { @@ -254,6 +255,19 @@ def test_intent_request_with_slots_and_name_resolution(alexa_client): } } ] + }, + { + "authority": BUILTIN_AUTH_ID, + "status": { + "code": "ER_SUCCESS_NO_MATCH" + }, + "values": [ + { + "value": { + "name": "Test" + } + } + ] } ] } @@ -270,6 +284,81 @@ def test_intent_request_with_slots_and_name_resolution(alexa_client): assert text == "You told us your sign is Virgo." +@asyncio.coroutine +def test_intent_request_with_slots_and_multi_synonym_resolution(alexa_client): + """Test a request with slots and multiple name synonyms.""" + 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": "GetZodiacHoroscopeIntent", + "slots": { + "ZodiacSign": { + "name": "ZodiacSign", + "value": "V zodiac", + "resolutions": { + "resolutionsPerAuthority": [ + { + "authority": AUTHORITY_ID, + "status": { + "code": "ER_SUCCESS_MATCH" + }, + "values": [ + { + "value": { + "name": "Virgo" + } + } + ] + }, + { + "authority": BUILTIN_AUTH_ID, + "status": { + "code": "ER_SUCCESS_MATCH" + }, + "values": [ + { + "value": { + "name": "Test" + } + } + ] + } + ] + } + } + } + } + } + } + req = yield from _intent_req(alexa_client, data) + assert req.status == 200 + data = yield from req.json() + text = data.get("response", {}).get("outputSpeech", + {}).get("text") + assert text == "You told us your sign is V zodiac." + + @asyncio.coroutine def test_intent_request_with_slots_but_no_value(alexa_client): """Test a request with slots but no value.""" @@ -300,7 +389,7 @@ def test_intent_request_with_slots_but_no_value(alexa_client): "name": "GetZodiacHoroscopeIntent", "slots": { "ZodiacSign": { - "name": "ZodiacSign", + "name": "ZodiacSign" } } }