Avoid unnecessary cast state updates#13770
Conversation
|
I am confused. I was already under the impression that we did not update the state of an entity if the position remains the same / can be calculated because player state hasn't changed 🤔 CC @OttoWinter |
|
The component was recently changed to be polling. I believe it's a result of that. But your comment made me think. Maybe we instead should have some generic handling of this in MediaPlayerDevice.state_attributes()? |
|
It's difficult to make this generic as every media player has a different way of getting data. Cast is push, VLC is polling etc. |
|
But inside MediaPlayerDevice.state_attributes() it doesn't matter if it's push or poll. What I mean is that the code there could get the old state from self.hass.states, compare it to the new state_attr from the component, and then modify the return value to keep the old position and update time in cases where an update would be unnecessary. It would then fix this type of problem for all media players. But maybe it's a bit unconventional to change the state attributes inside MediaPlayerDevice.state_attributes(). |
|
The cast integration is now "semi-polling" indeed (since #13275, tl;dr in theory, the cast protocol notifies us of all media state changes, but in practice quite a few apps including netflix never send those callbacks out 😕). We're only in polling mode iff an app is active, but yes the issue is definitely caused by this. So to summarise, the reason this is happening in the first place is because of I'm not 100% sure how this should be handled. Doing this inside the cast integration just seems quite messy and potentially lots of other polling |
|
I've been thinking about how to change MediaPlayerDevice.state_attributes() to handle all media players. This is totally untested but can at least show my idea: I think both the advantage and disadvantage of that is that the component will be in less control of the attribute updates. |
| z"""The tests for the Cast Media player platform.""" | ||
| # pylint: disable=protected-access | ||
| import asyncio | ||
| import datetime as dt |
There was a problem hiding this comment.
module level import not at top of file
| @@ -1,6 +1,7 @@ | |||
| """The tests for the Cast Media player platform.""" | |||
| z"""The tests for the Cast Media player platform.""" | |||
| entity.new_media_status(media_status) | ||
| await hass.async_block_till_done() | ||
| assert ATTR_MEDIA_POSITION not in state.attributes | ||
| assert ATTR_MEDIA_POSITION_UPDATED_AT not in state.attributes |
There was a problem hiding this comment.
undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'
| with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): | ||
| entity.new_media_status(media_status) | ||
| await hass.async_block_till_done() | ||
| assert ATTR_MEDIA_POSITION not in state.attributes |
There was a problem hiding this comment.
undefined name 'ATTR_MEDIA_POSITION'
| 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 |
There was a problem hiding this comment.
undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'
| 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 |
There was a problem hiding this comment.
undefined name 'ATTR_MEDIA_POSITION'
| 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 |
There was a problem hiding this comment.
undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'
| 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 |
There was a problem hiding this comment.
undefined name 'ATTR_MEDIA_POSITION'
| 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 |
There was a problem hiding this comment.
undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'
| 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 |
There was a problem hiding this comment.
undefined name 'ATTR_MEDIA_POSITION'
| 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 |
There was a problem hiding this comment.
undefined name 'ATTR_MEDIA_POSITION_UPDATED_AT'
| 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 |
There was a problem hiding this comment.
undefined name 'ATTR_MEDIA_POSITION'
|
Thanks for taking the time to fix this! 👍 🐬 |
|
So I am still seeing this issue locally. See here some logs (I removed duplicate attributes and formatted for readability) As you can see, |
|
I wrote the code so that it's supposed to update media_position if it doesn't increase as much as the elapsed time between old and new (and accepting a 1 second jitter as equal) in 'playing' state. It looks like that is true for all of the updates. At 15:53:05 the position jumps 30 seconds in almost no time. The later updates wouldn't have been created if the state had been 'paused' instead. |
|
Not sure what's going on. It's also not consistent wrong, only like 80% of the time. |
|
I did a small code review of https://github.com/balloob/pychromecast/blob/master/pychromecast/controllers/media.py. I wonder if Then current_time_last_updated could be used instead of |
| elapsed = now - self.media_status_position_received | ||
| do_update = abs(media_status.current_time - | ||
| (self.media_status_position + | ||
| elapsed.total_seconds())) > 1 |
There was a problem hiding this comment.
@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?
There was a problem hiding this comment.
ugggh. Can we distinguish between polling and non-polling updates?
There was a problem hiding this comment.
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 :(
Description:
Got a lot of state entries in the recorder database due to cast updates every 11 seconds or so.
Change it so that some state entries are skipped when position hasn't changed.
Checklist:
tox. Your PR cannot be merged unless tests passIf the code does not interact with devices: