From a3b733f1ec99c9802a4564edca7ce85ebf13dcd0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 14 Feb 2021 19:35:14 -0500 Subject: [PATCH] Add additional supported feature support to universal media player (#44711) * add additional supported feature support to universal media player * add missing services --- .../components/universal/media_player.py | 74 ++++++++++- .../components/universal/test_media_player.py | 116 +++++++++++++++++- 2 files changed, 188 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 8fff0e80dfb8..2a5fcee34dc3 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -20,6 +20,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, + ATTR_MEDIA_REPEAT, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE, @@ -28,13 +29,23 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, + ATTR_SOUND_MODE, + ATTR_SOUND_MODE_LIST, DOMAIN, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_REPEAT_SET, + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, @@ -55,7 +66,9 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP, + SERVICE_REPEAT_SET, SERVICE_SHUFFLE_SET, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, @@ -382,6 +395,16 @@ class UniversalMediaPlayer(MediaPlayerEntity): """Name of the current running app.""" return self._child_attr(ATTR_APP_NAME) + @property + def sound_mode(self): + """Return the current sound mode of the device.""" + return self._override_or_child_attr(ATTR_SOUND_MODE) + + @property + def sound_mode_list(self): + """List of available sound modes.""" + return self._override_or_child_attr(ATTR_SOUND_MODE_LIST) + @property def source(self): """Return the current input source of the device.""" @@ -392,6 +415,11 @@ class UniversalMediaPlayer(MediaPlayerEntity): """List of available input sources.""" return self._override_or_child_attr(ATTR_INPUT_SOURCE_LIST) + @property + def repeat(self): + """Boolean if repeating is enabled.""" + return self._override_or_child_attr(ATTR_MEDIA_REPEAT) + @property def shuffle(self): """Boolean if shuffling is enabled.""" @@ -407,6 +435,22 @@ class UniversalMediaPlayer(MediaPlayerEntity): if SERVICE_TURN_OFF in self._cmds: flags |= SUPPORT_TURN_OFF + if SERVICE_MEDIA_PLAY_PAUSE in self._cmds: + flags |= SUPPORT_PLAY | SUPPORT_PAUSE + else: + if SERVICE_MEDIA_PLAY in self._cmds: + flags |= SUPPORT_PLAY + if SERVICE_MEDIA_PAUSE in self._cmds: + flags |= SUPPORT_PAUSE + + if SERVICE_MEDIA_STOP in self._cmds: + flags |= SUPPORT_STOP + + if SERVICE_MEDIA_NEXT_TRACK in self._cmds: + flags |= SUPPORT_NEXT_TRACK + if SERVICE_MEDIA_PREVIOUS_TRACK in self._cmds: + flags |= SUPPORT_PREVIOUS_TRACK + if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN]]): flags |= SUPPORT_VOLUME_STEP if SERVICE_VOLUME_SET in self._cmds: @@ -415,7 +459,10 @@ class UniversalMediaPlayer(MediaPlayerEntity): if SERVICE_VOLUME_MUTE in self._cmds and ATTR_MEDIA_VOLUME_MUTED in self._attrs: flags |= SUPPORT_VOLUME_MUTE - if SERVICE_SELECT_SOURCE in self._cmds: + if ( + SERVICE_SELECT_SOURCE in self._cmds + and ATTR_INPUT_SOURCE_LIST in self._attrs + ): flags |= SUPPORT_SELECT_SOURCE if SERVICE_CLEAR_PLAYLIST in self._cmds: @@ -424,6 +471,15 @@ class UniversalMediaPlayer(MediaPlayerEntity): if SERVICE_SHUFFLE_SET in self._cmds and ATTR_MEDIA_SHUFFLE in self._attrs: flags |= SUPPORT_SHUFFLE_SET + if SERVICE_REPEAT_SET in self._cmds and ATTR_MEDIA_REPEAT in self._attrs: + flags |= SUPPORT_REPEAT_SET + + if ( + SERVICE_SELECT_SOUND_MODE in self._cmds + and ATTR_SOUND_MODE_LIST in self._attrs + ): + flags |= SUPPORT_SELECT_SOUND_MODE + return flags @property @@ -502,6 +558,13 @@ class UniversalMediaPlayer(MediaPlayerEntity): """Play or pause the media player.""" await self._async_call_service(SERVICE_MEDIA_PLAY_PAUSE) + async def async_select_sound_mode(self, sound_mode): + """Select sound mode.""" + data = {ATTR_SOUND_MODE: sound_mode} + await self._async_call_service( + SERVICE_SELECT_SOUND_MODE, data, allow_override=True + ) + async def async_select_source(self, source): """Set the input source.""" data = {ATTR_INPUT_SOURCE: source} @@ -516,6 +579,15 @@ class UniversalMediaPlayer(MediaPlayerEntity): data = {ATTR_MEDIA_SHUFFLE: shuffle} await self._async_call_service(SERVICE_SHUFFLE_SET, data, allow_override=True) + async def async_set_repeat(self, repeat): + """Set repeat mode.""" + data = {ATTR_MEDIA_REPEAT: repeat} + await self._async_call_service(SERVICE_REPEAT_SET, data, allow_override=True) + + async def async_toggle(self): + """Toggle the power on the media player.""" + await self._async_call_service(SERVICE_TOGGLE) + async def async_update(self): """Update state in HA.""" for child_name in self._children: diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index fd75620f3188..75cf029af40d 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -51,6 +51,7 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): self._tracks = 12 self._media_image_url = None self._shuffle = False + self._sound_mode = None self.service_calls = { "turn_on": mock_service( @@ -71,6 +72,9 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): "media_pause": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PAUSE ), + "media_stop": mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_STOP + ), "media_previous_track": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PREVIOUS_TRACK ), @@ -92,12 +96,21 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): "media_play_pause": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PLAY_PAUSE ), + "select_sound_mode": mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOUND_MODE + ), "select_source": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_SELECT_SOURCE ), + "toggle": mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_TOGGLE + ), "clear_playlist": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_CLEAR_PLAYLIST ), + "repeat_set": mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_REPEAT_SET + ), "shuffle_set": mock_service( hass, media_player.DOMAIN, media_player.SERVICE_SHUFFLE_SET ), @@ -162,18 +175,30 @@ class MockMediaPlayer(media_player.MediaPlayerEntity): """Mock pause.""" self._state = STATE_PAUSED + def select_sound_mode(self, sound_mode): + """Set the sound mode.""" + self._sound_mode = sound_mode + def select_source(self, source): """Set the input source.""" self._source = source + def async_toggle(self): + """Toggle the power on the media player.""" + self._state = STATE_OFF if self._state == STATE_ON else STATE_ON + def clear_playlist(self): """Clear players playlist.""" self._tracks = 0 def set_shuffle(self, shuffle): - """Clear players playlist.""" + """Enable/disable shuffle mode.""" self._shuffle = shuffle + def set_repeat(self, repeat): + """Enable/disable repeat mode.""" + self._repeat = repeat + class TestMediaPlayer(unittest.TestCase): """Test the media_player module.""" @@ -205,9 +230,18 @@ class TestMediaPlayer(unittest.TestCase): self.mock_source_id = f"{input_select.DOMAIN}.source" self.hass.states.set(self.mock_source_id, "dvd") + self.mock_sound_mode_list_id = f"{input_select.DOMAIN}.sound_mode_list" + self.hass.states.set(self.mock_sound_mode_list_id, ["music", "movie"]) + + self.mock_sound_mode_id = f"{input_select.DOMAIN}.sound_mode" + self.hass.states.set(self.mock_sound_mode_id, "music") + self.mock_shuffle_switch_id = switch.ENTITY_ID_FORMAT.format("shuffle") self.hass.states.set(self.mock_shuffle_switch_id, STATE_OFF) + self.mock_repeat_switch_id = switch.ENTITY_ID_FORMAT.format("repeat") + self.hass.states.set(self.mock_repeat_switch_id, STATE_OFF) + self.config_children_only = { "name": "test", "platform": "universal", @@ -230,6 +264,9 @@ class TestMediaPlayer(unittest.TestCase): "source_list": self.mock_source_list_id, "state": self.mock_state_switch_id, "shuffle": self.mock_shuffle_switch_id, + "repeat": self.mock_repeat_switch_id, + "sound_mode_list": self.mock_sound_mode_list_id, + "sound_mode": self.mock_sound_mode_id, }, } self.addCleanup(self.tear_down_cleanup) @@ -507,6 +544,17 @@ class TestMediaPlayer(unittest.TestCase): asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.is_volume_muted + def test_sound_mode_list_children_and_attr(self): + """Test sound mode list property w/ children and attrs.""" + config = validate_config(self.config_children_and_attr) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + assert "['music', 'movie']" == ump.sound_mode_list + + self.hass.states.set(self.mock_sound_mode_list_id, ["music", "movie", "game"]) + assert "['music', 'movie', 'game']" == ump.sound_mode_list + def test_source_list_children_and_attr(self): """Test source list property w/ children and attrs.""" config = validate_config(self.config_children_and_attr) @@ -518,6 +566,17 @@ class TestMediaPlayer(unittest.TestCase): self.hass.states.set(self.mock_source_list_id, ["dvd", "htpc", "game"]) assert "['dvd', 'htpc', 'game']" == ump.source_list + def test_sound_mode_children_and_attr(self): + """Test sound modeproperty w/ children and attrs.""" + config = validate_config(self.config_children_and_attr) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + assert "music" == ump.sound_mode + + self.hass.states.set(self.mock_sound_mode_id, "movie") + assert "movie" == ump.sound_mode + def test_source_children_and_attr(self): """Test source property w/ children and attrs.""" config = validate_config(self.config_children_and_attr) @@ -579,8 +638,17 @@ class TestMediaPlayer(unittest.TestCase): "volume_down": excmd, "volume_mute": excmd, "volume_set": excmd, + "select_sound_mode": excmd, "select_source": excmd, + "repeat_set": excmd, "shuffle_set": excmd, + "media_play": excmd, + "media_pause": excmd, + "media_stop": excmd, + "media_next_track": excmd, + "media_previous_track": excmd, + "toggle": excmd, + "clear_playlist": excmd, } config = validate_config(config) @@ -598,13 +666,41 @@ class TestMediaPlayer(unittest.TestCase): | universal.SUPPORT_TURN_OFF | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE + | universal.SUPPORT_SELECT_SOUND_MODE | universal.SUPPORT_SELECT_SOURCE + | universal.SUPPORT_REPEAT_SET | universal.SUPPORT_SHUFFLE_SET | universal.SUPPORT_VOLUME_SET + | universal.SUPPORT_PLAY + | universal.SUPPORT_PAUSE + | universal.SUPPORT_STOP + | universal.SUPPORT_NEXT_TRACK + | universal.SUPPORT_PREVIOUS_TRACK + | universal.SUPPORT_CLEAR_PLAYLIST ) assert check_flags == ump.supported_features + def test_supported_features_play_pause(self): + """Test supported media commands with play_pause function.""" + config = copy(self.config_children_and_attr) + excmd = {"service": "media_player.test", "data": {"entity_id": "test"}} + config["commands"] = {"media_play_pause": excmd} + config = validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.schedule_update_ha_state() + self.hass.block_till_done() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + + check_flags = universal.SUPPORT_PLAY | universal.SUPPORT_PAUSE + + assert check_flags == ump.supported_features + def test_service_call_no_active_child(self): """Test a service call to children with no active child.""" config = validate_config(self.config_children_and_attr) @@ -663,6 +759,11 @@ class TestMediaPlayer(unittest.TestCase): ).result() assert 1 == len(self.mock_mp_2.service_calls["media_pause"]) + asyncio.run_coroutine_threadsafe( + ump.async_media_stop(), self.hass.loop + ).result() + assert 1 == len(self.mock_mp_2.service_calls["media_stop"]) + asyncio.run_coroutine_threadsafe( ump.async_media_previous_track(), self.hass.loop ).result() @@ -696,6 +797,11 @@ class TestMediaPlayer(unittest.TestCase): ).result() assert 1 == len(self.mock_mp_2.service_calls["media_play_pause"]) + asyncio.run_coroutine_threadsafe( + ump.async_select_sound_mode("music"), self.hass.loop + ).result() + assert 1 == len(self.mock_mp_2.service_calls["select_sound_mode"]) + asyncio.run_coroutine_threadsafe( ump.async_select_source("dvd"), self.hass.loop ).result() @@ -706,11 +812,19 @@ class TestMediaPlayer(unittest.TestCase): ).result() assert 1 == len(self.mock_mp_2.service_calls["clear_playlist"]) + asyncio.run_coroutine_threadsafe( + ump.async_set_repeat(True), self.hass.loop + ).result() + assert 1 == len(self.mock_mp_2.service_calls["repeat_set"]) + asyncio.run_coroutine_threadsafe( ump.async_set_shuffle(True), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["shuffle_set"]) + asyncio.run_coroutine_threadsafe(ump.async_toggle(), self.hass.loop).result() + assert 1 == len(self.mock_mp_2.service_calls["toggle"]) + def test_service_call_to_command(self): """Test service call to command.""" config = copy(self.config_children_only)