diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 513a0c157d38a83a3798a6d660e44e82395c00d3..3fdad2b3c924c05739990e43abeb4bb76681b2aa 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -120,7 +120,20 @@ class AlexaCapability: @staticmethod def configuration(): - """Return the configuration object.""" + """Return the configuration object. + + Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController, + and EventDetectionSensor. + """ + return [] + + @staticmethod + def configurations(): + """Return the configurations object. + + The plural configurations object is different that the singular configuration object. + Applicable to EqualizerController interface. + """ return [] @staticmethod @@ -177,6 +190,11 @@ class AlexaCapability: if configuration: result["configuration"] = configuration + # The plural configurations object is different than the singular configuration object above. + configurations = self.configurations() + if configurations: + result["configurations"] = configurations + semantics = self.semantics() if semantics: result["semantics"] = semantics @@ -1356,3 +1374,55 @@ class AlexaEventDetectionSensor(AlexaCapability): } }, } + + +class AlexaEqualizerController(AlexaCapability): + """Implements Alexa.EqualizerController. + + https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.EqualizerController" + + def properties_supported(self): + """Return what properties this entity supports. + + Either bands, mode or both can be specified. Only mode is supported at this time. + """ + return [{"name": "mode"}] + + def get_property(self, name): + """Read and return a property.""" + if name != "mode": + raise UnsupportedProperty(name) + + sound_mode = self.entity.attributes.get(media_player.ATTR_SOUND_MODE) + if sound_mode and sound_mode.upper() in ( + "MOVIE", + "MUSIC", + "NIGHT", + "SPORT", + "TV", + ): + return sound_mode.upper() + + return None + + def configurations(self): + """Return the sound modes supported in the configurations object. + + Valid Values for modes are: MOVIE, MUSIC, NIGHT, SPORT, TV. + """ + configurations = None + sound_mode_list = self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) + if sound_mode_list: + supported_sound_modes = [] + for sound_mode in sound_mode_list: + if sound_mode.upper() in ("MOVIE", "MUSIC", "NIGHT", "SPORT", "TV"): + supported_sound_modes.append({"name": sound_mode.upper()}) + + configurations = {"modes": {"supported": supported_sound_modes}} + + return configurations diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index f1a86859da9dc31694fc86a512ab1f0a0708ddd1..6968ab3a6912ba0478123d572c071b9273763451 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -196,3 +196,11 @@ class Inputs: "video3": "VIDEO 3", "xbox": "XBOX", } + + VALID_SOUND_MODE_MAP = { + "movie": "MOVIE", + "music": "MUSIC", + "night": "NIGHT", + "sport": "SPORT", + "tv": "TV", + } diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index ab3dc75bd2ccc29af0becdf304f922c381cebae8..4321d289cecb41cdbc98c937d121796ac52b9ccf 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -42,6 +42,7 @@ from .capabilities import ( AlexaContactSensor, AlexaDoorbellEventSource, AlexaEndpointHealth, + AlexaEqualizerController, AlexaEventDetectionSensor, AlexaInputController, AlexaLockController, @@ -522,6 +523,9 @@ class MediaPlayerCapabilities(AlexaEntity): if supported & media_player.const.SUPPORT_PLAY_MEDIA: yield AlexaChannelController(self.entity) + if supported & media_player.const.SUPPORT_SELECT_SOUND_MODE: + yield AlexaEqualizerController(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 133919be84d6c92c2b8de2454273306d4b894e10..ce6c37a2b394d4f917d154d6036cd9bb1a8e79ea 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1352,3 +1352,43 @@ async def async_api_seek(hass, config, directive, context): return directive.response( name="StateReport", namespace="Alexa.SeekController", payload=payload ) + + +@HANDLERS.register(("Alexa.EqualizerController", "SetMode")) +async def async_api_set_eq_mode(hass, config, directive, context): + """Process a SetMode request for EqualizerController.""" + mode = directive.payload["mode"] + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST) + if sound_mode_list and mode.lower() in sound_mode_list: + data[media_player.const.ATTR_SOUND_MODE] = mode.lower() + else: + msg = "failed to map sound mode {} to a mode on {}".format( + mode, entity.entity_id + ) + raise AlexaInvalidValueError(msg) + + await hass.services.async_call( + entity.domain, + media_player.SERVICE_SELECT_SOUND_MODE, + data, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.EqualizerController", "AdjustBands")) +@HANDLERS.register(("Alexa.EqualizerController", "ResetBands")) +@HANDLERS.register(("Alexa.EqualizerController", "SetBands")) +async def async_api_bands_directive(hass, config, directive, context): + """Handle an AdjustBands, ResetBands, SetBands request. + + Only mode directives are currently supported for the EqualizerController. + """ + # Currently bands directives are not supported. + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3d7c2b118e7cf6fd6aeba0d3d2c4ecadf0d924bc..37301c3555e9d540d4b1eea56e323bfdb87c756a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -9,6 +9,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -2912,3 +2913,127 @@ async def test_input_number_float(hass): "value", instance="input_number.value", ) + + +async def test_media_player_eq_modes(hass): + """Test media player discovery with sound mode list.""" + device = ( + "media_player.test", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOUND_MODE, + "sound_mode": "tv", + "sound_mode_list": ["movie", "music", "night", "sport", "tv", "rocknroll"], + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test" + assert appliance["friendlyName"] == "Test media player" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa", + "Alexa.EqualizerController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + ) + + eq_capability = get_capability(capabilities, "Alexa.EqualizerController") + assert eq_capability is not None + assert "modes" in eq_capability["configurations"] + + eq_modes = eq_capability["configurations"]["modes"] + assert {"name": "rocknroll"} not in eq_modes["supported"] + assert {"name": "ROCKNROLL"} not in eq_modes["supported"] + + for mode in ("MOVIE", "MUSIC", "NIGHT", "SPORT", "TV"): + assert {"name": mode} in eq_modes["supported"] + + call, _ = await assert_request_calls_service( + "Alexa.EqualizerController", + "SetMode", + "media_player#test", + "media_player.select_sound_mode", + hass, + payload={"mode": mode}, + ) + assert call.data["sound_mode"] == mode.lower() + + +async def test_media_player_sound_mode_list_none(hass): + """Test EqualizerController bands directive not supported.""" + device = ( + "media_player.test", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOUND_MODE, + "sound_mode": "unknown", + "sound_mode_list": None, + }, + ) + appliance = await discovery_test(device, hass) + assert appliance["endpointId"] == "media_player#test" + assert appliance["friendlyName"] == "Test media player" + + +async def test_media_player_eq_bands_not_supported(hass): + """Test EqualizerController bands directive not supported.""" + device = ( + "media_player.test_bands", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOUND_MODE, + "sound_mode": "tv", + "sound_mode_list": ["movie", "music", "night", "sport", "tv", "rocknroll"], + }, + ) + await discovery_test(device, hass) + + context = Context() + + # Test for SetBands Error + request = get_new_request( + "Alexa.EqualizerController", "SetBands", "media_player#test_bands" + ) + request["directive"]["payload"] = {"bands": [{"name": "BASS", "value": -2}]} + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + # Test for AdjustBands Error + request = get_new_request( + "Alexa.EqualizerController", "AdjustBands", "media_player#test_bands" + ) + request["directive"]["payload"] = { + "bands": [{"name": "BASS", "levelDelta": 3, "levelDirection": "UP"}] + } + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + # Test for ResetBands Error + request = get_new_request( + "Alexa.EqualizerController", "ResetBands", "media_player#test_bands" + ) + request["directive"]["payload"] = { + "bands": [{"name": "BASS", "levelDelta": 3, "levelDirection": "UP"}] + } + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE"