Hello community,
here is the log from the commit of package python-PyChromecast for
openSUSE:Leap:15.2 checked in at 2020-05-06 20:41:51
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Leap:15.2/python-PyChromecast (Old)
and /work/SRC/openSUSE:Leap:15.2/.python-PyChromecast.new.2738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-PyChromecast"
Wed May 6 20:41:51 2020 rev:7 rq:800726 version:5.0.0
Changes:
--------
---
/work/SRC/openSUSE:Leap:15.2/python-PyChromecast/python-PyChromecast.changes
2020-03-29 14:56:26.179192667 +0200
+++
/work/SRC/openSUSE:Leap:15.2/.python-PyChromecast.new.2738/python-PyChromecast.changes
2020-05-06 20:41:52.180828967 +0200
@@ -1,0 +2,19 @@
+Tue May 5 06:46:04 UTC 2020 - Johannes Grassler <[email protected]>
+
+- Update to 5.0.0
+ * remove .travis.yml
+ * extract test reqs
+ * Update test.yml
+ * Create test.yml
+ * Bump linters, run flake and black on examples (#355)
+ * Speed up SocketClient shutdown (#352)
+ * Improve debug messages (#353)
+ * Fix bugs in Chromecast.is_idle and ReceiverController.launch_app (#350)
+ * Improve examples and docstrings (#351)
+ * Remove blocking option from Chromecast (#349)
+ * Add helper function get_listed_chromecasts (#348)
+ * Correct mistake in PR#345 (#346)
+ * Lookup manufacturer, remove multizone helper. (#345)
+- Update python-zeroconf Require
+
+-------------------------------------------------------------------
Old:
----
PyChromecast-4.2.0.tar.gz
New:
----
PyChromecast-5.0.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-PyChromecast.spec ++++++
--- /var/tmp/diff_new_pack.NwWcBW/_old 2020-05-06 20:41:52.584829803 +0200
+++ /var/tmp/diff_new_pack.NwWcBW/_new 2020-05-06 20:41:52.584829803 +0200
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%define skip_python2 1
Name: python-PyChromecast
-Version: 4.2.0
+Version: 5.0.0
Release: 0
Summary: Python module to talk to Google Chromecast
License: MIT
@@ -32,7 +32,7 @@
Requires: python-casttube >= 0.2.0
Requires: python-protobuf >= 3.0.0
Requires: python-requests >= 2.0
-Requires: python-zeroconf >= 0.24.4
+Requires: python-zeroconf >= 0.25.1
BuildArch: noarch
%python_subpackages
++++++ PyChromecast-4.2.0.tar.gz -> PyChromecast-5.0.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/PKG-INFO
new/PyChromecast-5.0.0/PKG-INFO
--- old/PyChromecast-4.2.0/PKG-INFO 2020-03-17 17:56:27.838616000 +0100
+++ new/PyChromecast-5.0.0/PKG-INFO 2020-04-20 17:28:40.250786500 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: PyChromecast
-Version: 4.2.0
+Version: 5.0.0
Summary: Python module to talk to Google Chromecast.
Home-page: https://github.com/balloob/pychromecast
Author: Paulus Schoutsen
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/PyChromecast.egg-info/PKG-INFO
new/PyChromecast-5.0.0/PyChromecast.egg-info/PKG-INFO
--- old/PyChromecast-4.2.0/PyChromecast.egg-info/PKG-INFO 2020-03-17
17:56:27.000000000 +0100
+++ new/PyChromecast-5.0.0/PyChromecast.egg-info/PKG-INFO 2020-04-20
17:28:40.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: PyChromecast
-Version: 4.2.0
+Version: 5.0.0
Summary: Python module to talk to Google Chromecast.
Home-page: https://github.com/balloob/pychromecast
Author: Paulus Schoutsen
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/PyChromecast.egg-info/SOURCES.txt
new/PyChromecast-5.0.0/PyChromecast.egg-info/SOURCES.txt
--- old/PyChromecast-4.2.0/PyChromecast.egg-info/SOURCES.txt 2020-03-17
17:56:27.000000000 +0100
+++ new/PyChromecast-5.0.0/PyChromecast.egg-info/SOURCES.txt 2020-04-20
17:28:40.000000000 +0200
@@ -14,6 +14,7 @@
pychromecast/authority_keys_pb2.py
pychromecast/cast_channel_pb2.py
pychromecast/config.py
+pychromecast/const.py
pychromecast/dial.py
pychromecast/discovery.py
pychromecast/error.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/PyChromecast-4.2.0/PyChromecast.egg-info/requires.txt
new/PyChromecast-5.0.0/PyChromecast.egg-info/requires.txt
--- old/PyChromecast-4.2.0/PyChromecast.egg-info/requires.txt 2020-03-17
17:56:27.000000000 +0100
+++ new/PyChromecast-5.0.0/PyChromecast.egg-info/requires.txt 2020-04-20
17:28:40.000000000 +0200
@@ -1,4 +1,4 @@
requests>=2.0
protobuf>=3.0.0
-zeroconf>=0.24.4
+zeroconf>=0.25.1
casttube>=0.2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/pychromecast/__init__.py
new/PyChromecast-5.0.0/pychromecast/__init__.py
--- old/PyChromecast-4.2.0/pychromecast/__init__.py 2020-03-17
17:56:15.000000000 +0100
+++ new/PyChromecast-5.0.0/pychromecast/__init__.py 2020-04-20
17:28:34.000000000 +0200
@@ -3,20 +3,21 @@
"""
import logging
import fnmatch
+from threading import Event
# pylint: disable=wildcard-import
import threading
from .config import * # noqa
from .error import * # noqa
from . import socket_client
-from .discovery import discover_chromecasts, start_discovery, stop_discovery
-from .dial import (
- get_device_status,
- reboot,
- DeviceStatus,
- CAST_TYPES,
- CAST_TYPE_CHROMECAST,
+from .discovery import (
+ DISCOVER_TIMEOUT,
+ discover_chromecasts,
+ start_discovery,
+ stop_discovery,
)
+from .dial import get_device_status, reboot, DeviceStatus
+from .const import CAST_MANUFACTURERS, CAST_TYPES, CAST_TYPE_CHROMECAST
from .controllers.media import STREAM_TYPE_BUFFERED # noqa
__all__ = ("__version__", "__version_info__", "get_chromecasts", "Chromecast")
@@ -29,9 +30,7 @@
_LOGGER = logging.getLogger(__name__)
-def _get_chromecast_from_host(
- host, tries=None, retry_wait=None, timeout=None, blocking=True
-):
+def get_chromecast_from_host(host, tries=None, retry_wait=None, timeout=None):
"""Creates a Chromecast object from a zeroconf host."""
# Build device status from the mDNS info, this information is
# the primary source and the remaining will be fetched
@@ -39,10 +38,11 @@
ip_address, port, uuid, model_name, friendly_name = host
_LOGGER.debug("_get_chromecast_from_host %s", host)
cast_type = CAST_TYPES.get(model_name.lower(), CAST_TYPE_CHROMECAST)
+ manufacturer = CAST_MANUFACTURERS.get(model_name.lower(), "Google Inc.")
device = DeviceStatus(
friendly_name=friendly_name,
model_name=model_name,
- manufacturer=None,
+ manufacturer=manufacturer,
uuid=uuid,
cast_type=cast_type,
)
@@ -53,13 +53,14 @@
tries=tries,
timeout=timeout,
retry_wait=retry_wait,
- blocking=blocking,
)
-def _get_chromecast_from_service(
- services, tries=None, retry_wait=None, timeout=None, blocking=True
-):
+# Alias for backwards compatibility
+_get_chromecast_from_host = get_chromecast_from_host # pylint:
disable=invalid-name
+
+
+def get_chromecast_from_service(services, tries=None, retry_wait=None,
timeout=None):
"""Creates a Chromecast object from a zeroconf service."""
# Build device status from the mDNS service name info, this
# information is the primary source and the remaining will be
@@ -67,10 +68,11 @@
services, zconf, uuid, model_name, friendly_name = services
_LOGGER.debug("_get_chromecast_from_service %s", services)
cast_type = CAST_TYPES.get(model_name.lower(), CAST_TYPE_CHROMECAST)
+ manufacturer = CAST_MANUFACTURERS.get(model_name.lower(), "Google Inc.")
device = DeviceStatus(
friendly_name=friendly_name,
model_name=model_name,
- manufacturer=None,
+ manufacturer=manufacturer,
uuid=uuid,
cast_type=cast_type,
)
@@ -80,32 +82,93 @@
tries=tries,
timeout=timeout,
retry_wait=retry_wait,
- blocking=blocking,
services=services,
zconf=zconf,
)
+# Alias for backwards compatibility
+_get_chromecast_from_service = ( # pylint: disable=invalid-name
+ get_chromecast_from_service
+)
+
+
+def get_listed_chromecasts(
+ friendly_names=None,
+ uuids=None,
+ tries=None,
+ retry_wait=None,
+ timeout=None,
+ discovery_timeout=DISCOVER_TIMEOUT,
+):
+ """
+ Searches the network for chromecast devices matching a list of friendly
+ names or a list of UUIDs.
+
+ Returns a list of discovered chromecast devices matching the criteria,
+ or an empty list if no matching chromecasts were found.
+
+ :param friendly_names: A list of wanted friendly names
+ :param uuids: A list of wanted uuids
+ :param tries: passed to get_chromecasts
+ :param retry_wait: passed to get_chromecasts
+ :param timeout: passed to get_chromecasts
+ :param discovery_timeout: A floating point number specifying the time to
wait
+ devices matching the criteria have been found.
+ """
+
+ cc_list = set()
+
+ def callback(chromecast):
+ _LOGGER.debug("Found chromecast %s", chromecast)
+ if uuids and chromecast.uuid in uuids:
+ cc_list.add(chromecast)
+ uuids.remove(chromecast.uuid)
+ elif friendly_names and chromecast.name in friendly_names:
+ cc_list.add(chromecast)
+ friendly_names.remove(chromecast.name)
+ if not friendly_names and not uuids:
+ discover_complete.set()
+
+ discover_complete = Event()
+ internal_stop = get_chromecasts(
+ tries=tries,
+ retry_wait=retry_wait,
+ timeout=timeout,
+ callback=callback,
+ blocking=False,
+ )
+ # Wait for the timeout or found all wanted devices
+ discover_complete.wait(discovery_timeout)
+ internal_stop()
+ return list(cc_list)
+
+
# pylint: disable=too-many-locals
def get_chromecasts(
tries=None, retry_wait=None, timeout=None, blocking=True, callback=None
):
"""
- Searches the network for chromecast devices.
-
- If blocking = True, returns a list of discovered chromecast devices.
- If blocking = False, triggers a callback for each discovered chromecast,
- and returns a function which can be executed to stop
- discovery.
+ Searches the network for chromecast devices and creates a Chromecast
instance
+ for each discovered device.
May return an empty list if no chromecasts were found.
- Tries is specified if you want to limit the number of times the
- underlying socket associated with your Chromecast objects will
- retry connecting if connection is lost or it fails to connect
- in the first place. The number of seconds spent between each retry
- can be defined by passing the retry_wait parameter, the default is
- to wait 5 seconds.
+ Parameters tries, timeout, retry_wait and blocking_app_launch controls the
+ behavior of the created Chromecast instances.
+
+ :param tries: Number of retries to perform if the connection fails.
+ None for inifinite retries.
+ :param timeout: A floating point number specifying the socket timeout in
+ seconds. None means to use the default which is 30 seconds.
+ :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 blocking: If True, returns a list of discovered chromecast devices.
+ If False, triggers a callback for each discovered
chromecast,
+ and returns a function which can be executed to stop
discovery.
+ :param callback: Callback which is triggerd for each discovered chromecast
when
+ blocking = False.
"""
if blocking:
# Thread blocking chromecast discovery
@@ -114,12 +177,8 @@
for host in hosts:
try:
cc_list.append(
- _get_chromecast_from_host(
- host,
- tries=tries,
- retry_wait=retry_wait,
- timeout=timeout,
- blocking=blocking,
+ get_chromecast_from_host(
+ host, tries=tries, retry_wait=retry_wait,
timeout=timeout
)
)
except ChromecastConnectionError: # noqa
@@ -134,12 +193,11 @@
"""Called when zeroconf has discovered a new chromecast."""
try:
callback(
- _get_chromecast_from_host(
+ get_chromecast_from_host(
listener.services[name],
tries=tries,
retry_wait=retry_wait,
timeout=timeout,
- blocking=blocking,
)
)
except ChromecastConnectionError: # noqa
@@ -158,6 +216,7 @@
"""
Class to interface with a ChromeCast.
+ :param host: The host to connect to.
:param port: The port to use when connecting to the device, set to None to
use the default of 8009. Special devices such as Cast Groups
may return a different port number so we need to use that.
@@ -170,13 +229,21 @@
: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 services: A list of mDNS services to try to connect to. If present,
+ parameters host and port are ignored and host and port are
+ instead resolved through mDNS. The list of services may be
+ modified, for example if speaker group leadership is
handed
+ over. SocketClient will catch modifications to the list
when
+ attempting reconnect.
+ :param zconf: A zeroconf instance, needed if a list of services is passed.
+ The zeroconf instance may be obtained from the browser
returned by
+ pychromecast.start_discovery().
"""
def __init__(self, host, port=None, device=None, **kwargs):
tries = kwargs.pop("tries", None)
timeout = kwargs.pop("timeout", None)
retry_wait = kwargs.pop("retry_wait", None)
- blocking = kwargs.pop("blocking", True)
services = kwargs.pop("services", None)
zconf = kwargs.pop("zconf", True)
@@ -223,7 +290,6 @@
tries=tries,
timeout=timeout,
retry_wait=retry_wait,
- blocking=blocking,
services=services,
zconf=zconf,
)
@@ -260,7 +326,11 @@
return (
self.status is None
or self.app_id in (None, IDLE_APP_ID)
- or (not self.status.is_active_input and not self.ignore_cec)
+ or (
+ self.cast_type == CAST_TYPE_CHROMECAST
+ and not self.status.is_active_input
+ and not self.ignore_cec
+ )
)
@property
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/pychromecast/const.py
new/PyChromecast-5.0.0/pychromecast/const.py
--- old/PyChromecast-4.2.0/pychromecast/const.py 1970-01-01
01:00:00.000000000 +0100
+++ new/PyChromecast-5.0.0/pychromecast/const.py 2020-04-20
17:28:34.000000000 +0200
@@ -0,0 +1,23 @@
+"""
+Chromecast constants
+"""
+# Regular chromecast, supports video/audio
+CAST_TYPE_CHROMECAST = "cast"
+# Cast Audio device, supports only audio
+CAST_TYPE_AUDIO = "audio"
+# Cast Audio group device, supports only audio
+CAST_TYPE_GROUP = "group"
+
+MF_GOOGLE = "Google Inc."
+
+CAST_TYPES = {
+ "chromecast": CAST_TYPE_CHROMECAST,
+ "eureka dongle": CAST_TYPE_CHROMECAST,
+ "chromecast audio": CAST_TYPE_AUDIO,
+ "google home": CAST_TYPE_AUDIO,
+ "google home mini": CAST_TYPE_AUDIO,
+ "google cast group": CAST_TYPE_GROUP,
+}
+
+# Known models not manufactured by Google
+CAST_MANUFACTURERS = {}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/pychromecast/controllers/plex.py
new/PyChromecast-5.0.0/pychromecast/controllers/plex.py
--- old/PyChromecast-4.2.0/pychromecast/controllers/plex.py 2020-03-17
17:56:15.000000000 +0100
+++ new/PyChromecast-5.0.0/pychromecast/controllers/plex.py 2020-04-20
17:28:34.000000000 +0200
@@ -322,10 +322,8 @@
Args:
status (None, optional): override for on/off
"""
- if status is not None:
- status = status
- else:
- status = not status.volume_muted
+ if status is None:
+ status = not self.status.volume_muted
self._socket_client.receiver_controller.set_volume_muted(status)
@@ -517,9 +515,11 @@
def disable_subtitle(self):
"""Disable a subtitle."""
- _, __, part = (
- self._get_current_media()
- ) # noqa: 501 pylint disable=unused-variable
+ (
+ _,
+ __,
+ part,
+ ) = self._get_current_media() # noqa: 501 pylint
disable=unused-variable
part.resetDefaultSubtitleStream()
self._reset_playback()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/pychromecast/dial.py
new/PyChromecast-5.0.0/pychromecast/dial.py
--- old/PyChromecast-4.2.0/pychromecast/dial.py 2020-03-17 17:56:15.000000000
+0100
+++ new/PyChromecast-5.0.0/pychromecast/dial.py 2020-04-20 17:28:34.000000000
+0200
@@ -7,28 +7,13 @@
import logging
import requests
+from .const import CAST_TYPE_CHROMECAST
from .discovery import get_info_from_service, get_host_from_service_info
XML_NS_UPNP_DEVICE = "{urn:schemas-upnp-org:device-1-0}"
FORMAT_BASE_URL = "http://{}:8008"
-# Regular chromecast, supports video/audio
-CAST_TYPE_CHROMECAST = "cast"
-# Cast Audio device, supports only audio
-CAST_TYPE_AUDIO = "audio"
-# Cast Audio group device, supports only audio
-CAST_TYPE_GROUP = "group"
-
-CAST_TYPES = {
- "chromecast": CAST_TYPE_CHROMECAST,
- "eureka dongle": CAST_TYPE_CHROMECAST,
- "chromecast audio": CAST_TYPE_AUDIO,
- "google home": CAST_TYPE_AUDIO,
- "google home mini": CAST_TYPE_AUDIO,
- "google cast group": CAST_TYPE_GROUP,
-}
-
_LOGGER = logging.getLogger(__name__)
@@ -90,15 +75,14 @@
status = _get_status(host, services, zconf,
"/setup/eureka_info?options=detail")
friendly_name = status.get("name", "Unknown Chromecast")
+ # model_name and manufacturer is no longer included in the response,
+ # mark as unknown
model_name = "Unknown model name"
manufacturer = "Unknown manufacturer"
- if "detail" in status:
- model_name = status["detail"].get("model_name", model_name)
- manufacturer = status["detail"].get("manufacturer", manufacturer)
udn = status.get("ssdp_udn", None)
- cast_type = CAST_TYPES.get(model_name.lower(), CAST_TYPE_CHROMECAST)
+ cast_type = CAST_TYPE_CHROMECAST
uuid = None
if udn:
@@ -110,49 +94,6 @@
return None
-def get_multizone_status(host, services=None, zconf=None):
- """
- :param host: Hostname or ip to fetch status from
- :type host: str
- :return: The multizone status as a named tuple.
- :rtype: pychromecast.dial.MultizoneStatus or None
- """
-
- try:
- status = status = _get_status(
- host, services, zconf, "/setup/eureka_info?params=multizone"
- )
-
- dynamic_groups = []
- if "multizone" in status and "dynamic_groups" in status["multizone"]:
- for group in status["multizone"]["dynamic_groups"]:
- name = group.get("name", "Unknown group name")
- udn = group.get("uuid", None)
- uuid = None
- if udn:
- uuid = UUID(udn.replace("-", ""))
- dynamic_groups.append(MultizoneInfo(name, uuid))
-
- groups = []
- if "multizone" in status and "groups" in status["multizone"]:
- for group in status["multizone"]["groups"]:
- name = group.get("name", "Unknown group name")
- udn = group.get("uuid", None)
- uuid = None
- if udn:
- uuid = UUID(udn.replace("-", ""))
- groups.append(MultizoneInfo(name, uuid))
-
- return MultizoneStatus(dynamic_groups, groups)
-
- except (requests.exceptions.RequestException, OSError, ValueError):
- return None
-
-
DeviceStatus = namedtuple(
"DeviceStatus", ["friendly_name", "model_name", "manufacturer", "uuid",
"cast_type"]
)
-
-MultizoneInfo = namedtuple("MultizoneInfo", ["friendly_name", "uuid"])
-
-MultizoneStatus = namedtuple("MultizoneStatus", ["dynamic_groups", "groups"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/pychromecast/discovery.py
new/PyChromecast-5.0.0/pychromecast/discovery.py
--- old/PyChromecast-4.2.0/pychromecast/discovery.py 2020-03-17
17:56:15.000000000 +0100
+++ new/PyChromecast-5.0.0/pychromecast/discovery.py 2020-04-20
17:28:34.000000000 +0200
@@ -1,6 +1,7 @@
"""Discovers Chromecasts on the network using mDNS/zeroconf."""
import logging
import socket
+from threading import Event
from uuid import UUID
import zeroconf
@@ -122,8 +123,6 @@
def discover_chromecasts(max_devices=None, timeout=DISCOVER_TIMEOUT):
""" Discover chromecasts on the network. """
- from threading import Event
-
browser = False
try:
# pylint: disable=unused-argument
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/pychromecast/socket_client.py
new/PyChromecast-5.0.0/pychromecast/socket_client.py
--- old/PyChromecast-4.2.0/pychromecast/socket_client.py 2020-03-17
17:56:15.000000000 +0100
+++ new/PyChromecast-5.0.0/pychromecast/socket_client.py 2020-04-20
17:28:34.000000000 +0200
@@ -23,14 +23,13 @@
from . import cast_channel_pb2
from .controllers import BaseController
from .controllers.media import MediaController
-from .dial import CAST_TYPE_CHROMECAST, CAST_TYPE_AUDIO, CAST_TYPE_GROUP
+from .const import CAST_TYPE_AUDIO, CAST_TYPE_CHROMECAST, CAST_TYPE_GROUP
from .discovery import get_info_from_service, get_host_from_service_info
from .error import (
ChromecastConnectionError,
UnsupportedNamespace,
NotConnected,
PyChromecastStopped,
- LaunchError,
)
NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection"
@@ -156,6 +155,7 @@
"""
Class to interact with a Chromecast through a socket.
+ :param host: The host to connect to.
:param port: The port to use when connecting to the device, set to None to
use the default of 8009. Special devices such as Cast Groups
may return a different port number so we need to use that.
@@ -163,24 +163,29 @@
dial.CAST_TYPE_* for types.
:param tries: Number of retries to perform if the connection fails.
None for inifinite retries.
+ :param timeout: A floating point number specifying the socket timeout in
+ seconds. None means to use the default which is 30 seconds.
: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 services: A list of mDNS services to try to connect to. If present,
+ parameters host and port are ignored and host and port are
+ instead resolved through mDNS. The list of services may be
+ modified, for example if speaker group leadership is
handed
+ over. SocketClient will catch modifications to the list
when
+ attempting reconnect.
+ :param zconf: A zeroconf instance, needed if a list of services is passed.
+ The zeroconf instance may be obtained from the browser
returned by
+ pychromecast.start_discovery().
"""
def __init__(self, host, port=None, cast_type=CAST_TYPE_CHROMECAST,
**kwargs):
tries = kwargs.pop("tries", None)
timeout = kwargs.pop("timeout", None)
retry_wait = kwargs.pop("retry_wait", None)
- self.blocking = kwargs.pop("blocking", True)
services = kwargs.pop("services", None)
zconf = kwargs.pop("zconf", None)
- if self.blocking:
- self.polltime = POLL_TIME_BLOCKING
- else:
- self.polltime = POLL_TIME_NON_BLOCKING
-
super(SocketClient, self).__init__()
self.daemon = True
@@ -201,6 +206,8 @@
self.source_id = "sender-0"
self.stop = threading.Event()
+ # socketpair used to interrupt the worker thread
+ self.socketpair = socket.socketpair()
self.app_namespaces = []
self.destination_id = None
@@ -211,13 +218,14 @@
self._open_channels = []
self.connecting = True
+ self.first_connection = True
self.socket = None
# dict mapping namespace on Controller objects
self._handlers = {}
self._connection_listeners = []
- self.receiver_controller = ReceiverController(cast_type, self.blocking)
+ self.receiver_controller = ReceiverController(cast_type)
self.media_controller = MediaController()
self.heartbeat_controller = HeartbeatController()
@@ -229,7 +237,7 @@
self.receiver_controller.register_status_listener(self)
def initialize_connection(
- self
+ self,
): # noqa: E501 pylint:disable=too-many-statements, too-many-branches
"""Initialize a socket to a Chromecast, retrying as necessary."""
tries = self.tries
@@ -302,8 +310,9 @@
except (AttributeError, KeyError, UnicodeError):
pass
self.logger.debug(
- "[%s:%s] Resolved service %s to %s:%s",
- self.fn or self.host,
+ "[%s(%s):%s] Resolved service %s to %s:%s",
+ self.fn or "",
+ self.host,
self.port,
service,
host,
@@ -313,8 +322,9 @@
self.port = port
else:
self.logger.debug(
- "[%s:%s] Failed to resolve service %s",
- self.fn or self.host,
+ "[%s(%s):%s] Failed to resolve service %s",
+ self.fn or "",
+ self.host,
self.port,
service,
)
@@ -330,8 +340,9 @@
continue
self.logger.debug(
- "[%s:%s] Connecting to %s:%s",
- self.fn or self.host,
+ "[%s(%s):%s] Connecting to %s:%s",
+ self.fn or "",
+ self.host,
self.port,
self.host,
self.port,
@@ -350,17 +361,29 @@
self.heartbeat_controller.ping()
self.heartbeat_controller.reset()
- self.logger.debug(
- "[%s:%s] Connected!", self.fn or self.host, self.port
- )
+ if self.first_connection:
+ self.first_connection = False
+ self.logger.debug(
+ "[%s(%s):%s] Connected!",
+ self.fn or "",
+ self.host,
+ self.port,
+ )
+ else:
+ self.logger.warning(
+ "[%s(%s):%s] Connection reestablished!",
+ self.fn or "",
+ self.host,
+ self.port,
+ )
return
except OSError as err:
self.connecting = True
if self.stop.is_set():
self.logger.error(
- "[%s:%s] Failed to connect: %s. "
- "aborting due to stop signal.",
- self.fn or self.host,
+ "[%s(%s):%s] Failed to connect: %s. aborting due
to stop signal.",
+ self.fn or "",
+ self.host,
self.port,
err,
)
@@ -374,9 +397,9 @@
)
if service is not None:
retry_log_fun(
- "[%s:%s] Failed to connect to service %s"
- ", retrying in %.1fs",
- self.fn or self.host,
+ "[%s(%s):%s] Failed to connect to service %s,
retrying in %.1fs",
+ self.fn or "",
+ self.host,
self.port,
service,
retry["delay"],
@@ -384,8 +407,9 @@
mdns_backoff(service, retry)
else:
retry_log_fun(
- "[%s:%s] Failed to connect, retrying in %.1fs",
- self.fn or self.host,
+ "[%s(%s):%s] Failed to connect, retrying in %.1fs",
+ self.fn or "",
+ self.host,
self.port,
self.retry_wait,
)
@@ -394,8 +418,9 @@
# Only sleep if we have another retry remaining
if tries is None or tries > 1:
self.logger.debug(
- "[%s:%s] Not connected, sleeping for %.1fs. Services: %s",
- self.fn or self.host,
+ "[%s(%s):%s] Not connected, sleeping for %.1fs. Services:
%s",
+ self.fn or "",
+ self.host,
self.port,
self.retry_wait,
self.services,
@@ -407,7 +432,10 @@
self.stop.set()
self.logger.error(
- "[%s:%s] Failed to connect. No retries.", self.fn or self.host,
self.port
+ "[%s(%s):%s] Failed to connect. No retries.",
+ self.fn or "",
+ self.host,
+ self.port,
)
raise ChromecastConnectionError("Failed to connect")
@@ -429,6 +457,12 @@
def disconnect(self):
""" Disconnect socket connection to Chromecast device """
self.stop.set()
+ try:
+ # Write to the socket to interrupt the worker thread
+ self.socketpair[1].send(b"x")
+ except socket.error:
+ # The socketpair may already be closed during shutdown, ignore it
+ pass
def register_handler(self, handler):
""" Register a new namespace handler. """
@@ -494,13 +528,13 @@
logging.debug("Thread started...")
while not self.stop.is_set():
- if self.run_once() == 1:
+ if self.run_once(timeout=POLL_TIME_BLOCKING) == 1:
break
# Clean up
self._cleanup()
- def run_once(self):
+ def run_once(self, timeout=POLL_TIME_NON_BLOCKING):
"""
Use run_once() in your own main loop after you
receive something on the socket (get_socket()).
@@ -513,8 +547,9 @@
except ChromecastConnectionError:
return 1
- # poll the socket
- can_read, _, _ = select.select([self.socket], [], [], self.polltime)
+ # poll the socket, as well as the socketpair to allow us to be
interrupted
+ rlist = [self.socket, self.socketpair[0]]
+ can_read, _, _ = select.select(rlist, [], [], timeout)
# read messages from chromecast
message = data = None
@@ -524,14 +559,16 @@
except InterruptLoop as exc:
if self.stop.is_set():
self.logger.info(
- "[%s:%s] Stopped while reading message, "
"disconnecting.",
- self.fn or self.host,
+ "[%s(%s):%s] Stopped while reading message,
disconnecting.",
+ self.fn or "",
+ self.host,
self.port,
)
else:
self.logger.error(
- "[%s:%s] Interruption caught without being stopped: "
"%s",
- self.fn or self.host,
+ "[%s(%s):%s] Interruption caught without being
stopped: %s",
+ self.fn or "",
+ self.host,
self.port,
exc,
)
@@ -544,20 +581,26 @@
except socket.error:
self._force_recon = True
self.logger.error(
- "[%s:%s] Error reading from socket.",
- self.fn or self.host,
+ "[%s(%s):%s] Error reading from socket.",
+ self.fn or "",
+ self.host,
self.port,
)
else:
data = _json_from_message(message)
- if not message:
- return 0
+
+ if self.socketpair[0] in can_read:
+ # Clear the socket's buffer
+ self.socketpair[0].recv(128)
# If we are stopped after receiving a message we skip the message
# and tear down the connection
if self.stop.is_set():
return 1
+ if not message:
+ return 0
+
# See if any handlers will accept this message
self._route_message(message, data)
@@ -591,16 +634,18 @@
reset = False
if self._force_recon:
self.logger.warning(
- "[%s:%s] Error communicating with socket, resetting "
"connection",
- self.fn or self.host,
+ "[%s(%s):%s] Error communicating with socket, resetting
connection",
+ self.fn or "",
+ self.host,
self.port,
)
reset = True
elif self.heartbeat_controller.is_expired():
self.logger.warning(
- "[%s:%s] Heartbeat timeout, resetting connection",
- self.fn or self.host,
+ "[%s(%s):%s] Heartbeat timeout, resetting connection",
+ self.fn or "",
+ self.host,
self.port,
)
reset = True
@@ -628,8 +673,9 @@
# debug messages
if message.namespace != NS_HEARTBEAT:
self.logger.debug(
- "[%s:%s] Received: %s",
- self.fn or self.host,
+ "[%s(%s):%s] Received: %s",
+ self.fn or "",
+ self.host,
self.port,
_message_to_string(message, data),
)
@@ -643,18 +689,20 @@
if not handled:
if data.get(REQUEST_ID) not in self._request_callbacks:
self.logger.debug(
- "[%s:%s] Message unhandled: %s",
- self.fn or self.host,
+ "[%s(%s):%s] Message unhandled: %s",
+ self.fn or "",
+ self.host,
self.port,
_message_to_string(message, data),
)
except Exception: # pylint: disable=broad-except
self.logger.exception(
(
- "[%s:%s] Exception caught while sending message to "
+ "[%s(%s):%s] Exception caught while sending message to
"
"controller %s: %s"
),
- self.fn or self.host,
+ self.fn or "",
+ self.host,
self.port,
type(self._handlers[message.namespace]).__name__,
_message_to_string(message, data),
@@ -662,8 +710,9 @@
else:
self.logger.debug(
- "[%s:%s] Received unknown namespace: %s",
- self.fn or self.host,
+ "[%s(%s):%s] Received unknown namespace: %s",
+ self.fn or "",
+ self.host,
self.port,
_message_to_string(message, data),
)
@@ -685,12 +734,18 @@
try:
self.socket.close()
except Exception: # pylint: disable=broad-except
- self.logger.exception("[%s:%s] _cleanup", self.fn or self.host,
self.port)
+ self.logger.exception(
+ "[%s(%s):%s] _cleanup", self.fn or "", self.host, self.port
+ )
self._report_connection_status(
ConnectionStatus(
CONNECTION_STATUS_DISCONNECTED, NetworkAddress(self.host,
self.port)
)
)
+
+ self.socketpair[0].close()
+ self.socketpair[1].close()
+
self.connecting = True
def _report_connection_status(self, status):
@@ -698,17 +753,20 @@
for listener in self._connection_listeners:
try:
self.logger.debug(
- "[%s:%s] connection listener: %x (%s)",
- self.fn or self.host,
+ "[%s(%s):%s] connection listener: %x (%s) %s",
+ self.fn or "",
+ self.host,
self.port,
id(listener),
type(listener).__name__,
+ status,
)
listener.new_connection_status(status)
except Exception: # pylint: disable=broad-except
self.logger.exception(
- "[%s:%s] Exception thrown when calling connection "
"listener",
- self.fn or self.host,
+ "[%s(%s):%s] Exception thrown when calling connection
listener",
+ self.fn or "",
+ self.host,
self.port,
)
@@ -794,8 +852,9 @@
# Log all messages except heartbeat
if msg.namespace != NS_HEARTBEAT:
self.logger.debug(
- "[%s:%s] Sending: %s",
- self.fn or self.host,
+ "[%s(%s):%s] Sending: %s",
+ self.fn or "",
+ self.host,
self.port,
_message_to_string(msg, data),
)
@@ -815,7 +874,10 @@
self._request_callbacks.pop(request_id, None)
self._force_recon = True
self.logger.info(
- "[%s:%s] Error writing to socket.", self.fn or self.host,
self.port
+ "[%s(%s):%s] Error writing to socket.",
+ self.fn or "",
+ self.host,
+ self.port,
)
else:
raise NotConnected(
@@ -898,7 +960,7 @@
pass
except Exception: # pylint: disable=broad-except
self.logger.exception(
- "[%s:%s] Exception", self.fn or self.host, self.port
+ "[%s(%s):%s] Exception", self.fn or "", self.host,
self.port
)
self._open_channels.remove(destination_id)
@@ -1003,14 +1065,13 @@
:param cast_type: Type of Chromecast device.
"""
- def __init__(self, cast_type=CAST_TYPE_CHROMECAST, blocking=True):
+ def __init__(self, cast_type=CAST_TYPE_CHROMECAST):
super(ReceiverController, self).__init__(NS_RECEIVER,
target_platform=True)
self.status = None
self.launch_failure = None
self.app_to_launch = None
self.cast_type = cast_type
- self.blocking = blocking
self.app_launch_event = threading.Event()
self.app_launch_event_function = None
@@ -1061,7 +1122,7 @@
Will only launch if it is not currently running unless
force_launch=True. """
- if not force_launch and self.app_id is None:
+ if not force_launch and self.status is None:
self.update_status(
lambda response: self._send_launch_message(
app_id, force_launch, callback_function
@@ -1079,25 +1140,12 @@
self.app_launch_event_function = callback_function
self.launch_failure = None
- self.send_message(
- {MESSAGE_TYPE: TYPE_LAUNCH, APP_ID: app_id},
- callback_function=lambda response:
self._block_till_launched(app_id),
- )
+ self.send_message({MESSAGE_TYPE: TYPE_LAUNCH, APP_ID: app_id})
else:
self.logger.info("Not launching app %s - already running", app_id)
if callback_function:
callback_function()
- def _block_till_launched(self, app_id):
- if self.blocking:
- self.app_launch_event.wait()
- if self.launch_failure:
- raise LaunchError(
- "Failed to launch app: {}, Reason: {}".format(
- app_id, self.launch_failure.reason
- )
- )
-
def stop_app(self, callback_function_param=False):
""" Stops the current running app on the Chromecast. """
self.logger.info("Receiver:Stopping current app '%s'", self.app_id)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/requirements.txt
new/PyChromecast-5.0.0/requirements.txt
--- old/PyChromecast-4.2.0/requirements.txt 2020-03-17 17:56:15.000000000
+0100
+++ new/PyChromecast-5.0.0/requirements.txt 2020-04-20 17:28:34.000000000
+0200
@@ -1,4 +1,4 @@
requests>=2.0
protobuf>=3.0.0
-zeroconf>=0.24.4
+zeroconf>=0.25.1
casttube>=0.2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/PyChromecast-4.2.0/setup.py
new/PyChromecast-5.0.0/setup.py
--- old/PyChromecast-4.2.0/setup.py 2020-03-17 17:56:15.000000000 +0100
+++ new/PyChromecast-5.0.0/setup.py 2020-04-20 17:28:34.000000000 +0200
@@ -5,7 +5,7 @@
setup(
name="PyChromecast",
- version="4.2.0",
+ version="5.0.0",
license="MIT",
url="https://github.com/balloob/pychromecast",
author="Paulus Schoutsen",