From d7f2947628eaf391c505dab16e87215390c2a7b9 Mon Sep 17 00:00:00 2001 From: Aruna Tennakoon Date: Fri, 20 Feb 2026 08:23:22 +0700 Subject: [PATCH 1/4] fix: disconnecting and reconnecting every 5 minutes due to a false "pong timeout." --- sinricpro/core/websocket_client.py | 55 ++++++++++++------------------ 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/sinricpro/core/websocket_client.py b/sinricpro/core/websocket_client.py index ab3959d..8b54a42 100644 --- a/sinricpro/core/websocket_client.py +++ b/sinricpro/core/websocket_client.py @@ -74,7 +74,6 @@ def __init__(self, config: WebSocketConfig) -> None: self.should_reconnect = True self.last_ping_time = 0.0 self._ping_task: asyncio.Task[None] | None = None - self._pong_timeout_task: asyncio.Task[None] | None = None self._reconnect_task: asyncio.Task[None] | None = None self._message_callbacks: list[Callable[[str], None]] = [] self._connected_callbacks: list[Callable[[], None]] = [] @@ -166,18 +165,6 @@ async def _handle_messages(self) -> None: SinricProLogger.debug(f"WebSocket received: {message}") for callback in self._message_callbacks: callback(message) - elif isinstance(message, bytes): - # Handle pong messages - latency = int((time.time() - self.last_ping_time) * 1000) - SinricProLogger.debug(f"WebSocket pong received (latency: {latency}ms)") - - # Cancel pong timeout - if self._pong_timeout_task: - self._pong_timeout_task.cancel() - self._pong_timeout_task = None - - for callback in self._pong_callbacks: - callback(latency) except websockets.exceptions.ConnectionClosed: SinricProLogger.info("WebSocket connection closed") @@ -226,35 +213,39 @@ def _start_heartbeat(self) -> None: self._ping_task = asyncio.create_task(self._heartbeat_loop()) async def _heartbeat_loop(self) -> None: - """Heartbeat loop to send pings.""" + """Heartbeat loop to send pings and await pong responses.""" while self.connected and self.ws: await asyncio.sleep(WEBSOCKET_PING_INTERVAL / 1000.0) # Convert to seconds if self.ws and self.connected: try: self.last_ping_time = time.time() - await self.ws.ping() + pong_waiter = await self.ws.ping() SinricProLogger.debug("WebSocket ping sent") - # Set timeout for pong - self._pong_timeout_task = asyncio.create_task(self._pong_timeout()) + # Wait for pong with timeout + await asyncio.wait_for( + pong_waiter, + timeout=WEBSOCKET_PING_TIMEOUT / 1000.0, + ) - except Exception as e: - SinricProLogger.error(f"Error sending ping: {e}") + latency = int((time.time() - self.last_ping_time) * 1000) + SinricProLogger.debug(f"WebSocket pong received (latency: {latency}ms)") - async def _pong_timeout(self) -> None: - """Handle pong timeout.""" - try: - await asyncio.sleep(WEBSOCKET_PING_TIMEOUT / 1000.0) - SinricProLogger.error("WebSocket pong timeout - connection appears dead") + for callback in self._pong_callbacks: + callback(latency) - # Force close connection - if self.ws: - await self.ws.close() + except asyncio.TimeoutError: + SinricProLogger.error("WebSocket pong timeout - connection appears dead") + if self.ws: + await self.ws.close() + return - except asyncio.CancelledError: - # Pong was received in time - pass + except asyncio.CancelledError: + return + + except Exception as e: + SinricProLogger.error(f"Error sending ping: {e}") def _stop_heartbeat(self) -> None: """Stop heartbeat tasks.""" @@ -262,10 +253,6 @@ def _stop_heartbeat(self) -> None: self._ping_task.cancel() self._ping_task = None - if self._pong_timeout_task: - self._pong_timeout_task.cancel() - self._pong_timeout_task = None - def _schedule_reconnect(self) -> None: """Schedule automatic reconnection.""" if self._reconnect_task: From 307289d3430e6817c8d0e765e1d8bec258af05f3 Mon Sep 17 00:00:00 2001 From: Aruna Tennakoon Date: Fri, 20 Feb 2026 08:32:49 +0700 Subject: [PATCH 2/4] feat: Only after 3 consecutive misses does it close the connection --- sinricpro/core/types.py | 1 + sinricpro/core/websocket_client.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/sinricpro/core/types.py b/sinricpro/core/types.py index 6404d68..a395c4d 100644 --- a/sinricpro/core/types.py +++ b/sinricpro/core/types.py @@ -16,6 +16,7 @@ SINRICPRO_SERVER_SSL_PORT = 443 WEBSOCKET_PING_INTERVAL = 300000 # 5 minutes in milliseconds WEBSOCKET_PING_TIMEOUT = 10000 # 10 seconds in milliseconds +WEBSOCKET_PONG_MISS_MAX = 3 # Close connection after this many consecutive missed pongs EVENT_LIMIT_STATE = 1000 # 1 second in milliseconds EVENT_LIMIT_SENSOR_VALUE = 60000 # 60 seconds in milliseconds diff --git a/sinricpro/core/websocket_client.py b/sinricpro/core/websocket_client.py index 8b54a42..c8b67d3 100644 --- a/sinricpro/core/websocket_client.py +++ b/sinricpro/core/websocket_client.py @@ -32,6 +32,7 @@ def get_mac_address() -> str: SINRICPRO_SERVER_SSL_PORT, WEBSOCKET_PING_INTERVAL, WEBSOCKET_PING_TIMEOUT, + WEBSOCKET_PONG_MISS_MAX, ) from sinricpro.utils.logger import SinricProLogger @@ -214,6 +215,8 @@ def _start_heartbeat(self) -> None: async def _heartbeat_loop(self) -> None: """Heartbeat loop to send pings and await pong responses.""" + consecutive_misses = 0 + while self.connected and self.ws: await asyncio.sleep(WEBSOCKET_PING_INTERVAL / 1000.0) # Convert to seconds @@ -231,15 +234,22 @@ async def _heartbeat_loop(self) -> None: latency = int((time.time() - self.last_ping_time) * 1000) SinricProLogger.debug(f"WebSocket pong received (latency: {latency}ms)") + consecutive_misses = 0 for callback in self._pong_callbacks: callback(latency) except asyncio.TimeoutError: - SinricProLogger.error("WebSocket pong timeout - connection appears dead") - if self.ws: - await self.ws.close() - return + consecutive_misses += 1 + SinricProLogger.warn( + f"WebSocket pong timeout ({consecutive_misses}/{WEBSOCKET_PONG_MISS_MAX})" + ) + + if consecutive_misses >= WEBSOCKET_PONG_MISS_MAX: + SinricProLogger.error("WebSocket connection appears dead, closing") + if self.ws: + await self.ws.close() + return except asyncio.CancelledError: return From 3404e636d053e2dcbb973446cec73c6af15356b0 Mon Sep 17 00:00:00 2001 From: Aruna Tennakoon Date: Fri, 20 Feb 2026 08:55:38 +0700 Subject: [PATCH 3/4] chore: bump version --- pyproject.toml | 2 +- sinricpro/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ffa2e66..432c00a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sinricpro" -version = "5.2.0" +version = "5.2.1" description = "Official SinricPro SDK for Python - Control IoT devices with Alexa and Google Home" authors = [{name = "SinricPro", email = "support@sinric.pro"}] readme = "README.md" diff --git a/sinricpro/__init__.py b/sinricpro/__init__.py index 598f70d..fdb9c91 100644 --- a/sinricpro/__init__.py +++ b/sinricpro/__init__.py @@ -9,7 +9,7 @@ This file is part of the SinricPro Python SDK (https://github.com/sinricpro/) """ -__version__ = "5.2.0" +__version__ = "5.2.1" from sinricpro.core.sinric_pro import SinricPro, SinricProConfig from sinricpro.core.sinric_pro_device import SinricProDevice From d7b46bf976acf105d7b1cbb545f7ed927042d244 Mon Sep 17 00:00:00 2001 From: Aruna Tennakoon Date: Fri, 20 Feb 2026 08:55:53 +0700 Subject: [PATCH 4/4] chore: bump version --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd730e7..24fe49f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [5.2.1] +- fix: [WebSocket pong timeout - connection appears dead - Reconnection loop annoys server](https://github.com/sinricpro/python-sdk/issues/83) +- feat: only after 3 consecutive misses does it close the connection + ## [5.2.0] - feat: Send a device setting event to SinricPro