PyChromecast 14.0.3__tar.gz → 14.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/PKG-INFO +1 -1
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/PyChromecast.egg-info/PKG-INFO +1 -1
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/PyChromecast.egg-info/SOURCES.txt +1 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/config.py +1 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/const.py +2 -0
- PyChromecast-14.0.5/pychromecast/controllers/heartbeat.py +90 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/socket_client.py +31 -83
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pyproject.toml +1 -1
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/LICENSE +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/MANIFEST.in +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/PyChromecast.egg-info/dependency_links.txt +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/PyChromecast.egg-info/not-zip-safe +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/PyChromecast.egg-info/requires.txt +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/PyChromecast.egg-info/top_level.txt +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/README.rst +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/__init__.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/__init__.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/bbciplayer.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/bbcsounds.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/bubbleupnp.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/dashcast.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/homeassistant.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/homeassistant_media.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/media.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/multizone.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/nrkradio.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/nrktv.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/plex.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/receiver.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/shaka.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/supla.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/yleareena.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/controllers/youtube.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/dial.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/discovery.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/error.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/generated/authority_keys_pb2.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/generated/authority_keys_pb2.pyi +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/generated/cast_channel_pb2.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/generated/cast_channel_pb2.pyi +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/generated/logging_pb2.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/generated/logging_pb2.pyi +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/generated/readme.txt +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/models.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/quick_play.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/pychromecast/response_handler.py +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/requirements.txt +0 -0
- {PyChromecast-14.0.3 → PyChromecast-14.0.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PyChromecast
|
|
3
|
-
Version: 14.0.
|
|
3
|
+
Version: 14.0.5
|
|
4
4
|
Summary: Python module to talk to Google Chromecast.
|
|
5
5
|
Author-email: Paulus Schoutsen <paulus@paulusschoutsen.nl>
|
|
6
6
|
Project-URL: Homepage, https://github.com/home-assistant-libs/pychromecast
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PyChromecast
|
|
3
|
-
Version: 14.0.
|
|
3
|
+
Version: 14.0.5
|
|
4
4
|
Summary: Python module to talk to Google Chromecast.
|
|
5
5
|
Author-email: Paulus Schoutsen <paulus@paulusschoutsen.nl>
|
|
6
6
|
Project-URL: Homepage, https://github.com/home-assistant-libs/pychromecast
|
|
@@ -25,6 +25,7 @@ pychromecast/controllers/bbciplayer.py
|
|
|
25
25
|
pychromecast/controllers/bbcsounds.py
|
|
26
26
|
pychromecast/controllers/bubbleupnp.py
|
|
27
27
|
pychromecast/controllers/dashcast.py
|
|
28
|
+
pychromecast/controllers/heartbeat.py
|
|
28
29
|
pychromecast/controllers/homeassistant.py
|
|
29
30
|
pychromecast/controllers/homeassistant_media.py
|
|
30
31
|
pychromecast/controllers/media.py
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Controller to send and respond to heartbeat messages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from . import BaseController
|
|
9
|
+
|
|
10
|
+
from ..const import MESSAGE_TYPE, PLATFORM_DESTINATION_ID
|
|
11
|
+
from ..error import ControllerNotRegistered, NotConnected, PyChromecastStopped
|
|
12
|
+
|
|
13
|
+
# pylint: disable-next=no-name-in-module
|
|
14
|
+
from ..generated.cast_channel_pb2 import CastMessage
|
|
15
|
+
|
|
16
|
+
NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat"
|
|
17
|
+
|
|
18
|
+
TYPE_PING = "PING"
|
|
19
|
+
TYPE_PONG = "PONG"
|
|
20
|
+
|
|
21
|
+
HB_PING_TIME = 10
|
|
22
|
+
HB_PONG_TIME = 10
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HeartbeatController(BaseController):
|
|
26
|
+
"""Controller to send and respond to heartbeat messages."""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
super().__init__(NS_HEARTBEAT, target_platform=True)
|
|
30
|
+
self.last_ping = 0.0
|
|
31
|
+
self.last_pong = time.time()
|
|
32
|
+
self.logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
def receive_message(self, _message: CastMessage, data: dict) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Called when a heartbeat message is received.
|
|
37
|
+
|
|
38
|
+
data is message.payload_utf8 interpreted as a JSON dict.
|
|
39
|
+
"""
|
|
40
|
+
if self._socket_client is None:
|
|
41
|
+
raise ControllerNotRegistered
|
|
42
|
+
|
|
43
|
+
if self._socket_client.is_stopped:
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
if data[MESSAGE_TYPE] == TYPE_PING:
|
|
47
|
+
try:
|
|
48
|
+
self._socket_client.send_message(
|
|
49
|
+
PLATFORM_DESTINATION_ID,
|
|
50
|
+
self.namespace,
|
|
51
|
+
{MESSAGE_TYPE: TYPE_PONG},
|
|
52
|
+
no_add_request_id=True,
|
|
53
|
+
)
|
|
54
|
+
except PyChromecastStopped:
|
|
55
|
+
self._socket_client.logger.debug(
|
|
56
|
+
"Heartbeat error when sending response, "
|
|
57
|
+
"Chromecast connection has stopped"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
if data[MESSAGE_TYPE] == TYPE_PONG:
|
|
63
|
+
self.reset()
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def ping(self) -> None:
|
|
69
|
+
"""Send a ping message."""
|
|
70
|
+
if self._socket_client is None:
|
|
71
|
+
raise ControllerNotRegistered
|
|
72
|
+
|
|
73
|
+
self.last_ping = time.time()
|
|
74
|
+
try:
|
|
75
|
+
self.send_message({MESSAGE_TYPE: TYPE_PING})
|
|
76
|
+
except NotConnected:
|
|
77
|
+
self._socket_client.logger.error(
|
|
78
|
+
"Chromecast is disconnected. Cannot ping until reconnected."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def reset(self) -> None:
|
|
82
|
+
"""Reset expired counter."""
|
|
83
|
+
self.last_pong = time.time()
|
|
84
|
+
|
|
85
|
+
def is_expired(self) -> bool:
|
|
86
|
+
"""Indicates if connection has expired."""
|
|
87
|
+
if time.time() - self.last_ping > HB_PING_TIME:
|
|
88
|
+
self.ping()
|
|
89
|
+
|
|
90
|
+
return (time.time() - self.last_pong) > HB_PING_TIME + HB_PONG_TIME
|
|
@@ -24,8 +24,10 @@ from struct import pack, unpack
|
|
|
24
24
|
|
|
25
25
|
import zeroconf
|
|
26
26
|
|
|
27
|
-
from .
|
|
27
|
+
from .config import APP_AUDIBLE
|
|
28
|
+
from .const import MESSAGE_TYPE, PLATFORM_DESTINATION_ID, REQUEST_ID, SESSION_ID
|
|
28
29
|
from .controllers import BaseController, CallbackType
|
|
30
|
+
from .controllers.heartbeat import NS_HEARTBEAT, HeartbeatController
|
|
29
31
|
from .controllers.media import MediaController
|
|
30
32
|
from .controllers.receiver import CastStatus, CastStatusListener, ReceiverController
|
|
31
33
|
from .dial import get_host_from_service
|
|
@@ -42,12 +44,7 @@ from .generated.cast_channel_pb2 import CastMessage
|
|
|
42
44
|
from .models import HostServiceInfo, MDNSServiceInfo
|
|
43
45
|
|
|
44
46
|
NS_CONNECTION = "urn:x-cast:com.google.cast.tp.connection"
|
|
45
|
-
NS_HEARTBEAT = "urn:x-cast:com.google.cast.tp.heartbeat"
|
|
46
47
|
|
|
47
|
-
PLATFORM_DESTINATION_ID = "receiver-0"
|
|
48
|
-
|
|
49
|
-
TYPE_PING = "PING"
|
|
50
|
-
TYPE_PONG = "PONG"
|
|
51
48
|
TYPE_CONNECT = "CONNECT"
|
|
52
49
|
TYPE_CLOSE = "CLOSE"
|
|
53
50
|
TYPE_LOAD = "LOAD"
|
|
@@ -65,10 +62,7 @@ CONNECTION_STATUS_FAILED_RESOLVE = "FAILED_RESOLVE"
|
|
|
65
62
|
# The socket connection was lost and needs to be retried
|
|
66
63
|
CONNECTION_STATUS_LOST = "LOST"
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
HB_PONG_TIME = 10
|
|
70
|
-
POLL_TIME_BLOCKING = 5.0
|
|
71
|
-
POLL_TIME_NON_BLOCKING = 0.01
|
|
65
|
+
SELECT_TIMEOUT = 5.0
|
|
72
66
|
TIMEOUT_TIME = 30.0
|
|
73
67
|
RETRY_TIME = 5.0
|
|
74
68
|
|
|
@@ -492,6 +486,17 @@ class SocketClient(threading.Thread, CastStatusListener):
|
|
|
492
486
|
self.session_id = status.session_id
|
|
493
487
|
|
|
494
488
|
if new_channel and self.destination_id is not None:
|
|
489
|
+
# App quirk: If the Audible app is running, wait 1s after receiving cast status
|
|
490
|
+
# before connecting to the media player channel. This is a workaround for
|
|
491
|
+
# https://github.com/home-assistant-libs/pychromecast/issues/738
|
|
492
|
+
if (
|
|
493
|
+
any(namespace in self._handlers for namespace in self.app_namespaces)
|
|
494
|
+
and self.destination_id not in self._open_channels
|
|
495
|
+
and status.app_id == APP_AUDIBLE
|
|
496
|
+
):
|
|
497
|
+
self.logger.debug("Detected Audible connection. Sleeping for 1s")
|
|
498
|
+
time.sleep(1)
|
|
499
|
+
|
|
495
500
|
# If any of the namespaces of the new app are supported
|
|
496
501
|
# we will automatically connect to it to receive updates
|
|
497
502
|
for namespace in self.app_namespaces:
|
|
@@ -540,7 +545,7 @@ class SocketClient(threading.Thread, CastStatusListener):
|
|
|
540
545
|
self.logger.debug("Thread started...")
|
|
541
546
|
while not self.stop.is_set():
|
|
542
547
|
try:
|
|
543
|
-
if self._run_once(
|
|
548
|
+
if self._run_once() == 1:
|
|
544
549
|
break
|
|
545
550
|
except Exception: # pylint: disable=broad-except
|
|
546
551
|
self._force_recon = True
|
|
@@ -555,7 +560,7 @@ class SocketClient(threading.Thread, CastStatusListener):
|
|
|
555
560
|
# Clean up
|
|
556
561
|
self._cleanup()
|
|
557
562
|
|
|
558
|
-
def _run_once(self
|
|
563
|
+
def _run_once(self) -> int:
|
|
559
564
|
"""Receive from the socket and handle data."""
|
|
560
565
|
# pylint: disable=too-many-branches, too-many-statements, too-many-return-statements
|
|
561
566
|
|
|
@@ -568,9 +573,13 @@ class SocketClient(threading.Thread, CastStatusListener):
|
|
|
568
573
|
# A connection has been established at this point by self._check_connection
|
|
569
574
|
assert self.socket is not None
|
|
570
575
|
|
|
571
|
-
#
|
|
576
|
+
# Poll the socket and the socketpair, with a timeout of SELECT_TIMEOUT
|
|
577
|
+
# The timeout ensures we call _check_connection often enough to avoid
|
|
578
|
+
# the HeartbeatController from detecting a timeout
|
|
579
|
+
# The socketpair allow us to be interrupted on shutdown without waiting
|
|
580
|
+
# for the SELECT_TIMEOUT timout to expire
|
|
572
581
|
try:
|
|
573
|
-
ready = self.selector.select(
|
|
582
|
+
ready = self.selector.select(SELECT_TIMEOUT)
|
|
574
583
|
except (ValueError, OSError) as exc:
|
|
575
584
|
self.logger.error(
|
|
576
585
|
"[%s(%s):%s] Error in select call: %s",
|
|
@@ -689,7 +698,10 @@ class SocketClient(threading.Thread, CastStatusListener):
|
|
|
689
698
|
# route message to handlers
|
|
690
699
|
if message.namespace in self._handlers:
|
|
691
700
|
# debug messages
|
|
692
|
-
if
|
|
701
|
+
if (
|
|
702
|
+
message.namespace != NS_HEARTBEAT
|
|
703
|
+
or self.heartbeat_controller.logger.isEnabledFor(logging.DEBUG)
|
|
704
|
+
):
|
|
693
705
|
self.logger.debug(
|
|
694
706
|
"[%s(%s):%s] Received: %s",
|
|
695
707
|
self.fn or "",
|
|
@@ -873,7 +885,10 @@ class SocketClient(threading.Thread, CastStatusListener):
|
|
|
873
885
|
be_size = pack(">I", msg.ByteSize())
|
|
874
886
|
|
|
875
887
|
# Log all messages except heartbeat
|
|
876
|
-
if
|
|
888
|
+
if (
|
|
889
|
+
msg.namespace != NS_HEARTBEAT
|
|
890
|
+
or self.heartbeat_controller.logger.isEnabledFor(logging.DEBUG)
|
|
891
|
+
):
|
|
877
892
|
self.logger.debug(
|
|
878
893
|
"[%s(%s):%s] Sending: %s",
|
|
879
894
|
self.fn or "",
|
|
@@ -1062,73 +1077,6 @@ class ConnectionController(BaseController):
|
|
|
1062
1077
|
return False
|
|
1063
1078
|
|
|
1064
1079
|
|
|
1065
|
-
class HeartbeatController(BaseController):
|
|
1066
|
-
"""Controller to respond to heartbeat messages."""
|
|
1067
|
-
|
|
1068
|
-
def __init__(self) -> None:
|
|
1069
|
-
super().__init__(NS_HEARTBEAT, target_platform=True)
|
|
1070
|
-
self.last_ping = 0.0
|
|
1071
|
-
self.last_pong = time.time()
|
|
1072
|
-
|
|
1073
|
-
def receive_message(self, _message: CastMessage, data: dict) -> bool:
|
|
1074
|
-
"""
|
|
1075
|
-
Called when a heartbeat message is received.
|
|
1076
|
-
|
|
1077
|
-
data is message.payload_utf8 interpreted as a JSON dict.
|
|
1078
|
-
"""
|
|
1079
|
-
if self._socket_client is None:
|
|
1080
|
-
raise ControllerNotRegistered
|
|
1081
|
-
|
|
1082
|
-
if self._socket_client.is_stopped:
|
|
1083
|
-
return True
|
|
1084
|
-
|
|
1085
|
-
if data[MESSAGE_TYPE] == TYPE_PING:
|
|
1086
|
-
try:
|
|
1087
|
-
self._socket_client.send_message(
|
|
1088
|
-
PLATFORM_DESTINATION_ID,
|
|
1089
|
-
self.namespace,
|
|
1090
|
-
{MESSAGE_TYPE: TYPE_PONG},
|
|
1091
|
-
no_add_request_id=True,
|
|
1092
|
-
)
|
|
1093
|
-
except PyChromecastStopped:
|
|
1094
|
-
self._socket_client.logger.debug(
|
|
1095
|
-
"Heartbeat error when sending response, "
|
|
1096
|
-
"Chromecast connection has stopped"
|
|
1097
|
-
)
|
|
1098
|
-
|
|
1099
|
-
return True
|
|
1100
|
-
|
|
1101
|
-
if data[MESSAGE_TYPE] == TYPE_PONG:
|
|
1102
|
-
self.reset()
|
|
1103
|
-
return True
|
|
1104
|
-
|
|
1105
|
-
return False
|
|
1106
|
-
|
|
1107
|
-
def ping(self) -> None:
|
|
1108
|
-
"""Send a ping message."""
|
|
1109
|
-
if self._socket_client is None:
|
|
1110
|
-
raise ControllerNotRegistered
|
|
1111
|
-
|
|
1112
|
-
self.last_ping = time.time()
|
|
1113
|
-
try:
|
|
1114
|
-
self.send_message({MESSAGE_TYPE: TYPE_PING})
|
|
1115
|
-
except NotConnected:
|
|
1116
|
-
self._socket_client.logger.error(
|
|
1117
|
-
"Chromecast is disconnected. Cannot ping until reconnected."
|
|
1118
|
-
)
|
|
1119
|
-
|
|
1120
|
-
def reset(self) -> None:
|
|
1121
|
-
"""Reset expired counter."""
|
|
1122
|
-
self.last_pong = time.time()
|
|
1123
|
-
|
|
1124
|
-
def is_expired(self) -> bool:
|
|
1125
|
-
"""Indicates if connection has expired."""
|
|
1126
|
-
if time.time() - self.last_ping > HB_PING_TIME:
|
|
1127
|
-
self.ping()
|
|
1128
|
-
|
|
1129
|
-
return (time.time() - self.last_pong) > HB_PING_TIME + HB_PONG_TIME
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
1080
|
def new_socket() -> socket.socket:
|
|
1133
1081
|
"""
|
|
1134
1082
|
Create a new socket with OS-specific parameters
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|