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",
|
"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:
|
async def _ws_loop(self) -> None:
|
||||||
"""WebSocket connection loop with reconnection."""
|
"""WebSocket connection loop with reconnection."""
|
||||||
delay = WS_RECONNECT_DELAY
|
delay = WS_RECONNECT_DELAY
|
||||||
@@ -52,11 +78,19 @@ class EventStreamListener:
|
|||||||
ws_base = self._server_url.replace("http://", "ws://").replace(
|
ws_base = self._server_url.replace("http://", "ws://").replace(
|
||||||
"https://", "wss://"
|
"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:
|
while not self._shutting_down:
|
||||||
try:
|
try:
|
||||||
async with session.ws_connect(url) as ws:
|
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
|
delay = WS_RECONNECT_DELAY # reset on successful connect
|
||||||
_LOGGER.debug("Event stream connected")
|
_LOGGER.debug("Event stream connected")
|
||||||
async for msg in ws:
|
async for msg in ws:
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"issue_tracker": "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab-haos-integration/issues",
|
"issue_tracker": "https://git.dolgolyov-family.by/alexei.dolgolyov/ledgrab-haos-integration/issues",
|
||||||
"requirements": ["aiohttp>=3.9.0"],
|
"requirements": ["aiohttp>=3.9.0"],
|
||||||
"version": "0.2.0"
|
"version": "0.2.1"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user