Skip to content
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

Closed
wants to merge 7 commits into from
Closed

Dynamic host ip tracking for cast groups #167

wants to merge 7 commits into from

Conversation

freol35241
Copy link

fixes #165

emlove
emlove previously requested changes Mar 18, 2017
Copy link
Collaborator

@emlove emlove left a 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.

else False
self.dynamic_host = kwargs.pop('dynamic_host', self.dynamic_host)
if self.dynamic_host:
self.start_dynamic_host_tracking()
Copy link
Collaborator

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.

Copy link
Author

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.

Copy link
Collaborator

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.

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)
Copy link
Collaborator

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.

Copy link
Author

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?

Copy link
Collaborator

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.

@balloob
Copy link
Collaborator

balloob commented Mar 19, 2017

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.

@emlove emlove dismissed their stale review March 19, 2017 18:48

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.
"""
Method for setting up, and resetting the chromecast object
"""
tries = kwargs.pop('tries', None)
Copy link
Author

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?

@freol35241
Copy link
Author

Decided that listening for DISCONNECTED events was a cleaner approach. I am fairly happy with the solution now, and it seems to be stable.

Copy link
Collaborator

@balloob balloob left a 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):
Copy link
Collaborator

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):
Copy link
Collaborator

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 ?

Copy link
Collaborator

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 \
Copy link
Collaborator

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.

Copy link
Author

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

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

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')
Copy link
Collaborator

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(
Copy link
Collaborator

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.

Copy link
Author

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

Choose a reason for hiding this comment

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

Why noqa ?

Copy link
Author

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?

Copy link
Collaborator

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?

Copy link
Author

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)
Copy link
Collaborator

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?

Copy link
Collaborator

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

Copy link
Author

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')
Copy link
Collaborator

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.

@balloob
Copy link
Collaborator

balloob commented Apr 12, 2017

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?

image

To show discovery info on your network, run:

pip3 install netdisco==1.0.0rc2
pip3 -m netdisco

@freol35241
Copy link
Author

Sorry, have been busy with other commitments. Will try to find some time soon to finish this PR.

Had a look at the netdisco output on my network and it seems like the rm property is false for all the chromecasts?
image

@balloob
Copy link
Collaborator

balloob commented May 15, 2017

Not sure if that is correct. My Google Home has an rm

google_cast:
[{'host': '192.168.1.109',
  'hostname': '92f538f5-433e-3500-075f-1f8ae1c92467.local.',
  'port': 8009,
  'properties': {'bs': 'FA8FCA87650A',
                 'ca': '4100',
                 'fn': 'Living room Home',
                 'ic': '/setup/icon.png',
                 'id': '92f538f5433e3500075f1f8ae1c92467',
                 'md': 'Google Home',
                 'rm': '4E05B1EDB9474484',
                 'rs': False,
                 'st': '0',
                 've': '05'}}]

@foxel
Copy link

foxel commented Aug 31, 2017

Hi. Is there any chance this PR will be incorporated some way?

@balloob
Copy link
Collaborator

balloob commented Sep 4, 2017

Doesn't look like it. Will close it for now as it has gone stale.

@balloob balloob closed this Sep 4, 2017
@foxel
Copy link

foxel commented Sep 5, 2017

@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.

@balloob
Copy link
Collaborator

balloob commented Sep 5, 2017

No plans here, I'm busy as it is. You're welcome to implement it and open a PR to contribute it to pychromecast.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cast groups changing "elected leader"
4 participants