From a7b79fc8b26f7886b7f4dce59d568369a1fed0c7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Jun 2015 00:24:17 -0700 Subject: [PATCH] Initial refactor media player --- homeassistant/components/keyboard.py | 6 +- .../components/media_player/__init__.py | 339 +++++++++++++----- homeassistant/components/media_player/demo.py | 143 ++++---- homeassistant/const.py | 4 +- tests/test_component_media_player.py | 7 +- 5 files changed, 345 insertions(+), 154 deletions(-) diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index b4959b48055..b59fe8d39dc 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -8,7 +8,7 @@ import logging from homeassistant.const import ( SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_PLAY_PAUSE) @@ -43,7 +43,7 @@ def media_next_track(hass): def media_prev_track(hass): """ Press the keyboard button for prev track. """ - hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK) + hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK) def setup(hass, config): @@ -79,7 +79,7 @@ def setup(hass, config): lambda service: keyboard.tap_key(keyboard.media_next_track_key)) - hass.services.register(DOMAIN, SERVICE_MEDIA_PREV_TRACK, + hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, lambda service: keyboard.tap_key(keyboard.media_prev_track_key)) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8a080e828da..1d11ef2a37d 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -10,11 +10,12 @@ from homeassistant.components import discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, + ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET, SERVICE_VOLUME_MUTE, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK) + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK) DOMAIN = 'media_player' DEPENDENCIES = [] @@ -28,29 +29,67 @@ DISCOVERY_PLATFORMS = { SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' -STATE_NO_APP = 'idle' - -ATTR_STATE = 'state' -ATTR_OPTIONS = 'options' -ATTR_MEDIA_STATE = 'media_state' +ATTR_MEDIA_VOLUME_LEVEL = 'volume_level' +ATTR_MEDIA_VOLUME_MUTED = 'volume_muted' ATTR_MEDIA_CONTENT_ID = 'media_content_id' +ATTR_MEDIA_CONTENT_TYPE = 'media_content_type' +ATTR_MEDIA_DURATION = 'media_duration' ATTR_MEDIA_TITLE = 'media_title' ATTR_MEDIA_ARTIST = 'media_artist' ATTR_MEDIA_ALBUM = 'media_album' -ATTR_MEDIA_IMAGE_URL = 'media_image_url' -ATTR_MEDIA_VOLUME = 'media_volume' -ATTR_MEDIA_IS_VOLUME_MUTED = 'media_is_volume_muted' -ATTR_MEDIA_DURATION = 'media_duration' -ATTR_MEDIA_DATE = 'media_date' +ATTR_MEDIA_SERIES_TITLE = 'media_series_title' +ATTR_MEDIA_SEASON = 'media_season' +ATTR_MEDIA_EPISODE = 'media_episode' +ATTR_APP_ID = 'app_id' +ATTR_APP_NAME = 'app_name' +ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands' -MEDIA_STATE_UNKNOWN = 'unknown' -MEDIA_STATE_PLAYING = 'playing' -MEDIA_STATE_PAUSED = 'paused' -MEDIA_STATE_STOPPED = 'stopped' +MEDIA_TYPE_MUSIC = 'music' +MEDIA_TYPE_TVSHOW = 'tvshow' +MEDIA_TYPE_VIDEO = 'movie' +SUPPORT_PAUSE = 1 +SUPPORT_SEEK = 2 +SUPPORT_VOLUME_SET = 4 +SUPPORT_VOLUME_MUTE = 8 +SUPPORT_PREVIOUS_TRACK = 16 +SUPPORT_NEXT_TRACK = 32 +SUPPORT_YOUTUBE = 64 +SUPPORT_TURN_ON = 128 +SUPPORT_TURN_OFF = 256 YOUTUBE_COVER_URL_FORMAT = 'http://img.youtube.com/vi/{}/1.jpg' +SERVICE_TO_METHOD = { + SERVICE_TURN_ON: 'turn_on', + SERVICE_TURN_OFF: 'turn_off', + SERVICE_VOLUME_UP: 'volume_up', + SERVICE_VOLUME_DOWN: 'volume_down', + SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause', + SERVICE_MEDIA_PLAY: 'media_play', + SERVICE_MEDIA_PAUSE: 'media_pause', + SERVICE_MEDIA_NEXT_TRACK: 'media_next_track', + SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track', +} + +ATTR_TO_PROPERTY = { + ATTR_MEDIA_VOLUME_LEVEL: 'volume_level', + ATTR_MEDIA_VOLUME_MUTED: 'is_volume_muted', + ATTR_MEDIA_CONTENT_ID: 'media_content_id', + ATTR_MEDIA_CONTENT_TYPE: 'media_content_type', + ATTR_MEDIA_DURATION: 'media_duration', + ATTR_ENTITY_PICTURE: 'media_image_url', + ATTR_MEDIA_TITLE: 'media_title', + ATTR_MEDIA_ARTIST: 'media_artist', + ATTR_MEDIA_ALBUM: 'media_album', + ATTR_MEDIA_SERIES_TITLE: 'media_series_title', + ATTR_MEDIA_SEASON: 'media_season', + ATTR_MEDIA_EPISODE: 'media_episode', + ATTR_APP_ID: 'app_id', + ATTR_APP_NAME: 'app_name', + ATTR_SUPPORTED_MEDIA_COMMANDS: 'supported_media_commands', +} + def is_on(hass, entity_id=None): """ Returns true if specified media player entity_id is on. @@ -58,7 +97,7 @@ def is_on(hass, entity_id=None): entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN) - return any(not hass.states.is_state(entity_id, STATE_NO_APP) + return any(not hass.states.is_state(entity_id, STATE_OFF) for entity_id in entity_ids) @@ -90,21 +129,22 @@ def volume_down(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) -def volume_mute(hass, entity_id=None): - """ Send the media player the command to toggle its mute state. """ - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} +def mute_volume(hass, mute, entity_id=None): + """ Send the media player the command for volume down. """ + data = {ATTR_MEDIA_VOLUME_MUTED: mute} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data) -def volume_set(hass, entity_id=None, volume=None): - """ Set volume on media player. """ - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_MEDIA_VOLUME, volume), - ] if value is not None - } +def set_volume_level(hass, volume, entity_id=None): + """ Send the media player the command for volume down. """ + data = {ATTR_MEDIA_VOLUME_LEVEL: volume} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data) @@ -137,24 +177,11 @@ def media_next_track(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) -def media_prev_track(hass, entity_id=None): +def media_previous_track(hass, entity_id=None): """ Send the media player the command for prev track. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data) - - -SERVICE_TO_METHOD = { - SERVICE_TURN_ON: 'turn_on', - SERVICE_TURN_OFF: 'turn_off', - SERVICE_VOLUME_UP: 'volume_up', - SERVICE_VOLUME_DOWN: 'volume_down', - SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause', - SERVICE_MEDIA_PLAY: 'media_play', - SERVICE_MEDIA_PAUSE: 'media_pause', - SERVICE_MEDIA_NEXT_TRACK: 'media_next_track', - SERVICE_MEDIA_PREV_TRACK: 'media_prev_track', -} + hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) def setup(hass, config): @@ -183,17 +210,16 @@ def setup(hass, config): def volume_set_service(service, volume): """ Set specified volume on the media player. """ target_players = component.extract_from_service(service) + volume = service.data.get(ATTR_MEDIA_VOLUME_LEVEL) - for player in target_players: - player.volume_set(volume) + if volume is not None: + for player in target_players: + player.set_volume_level(volume) - if player.should_poll: - player.update_ha_state(True) + if player.should_poll: + player.update_ha_state(True) - hass.services.register(DOMAIN, SERVICE_VOLUME_SET, - lambda service: - volume_set_service( - service, service.data.get('volume'))) + hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service) def volume_mute_service(service, mute): """ Mute (true) or unmute (false) the media player. """ @@ -239,51 +265,198 @@ def setup(hass, config): class MediaPlayerDevice(Entity): """ ABC for media player devices. """ + # pylint: disable=too-many-public-methods,no-self-use + + # Implement these for your media player + + @property + def state(self): + """ State of the player. """ + return STATE_UNKNOWN + + @property + def volume_level(self): + """ Volume level of the media player (0..1). """ + return None + + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return None + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return None + + @property + def media_content_type(self): + """ Content type of current playing media. """ + return None + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + return None + + @property + def media_image_url(self): + """ Image url of current playing media. """ + return None + + @property + def media_title(self): + """ Title of current playing media. """ + return None + + @property + def media_artist(self): + """ Artist of current playing media. (Music track only) """ + return None + + @property + def media_album(self): + """ Album of current playing media. (Music track only) """ + return None + + @property + def media_series_title(self): + """ Series title of current playing media. (TV Show only)""" + return None + + @property + def media_season(self): + """ Season of current playing media. (TV Show only) """ + return None + + @property + def media_episode(self): + """ Episode of current playing media. (TV Show only) """ + return None + + @property + def app_id(self): + """ ID of the current running app. """ + return None + + @property + def app_name(self): + """ Name of the current running app. """ + return None + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return 0 + + @property + def device_state_attributes(self): + """ Extra attributes a device wants to expose. """ + return None def turn_on(self): - """ turn media player on. """ - pass + """ turn the media player on. """ + raise NotImplementedError() def turn_off(self): - """ turn media player off. """ - pass + """ turn the media player off. """ + raise NotImplementedError() - def volume_up(self): - """ volume_up media player. """ - pass + def mute_volume(self, mute): + """ mute the volume. """ + raise NotImplementedError() - def volume_down(self): - """ volume_down media player. """ - pass - - def volume_mute(self, mute): - """ mute (true) or unmute (false) media player. """ - pass - - def volume_set(self, volume): - """ set volume level of media player. """ - pass - - def media_play_pause(self): - """ media_play_pause media player. """ - pass + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + raise NotImplementedError() def media_play(self): - """ media_play media player. """ - pass + """ Send play commmand. """ + raise NotImplementedError() def media_pause(self): - """ media_pause media player. """ - pass + """ Send pause command. """ + raise NotImplementedError() - def media_prev_track(self): - """ media_prev_track media player. """ - pass + def media_previous_track(self): + """ Send previous track command. """ + raise NotImplementedError() def media_next_track(self): - """ media_next_track media player. """ - pass + """ Send next track command. """ + raise NotImplementedError() def play_youtube(self, media_id): """ Plays a YouTube media. """ - pass + raise NotImplementedError() + + # No need to overwrite these. + @property + def support_pause(self): + """ Boolean if pause is supported. """ + return bool(self.supported_media_commands & SUPPORT_PAUSE) + + @property + def support_seek(self): + """ Boolean if seek is supported. """ + return bool(self.supported_media_commands & SUPPORT_SEEK) + + @property + def support_volume_set(self): + """ Boolean if setting volume is supported. """ + return bool(self.supported_media_commands & SUPPORT_VOLUME_SET) + + @property + def support_volume_mute(self): + """ Boolean if muting volume is supported. """ + return bool(self.supported_media_commands & SUPPORT_VOLUME_MUTE) + + @property + def support_previous_track(self): + """ Boolean if previous track command supported. """ + return bool(self.supported_media_commands & SUPPORT_PREVIOUS_TRACK) + + @property + def support_next_track(self): + """ Boolean if next track command supported. """ + return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK) + + @property + def support_youtube(self): + """ Boolean if YouTube is supported. """ + return bool(self.supported_media_commands & SUPPORT_YOUTUBE) + + def volume_up(self): + """ volume_up media player. """ + if self.volume_level < 1: + self.set_volume_level(min(1, self.volume_level + .1)) + + def volume_down(self): + """ volume_down media player. """ + if self.volume_level > 0: + self.set_volume_level(max(0, self.volume_level - .1)) + + def media_play_pause(self): + """ media_play_pause media player. """ + if self.player_state == STATE_PLAYING: + self.media_pause() + else: + self.media_play() + + @property + def state_attributes(self): + """ Return the state attributes. """ + if self.state == STATE_OFF: + state_attr = {} + else: + state_attr = { + attr: getattr(self, prop) for attr, prop + in ATTR_TO_PROPERTY.items() if getattr(self, prop) + } + + device_attr = self.device_state_attributes + + if device_attr: + state_attr.update(device_attr) + + return state_attr diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index fdc17594b14..da2eaec69f1 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -5,121 +5,136 @@ homeassistant.components.media_player.demo Demo implementation of the media player. """ +from homeassistant.const import ( + STATE_PLAYING, STATE_PAUSED, STATE_OFF) + from homeassistant.components.media_player import ( - MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, - ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_DURATION, - ATTR_MEDIA_VOLUME, MEDIA_STATE_PLAYING, MEDIA_STATE_STOPPED, - YOUTUBE_COVER_URL_FORMAT, ATTR_MEDIA_IS_VOLUME_MUTED) -from homeassistant.const import ATTR_ENTITY_PICTURE + MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT, MEDIA_TYPE_VIDEO, + SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the cast platform. """ add_devices([ - DemoMediaPlayer( + DemoYoutubePlayer( 'Living Room', 'eyU3bRy2x44', '♥♥ The Best Fireplace Video (3 hours)'), - DemoMediaPlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours') + DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours') ]) -class DemoMediaPlayer(MediaPlayerDevice): +YOUTUBE_PLAYER_SUPPORT = \ + SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF + + +class DemoYoutubePlayer(MediaPlayerDevice): """ A Demo media player that only supports YouTube. """ + # We only implement the methods that we support + # pylint: disable=abstract-method def __init__(self, name, youtube_id=None, media_title=None): self._name = name - self.is_playing = youtube_id is not None + self._player_state = STATE_PLAYING self.youtube_id = youtube_id - self.media_title = media_title - self.volume = 1.0 - self.is_volume_muted = False + self._media_title = media_title + self._volume_level = 1.0 + self._volume_muted = False @property def should_poll(self): - """ No polling needed for a demo componentn. """ + """ We will push an update after each command. """ return False @property def name(self): - """ Returns the name of the device. """ + """ Name of the media player. """ return self._name @property def state(self): - """ Returns the state of the device. """ - return STATE_NO_APP if self.youtube_id is None else "YouTube" + """ State of the player. """ + return self._player_state @property - def state_attributes(self): - """ Returns the state attributes. """ - if self.youtube_id is None: - return + def volume_level(self): + """ Volume level of the media player (0..1). """ + return self._volume_level - state_attr = { - ATTR_MEDIA_CONTENT_ID: self.youtube_id, - ATTR_MEDIA_TITLE: self.media_title, - ATTR_MEDIA_DURATION: 100, - ATTR_MEDIA_VOLUME: self.volume, - ATTR_MEDIA_IS_VOLUME_MUTED: self.is_volume_muted, - ATTR_ENTITY_PICTURE: - YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id) - } + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return self._volume_muted - if self.is_playing: - state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_PLAYING - else: - state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_STOPPED + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return self.youtube_id - return state_attr + @property + def media_content_type(self): + """ Content type of current playing media. """ + return MEDIA_TYPE_VIDEO + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + return 360 + + @property + def media_image_url(self): + """ Image url of current playing media. """ + return YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id) + + @property + def media_title(self): + """ Title of current playing media. """ + return self._media_title + + @property + def app_name(self): + """ Current running app. """ + return "YouTube" + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return YOUTUBE_PLAYER_SUPPORT def turn_on(self): - """ turn_off media player. """ - self.youtube_id = "eyU3bRy2x44" - self.is_playing = False + """ turn the media player on. """ + self._player_state = STATE_PLAYING self.update_ha_state() def turn_off(self): - """ turn_off media player. """ - self.youtube_id = None - self.is_playing = False + """ turn the media player off. """ + self._player_state = STATE_OFF self.update_ha_state() - def volume_up(self): - """ volume_up media player. """ - if self.volume < 1: - self.volume += 0.1 - self.update_ha_state() - - def volume_down(self): - """ volume_down media player. """ - if self.volume > 0: - self.volume -= 0.1 - self.update_ha_state() - - def volume_mute(self, mute): - """ mute (true) or unmute (false) media player. """ - self.is_volume_muted = mute + def mute_volume(self, mute): + """ mute the volume. """ + self._volume_muted = mute self.update_ha_state() - def media_play_pause(self): - """ media_play_pause media player. """ - self.is_playing = not self.is_playing + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + self._volume_level = volume self.update_ha_state() def media_play(self): - """ media_play media player. """ - self.is_playing = True + """ Send play commmand. """ + self._player_state = STATE_PLAYING self.update_ha_state() def media_pause(self): - """ media_pause media player. """ - self.is_playing = False + """ Send pause command. """ + self._player_state = STATE_PAUSED self.update_ha_state() def play_youtube(self, media_id): """ Plays a YouTube media. """ self.youtube_id = media_id - self.media_title = 'Demo media title' - self.is_playing = True + self._media_title = 'some YouTube video' self.update_ha_state() diff --git a/homeassistant/const.py b/homeassistant/const.py index a4ea2651d28..639f8159338 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -40,6 +40,8 @@ STATE_NOT_HOME = 'not_home' STATE_UNKNOWN = "unknown" STATE_OPEN = 'open' STATE_CLOSED = 'closed' +STATE_PLAYING = 'playing' +STATE_PAUSED = 'paused' # #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event @@ -104,7 +106,7 @@ SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause" SERVICE_MEDIA_PLAY = "media_play" SERVICE_MEDIA_PAUSE = "media_pause" SERVICE_MEDIA_NEXT_TRACK = "media_next_track" -SERVICE_MEDIA_PREV_TRACK = "media_prev_track" +SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track" # #### API / REMOTE #### SERVER_PORT = 8123 diff --git a/tests/test_component_media_player.py b/tests/test_component_media_player.py index fdde02e5594..b7f0b847e80 100644 --- a/tests/test_component_media_player.py +++ b/tests/test_component_media_player.py @@ -10,9 +10,10 @@ import unittest import homeassistant as ha from homeassistant.const import ( + STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, ATTR_ENTITY_ID) + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID) import homeassistant.components.media_player as media_player from helpers import mock_service @@ -29,7 +30,7 @@ class TestMediaPlayer(unittest.TestCase): self.hass = ha.HomeAssistant() self.test_entity = media_player.ENTITY_ID_FORMAT.format('living_room') - self.hass.states.set(self.test_entity, media_player.STATE_NO_APP) + self.hass.states.set(self.test_entity, STATE_OFF) self.test_entity2 = media_player.ENTITY_ID_FORMAT.format('bedroom') self.hass.states.set(self.test_entity2, "YouTube") @@ -56,7 +57,7 @@ class TestMediaPlayer(unittest.TestCase): SERVICE_MEDIA_PLAY: media_player.media_play, SERVICE_MEDIA_PAUSE: media_player.media_pause, SERVICE_MEDIA_NEXT_TRACK: media_player.media_next_track, - SERVICE_MEDIA_PREV_TRACK: media_player.media_prev_track + SERVICE_MEDIA_PREVIOUS_TRACK: media_player.media_previous_track } for service_name, service_method in services.items():