from __future__ import annotations import asyncio from collections import deque from datetime import datetime, timezone from typing import Any from fastapi import WebSocket from .models import ConnectedDevice, DeviceHello, Platform def _utc_now_iso() -> str: return datetime.now(timezone.utc).isoformat() class ConnectionManager: def __init__(self) -> None: self._lock = asyncio.Lock() self._sockets: dict[str, WebSocket] = {} self._meta: dict[str, ConnectedDevice] = {} self._events: deque[dict[str, Any]] = deque(maxlen=500) async def connect(self, hello: DeviceHello, websocket: WebSocket) -> None: now = _utc_now_iso() async with self._lock: self._sockets[hello.device_id] = websocket self._meta[hello.device_id] = ConnectedDevice( device_id=hello.device_id, platform=hello.platform, version=hello.version, hostname=hello.hostname, capabilities=hello.capabilities, connected_at=now, last_seen_at=now, ) self._events.appendleft( { "time": now, "event": "device_connected", "device_id": hello.device_id, "platform": hello.platform.value, "version": hello.version, } ) async def disconnect(self, device_id: str) -> None: now = _utc_now_iso() async with self._lock: had = device_id in self._sockets self._sockets.pop(device_id, None) if device_id in self._meta: self._meta[device_id].last_seen_at = now if had: self._events.appendleft( { "time": now, "event": "device_disconnected", "device_id": device_id, } ) async def touch(self, device_id: str) -> None: async with self._lock: meta = self._meta.get(device_id) if meta: meta.last_seen_at = _utc_now_iso() async def list_devices(self) -> list[ConnectedDevice]: async with self._lock: return [self._meta[did] for did in self._sockets.keys() if did in self._meta] async def get_logs(self) -> list[dict[str, Any]]: async with self._lock: return list(self._events) async def add_event(self, payload: dict[str, Any]) -> None: async with self._lock: payload = dict(payload) payload.setdefault("time", _utc_now_iso()) self._events.appendleft(payload) async def dispatch_command( self, command_id: str, action: str, data: dict[str, Any], target_scope: str, device_ids: list[str], ) -> tuple[list[str], list[str]]: async with self._lock: targets: list[tuple[str, WebSocket, ConnectedDevice]] = [] skipped: list[str] = [] for device_id, ws in self._sockets.items(): meta = self._meta.get(device_id) if not meta: continue allowed = False if target_scope == "all": allowed = True elif target_scope == "windows": allowed = meta.platform == Platform.windows elif target_scope == "pwa": allowed = meta.platform == Platform.pwa elif target_scope == "web": allowed = meta.platform == Platform.web elif target_scope == "device_ids": allowed = device_id in device_ids if allowed: targets.append((device_id, ws, meta)) else: skipped.append(device_id) delivered: list[str] = [] for device_id, ws, _meta in targets: try: await ws.send_json( { "type": "command", "id": command_id, "action": action, "data": data, "sent_at": _utc_now_iso(), } ) delivered.append(device_id) except Exception: skipped.append(device_id) await self.disconnect(device_id) await self.add_event( { "event": "command_dispatched", "command_id": command_id, "action": action, "target_scope": target_scope, "delivered_count": len(delivered), "delivered_to": delivered, } ) return delivered, skipped