Spaces:
Sleeping
Sleeping
Download app/services/command_hub.py from riad-hf/rr-locker-api: direct link, hf CLI and curl.
- Browser
- Download file 3.04 kB
-
https://huggingface.co/spaces/riad-hf/rr-locker-api/resolve/main/app/services/command_hub.py
- Command line
-
hf download hf://spaces/riad-hf/rr-locker-api/app/services/command_hub.py
-
curl -L -o command_hub.py https://huggingface.co/spaces/riad-hf/rr-locker-api/resolve/main/app/services/command_hub.py
3.04 kB
| """Real-time command push hub. | |
| Each enrolled device keeps a persistent WebSocket open to | |
| ``/api/v1/commands/ws/{device_id}``. When the admin panel issues a command we | |
| ``db.commit()`` it (so the poll fallback still works) and then *wake* the device | |
| over its socket, so the app fetches and executes the command instantly instead | |
| of waiting for the next 2s poll. | |
| The hub holds no durable state — if the API restarts, devices reconnect via | |
| ``StreamingWsClient`` automatically and the 2s poll covers the gap. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import logging | |
| from collections import defaultdict | |
| from typing import Dict, Optional, Set | |
| from fastapi import WebSocket | |
| logger = logging.getLogger(__name__) | |
| class CommandHub: | |
| """In-memory registry of device command sockets keyed by ``device_id``.""" | |
| def __init__(self) -> None: | |
| self._devices: Dict[str, Set[WebSocket]] = defaultdict(set) | |
| self._lock = asyncio.Lock() | |
| # Captured on the first WS connect so synchronous request handlers | |
| # (which run in a threadpool) can schedule a wake on the event loop. | |
| self._loop: Optional[asyncio.AbstractEventLoop] = None | |
| async def attach(self, device_id: str, ws: WebSocket) -> None: | |
| self._loop = asyncio.get_running_loop() | |
| async with self._lock: | |
| self._devices[device_id].add(ws) | |
| logger.info("CommandHub: device attached device=%s total=%d", | |
| device_id, len(self._devices[device_id])) | |
| async def detach(self, device_id: str, ws: WebSocket) -> None: | |
| async with self._lock: | |
| self._devices[device_id].discard(ws) | |
| if not self._devices[device_id]: | |
| self._devices.pop(device_id, None) | |
| def is_online(self, device_id: str) -> bool: | |
| return bool(self._devices.get(device_id)) | |
| async def _wake(self, device_id: str) -> None: | |
| targets = list(self._devices.get(device_id, ())) | |
| if not targets: | |
| return | |
| dead: list[WebSocket] = [] | |
| for ws in targets: | |
| try: | |
| await ws.send_text('{"event":"command"}') | |
| except Exception: | |
| dead.append(ws) | |
| if dead: | |
| async with self._lock: | |
| for d in dead: | |
| self._devices[device_id].discard(d) | |
| def wake_threadsafe(self, device_id) -> None: | |
| """Wake a device from synchronous code (e.g. a request handler). | |
| Safe to call even if no event loop / socket is available — it is a | |
| best-effort nudge layered on top of the existing poll + FCM fallbacks. | |
| """ | |
| loop = self._loop | |
| if loop is None: | |
| return | |
| try: | |
| asyncio.run_coroutine_threadsafe(self._wake(str(device_id)), loop) | |
| except Exception as exc: # pragma: no cover - best effort | |
| logger.debug("CommandHub wake failed for %s: %s", device_id, exc) | |
| command_hub = CommandHub() | |