"""Unraid servers status router.""" from __future__ import annotations import logging from typing import Any, Dict, List from fastapi import APIRouter from server.cache import cache from server.config import get_settings from server.services.mqtt_service import mqtt_service from server.services.unraid_service import ServerConfig, fetch_all_servers logger = logging.getLogger(__name__) router = APIRouter(prefix="/api", tags=["servers"]) CACHE_KEY = "servers" # --------------------------------------------------------------------------- # MQTT enrichment — overlay live system metrics from MQTT topics # --------------------------------------------------------------------------- def _enrich_from_mqtt(servers: List[Dict[str, Any]]) -> None: """Merge live CPU/RAM data from MQTT ``/system`` topics. The Unraid MQTT Agent plugin publishes JSON payloads to topics like ``unraid-daddelolymp/system`` or ``Adriahub/system`` every ~15 s. These contain live ``cpu_usage_percent``, ``ram_usage_percent``, etc. that the GraphQL API does not expose. """ store = mqtt_service.store for srv in servers: name = srv.get("name", "") if not name: continue # Try common topic patterns system_data: Dict[str, Any] | None = None for pattern in ( f"{name}/system", # "Adriahub/system" f"unraid-{name.lower()}/system", # "unraid-daddelolymp/system" f"unraid-{name}/system", # "unraid-Daddelolymp/system" ): msg = store.get(pattern) if msg is not None and isinstance(msg.payload, dict): system_data = msg.payload break if not system_data: continue # --- CPU --- cpu_pct = system_data.get("cpu_usage_percent") if cpu_pct is not None: srv["cpu"]["usage_pct"] = round(float(cpu_pct), 1) cpu_model = system_data.get("cpu_model") if cpu_model: srv["cpu"]["brand"] = cpu_model cpu_temp = system_data.get("cpu_temp_celsius") if cpu_temp is not None: srv["cpu"]["temp_c"] = cpu_temp cores = system_data.get("cpu_cores") if cores: srv["cpu"]["cores"] = cores threads = system_data.get("cpu_threads") if threads: srv["cpu"]["threads"] = threads # --- RAM --- ram_pct = system_data.get("ram_usage_percent") if ram_pct is not None: srv["ram"]["pct"] = round(float(ram_pct), 1) ram_total = system_data.get("ram_total_bytes") if ram_total: srv["ram"]["total_gb"] = round(ram_total / (1024 ** 3), 1) ram_used = system_data.get("ram_used_bytes") if ram_used: srv["ram"]["used_gb"] = round(ram_used / (1024 ** 3), 1) # --- Uptime --- uptime_secs = system_data.get("uptime_seconds") if uptime_secs: days = uptime_secs // 86400 hours = (uptime_secs % 86400) // 3600 srv["uptime"] = f"{days}d {hours}h" srv["online"] = True logger.debug( "[UNRAID] %s: MQTT enriched — CPU %.1f%% %.0f°C, RAM %.1f%%", name, srv["cpu"].get("usage_pct", 0), srv["cpu"].get("temp_c", 0) or 0, srv["ram"].get("pct", 0), ) @router.get("/servers") async def get_servers() -> Dict[str, Any]: """Return status information for all configured Unraid servers. Response shape:: { "servers": [ ... server dicts ... ] } """ # --- cache hit? ----------------------------------------------------------- cached = await cache.get(CACHE_KEY) if cached is not None: # Always overlay fresh MQTT data even on cache hits _enrich_from_mqtt(cached.get("servers", [])) return cached # --- cache miss ----------------------------------------------------------- server_configs: List[ServerConfig] = [ ServerConfig( name=srv.name, host=srv.host, api_key=srv.api_key, port=srv.port, ) for srv in get_settings().unraid_servers ] servers_data: List[Dict[str, Any]] = [] try: servers_data = await fetch_all_servers(server_configs) except Exception as exc: logger.exception("Failed to fetch Unraid server data") return { "servers": [], "error": True, "message": str(exc), } # Overlay live MQTT system metrics _enrich_from_mqtt(servers_data) payload: Dict[str, Any] = { "servers": servers_data, } await cache.set(CACHE_KEY, payload, get_settings().unraid_cache_ttl) return payload