Skip to content

Commit 0df6d2a

Browse files
committed
Merge remote-tracking branch 'origin/master' into dual-stack
2 parents 4529903 + b2e3c9d commit 0df6d2a

File tree

8 files changed

+70
-38
lines changed

8 files changed

+70
-38
lines changed

.github/workflows/pythonpublish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
deploy:
1212
runs-on: ubuntu-latest
1313
steps:
14-
- uses: actions/checkout@v4.1.1
14+
- uses: actions/checkout@v4.1.2
1515
- name: Set up Python
1616
uses: actions/setup-python@v5.0.0
1717
with:

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-latest
1515

1616
steps:
17-
- uses: actions/checkout@v4.1.1
17+
- uses: actions/checkout@v4.1.2
1818
- name: Set up Python 3.11
1919
uses: actions/setup-python@v5.0.0
2020
with:

pychromecast/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
MF_CANTON = "Canton Elektronik GmbH + Co. KG"
1616
MF_GOOGLE = "Google Inc."
17+
MF_HARMAN = "HARMAN International Industries"
1718
MF_JBL = "JBL"
1819
MF_LENOVO = "LENOVO"
1920
MF_LG = "LG"
@@ -47,6 +48,7 @@
4748
"JBL Link 20": (CAST_TYPE_AUDIO, MF_JBL),
4849
"JBL Link 300": (CAST_TYPE_AUDIO, MF_JBL),
4950
"JBL Link 500": (CAST_TYPE_AUDIO, MF_JBL),
51+
"JBL Link Portable": (CAST_TYPE_AUDIO, MF_HARMAN),
5052
"lenovocd-24502f": (CAST_TYPE_AUDIO, MF_LENOVO),
5153
"Lenovo Smart Display 7": (CAST_TYPE_CHROMECAST, MF_LENOVO),
5254
"LG WK7 ThinQ Speaker": (CAST_TYPE_AUDIO, MF_LG),

pychromecast/controllers/homeassistant.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from collections.abc import Callable
66
from functools import partial
7+
import logging
78
import threading
89
from typing import Any
910

@@ -29,6 +30,8 @@
2930
ERR_NOT_CONNECTED = 21
3031
ERR_FETCH_CONFIG_FAILED = 22
3132

33+
_LOGGER = logging.getLogger(__name__)
34+
3235

3336
class HomeAssistantController(BaseController):
3437
"""Controller to interact with Home Assistant."""
@@ -45,6 +48,7 @@ def __init__(
4548
app_id: str = APP_HOMEASSISTANT_LOVELACE,
4649
hass_connect_timeout: float = DEFAULT_HASS_CONNECT_TIMEOUT,
4750
) -> None:
51+
_LOGGER.debug("HomeAssistantController.__init__")
4852
super().__init__(app_namespace, app_id)
4953
self.hass_url = hass_url
5054
self.hass_uuid = hass_uuid
@@ -77,16 +81,19 @@ def hass_connected(self) -> bool:
7781
def channel_connected(self) -> None:
7882
"""Called when a channel has been openend that supports the
7983
namespace of this controller."""
84+
_LOGGER.debug("HomeAssistantController.channel_connected")
8085
self.get_status()
8186

8287
def channel_disconnected(self) -> None:
8388
"""Called when a channel is disconnected."""
89+
_LOGGER.debug("HomeAssistantController.channel_disconnected")
8490
self.status = None
8591
self._hass_connecting_event.set()
8692

8793
def receive_message(self, _message: CastMessage, data: dict) -> bool:
8894
"""Called when a message is received."""
8995
if data.get("type") == "receiver_status":
96+
_LOGGER.debug("HomeAssistantController.receive_message %s", data)
9097
if data["hassUrl"] != self.hass_url or data["hassUUID"] != self.hass_uuid:
9198
self.logger.info("Received status for another instance")
9299
self.unregister()
@@ -96,6 +103,9 @@ def receive_message(self, _message: CastMessage, data: dict) -> bool:
96103
self.status = data
97104

98105
if was_connected or not self.hass_connected:
106+
_LOGGER.debug(
107+
"HomeAssistantController.receive_message already connected"
108+
)
99109
return True
100110

101111
# We just got connected, call the callbacks.
@@ -104,6 +114,7 @@ def receive_message(self, _message: CastMessage, data: dict) -> bool:
104114
return True
105115

106116
if data.get("type") == "receiver_error":
117+
_LOGGER.debug("HomeAssistantController.receive_message %s", data)
107118
if data.get("error_code") == ERR_WRONG_INSTANCE:
108119
self.logger.info("Received ERR_WRONG_INSTANCE")
109120
self.unregister()
@@ -113,14 +124,19 @@ def receive_message(self, _message: CastMessage, data: dict) -> bool:
113124

114125
def _call_on_connect_callbacks(self, msg_sent: bool) -> None:
115126
"""Call on connect callbacks."""
127+
_LOGGER.debug("HomeAssistantController._call_on_connect_callbacks %s", msg_sent)
116128
while self._on_connect:
117129
self._on_connect.pop()(msg_sent, None)
118130

119131
def _connect_hass(self, callback_function: CallbackType) -> None:
120132
"""Connect to Home Assistant and call the provided callback."""
133+
_LOGGER.debug("HomeAssistantController._connect_hass")
121134
self._on_connect.append(callback_function)
122135

123136
if not self._hass_connecting_event.is_set():
137+
_LOGGER.debug(
138+
"HomeAssistantController._connect_hass _hass_connecting_event not set"
139+
)
124140
return
125141

126142
self._hass_connecting_event.clear()
@@ -135,6 +151,9 @@ def _connect_hass(self, callback_function: CallbackType) -> None:
135151
}
136152
)
137153
except Exception: # pylint: disable=broad-except
154+
_LOGGER.debug(
155+
"HomeAssistantController._connect_hass failed to send connect message"
156+
)
138157
self._hass_connecting_event.set()
139158
self._call_on_connect_callbacks(False)
140159
raise
@@ -143,17 +162,18 @@ def _connect_hass(self, callback_function: CallbackType) -> None:
143162
try:
144163
if not self._hass_connecting_event.is_set():
145164
self.logger.warning("_connect_hass failed for %s", self.hass_url)
165+
self._call_on_connect_callbacks(False)
146166
raise PyChromecastError() # pylint: disable=broad-exception-raised
147167
finally:
148168
self._hass_connecting_event.set()
149-
self._call_on_connect_callbacks(False)
150169

151170
def show_demo(self) -> None:
152171
"""Show the demo."""
153172
self.send_message({"type": "show_demo"})
154173

155174
def get_status(self, *, callback_function: CallbackType | None = None) -> None:
156175
"""Get status of Home Assistant Cast."""
176+
_LOGGER.debug("HomeAssistantController.get_status")
157177
self._send_connected_message(
158178
{
159179
"type": "get_status",
@@ -171,6 +191,7 @@ def show_lovelace_view(
171191
callback_function: CallbackType | None = None,
172192
) -> None:
173193
"""Show a Lovelace UI."""
194+
_LOGGER.debug("HomeAssistantController.show_lovelace_view")
174195
self._send_connected_message(
175196
{
176197
"type": "show_lovelace_view",
@@ -186,10 +207,17 @@ def _send_connected_message(
186207
self, data: dict[str, Any], callback_function: CallbackType | None
187208
) -> None:
188209
"""Send a message to a connected Home Assistant Cast"""
210+
_LOGGER.debug("HomeAssistantController._send_connected_message %s", data)
189211
if self.hass_connected:
212+
_LOGGER.debug(
213+
"HomeAssistantController._send_connected_message already connected"
214+
)
190215
self.send_message_nocheck(data, callback_function=callback_function)
191216
return
192217

218+
_LOGGER.debug(
219+
"HomeAssistantController._send_connected_message not yet connected"
220+
)
193221
self._connect_hass(
194222
chain_on_success(
195223
partial(self.send_message_nocheck, data), callback_function

pychromecast/response_handler.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
from __future__ import annotations
44

55
from collections.abc import Callable
6+
import logging
67
import threading
78
from typing import Protocol
89

910
from .error import RequestFailed, RequestTimeout
1011

12+
_LOGGER = logging.getLogger(__name__)
13+
1114
CallbackType = Callable[[bool, dict | None], None]
1215
"""Signature of optional callback functions supported by methods sending messages.
1316
@@ -61,6 +64,7 @@ def chain_on_success(
6164

6265
def _callback(msg_sent: bool, response: dict | None) -> None:
6366
if not msg_sent:
67+
_LOGGER.debug("Not calling on_success %s", on_success)
6468
if callback_function:
6569
callback_function(msg_sent, response)
6670
return

pychromecast/socket_client.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,31 @@
1010
from __future__ import annotations
1111

1212
import abc
13-
from dataclasses import dataclass
1413
import errno
1514
import json
1615
import logging
17-
import select
16+
import selectors
1817
import socket
1918
import ssl
2019
import threading
2120
import time
2221
from collections import defaultdict
22+
from dataclasses import dataclass
2323
from struct import pack, unpack
2424

2525
import zeroconf
2626

27-
from .controllers import CallbackType, BaseController
27+
from .const import MESSAGE_TYPE, REQUEST_ID, SESSION_ID
28+
from .controllers import BaseController, CallbackType
2829
from .controllers.media import MediaController
2930
from .controllers.receiver import CastStatus, CastStatusListener, ReceiverController
30-
from .const import MESSAGE_TYPE, REQUEST_ID, SESSION_ID
3131
from .dial import get_host_from_service
3232
from .error import (
3333
ChromecastConnectionError,
3434
ControllerNotRegistered,
35-
UnsupportedNamespace,
3635
NotConnected,
3736
PyChromecastStopped,
37+
UnsupportedNamespace,
3838
)
3939

4040
# pylint: disable-next=no-name-in-module
@@ -64,13 +64,9 @@
6464
CONNECTION_STATUS_FAILED_RESOLVE = "FAILED_RESOLVE"
6565
# The socket connection was lost and needs to be retried
6666
CONNECTION_STATUS_LOST = "LOST"
67-
# Check for select poll method
68-
SELECT_HAS_POLL = hasattr(select, "poll")
6967

7068
HB_PING_TIME = 10
7169
HB_PONG_TIME = 10
72-
POLL_TIME_BLOCKING = 5.0
73-
POLL_TIME_NON_BLOCKING = 0.01
7470
TIMEOUT_TIME = 30.0
7571
RETRY_TIME = 5.0
7672

@@ -215,6 +211,11 @@ def __init__(
215211
self.connecting = True
216212
self.first_connection = True
217213
self.socket: socket.socket | ssl.SSLSocket | None = None
214+
self.selector = selectors.DefaultSelector()
215+
self.wakeup_selector_key = self.selector.register(
216+
self.socketpair[0], selectors.EVENT_READ
217+
)
218+
self.remote_selector_key: selectors.SelectorKey | None = None
218219

219220
# dict mapping namespace on Controller objects
220221
self._handlers: dict[str, set[BaseController]] = defaultdict(set)
@@ -238,8 +239,10 @@ def initialize_connection( # pylint:disable=too-many-statements, too-many-branc
238239
tries = self.tries
239240

240241
if self.socket is not None:
242+
self.selector.unregister(self.socket)
241243
self.socket.close()
242244
self.socket = None
245+
self.remote_selector_key = None
243246

244247
# Make sure nobody is blocking.
245248
for callback_function in self._request_callbacks.values():
@@ -288,10 +291,15 @@ def mdns_backoff(
288291
try:
289292
if self.socket is not None:
290293
# If we retry connecting, we need to clean up the socket again
291-
self.socket.close() # type: ignore[unreachable]
294+
self.selector.unregister(self.socket) # type: ignore[unreachable]
295+
self.socket.close()
292296
self.socket = None
297+
self.remote_selector_key = None
293298

294299
self.socket = new_socket()
300+
self.remote_selector_key = self.selector.register(
301+
self.socket, selectors.EVENT_READ
302+
)
295303
self.socket.settimeout(self.timeout)
296304
self._report_connection_status(
297305
ConnectionStatus(
@@ -539,7 +547,7 @@ def run(self) -> None:
539547
self.logger.debug("Thread started...")
540548
while not self.stop.is_set():
541549
try:
542-
if self._run_once(timeout=POLL_TIME_BLOCKING) == 1:
550+
if self._run_once() == 1:
543551
break
544552
except Exception: # pylint: disable=broad-except
545553
self._force_recon = True
@@ -554,7 +562,7 @@ def run(self) -> None:
554562
# Clean up
555563
self._cleanup()
556564

557-
def _run_once(self, timeout: float = POLL_TIME_NON_BLOCKING) -> int:
565+
def _run_once(self) -> int:
558566
"""Receive from the socket and handle data."""
559567
# pylint: disable=too-many-branches, too-many-statements, too-many-return-statements
560568

@@ -568,20 +576,8 @@ def _run_once(self, timeout: float = POLL_TIME_NON_BLOCKING) -> int:
568576
assert self.socket is not None
569577

570578
# poll the socket, as well as the socketpair to allow us to be interrupted
571-
rlist = [self.socket, self.socketpair[0]]
572579
try:
573-
if SELECT_HAS_POLL is True:
574-
# Map file descriptors to socket objects because select.select does not support fd > 1024
575-
# https://stackoverflow.com/questions/14250751/how-to-increase-filedescriptors-range-in-python-select
576-
fd_to_socket = {rlist_item.fileno(): rlist_item for rlist_item in rlist}
577-
578-
poll_obj = select.poll()
579-
for poll_fd in rlist:
580-
poll_obj.register(poll_fd, select.POLLIN)
581-
poll_result = poll_obj.poll(timeout * 1000) # timeout in milliseconds
582-
can_read = [fd_to_socket[fd] for fd, _status in poll_result]
583-
else:
584-
can_read, _, _ = select.select(rlist, [], [], timeout)
580+
ready = self.selector.select()
585581
except (ValueError, OSError) as exc:
586582
self.logger.error(
587583
"[%s(%s):%s] Error in select call: %s",
@@ -593,9 +589,10 @@ def _run_once(self, timeout: float = POLL_TIME_NON_BLOCKING) -> int:
593589
self._force_recon = True
594590
return 0
595591

592+
can_read = {key for key, _ in ready}
596593
# read message from chromecast
597594
message = None
598-
if self.socket in can_read and not self._force_recon:
595+
if self.remote_selector_key in can_read and not self._force_recon:
599596
try:
600597
message = self._read_message()
601598
except InterruptLoop as exc:
@@ -631,7 +628,7 @@ def _run_once(self, timeout: float = POLL_TIME_NON_BLOCKING) -> int:
631628
else:
632629
data = _dict_from_message_payload(message)
633630

634-
if self.socketpair[0] in can_read:
631+
if self.wakeup_selector_key in can_read:
635632
# Clear the socket's buffer
636633
self.socketpair[0].recv(128)
637634

@@ -776,6 +773,7 @@ def _cleanup(self) -> None:
776773

777774
self.socketpair[0].close()
778775
self.socketpair[1].close()
776+
self.selector.close()
779777

780778
self.connecting = True
781779

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "PyChromecast"
7-
version = "14.0.0"
7+
version = "14.0.1"
88
description = "Python module to talk to Google Chromecast."
99
readme = "README.rst"
1010
authors = [

requirements-test.txt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
beautifulsoup4==4.12.3
2-
black==24.2.0
2+
black==24.3.0
33
flake8==7.0.0
4-
mypy==1.8.0
4+
mypy==1.9.0
55
PlexAPI==4.15.10
6-
pylint==3.0.3
6+
pylint==3.1.0
77
rstcheck==6.2.0
8-
types-beautifulsoup4==4.12.0.20240106
9-
types-html5lib==1.1.11.20240217
10-
types-protobuf==4.24.0.20240129
11-
types-requests==2.31.0.20240218
8+
types-beautifulsoup4==4.12.0.20240229
9+
types-html5lib==1.1.11.20240228
10+
types-protobuf==4.24.0.20240311
11+
types-requests==2.31.0.20240311
1212
yle-dl==20240130

0 commit comments

Comments
 (0)