Skip to content

Commit 6527a18

Browse files
authored
Merge pull request #84 from sinricpro/5.0.0-dev
Fix: #83
2 parents 34a4f25 + d7b46bf commit 6527a18

5 files changed

Lines changed: 38 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## [5.2.1]
2+
- fix: [WebSocket pong timeout - connection appears dead - Reconnection loop annoys server](https://github.com/sinricpro/python-sdk/issues/83)
3+
- feat: only after 3 consecutive misses does it close the connection
4+
15
## [5.2.0]
26
- feat: Send a device setting event to SinricPro
37

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 = "sinricpro"
7-
version = "5.2.0"
7+
version = "5.2.1"
88
description = "Official SinricPro SDK for Python - Control IoT devices with Alexa and Google Home"
99
authors = [{name = "SinricPro", email = "support@sinric.pro"}]
1010
readme = "README.md"

sinricpro/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
This file is part of the SinricPro Python SDK (https://github.com/sinricpro/)
1010
"""
1111

12-
__version__ = "5.2.0"
12+
__version__ = "5.2.1"
1313

1414
from sinricpro.core.sinric_pro import SinricPro, SinricProConfig
1515
from sinricpro.core.sinric_pro_device import SinricProDevice

sinricpro/core/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
SINRICPRO_SERVER_SSL_PORT = 443
1717
WEBSOCKET_PING_INTERVAL = 300000 # 5 minutes in milliseconds
1818
WEBSOCKET_PING_TIMEOUT = 10000 # 10 seconds in milliseconds
19+
WEBSOCKET_PONG_MISS_MAX = 3 # Close connection after this many consecutive missed pongs
1920
EVENT_LIMIT_STATE = 1000 # 1 second in milliseconds
2021
EVENT_LIMIT_SENSOR_VALUE = 60000 # 60 seconds in milliseconds
2122

sinricpro/core/websocket_client.py

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def get_mac_address() -> str:
3232
SINRICPRO_SERVER_SSL_PORT,
3333
WEBSOCKET_PING_INTERVAL,
3434
WEBSOCKET_PING_TIMEOUT,
35+
WEBSOCKET_PONG_MISS_MAX,
3536
)
3637
from sinricpro.utils.logger import SinricProLogger
3738

@@ -74,7 +75,6 @@ def __init__(self, config: WebSocketConfig) -> None:
7475
self.should_reconnect = True
7576
self.last_ping_time = 0.0
7677
self._ping_task: asyncio.Task[None] | None = None
77-
self._pong_timeout_task: asyncio.Task[None] | None = None
7878
self._reconnect_task: asyncio.Task[None] | None = None
7979
self._message_callbacks: list[Callable[[str], None]] = []
8080
self._connected_callbacks: list[Callable[[], None]] = []
@@ -166,18 +166,6 @@ async def _handle_messages(self) -> None:
166166
SinricProLogger.debug(f"WebSocket received: {message}")
167167
for callback in self._message_callbacks:
168168
callback(message)
169-
elif isinstance(message, bytes):
170-
# Handle pong messages
171-
latency = int((time.time() - self.last_ping_time) * 1000)
172-
SinricProLogger.debug(f"WebSocket pong received (latency: {latency}ms)")
173-
174-
# Cancel pong timeout
175-
if self._pong_timeout_task:
176-
self._pong_timeout_task.cancel()
177-
self._pong_timeout_task = None
178-
179-
for callback in self._pong_callbacks:
180-
callback(latency)
181169

182170
except websockets.exceptions.ConnectionClosed:
183171
SinricProLogger.info("WebSocket connection closed")
@@ -226,46 +214,55 @@ def _start_heartbeat(self) -> None:
226214
self._ping_task = asyncio.create_task(self._heartbeat_loop())
227215

228216
async def _heartbeat_loop(self) -> None:
229-
"""Heartbeat loop to send pings."""
217+
"""Heartbeat loop to send pings and await pong responses."""
218+
consecutive_misses = 0
219+
230220
while self.connected and self.ws:
231221
await asyncio.sleep(WEBSOCKET_PING_INTERVAL / 1000.0) # Convert to seconds
232222

233223
if self.ws and self.connected:
234224
try:
235225
self.last_ping_time = time.time()
236-
await self.ws.ping()
226+
pong_waiter = await self.ws.ping()
237227
SinricProLogger.debug("WebSocket ping sent")
238228

239-
# Set timeout for pong
240-
self._pong_timeout_task = asyncio.create_task(self._pong_timeout())
229+
# Wait for pong with timeout
230+
await asyncio.wait_for(
231+
pong_waiter,
232+
timeout=WEBSOCKET_PING_TIMEOUT / 1000.0,
233+
)
241234

242-
except Exception as e:
243-
SinricProLogger.error(f"Error sending ping: {e}")
235+
latency = int((time.time() - self.last_ping_time) * 1000)
236+
SinricProLogger.debug(f"WebSocket pong received (latency: {latency}ms)")
237+
consecutive_misses = 0
244238

245-
async def _pong_timeout(self) -> None:
246-
"""Handle pong timeout."""
247-
try:
248-
await asyncio.sleep(WEBSOCKET_PING_TIMEOUT / 1000.0)
249-
SinricProLogger.error("WebSocket pong timeout - connection appears dead")
239+
for callback in self._pong_callbacks:
240+
callback(latency)
241+
242+
except asyncio.TimeoutError:
243+
consecutive_misses += 1
244+
SinricProLogger.warn(
245+
f"WebSocket pong timeout ({consecutive_misses}/{WEBSOCKET_PONG_MISS_MAX})"
246+
)
250247

251-
# Force close connection
252-
if self.ws:
253-
await self.ws.close()
248+
if consecutive_misses >= WEBSOCKET_PONG_MISS_MAX:
249+
SinricProLogger.error("WebSocket connection appears dead, closing")
250+
if self.ws:
251+
await self.ws.close()
252+
return
254253

255-
except asyncio.CancelledError:
256-
# Pong was received in time
257-
pass
254+
except asyncio.CancelledError:
255+
return
256+
257+
except Exception as e:
258+
SinricProLogger.error(f"Error sending ping: {e}")
258259

259260
def _stop_heartbeat(self) -> None:
260261
"""Stop heartbeat tasks."""
261262
if self._ping_task:
262263
self._ping_task.cancel()
263264
self._ping_task = None
264265

265-
if self._pong_timeout_task:
266-
self._pong_timeout_task.cancel()
267-
self._pong_timeout_task = None
268-
269266
def _schedule_reconnect(self) -> None:
270267
"""Schedule automatic reconnection."""
271268
if self._reconnect_task:

0 commit comments

Comments
 (0)