Cast Integration Cleanup#13275
Conversation
114167f to
827d524
Compare
| """Helper class to handle pychromecast status callbacks. | ||
|
|
||
| Necessary because a CastDevice entity can create a new socket client | ||
| and therefore callbacks from multiple chromecast connections can potentially |
There was a problem hiding this comment.
line too long (80 > 79 characters)
|
Tested extensively with chromecast, google home, cast audio and audio groups now. On to the tests! |
| """Turn on the cast device.""" | ||
| import pychromecast | ||
|
|
||
| if self._chromecast is None: |
There was a problem hiding this comment.
When can this happen? We set the cast info in async_added_to_hass.
There was a problem hiding this comment.
When the chromecast "switched host". This happens for example when a cast audio group changes its "elected leader"/master. Then we first disconnect the old chromecast object and then connect the new one.
In that process, self._chromecast is briefly changed to None to indicate that the old chromecast object is invalid (any commands on the old object would result in exceptions, as the socket is no longer connected)
Additionally, when starting up, there might be a phase when the chromecast object is not connected yet but the entity is already visible in the frontend. This can be reproduced by manually specifying the cast device by host: in configuration.yaml and disabling any internet connection of your computer.
There was a problem hiding this comment.
Shouldn't we set the entity to unavailable if it looses the connection to the chromecast? Then you wouldn't need to guard in all the methods.
It sounds weird that we allow entities to be created without a working connection during setup.
There was a problem hiding this comment.
Shouldn't we set the entity to unavailable if it looses the connection to the chromecast?
Yes, that's also what this PR does. When self._chromecast is None available will also be False.
Then you wouldn't need to guard in all the methods.
Ok, that's good. I put those checks everywhere to protect for potential race conditions, where the service calls are executed even though we're not connected. If the core can guarantee that these methods will never be called with an unavailable state I can also remove those checks.
It sounds weird that we allow entities to be created without a working connection during setup.
That's true, and also was the previous behavior. The problem with that was that we would block the entire Home Assistant startup waiting for the socket client to connect. If no connection was available, we would even stop startup indefinitely - not good. I tried looking for a solution to that with asyncio for some time, but ultimately I think unavailable is better (and quicker).
I mean if we want to we can also try checking the HTTP API of the chromecast before adding the device, that would fix part of the problem (for standard chromecasts).
| """Return the state of the player.""" | ||
| if self.media_status is None: | ||
| if self._media_status is None: | ||
| return STATE_UNKNOWN |
There was a problem hiding this comment.
Return None for unknown state.
| elif self.cast.is_idle: | ||
| elif self._chromecast is not None and self._chromecast.is_idle: | ||
| return STATE_OFF | ||
| return STATE_UNKNOWN |
Gets rid of those pesky "Setup of platform cast is taking over 10 seconds." messages.
|
Once ready let me know and I can test if you'd like. |
|
FYI, regarding core guarding from calling services for unavailable entities: Components that use |
It's not using async anyway
There was a problem hiding this comment.
Added some comments here to point out some aspects of the changes that didn't really fit in code comments.
Since yesterday I also went through all sorts of scenarios of chromecasts disconnecting at certain times, no network connection, manually moving hosts around and so on. All of those "physical tests" appear to be working correctly with my set up + the available property now gives immediate feedback as to what is happening.
@edif30 If you want you could test these changes. It should essentially be as simple as copying this file into [config_dir]/custom_components/media_player/cast.py.
| vol.Optional(CONF_HOST): cv.string, | ||
| vol.Optional(CONF_IGNORE_CEC): [cv.string], | ||
| vol.Optional(CONF_IGNORE_CEC, default=[]): vol.All(cv.ensure_list, | ||
| [cv.string]) |
There was a problem hiding this comment.
It would be great if someone with more knowledge of voluptuous could take a look at this changed line. As far as I know, we should use cv.ensure_list wherever possible. Another question is whether to put it in this PR, but it's title "cleanup" after all...
| # Remove previous cast infos with same uuid from known chromecasts. | ||
| same_uuid = set(x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] | ||
| if info.uuid == x.uuid) | ||
| hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid |
There was a problem hiding this comment.
I mean there can only be a single uuid removed from this set anyway as non-None UUIDs are unique, but the alternative solutions of using set#remove and a dictionary for hass.data[KNOWN_CHROMECAST_INFO_KEY] were just way messier (the latter one getting particularly complicated because of None UUID handling).
| """Set up the pychromecast internal discovery.""" | ||
| hass.data.setdefault(INTERNAL_DISCOVERY_RUNNING_KEY, threading.Lock()) | ||
| if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: | ||
| hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() |
There was a problem hiding this comment.
Changed this to be the non-setdefault syntax because creating a threading.Lock instance is quite expensive.
| @@ -139,191 +191,402 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, | |||
|
|
|||
| # Import CEC IGNORE attributes | |||
| pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) | |||
There was a problem hiding this comment.
If you're wondering why I'm still supplying [] here even though a default is set in the platform schema, it's because discovery sends an empty dict as config.
| info) | ||
| if info.friendly_name is None: | ||
| # HTTP dial failed, so we won't be able to connect. | ||
| raise PlatformNotReady |
There was a problem hiding this comment.
When manually specifying a host in configuration.yaml like this:
media_player:
- platform: cast
host: 192.168.178.42this will block startup for 10 seconds; though I suppose that is okay when users manually specify the host.
I like "protected" access, but I like reviewing more :)
|
Awesome work 🐬 |
|
@OttoWinter Is it already merged?
with beta 66. See #13359 |
|
Yes it is in the beta. And that error message is expected now, it signals that one of Chromecast devices defined in your configuration.yaml can't be reached and therefore can't be setup. |
|
You mean the automatically created entity_registry.yaml? |
|
|
Sorry, I have tried to summarize everything in #13483 |
Description:
Clean up the cast integration.
Most notably the
pychromecast.Chromecastobjects are now managed by the entities, not globally. This makes handling "moved" hosts, unavailable chromecasts and disconnecting much cleaner.availableproperty.Todo:
Out of scope:
fixes #13359, fixes #13319, fixes #13090
Example entry for
configuration.yaml(if applicable):Checklist:
tox. Your PR cannot be merged unless tests passIf the code does not interact with devices: