fix(events): use first-message auth handshake for event WebSocket

The server dropped the ?token=<key> query-param WS auth in favor of a
first-message {"type":"auth","token":...} handshake that must complete
within 3 s, so the old query-param connection silently timed out on the
server side and we reconnected every ~8 s in a tight loop.

Send the handshake right after ws_connect, wait up to 5 s for auth_ok,
and treat auth rejection as a connection error so the reconnect backoff
kicks in instead of hot-looping. Bumps manifest to 0.2.1.
This commit is contained in:
2026-04-21 15:16:54 +03:00
parent 579553a850
commit 5c8b3c259a
2 changed files with 36 additions and 2 deletions
+35 -1
View File
@@ -45,6 +45,32 @@ class EventStreamListener:
"ledgrab_events",
)
async def _authenticate(self, ws: aiohttp.ClientWebSocketResponse) -> bool:
"""Perform the first-message auth handshake required by the server.
Returns True on ``auth_ok``, False on timeout or ``auth_error``.
The server closes the socket on its side after a failure, so the
caller just needs to back off and try again.
"""
try:
await ws.send_json({"type": "auth", "token": self._api_key})
msg = await asyncio.wait_for(ws.receive(), timeout=5.0)
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.debug("Event stream auth handshake failed: %s", err)
return False
if msg.type != aiohttp.WSMsgType.TEXT:
return False
try:
payload = json.loads(msg.data)
except json.JSONDecodeError:
return False
if payload.get("type") != "auth_ok":
_LOGGER.debug(
"Event stream auth rejected: %s", payload.get("reason", "unknown")
)
return False
return True
async def _ws_loop(self) -> None:
"""WebSocket connection loop with reconnection."""
delay = WS_RECONNECT_DELAY
@@ -52,11 +78,19 @@ class EventStreamListener:
ws_base = self._server_url.replace("http://", "ws://").replace(
"https://", "wss://"
)
url = f"{ws_base}/api/v1/events/ws?token={self._api_key}"
# The server used to accept ``?token=<key>`` query params, but now
# requires a first-message auth handshake: send ``{"type":"auth",
# "token": ...}`` within 3 seconds and wait for ``{"type":"auth_ok"}``
# before consuming events.
url = f"{ws_base}/api/v1/events/ws"
while not self._shutting_down:
try:
async with session.ws_connect(url) as ws:
if not await self._authenticate(ws):
# Auth failed — treat like a connection error so we
# back off instead of reconnecting in a tight loop.
raise aiohttp.ClientError("LedGrab WS auth rejected")
delay = WS_RECONNECT_DELAY # reset on successful connect
_LOGGER.debug("Event stream connected")
async for msg in ws:
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"issue_tracker": "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab-haos-integration/issues",
"requirements": ["aiohttp>=3.9.0"],
"version": "0.2.0"
"version": "0.2.1"
}