Implement Alexa.SeekController Interface for media_player in Alexa (#28299)
* Implement Alexa.SeekController Interface for Alexa * Added error handling and duration checks. * Split out media_player SeekController tests and added error test.
This commit is contained in:
parent
bfe4a85e9d
commit
ff5b070f4b
5 changed files with 190 additions and 9 deletions
|
@ -1175,3 +1175,14 @@ class AlexaPlaybackStateReporter(AlexaCapability):
|
||||||
return {"state": "PAUSED"}
|
return {"state": "PAUSED"}
|
||||||
|
|
||||||
return {"state": "STOPPED"}
|
return {"state": "STOPPED"}
|
||||||
|
|
||||||
|
|
||||||
|
class AlexaSeekController(AlexaCapability):
|
||||||
|
"""Implements Alexa.SeekController.
|
||||||
|
|
||||||
|
https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
def name(self):
|
||||||
|
"""Return the Alexa API name of this interface."""
|
||||||
|
return "Alexa.SeekController"
|
||||||
|
|
|
@ -52,6 +52,7 @@ from .capabilities import (
|
||||||
AlexaRangeController,
|
AlexaRangeController,
|
||||||
AlexaSceneController,
|
AlexaSceneController,
|
||||||
AlexaSecurityPanelController,
|
AlexaSecurityPanelController,
|
||||||
|
AlexaSeekController,
|
||||||
AlexaSpeaker,
|
AlexaSpeaker,
|
||||||
AlexaStepSpeaker,
|
AlexaStepSpeaker,
|
||||||
AlexaTemperatureSensor,
|
AlexaTemperatureSensor,
|
||||||
|
@ -425,6 +426,9 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||||
yield AlexaPlaybackController(self.entity)
|
yield AlexaPlaybackController(self.entity)
|
||||||
yield AlexaPlaybackStateReporter(self.entity)
|
yield AlexaPlaybackStateReporter(self.entity)
|
||||||
|
|
||||||
|
if supported & media_player.const.SUPPORT_SEEK:
|
||||||
|
yield AlexaSeekController(self.entity)
|
||||||
|
|
||||||
if supported & media_player.SUPPORT_SELECT_SOURCE:
|
if supported & media_player.SUPPORT_SELECT_SOURCE:
|
||||||
yield AlexaInputController(self.entity)
|
yield AlexaInputController(self.entity)
|
||||||
|
|
||||||
|
|
|
@ -111,3 +111,10 @@ class AlexaInvalidDirectiveError(AlexaError):
|
||||||
|
|
||||||
namespace = "Alexa"
|
namespace = "Alexa"
|
||||||
error_type = "INVALID_DIRECTIVE"
|
error_type = "INVALID_DIRECTIVE"
|
||||||
|
|
||||||
|
|
||||||
|
class AlexaVideoActionNotPermittedForContentError(AlexaError):
|
||||||
|
"""Class to represent action not permitted for content errors."""
|
||||||
|
|
||||||
|
namespace = "Alexa.Video"
|
||||||
|
error_type = "ACTION_NOT_PERMITTED_FOR_CONTENT"
|
||||||
|
|
|
@ -54,6 +54,7 @@ from .errors import (
|
||||||
AlexaSecurityPanelUnauthorizedError,
|
AlexaSecurityPanelUnauthorizedError,
|
||||||
AlexaTempRangeError,
|
AlexaTempRangeError,
|
||||||
AlexaUnsupportedThermostatModeError,
|
AlexaUnsupportedThermostatModeError,
|
||||||
|
AlexaVideoActionNotPermittedForContentError,
|
||||||
)
|
)
|
||||||
from .state_report import async_enable_proactive_mode
|
from .state_report import async_enable_proactive_mode
|
||||||
|
|
||||||
|
@ -1186,3 +1187,45 @@ async def async_api_skipchannel(hass, config, directive, context):
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@HANDLERS.register(("Alexa.SeekController", "AdjustSeekPosition"))
|
||||||
|
async def async_api_seek(hass, config, directive, context):
|
||||||
|
"""Process a seek request."""
|
||||||
|
entity = directive.entity
|
||||||
|
position_delta = int(directive.payload["deltaPositionMilliseconds"])
|
||||||
|
|
||||||
|
current_position = entity.attributes.get(media_player.ATTR_MEDIA_POSITION)
|
||||||
|
if not current_position:
|
||||||
|
msg = f"{entity} did not return the current media position."
|
||||||
|
raise AlexaVideoActionNotPermittedForContentError(msg)
|
||||||
|
|
||||||
|
seek_position = int(current_position) + int(position_delta / 1000)
|
||||||
|
|
||||||
|
if seek_position < 0:
|
||||||
|
seek_position = 0
|
||||||
|
|
||||||
|
media_duration = entity.attributes.get(media_player.ATTR_MEDIA_DURATION)
|
||||||
|
if media_duration and 0 < int(media_duration) < seek_position:
|
||||||
|
seek_position = media_duration
|
||||||
|
|
||||||
|
data = {
|
||||||
|
ATTR_ENTITY_ID: entity.entity_id,
|
||||||
|
media_player.ATTR_MEDIA_SEEK_POSITION: seek_position,
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
media_player.DOMAIN,
|
||||||
|
media_player.SERVICE_MEDIA_SEEK,
|
||||||
|
data,
|
||||||
|
blocking=False,
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
# convert seconds to milliseconds for StateReport.
|
||||||
|
seek_position = int(seek_position * 1000)
|
||||||
|
|
||||||
|
payload = {"properties": [{"name": "positionMilliseconds", "value": seek_position}]}
|
||||||
|
return directive.response(
|
||||||
|
name="StateReport", namespace="Alexa.SeekController", payload=payload
|
||||||
|
)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from homeassistant.components.media_player.const import (
|
||||||
SUPPORT_PLAY,
|
SUPPORT_PLAY,
|
||||||
SUPPORT_PLAY_MEDIA,
|
SUPPORT_PLAY_MEDIA,
|
||||||
SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_PREVIOUS_TRACK,
|
||||||
|
SUPPORT_SEEK,
|
||||||
SUPPORT_SELECT_SOURCE,
|
SUPPORT_SELECT_SOURCE,
|
||||||
SUPPORT_STOP,
|
SUPPORT_STOP,
|
||||||
SUPPORT_TURN_OFF,
|
SUPPORT_TURN_OFF,
|
||||||
|
@ -728,14 +729,14 @@ async def test_media_player(hass):
|
||||||
|
|
||||||
capabilities = assert_endpoint_capabilities(
|
capabilities = assert_endpoint_capabilities(
|
||||||
appliance,
|
appliance,
|
||||||
|
"Alexa.ChannelController",
|
||||||
|
"Alexa.EndpointHealth",
|
||||||
"Alexa.InputController",
|
"Alexa.InputController",
|
||||||
|
"Alexa.PlaybackController",
|
||||||
|
"Alexa.PlaybackStateReporter",
|
||||||
"Alexa.PowerController",
|
"Alexa.PowerController",
|
||||||
"Alexa.Speaker",
|
"Alexa.Speaker",
|
||||||
"Alexa.StepSpeaker",
|
"Alexa.StepSpeaker",
|
||||||
"Alexa.PlaybackController",
|
|
||||||
"Alexa.PlaybackStateReporter",
|
|
||||||
"Alexa.EndpointHealth",
|
|
||||||
"Alexa.ChannelController",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
playback_capability = get_capability(capabilities, "Alexa.PlaybackController")
|
playback_capability = get_capability(capabilities, "Alexa.PlaybackController")
|
||||||
|
@ -950,14 +951,15 @@ async def test_media_player_power(hass):
|
||||||
|
|
||||||
assert_endpoint_capabilities(
|
assert_endpoint_capabilities(
|
||||||
appliance,
|
appliance,
|
||||||
|
"Alexa.ChannelController",
|
||||||
|
"Alexa.EndpointHealth",
|
||||||
"Alexa.InputController",
|
"Alexa.InputController",
|
||||||
"Alexa.PowerController",
|
|
||||||
"Alexa.Speaker",
|
|
||||||
"Alexa.StepSpeaker",
|
|
||||||
"Alexa.PlaybackController",
|
"Alexa.PlaybackController",
|
||||||
"Alexa.PlaybackStateReporter",
|
"Alexa.PlaybackStateReporter",
|
||||||
"Alexa.EndpointHealth",
|
"Alexa.PowerController",
|
||||||
"Alexa.ChannelController",
|
"Alexa.SeekController",
|
||||||
|
"Alexa.Speaker",
|
||||||
|
"Alexa.StepSpeaker",
|
||||||
)
|
)
|
||||||
|
|
||||||
await assert_request_calls_service(
|
await assert_request_calls_service(
|
||||||
|
@ -996,6 +998,120 @@ async def test_media_player_speaker(hass):
|
||||||
assert appliance["friendlyName"] == "Test media player"
|
assert appliance["friendlyName"] == "Test media player"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_media_player_seek(hass):
|
||||||
|
"""Test media player seek capability."""
|
||||||
|
device = (
|
||||||
|
"media_player.test_seek",
|
||||||
|
"playing",
|
||||||
|
{
|
||||||
|
"friendly_name": "Test media player seek",
|
||||||
|
"supported_features": SUPPORT_SEEK,
|
||||||
|
"media_position": 300, # 5min
|
||||||
|
"media_duration": 600, # 10min
|
||||||
|
},
|
||||||
|
)
|
||||||
|
appliance = await discovery_test(device, hass)
|
||||||
|
|
||||||
|
assert appliance["endpointId"] == "media_player#test_seek"
|
||||||
|
assert appliance["displayCategories"][0] == "TV"
|
||||||
|
assert appliance["friendlyName"] == "Test media player seek"
|
||||||
|
|
||||||
|
assert_endpoint_capabilities(
|
||||||
|
appliance,
|
||||||
|
"Alexa.EndpointHealth",
|
||||||
|
"Alexa.PowerController",
|
||||||
|
"Alexa.SeekController",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test seek forward 30 seconds.
|
||||||
|
call, msg = await assert_request_calls_service(
|
||||||
|
"Alexa.SeekController",
|
||||||
|
"AdjustSeekPosition",
|
||||||
|
"media_player#test_seek",
|
||||||
|
"media_player.media_seek",
|
||||||
|
hass,
|
||||||
|
response_type="StateReport",
|
||||||
|
payload={"deltaPositionMilliseconds": 30000},
|
||||||
|
)
|
||||||
|
assert call.data["seek_position"] == 330
|
||||||
|
assert "properties" in msg["event"]["payload"]
|
||||||
|
properties = msg["event"]["payload"]["properties"]
|
||||||
|
assert {"name": "positionMilliseconds", "value": 330000} in properties
|
||||||
|
|
||||||
|
# Test seek reverse 30 seconds.
|
||||||
|
call, msg = await assert_request_calls_service(
|
||||||
|
"Alexa.SeekController",
|
||||||
|
"AdjustSeekPosition",
|
||||||
|
"media_player#test_seek",
|
||||||
|
"media_player.media_seek",
|
||||||
|
hass,
|
||||||
|
response_type="StateReport",
|
||||||
|
payload={"deltaPositionMilliseconds": -30000},
|
||||||
|
)
|
||||||
|
assert call.data["seek_position"] == 270
|
||||||
|
assert "properties" in msg["event"]["payload"]
|
||||||
|
properties = msg["event"]["payload"]["properties"]
|
||||||
|
assert {"name": "positionMilliseconds", "value": 270000} in properties
|
||||||
|
|
||||||
|
# Test seek backwards more than current position (5 min.) result = 0.
|
||||||
|
call, msg = await assert_request_calls_service(
|
||||||
|
"Alexa.SeekController",
|
||||||
|
"AdjustSeekPosition",
|
||||||
|
"media_player#test_seek",
|
||||||
|
"media_player.media_seek",
|
||||||
|
hass,
|
||||||
|
response_type="StateReport",
|
||||||
|
payload={"deltaPositionMilliseconds": -500000},
|
||||||
|
)
|
||||||
|
assert call.data["seek_position"] == 0
|
||||||
|
assert "properties" in msg["event"]["payload"]
|
||||||
|
properties = msg["event"]["payload"]["properties"]
|
||||||
|
assert {"name": "positionMilliseconds", "value": 0} in properties
|
||||||
|
|
||||||
|
# Test seek forward more than current duration (10 min.) result = 600 sec.
|
||||||
|
call, msg = await assert_request_calls_service(
|
||||||
|
"Alexa.SeekController",
|
||||||
|
"AdjustSeekPosition",
|
||||||
|
"media_player#test_seek",
|
||||||
|
"media_player.media_seek",
|
||||||
|
hass,
|
||||||
|
response_type="StateReport",
|
||||||
|
payload={"deltaPositionMilliseconds": 800000},
|
||||||
|
)
|
||||||
|
assert call.data["seek_position"] == 600
|
||||||
|
assert "properties" in msg["event"]["payload"]
|
||||||
|
properties = msg["event"]["payload"]["properties"]
|
||||||
|
assert {"name": "positionMilliseconds", "value": 600000} in properties
|
||||||
|
|
||||||
|
|
||||||
|
async def test_media_player_seek_error(hass):
|
||||||
|
"""Test media player seek capability for media_position Error."""
|
||||||
|
device = (
|
||||||
|
"media_player.test_seek",
|
||||||
|
"playing",
|
||||||
|
{"friendly_name": "Test media player seek", "supported_features": SUPPORT_SEEK},
|
||||||
|
)
|
||||||
|
await discovery_test(device, hass)
|
||||||
|
|
||||||
|
# Test for media_position error.
|
||||||
|
with pytest.raises(AssertionError):
|
||||||
|
call, msg = await assert_request_calls_service(
|
||||||
|
"Alexa.SeekController",
|
||||||
|
"AdjustSeekPosition",
|
||||||
|
"media_player#test_seek",
|
||||||
|
"media_player.media_seek",
|
||||||
|
hass,
|
||||||
|
response_type="StateReport",
|
||||||
|
payload={"deltaPositionMilliseconds": 30000},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "event" in msg
|
||||||
|
msg = msg["event"]
|
||||||
|
assert msg["header"]["name"] == "ErrorResponse"
|
||||||
|
assert msg["header"]["namespace"] == "Alexa.Video"
|
||||||
|
assert msg["payload"]["type"] == "ACTION_NOT_PERMITTED_FOR_CONTENT"
|
||||||
|
|
||||||
|
|
||||||
async def test_alert(hass):
|
async def test_alert(hass):
|
||||||
"""Test alert discovery."""
|
"""Test alert discovery."""
|
||||||
device = ("alert.test", "off", {"friendly_name": "Test alert"})
|
device = ("alert.test", "off", {"friendly_name": "Test alert"})
|
||||||
|
|
Loading…
Add table
Reference in a new issue