inital v4
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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
|
||||
Reference in New Issue
Block a user