Files
Goddess/v4/server/app/connection_manager.py
TutorialsGHG 1adec1b88f inital v4
2026-05-25 20:46:00 +02:00

145 lines
4.8 KiB
Python

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