diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index f4d93026649..246429ad6c9 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1039,3 +1039,14 @@ class AlexaToggleController(AlexaCapability): ] return capability_resources + + +class AlexaChannelController(AlexaCapability): + """Implements Alexa.ChannelController. + + https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ChannelController" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index dd640aed0a6..f6fc9936a02 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -34,6 +34,7 @@ from homeassistant.components import ( from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES from .capabilities import ( AlexaBrightnessController, + AlexaChannelController, AlexaColorController, AlexaColorTemperatureController, AlexaContactSensor, @@ -420,6 +421,9 @@ class MediaPlayerCapabilities(AlexaEntity): if supported & media_player.SUPPORT_SELECT_SOURCE: yield AlexaInputController(self.entity) + if supported & media_player.const.SUPPORT_PLAY_MEDIA: + yield AlexaChannelController(self.entity) + @ENTITY_ADAPTERS.register(scene.DOMAIN) class SceneCapabilities(AlexaEntity): diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 64feacb92f5..331990dc4a4 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -521,20 +521,28 @@ async def async_api_adjust_volume_step(hass, config, directive, context): """Process an adjust volume step request.""" # media_player volume up/down service does not support specifying steps # each component handles it differently e.g. via config. - # For now we use the volumeSteps returned to figure out if we - # should step up/down - volume_step = directive.payload["volumeSteps"] + # This workaround will simply call the volume up/Volume down the amount of steps asked for + # When no steps are called in the request, Alexa sends a default of 10 steps which for most + # purposes is too high. The default is set 1 in this case. entity = directive.entity + volume_int = int(directive.payload["volumeSteps"]) + is_default = bool(directive.payload["volumeStepsDefault"]) + default_steps = 1 + + if volume_int < 0: + service_volume = SERVICE_VOLUME_DOWN + if is_default: + volume_int = -default_steps + else: + service_volume = SERVICE_VOLUME_UP + if is_default: + volume_int = default_steps data = {ATTR_ENTITY_ID: entity.entity_id} - if volume_step > 0: + for _ in range(0, abs(volume_int)): await hass.services.async_call( - entity.domain, SERVICE_VOLUME_UP, data, blocking=False, context=context - ) - elif volume_step < 0: - await hass.services.async_call( - entity.domain, SERVICE_VOLUME_DOWN, data, blocking=False, context=context + entity.domain, service_volume, data, blocking=False, context=context ) return directive.response() @@ -546,7 +554,6 @@ async def async_api_set_mute(hass, config, directive, context): """Process a set mute request.""" mute = bool(directive.payload["mute"]) entity = directive.entity - data = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute, @@ -1082,3 +1089,82 @@ async def async_api_adjust_range(hass, config, directive, context): ) return directive.response() + + +@HANDLERS.register(("Alexa.ChannelController", "ChangeChannel")) +async def async_api_changechannel(hass, config, directive, context): + """Process a change channel request.""" + channel = "0" + entity = directive.entity + payload = directive.payload["channel"] + payload_name = "number" + + if "number" in payload: + channel = payload["number"] + payload_name = "number" + elif "callSign" in payload: + channel = payload["callSign"] + payload_name = "callSign" + elif "affiliateCallSign" in payload: + channel = payload["affiliateCallSign"] + payload_name = "affiliateCallSign" + elif "uri" in payload: + channel = payload["uri"] + payload_name = "uri" + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.const.ATTR_MEDIA_CONTENT_ID: channel, + media_player.const.ATTR_MEDIA_CONTENT_TYPE: media_player.const.MEDIA_TYPE_CHANNEL, + } + + await hass.services.async_call( + entity.domain, + media_player.const.SERVICE_PLAY_MEDIA, + data, + blocking=False, + context=context, + ) + + response = directive.response() + + response.add_context_property( + { + "namespace": "Alexa.ChannelController", + "name": "channel", + "value": {payload_name: channel}, + } + ) + + return response + + +@HANDLERS.register(("Alexa.ChannelController", "SkipChannels")) +async def async_api_skipchannel(hass, config, directive, context): + """Process a skipchannel request.""" + channel = int(directive.payload["channelCount"]) + entity = directive.entity + + data = {ATTR_ENTITY_ID: entity.entity_id} + + if channel < 0: + service_media = SERVICE_MEDIA_PREVIOUS_TRACK + else: + service_media = SERVICE_MEDIA_NEXT_TRACK + + for _ in range(0, abs(channel)): + await hass.services.async_call( + entity.domain, service_media, data, blocking=False, context=context + ) + + response = directive.response() + + response.add_context_property( + { + "namespace": "Alexa.ChannelController", + "name": "channel", + "value": {"number": ""}, + } + ) + + return response diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 5a39036a30f..139c8c9740b 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -4,6 +4,19 @@ import pytest from homeassistant.core import Context, callback from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.components.alexa import smart_home, messages +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) from homeassistant.helpers import entityfilter from tests.common import async_mock_service @@ -693,7 +706,17 @@ async def test_media_player(hass): "off", { "friendly_name": "Test media player", - "supported_features": 0x59BD, + "supported_features": SUPPORT_NEXT_TRACK + | SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_SELECT_SOURCE + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET, "volume_level": 0.75, }, ) @@ -711,6 +734,7 @@ async def test_media_player(hass): "Alexa.StepSpeaker", "Alexa.PlaybackController", "Alexa.EndpointHealth", + "Alexa.ChannelController", ) await assert_power_controller_works( @@ -824,7 +848,7 @@ async def test_media_player(hass): "media_player#test", "media_player.volume_up", hass, - payload={"volumeSteps": 20}, + payload={"volumeSteps": 1, "volumeStepsDefault": False}, ) call, _ = await assert_request_calls_service( @@ -833,7 +857,69 @@ async def test_media_player(hass): "media_player#test", "media_player.volume_down", hass, - payload={"volumeSteps": -20}, + payload={"volumeSteps": -1, "volumeStepsDefault": False}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.StepSpeaker", + "AdjustVolume", + "media_player#test", + "media_player.volume_up", + hass, + payload={"volumeSteps": 10, "volumeStepsDefault": True}, + ) + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"number": 24}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"callSign": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"affiliateCallSign": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"uri": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "SkipChannels", + "media_player#test", + "media_player.media_next_track", + hass, + payload={"channelCount": 1}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "SkipChannels", + "media_player#test", + "media_player.media_previous_track", + hass, + payload={"channelCount": -1}, ) @@ -862,6 +948,7 @@ async def test_media_player_power(hass): "Alexa.StepSpeaker", "Alexa.PlaybackController", "Alexa.EndpointHealth", + "Alexa.ChannelController", ) await assert_request_calls_service(