diff --git a/custom_components/ledgrab/event_listener.py b/custom_components/ledgrab/event_listener.py index 9fbefc1..5ffa7a0 100644 --- a/custom_components/ledgrab/event_listener.py +++ b/custom_components/ledgrab/event_listener.py @@ -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=`` 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: diff --git a/custom_components/ledgrab/manifest.json b/custom_components/ledgrab/manifest.json index 47b3ee5..4fac256 100644 --- a/custom_components/ledgrab/manifest.json +++ b/custom_components/ledgrab/manifest.json @@ -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" }