diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 2edda0645b0..30d4bd166d0 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -288,7 +288,8 @@ class CastDevice(MediaPlayerDevice): self._chromecast = None # type: Optional[pychromecast.Chromecast] self.cast_status = None self.media_status = None - self.media_status_received = None + self.media_status_position = None + self.media_status_position_received = None self._available = False # type: bool self._status_listener = None # type: Optional[CastStatusListener] @@ -361,7 +362,8 @@ class CastDevice(MediaPlayerDevice): self._chromecast = None self.cast_status = None self.media_status = None - self.media_status_received = None + self.media_status_position = None + self.media_status_position_received = None self._status_listener.invalidate() self._status_listener = None @@ -388,8 +390,36 @@ class CastDevice(MediaPlayerDevice): def new_media_status(self, media_status): """Handle updates of the media status.""" + # Only use media position for playing/paused, + # and for normal playback rate + if (media_status is None or + abs(media_status.playback_rate - 1) > 0.01 or + not (media_status.player_is_playing or + media_status.player_is_paused)): + self.media_status_position = None + self.media_status_position_received = None + else: + # Avoid unnecessary state attribute updates if player_state and + # calculated position stay the same + now = dt_util.utcnow() + do_update = \ + (self.media_status is None or + self.media_status_position is None or + self.media_status.player_state != media_status.player_state) + if not do_update: + if media_status.player_is_playing: + elapsed = now - self.media_status_position_received + do_update = abs(media_status.current_time - + (self.media_status_position + + elapsed.total_seconds())) > 1 + else: + do_update = \ + self.media_status_position != media_status.current_time + if do_update: + self.media_status_position = media_status.current_time + self.media_status_position_received = now + self.media_status = media_status - self.media_status_received = dt_util.utcnow() self.schedule_update_ha_state() def new_connection_status(self, connection_status): @@ -595,13 +625,7 @@ class CastDevice(MediaPlayerDevice): @property def media_position(self): """Position of current playing media in seconds.""" - if self.media_status is None or \ - not (self.media_status.player_is_playing or - self.media_status.player_is_paused or - self.media_status.player_is_idle): - return None - - return self.media_status.current_time + return self.media_status_position @property def media_position_updated_at(self): @@ -609,7 +633,7 @@ class CastDevice(MediaPlayerDevice): Returns value from homeassistant.util.dt.utcnow(). """ - return self.media_status_received + return self.media_status_position_received @property def unique_id(self) -> Optional[str]: diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index ee69ec1c85d..0c0f3906dc2 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -1,6 +1,7 @@ """The tests for the Cast Media player platform.""" # pylint: disable=protected-access import asyncio +import datetime as dt from typing import Optional from unittest.mock import patch, MagicMock, Mock from uuid import UUID @@ -14,7 +15,8 @@ from homeassistant.components.media_player.cast import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ async_dispatcher_send -from homeassistant.components.media_player import cast +from homeassistant.components.media_player import cast, \ + ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT from homeassistant.setup import async_setup_component @@ -286,6 +288,8 @@ async def test_entity_media_states(hass: HomeAssistantType): assert entity.unique_id == full_info.uuid media_status = MagicMock(images=None) + media_status.current_time = 0 + media_status.playback_rate = 1 media_status.player_is_playing = True entity.new_media_status(media_status) await hass.async_block_till_done() @@ -320,6 +324,85 @@ async def test_entity_media_states(hass: HomeAssistantType): assert state.state == 'unknown' +async def test_entity_media_position(hass: HomeAssistantType): + """Test various entity media states.""" + 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) + + media_status = MagicMock(images=None) + media_status.current_time = 10 + media_status.playback_rate = 1 + media_status.player_is_playing = True + media_status.player_is_paused = False + media_status.player_is_idle = False + now = dt.datetime.now(dt.timezone.utc) + with patch('homeassistant.util.dt.utcnow', return_value=now): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 10 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now + + media_status.current_time = 15 + now_plus_5 = now + dt.timedelta(seconds=5) + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 10 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now + + media_status.current_time = 20 + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 20 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_5 + + media_status.current_time = 25 + now_plus_10 = now + dt.timedelta(seconds=10) + media_status.player_is_playing = False + media_status.player_is_paused = True + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_10): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 25 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10 + + now_plus_15 = now + dt.timedelta(seconds=15) + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_15): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 25 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10 + + media_status.current_time = 30 + now_plus_20 = now + dt.timedelta(seconds=20) + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 30 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_20 + + media_status.player_is_paused = False + media_status.player_is_idle = True + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert ATTR_MEDIA_POSITION not in state.attributes + assert ATTR_MEDIA_POSITION_UPDATED_AT not in state.attributes + + async def test_switched_host(hass: HomeAssistantType): """Test cast device listens for changed hosts and disconnects old cast.""" info = get_fake_chromecast_info()