Forward media control to playing group (#22566)

* Forward media control to playing group

* Fix forwarding control to dynamic group

* Fix, add tests
This commit is contained in:
emontnemery 2019-03-31 05:07:01 +02:00 committed by Paulus Schoutsen
parent 71ecaa4385
commit 54777a81bc
2 changed files with 150 additions and 9 deletions

View File

@ -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 ==========

View File

@ -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()