diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 12f524f2121..cb60cdc2967 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -508,11 +508,12 @@ class CastDevice(MediaPlayerDevice): self.mz_cast_status = {} self.mz_media_status = {} self.mz_media_status_received = {} + self.mz_mgr = None self._available = False # type: bool self._dynamic_group_available = False # type: bool self._status_listener = None # type: Optional[CastStatusListener] self._dynamic_group_status_listener = None \ - # type: Optional[CastStatusListener] + # type: Optional[DynamicGroupCastStatusListener] self._add_remove_handler = None self._del_remove_handler = None @@ -638,10 +639,10 @@ class CastDevice(MediaPlayerDevice): if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: from pychromecast.controllers.multizone import MultizoneManager self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager() - mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] + self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] self._status_listener = CastStatusListener( - self, chromecast, mz_mgr) + self, chromecast, self.mz_mgr) self._available = False self.cast_status = chromecast.status self.media_status = chromecast.media_controller.status @@ -736,6 +737,7 @@ class CastDevice(MediaPlayerDevice): self.mz_cast_status = {} self.mz_media_status = {} self.mz_media_status_received = {} + self.mz_mgr = None if self._status_listener is not None: self._status_listener.invalidate() self._status_listener = None @@ -857,6 +859,32 @@ class CastDevice(MediaPlayerDevice): self.schedule_update_ha_state() # ========== Service Calls ========== + def _media_controller(self): + """ + Return media status. + + First try from our own cast, then dynamic groups and finally + groups which our cast is a member in. + """ + media_status = self.media_status + media_controller = self._chromecast.media_controller + + if ((media_status is None or media_status.player_state == "UNKNOWN") + and self._dynamic_group_cast is not None): + media_status = self.dynamic_group_media_status + media_controller = \ + self._dynamic_group_cast.media_controller + + if media_status is None or media_status.player_state == "UNKNOWN": + groups = self.mz_media_status + for k, val in groups.items(): + if val and val.player_state != "UNKNOWN": + media_controller = \ + self.mz_mgr.get_multizone_mediacontroller(k) + break + + return media_controller + def turn_on(self): """Turn on the cast device.""" import pychromecast @@ -887,30 +915,37 @@ class CastDevice(MediaPlayerDevice): def media_play(self): """Send play command.""" - self._chromecast.media_controller.play() + media_controller = self._media_controller() + media_controller.play() def media_pause(self): """Send pause command.""" - self._chromecast.media_controller.pause() + media_controller = self._media_controller() + media_controller.pause() def media_stop(self): """Send stop command.""" - self._chromecast.media_controller.stop() + media_controller = self._media_controller() + media_controller.stop() def media_previous_track(self): """Send previous track command.""" - self._chromecast.media_controller.rewind() + media_controller = self._media_controller() + media_controller.rewind() def media_next_track(self): """Send next track command.""" - self._chromecast.media_controller.skip() + media_controller = self._media_controller() + media_controller.skip() def media_seek(self, position): """Seek the media to a specific location.""" - self._chromecast.media_controller.seek(position) + media_controller = self._media_controller() + media_controller.seek(position) def play_media(self, media_type, media_id, **kwargs): """Play media from a URL.""" + # We do not want this to be forwarded to a group / dynamic group self._chromecast.media_controller.play_media(media_id, media_type) # ========== Properties ========== diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 85012a4a710..7c40b09d03e 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -397,6 +397,112 @@ async def test_dynamic_group_media_states(hass: HomeAssistantType): assert state.state == 'playing' +async def test_group_media_control(hass: HomeAssistantType): + """Test media states are read from group if entity has no state.""" + info = get_fake_chromecast_info() + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) + + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + chromecast, entity = await async_setup_media_player_cast(hass, info) + + entity._available = True + entity.schedule_update_ha_state() + await hass.async_block_till_done() + + state = hass.states.get('media_player.speaker') + assert state is not None + assert state.name == 'Speaker' + assert state.state == 'unknown' + assert entity.unique_id == full_info.uuid + + group_media_status = MagicMock(images=None) + player_media_status = MagicMock(images=None) + + # Player has no state, group is playing -> Should forward calls to group + group_media_status.player_is_playing = True + entity.multizone_new_media_status(str(FakeGroupUUID), group_media_status) + entity.media_play() + grp_media = entity.mz_mgr.get_multizone_mediacontroller(str(FakeGroupUUID)) + assert grp_media.play.called + assert not chromecast.media_controller.play.called + + # Player is paused, group is playing -> Should not forward + player_media_status.player_is_playing = False + player_media_status.player_is_paused = True + entity.new_media_status(player_media_status) + entity.media_pause() + grp_media = entity.mz_mgr.get_multizone_mediacontroller(str(FakeGroupUUID)) + assert not grp_media.pause.called + assert chromecast.media_controller.pause.called + + # Player is in unknown state, group is playing -> Should forward to group + player_media_status.player_state = "UNKNOWN" + entity.new_media_status(player_media_status) + entity.media_stop() + grp_media = entity.mz_mgr.get_multizone_mediacontroller(str(FakeGroupUUID)) + assert grp_media.stop.called + assert not chromecast.media_controller.stop.called + + # Verify play_media is not forwarded + entity.play_media(None, None) + assert not grp_media.play_media.called + assert chromecast.media_controller.play_media.called + + +async def test_dynamic_group_media_control(hass: HomeAssistantType): + """Test media states are read from group if entity has no state.""" + info = get_fake_chromecast_info() + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) + + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + chromecast, entity = await async_setup_media_player_cast(hass, info) + + entity._available = True + entity.schedule_update_ha_state() + entity._dynamic_group_cast = MagicMock() + await hass.async_block_till_done() + + state = hass.states.get('media_player.speaker') + assert state is not None + assert state.name == 'Speaker' + assert state.state == 'unknown' + assert entity.unique_id == full_info.uuid + + group_media_status = MagicMock(images=None) + player_media_status = MagicMock(images=None) + + # Player has no state, dynamic group is playing -> Should forward + group_media_status.player_is_playing = True + entity.new_dynamic_group_media_status(group_media_status) + entity.media_previous_track() + assert entity._dynamic_group_cast.media_controller.rewind.called + assert not chromecast.media_controller.rewind.called + + # Player is paused, dynamic group is playing -> Should not forward + player_media_status.player_is_playing = False + player_media_status.player_is_paused = True + entity.new_media_status(player_media_status) + entity.media_next_track() + assert not entity._dynamic_group_cast.media_controller.skip.called + assert chromecast.media_controller.skip.called + + # Player is in unknown state, dynamic group is playing -> Should forward + player_media_status.player_state = "UNKNOWN" + entity.new_media_status(player_media_status) + entity.media_seek(None) + assert entity._dynamic_group_cast.media_controller.seek.called + assert not chromecast.media_controller.seek.called + + # Verify play_media is not forwarded + entity.play_media(None, None) + assert not entity._dynamic_group_cast.media_controller.play_media.called + assert chromecast.media_controller.play_media.called + + async def test_disconnect_on_stop(hass: HomeAssistantType): """Test cast device disconnects socket on stop.""" info = get_fake_chromecast_info()