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:
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user