Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 35 additions & 11 deletions homeassistant/components/media_player/cast.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ def __init__(self, cast_info):
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]

Expand Down Expand Up @@ -361,7 +362,8 @@ def _async_disconnect(self):
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

Expand All @@ -388,8 +390,36 @@ def new_cast_status(self, cast_status):

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dersger That shouldn't be an issue since the MEDIA_STATUS package always contains the currentTime attribute. At least that's the case with all apps I just tested: Spotify, YouTube and Netflix.

However, there's a really weird thing going on: With the packets we receive when we manually poll the media status with GET_STATUS. We don't get the actual currentTime in the track, but the current time from the last callback! So for example, if I pause and play a track at 30 seconds, we will first receive MEDIA_STATUS packages with currentTime = 30 from the callback. But even 30 and 60 seconds later when manually poll the device, currentTime in the packet will still be 30 - even though it should be 60/90 by now.

As there's no information from the cast packages about relative to which timestamp currentTime is measured, I think we can't really solve this problem :( So I'd say maybe remove the half-polling feature again?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugggh. Can we distinguish between polling and non-polling updates?

Copy link
Copy Markdown
Member

@OttoWinter OttoWinter Apr 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current pychromecast library, AFAIK not. We might be able to create two MediaControllers on two different cast socket "channels" (not actually new sockets, just IDs that identify a stream of packets), and use one of them for Push-Updates and the other one for polling updates. But that would probably just end up being a lot of spaghetti code :(

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):
Expand Down Expand Up @@ -595,21 +625,15 @@ def supported_features(self):
@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):
"""When was the position of the current playing media valid.

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]:
Expand Down
85 changes: 84 additions & 1 deletion tests/components/media_player/test_cast.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,7 +15,8 @@
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


Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION'

assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'


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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION'

assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'


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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION'

assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_5
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'


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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION'

assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'


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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION'

assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'


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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION'

assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'


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()
Expand Down