-
Notifications
You must be signed in to change notification settings - Fork 380
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dynamic host ip tracking for cast groups #167
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might want to wait for feedback from balloob. I'm not sure if this is something he wants to support or not. In any case I added some feedback here on the implementation details.
pychromecast/__init__.py
Outdated
else False | ||
self.dynamic_host = kwargs.pop('dynamic_host', self.dynamic_host) | ||
if self.dynamic_host: | ||
self.start_dynamic_host_tracking() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't want to create a new thread for each cast group. I think what we'd have to do is start the thread for the first group, and add each listening object id to a list. The objects can remove themselves from the list in __del__
, and when the list is empty the thread can be stopped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback, the single thread for all groups is a good idea. Had a quick look at it but ran into some issues with the __del__
-method. It seems like it never gets called. Probably due to using strong (non-weak?) references for the listener registration, see for example here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, as I understand, CPython doesn't provide any guarantee objects will necessarily be cleaned up in a timely manner. They should at least all be cleaned up with the interpreter is shutting down, though.
pychromecast/__init__.py
Outdated
self.logger.info('Change in host ip detected, ' | ||
'reinitializing object...') | ||
stop_discovery(self.browser) | ||
self.__init__(host, port=port, device=self.device, dynamic_host=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We definitely don't want to re-run __init__
when the host changes. Beyond being bad practice this is going to have a lot of unintended side-effects, such as dropping all the old kwargs passed in.
What we'll want to do is pull everything from init that needs to be re-run when the host changes into a separate method, and call that method in init, and here. (Probably everything from here down.)
You'll also need to clean-up the old socket client before creating the new one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. Out of interest, is calling the __init__
-method not recommended due to some python under-the-hood magic that might go bad or is it just to avoid confusion?
What exactly is required for this?
You'll also need to clean-up the old socket client before creating the new one.
Same as in the __del__
-method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's pretty much it. AFAIK there isn't a technical reason it can't be called twice, it's just a confusing pattern.
For cleanup, you'll probably just need to call self.disconnect
. You'll want to keep the default of blocking=True to ensure it closes successfully first.
I think that we should tackle it slightly different. When the group leader changes, it means that the old Chromecast object has been disconnected. We should listen for disconnect events and then try to reconnect, if that fails, try to see if we can find the same Cast Audio somewhere on the network and reconnect there. This should be handled transparently for the consumers of the Cast object. |
New implementation should follow balloob's plan
Subscribing to connection ststaus messages from socket_client and allows for one reconnection attempt before starting to search forthe group on another ip on the network. If a new ip is found, re-initialize the chromecast object. If reconnection attemp succeds with old ip, stop discovery and do nothing.
pychromecast/__init__.py
Outdated
""" | ||
Method for setting up, and resetting the chromecast object | ||
""" | ||
tries = kwargs.pop('tries', None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another approach would be to default to a certain number (2?) of tries for a cast group and only listen to DISCONNECTED events. Might be a cleaner solution actually. What do you think?
…es for cast groups
Decided that listening for DISCONNECTED events was a cleaner approach. I am fairly happy with the solution now, and it seems to be stable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This already looks a lot better. Really cool.
@@ -122,13 +129,14 @@ class Chromecast(object): | |||
:param retry_wait: A floating point number specifying how many seconds to | |||
wait between each retry. None means to use the default | |||
which is 5 seconds. | |||
:param dynamic_host: Dynamic or non_dynamic host, if True dynamic host | |||
tracking is enabled. Will be True by default for | |||
cast_type == CAST_TYPE_GROUP. | |||
""" | |||
|
|||
def __init__(self, host, port=None, device=None, **kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of storing kwargs
, please write out all the attributes that we can have and their default values and store as instance variables.
def __init__(self, host, port=None, device=None, *, tries=None, timeout=None, retry_wait=None, blocking=True):
self._tries = tries
self._timeout = timeout
|
||
self.setup(host, port, self.device, self.kwargs) | ||
|
||
def setup(self, host, port, device, kwargs): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why take in port if that will always be equal to self.port
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with kwargs
and device
""" | ||
Method for setting up, and resetting the chromecast object | ||
""" | ||
tries = 2 if self.cast_type == CAST_TYPE_GROUP \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is tries bigger for a group? We should store these values in the constructor.
Also, please rewrite as normal if statement instead of inline if statement. It is confusing to read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tries is None
by default for all other CAST_TYPES
meaning that the socket client will try to reconnect forever. By defining a finite number of tries we make sure that the socket client will eventually fail (when ip is switched) and then sends a DISCONNECTED
event which we listen for.
@@ -194,6 +217,13 @@ def __init__(self, host, port=None, device=None, **kwargs): | |||
if blocking: | |||
self.socket_client.start() | |||
|
|||
# Set to True by default if cast_type == CAST_TYPE_GROUP | |||
self.dynamic_host = kwargs.pop('dynamic_host', True if |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be set in the constructor.
@@ -122,13 +129,14 @@ class Chromecast(object): | |||
:param retry_wait: A floating point number specifying how many seconds to | |||
wait between each retry. None means to use the default | |||
which is 5 seconds. | |||
:param dynamic_host: Dynamic or non_dynamic host, if True dynamic host |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain what dynamic host tracking is in the comment.
self.disconnect() | ||
self.setup(self.host, self.port, self.device, self.kwargs) | ||
|
||
self.logger.info('Dynamic host discovery started') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you include the name of the cast device?
timeout = kwargs.pop('timeout', None) | ||
retry_wait = kwargs.pop('retry_wait', None) | ||
blocking = kwargs.pop('blocking', True) | ||
|
||
self.socket_client = socket_client.SocketClient( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Socket client will automatically try to reconnect. After re-discovery, we should make sure we stop the old socket client or else it will keep reconnecting in the background.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As the number of tries is a finite number the old socket client will not try to reconnect forever, also it is stopped here
self.host = host | ||
try: | ||
self.setup(host, port, self.device, self.kwargs) | ||
except ChromecastConnectionError: # noqa |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why noqa
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be fair, not sure. But flake8 throws an error otherwise.
I guess it has something to do with this noqa
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noqa disabled the linter. What is the error it raises? Why can't it be fixed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can be fixed by removing the
from .error import *
in the beginning of the file. Not sure way that import is there though. Do you have any guess?
""" | ||
Internal scheduled timeout for discovery, 5 seconds | ||
""" | ||
time.sleep(10) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason why we would wait and not just start discovery right away? The function doc also seems to state 5 seconds?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of starting a thread that sleeps for 10 seconds, consider using a Timer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its a timeout to not let the discovery session run forever if no new ip is found.
A timer is a good idea, did not know about that one.
if not self.browser: | ||
return # Change in ip detected, discovery did not time out | ||
|
||
self.logger.info('Discovery timed out, stopping') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You use logger.info
a lot. I think that for example this one should be reduced to debug level.
Sorry, I have been very busy lately. This PR is still on my radar. Will look at it soon. In the meanwhile, I just came by a dump of the output of the cast discovery. I wonder that instead of looking at port numbers, we can look at the 'rm' property? To show discovery info on your network, run:
|
Not sure if that is correct. My Google Home has an
|
Hi. Is there any chance this PR will be incorporated some way? |
Doesn't look like it. Will close it for now as it has gone stale. |
@balloob is there any plan for implementing this feature? I see this lack of cromecast leader tracking as a blocker for home-assistant cast integration to be usable for groups. |
No plans here, I'm busy as it is. You're welcome to implement it and open a PR to contribute it to pychromecast. |
fixes #165