From 970a80216db902ebe3b998c07260f96f4a22d09a Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Mon, 25 Nov 2019 18:17:12 -0500 Subject: [PATCH] Add valid inputs to alexa InputController (#28483) * Add supported Inputs for Alexa.InputController. * Fixed Test. * Added default parameter for get() per @quthla suggestion. * Added additional tests, assets call data. * Added additional tests, asserts call data. * Accounted for space in input name, added tests to handle space. --- .../components/alexa/capabilities.py | 27 +++++ homeassistant/components/alexa/const.py | 81 ++++++++++++++ homeassistant/components/alexa/handlers.py | 20 +++- tests/components/alexa/test_smart_home.py | 103 ++++++++++++++++++ 4 files changed, 225 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 2802dfd3a48..56622084b48 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -35,6 +35,7 @@ from .const import ( DATE_FORMAT, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, + Inputs, ) from .errors import UnsupportedProperty @@ -115,6 +116,11 @@ class AlexaCapability: """Return the Configuration object.""" return [] + @staticmethod + def inputs(): + """Applicable only to media players.""" + return [] + @staticmethod def supported_operations(): """Return the supportedOperations object.""" @@ -164,6 +170,10 @@ class AlexaCapability: if supported_operations: result["supportedOperations"] = supported_operations + inputs = self.inputs() + if inputs: + result["inputs"] = inputs + return result def serialize_properties(self): @@ -531,6 +541,23 @@ class AlexaInputController(AlexaCapability): """Return the Alexa API name of this interface.""" return "Alexa.InputController" + def inputs(self): + """Return the list of valid supported inputs.""" + source_list = self.entity.attributes.get( + media_player.ATTR_INPUT_SOURCE_LIST, [] + ) + input_list = [] + for source in source_list: + formatted_source = ( + source.lower().replace("-", "").replace("_", "").replace(" ", "") + ) + if formatted_source in Inputs.VALID_SOURCE_NAME_MAP.keys(): + input_list.append( + {"name": Inputs.VALID_SOURCE_NAME_MAP[formatted_source]} + ) + + return input_list + class AlexaTemperatureSensor(AlexaCapability): """Implements Alexa.TemperatureSensor. diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 2a5f9a512b3..1aa9d4f2c1d 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -272,3 +272,84 @@ class Unit: WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" + + +class Inputs: + """Valid names for the InputController. + + https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#input + """ + + VALID_SOURCE_NAME_MAP = { + "aux": "AUX 1", + "aux1": "AUX 1", + "aux2": "AUX 2", + "aux3": "AUX 3", + "aux4": "AUX 4", + "aux5": "AUX 5", + "aux6": "AUX 6", + "aux7": "AUX 7", + "bluray": "BLURAY", + "cable": "CABLE", + "cd": "CD", + "coax": "COAX 1", + "coax1": "COAX 1", + "coax2": "COAX 2", + "composite": "COMPOSITE 1", + "composite1": "COMPOSITE 1", + "dvd": "DVD", + "game": "GAME", + "gameconsole": "GAME", + "hdradio": "HD RADIO", + "hdmi": "HDMI 1", + "hdmi1": "HDMI 1", + "hdmi2": "HDMI 2", + "hdmi3": "HDMI 3", + "hdmi4": "HDMI 4", + "hdmi5": "HDMI 5", + "hdmi6": "HDMI 6", + "hdmi7": "HDMI 7", + "hdmi8": "HDMI 8", + "hdmi9": "HDMI 9", + "hdmi10": "HDMI 10", + "hdmiarc": "HDMI ARC", + "input": "INPUT 1", + "input1": "INPUT 1", + "input2": "INPUT 2", + "input3": "INPUT 3", + "input4": "INPUT 4", + "input5": "INPUT 5", + "input6": "INPUT 6", + "input7": "INPUT 7", + "input8": "INPUT 8", + "input9": "INPUT 9", + "input10": "INPUT 10", + "ipod": "IPOD", + "line": "LINE 1", + "line1": "LINE 1", + "line2": "LINE 2", + "line3": "LINE 3", + "line4": "LINE 4", + "line5": "LINE 5", + "line6": "LINE 6", + "line7": "LINE 7", + "mediaplayer": "MEDIA PLAYER", + "optical": "OPTICAL 1", + "optical1": "OPTICAL 1", + "optical2": "OPTICAL 2", + "phono": "PHONO", + "playstation": "PLAYSTATION", + "playstation3": "PLAYSTATION 3", + "playstation4": "PLAYSTATION 4", + "satellite": "SATELLITE", + "satellitetv": "SATELLITE", + "smartcast": "SMARTCAST", + "tuner": "TUNER", + "tv": "TV", + "usbdac": "USB DAC", + "video": "VIDEO 1", + "video1": "VIDEO 1", + "video2": "VIDEO 2", + "video3": "VIDEO 3", + "xbox": "XBOX", + } diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index eae8b4a5520..2e360fba7e2 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -44,6 +44,7 @@ from .const import ( API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause, + Inputs, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, SPEED_FAN_MAP, @@ -461,13 +462,20 @@ async def async_api_select_input(hass, config, directive, context): media_input = directive.payload["input"] entity = directive.entity - # attempt to map the ALL UPPERCASE payload name to a source - source_list = entity.attributes[media_player.const.ATTR_INPUT_SOURCE_LIST] or [] + # Attempt to map the ALL UPPERCASE payload name to a source. + # Strips trailing 1 to match single input devices. + source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST, []) for source in source_list: - # response will always be space separated, so format the source in the - # most likely way to find a match - formatted_source = source.lower().replace("-", " ").replace("_", " ") - if formatted_source in media_input.lower(): + formatted_source = ( + source.lower().replace("-", "").replace("_", "").replace(" ", "") + ) + media_input = media_input.lower().replace(" ", "") + if ( + formatted_source in Inputs.VALID_SOURCE_NAME_MAP.keys() + and formatted_source == media_input + ) or ( + media_input.endswith("1") and formatted_source == media_input.rstrip("1") + ): media_input = source break else: diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 7738df5cd78..09d9b6bd7b6 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1015,6 +1015,109 @@ async def test_media_player_power(hass): ) +async def test_media_player_inputs(hass): + """Test media player discovery with source list inputs.""" + device = ( + "media_player.test", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOURCE, + "volume_level": 0.75, + "source_list": [ + "foo", + "foo_2", + "hdmi", + "hdmi_2", + "hdmi-3", + "hdmi4", + "hdmi 5", + "HDMI 6", + "hdmi_arc", + "aux", + "input 1", + "tv", + ], + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test" + assert appliance["displayCategories"][0] == "TV" + assert appliance["friendlyName"] == "Test media player" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.InputController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + ) + + input_capability = get_capability(capabilities, "Alexa.InputController") + assert input_capability is not None + assert {"name": "AUX"} not in input_capability["inputs"] + assert {"name": "AUX 1"} in input_capability["inputs"] + assert {"name": "HDMI 1"} in input_capability["inputs"] + assert {"name": "HDMI 2"} in input_capability["inputs"] + assert {"name": "HDMI 3"} in input_capability["inputs"] + assert {"name": "HDMI 4"} in input_capability["inputs"] + assert {"name": "HDMI 5"} in input_capability["inputs"] + assert {"name": "HDMI 6"} in input_capability["inputs"] + assert {"name": "HDMI ARC"} in input_capability["inputs"] + assert {"name": "FOO 1"} not in input_capability["inputs"] + assert {"name": "TV"} in input_capability["inputs"] + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 1"}, + ) + assert call.data["source"] == "hdmi" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 2"}, + ) + assert call.data["source"] == "hdmi_2" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 5"}, + ) + assert call.data["source"] == "hdmi 5" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 6"}, + ) + assert call.data["source"] == "HDMI 6" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "TV"}, + ) + assert call.data["source"] == "tv" + + async def test_media_player_speaker(hass): """Test media player discovery with device class speaker.""" device = (