monitoring: Messwerte mit Verlauf fuer KI-Box und Homelab

- kern/messreihen.py: Minutenwerte als JSON-Zeilen je Quelle und Tag unter
  <Datenordner>/mc2-messwerte/, Anhaengen unter flock, Aufraeumen nach 8 Tagen,
  Lesen fuer 1h/24h/7d (60 s, 5-min- und 30-min-Mittel), Luecken bleiben null,
  kaputte Zeilen werden uebersprungen, Zaehler-Raten ohne Spruenge
- KI-Box: Taktgeber im Steward (services/messwerte.py) schreibt jede Minute
  cpu, ram, gpu, Temperaturen, platte, Netz in Bytes/s und Tokens pro Minute;
  GET /api/messwerte (nur Rolle box)
- Homelab: Der Ausfuehrer schickt jede Minute in einem eigenen Faden Host-Werte
  aus /proc und die Gaeste aus einem pvesh-Aufruf an
  POST /api/homelab/ausfuehrer/messwerte; services/homelab/messwerte.py rechnet
  die Zaehler in Bytes/s um (Neustarts und Spruenge ergeben null),
  GET /api/homelab/messwerte liefert Host und Gaeste wie im Inventar
- Waechter (homelab): gelb, wenn der Host 10 Minuten ueber 95 % RAM oder 90 °C liegt
- Aufraeumen der Modell-Platte bietet mc2-messwerte nie zum Loeschen an

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 22:41:53 +02:00
co-authored by Claude Opus 5.5
parent bf1153cb66
commit 704c21d839
12 changed files with 1398 additions and 3 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ from config import MODELS_DIR
from services import llamaswap
AKTIVE_ROLLEN = {"hermes", "fast", "coder", "heavy", "vision", "coder-bild", "embed", "reranker"}
SYSTEM_ORDNER = {"mc2-backups", "radar", "pruefstand-cache", "lost+found"}
SYSTEM_ORDNER = {"mc2-backups", "mc2-messwerte", "radar", "pruefstand-cache", "lost+found"}
AUF_DER_BOX = os.name == "posix" # auf dem Windows-PC (Entwicklung) wird nie gelöscht
# Was der Nutzer über bekannte Ordner wissen sollte, bevor er löscht (Stand 24.09.2026).
+247
View File
@@ -0,0 +1,247 @@
"""Messwerte des Homelabs mit Verlauf (Monitoring, seit 24.09.2026).
Der Ausführer auf dem Proxmox-Host schickt jede Minute POST /api/homelab/ausfuehrer/messwerte (eigener Faden, unabhängig
vom 10-min-Bericht und von Aufträgen):
{"zeit": Unix-Sekunden der Messung,
"host": {"cpu": 0–1 (Mittel der Minute), "speicher": {"belegt", "gesamt"}, "rootfs": {"belegt", "gesamt"}, "load1",
"uptime", "temp_cpu" (°C oder null), "netz": {"rx", "tx"} (kumulativ, Bytes)},
"gaeste": [{"vmid", "art" (lxc|qemu), "name", "etiketten", "status", "cpu" (0–1 der eigenen Kerne), "maxcpu", "mem",
"maxmem", "disk", "maxdisk", "netin", "netout" (kumulativ, Bytes), "uptime"}]}
Hier wird daraus je Gerät ein Punkt über kern/messreihen.py (Quellen „pve“, „ct-<vmid>“, „vm-<vmid>“ wie im Inventar):
CPU, RAM und Platte in %, Netz in Bytes/s aus der Differenz zum vorigen Zählerstand. Ist ein Zähler kleiner geworden
oder die Laufzeit (Neustart des Gasts oder Hosts), fehlt der vorige Stand (Neustart dieses Teils) oder liegt er mehr als
fünf Minuten zurück, bleibt die Rate dieser Minute null. Gestoppte Gäste haben keine Messwerte (null), keine Nullen.
Der eigene Container (Etikett „mc2“) wird wie im Inventar nicht erfasst. Die Platte von VMs sieht Proxmox nicht (null).
GET /api/homelab/messwerte liefert je Gerät die Reihen (1h, 24h, 7d) und den letzten Punkt; pruefe_host() meldet dem
Wächter, wenn der Host zehn Minuten lang über 95 % RAM oder 90 °C liegt.
"""
import os
import re
import threading
import time
from kern import messreihen
from services.homelab import apps, inventar, kanal
HOST = "pve"
_GAST = re.compile(r"(ct|vm)-\d+")
ZEITRAEUME = messreihen.ZEITRAEUME
HOST_REIHEN = ("cpu", "ram", "platte", "netz_rx", "netz_tx", "temp_cpu")
GAST_REIHEN = ("cpu", "ram", "platte", "netz_rx", "netz_tx")
AKTUELL = ("cpu", "ram", "ram_used", "ram_total", "platte", "netz_rx", "netz_tx", "temp_cpu", "load1", "uptime_s")
AKTUELL_MAX_S = 180 # älter ist der letzte Punkt nicht „aktuell“ (der Ausführer schickt gerade nichts)
ZEIT_TOLERANZ_S = 120 # die Messzeit des Ausführers gilt, wenn sie so nah an der eigenen Uhr liegt
# Wächter: der Host zehn Minuten lang über diesen Schwellen → gelber Hinweis.
WACHE_S = 600
WACHE_MIN_PUNKTE = 8 # so viele Minutenpunkte müssen in den zehn Minuten liegen (ein, zwei dürfen fehlen)
RAM_GELB = float(os.environ.get("MC_WAECHTER_HOST_RAM", "95"))
TEMP_GELB = float(os.environ.get("MC_WAECHTER_HOST_TEMP", "90"))
_lock = threading.Lock()
# Vorige Zählerstände je Gerät (für die Raten) und die Geräte der letzten Meldung (für Namen und die Geräteliste).
# Nur im Speicher: Nach einem Neustart dieses Teils fehlt für eine Minute die Netz-Rate, sonst nichts.
_zustand: dict = {"zaehler": {}, "geraete": {}}
def _zahl(wert: object) -> float | None:
return float(wert) if messreihen.ist_zahl(wert) else None
def _ganz(wert: object) -> int | None:
return int(wert) if messreihen.ist_zahl(wert) else None
def _prozent(anteil: object) -> float | None:
"""0–1 → %, eine Nachkommastelle, auf 0–100 begrenzt."""
return round(min(100.0, max(0.0, float(anteil) * 100)), 1) if messreihen.ist_zahl(anteil) else None
def _anteil(teil: object, ganz: object) -> float | None:
if not (messreihen.ist_zahl(teil) and messreihen.ist_zahl(ganz)) or float(ganz) <= 0:
return None
return _prozent(float(teil) / float(ganz))
def _dict(wert: object) -> dict:
return wert if isinstance(wert, dict) else {}
def gast_id(gast: dict) -> str | None:
"""ct-<vmid> bzw. vm-<vmid> wie im Inventar; None bei allem, was kein Gast ist."""
vmid, art = gast.get("vmid"), gast.get("art")
if not isinstance(vmid, int) or isinstance(vmid, bool) or not 100 <= vmid <= 999_999_999:
return None
return {"lxc": f"ct-{vmid}", "qemu": f"vm-{vmid}"}.get(art)
def _art(geraet_id: str) -> str:
return "proxmox-host" if geraet_id == HOST else "container" if geraet_id.startswith("ct-") else "vm"
# --- Zähler → Bytes/s ------------------------------------------------------------------------------
def raten(vorher: dict | None, jetzt: dict) -> tuple[int | None, int | None]:
"""Empfangen und gesendet in Bytes/s zwischen zwei Zählerständen {"t", "rx", "tx", "uptime", "laeuft"}. None, wenn
es keinen vorigen Stand gibt, einer der beiden nicht lief, die Laufzeit kleiner wurde (neu gestartet: die Zähler
begannen von vorn) oder messreihen.rate() den Zuwachs verwirft (Zähler kleiner, Lücke, Sprung)."""
if not vorher or not vorher.get("laeuft") or not jetzt.get("laeuft"):
return None, None
alt_lauf, neu_lauf = vorher.get("uptime"), jetzt.get("uptime")
if messreihen.ist_zahl(alt_lauf) and messreihen.ist_zahl(neu_lauf) and neu_lauf < alt_lauf:
return None, None
dt = jetzt["t"] - vorher["t"]
rx, tx = messreihen.rate(vorher.get("rx"), jetzt.get("rx"), dt), messreihen.rate(vorher.get("tx"), jetzt.get("tx"), dt)
return (None if rx is None else round(rx)), (None if tx is None else round(tx))
def _zaehlen(geraet_id: str, t: float, rx: object, tx: object, uptime: object, laeuft: bool) -> tuple:
"""Den neuen Zählerstand merken und die Raten gegen den vorigen liefern (unter _lock aufrufen)."""
stand = {"t": t, "rx": rx, "tx": tx, "uptime": uptime, "laeuft": laeuft}
ergebnis = raten(_zustand["zaehler"].get(geraet_id), stand)
_zustand["zaehler"][geraet_id] = stand
return ergebnis
def _host_punkt(host: dict, t: float) -> dict:
speicher, rootfs, netz = _dict(host.get("speicher")), _dict(host.get("rootfs")), _dict(host.get("netz"))
rx, tx = _zaehlen(HOST, t, netz.get("rx"), netz.get("tx"), host.get("uptime"), True)
temp, last = _zahl(host.get("temp_cpu")), _zahl(host.get("load1"))
return {"cpu": _prozent(host.get("cpu")),
"ram": _anteil(speicher.get("belegt"), speicher.get("gesamt")),
"ram_used": _ganz(speicher.get("belegt")), "ram_total": _ganz(speicher.get("gesamt")),
"platte": _anteil(rootfs.get("belegt"), rootfs.get("gesamt")),
"netz_rx": rx, "netz_tx": tx,
"temp_cpu": None if temp is None else round(temp, 1),
"load1": None if last is None else round(last, 2),
"uptime_s": _ganz(host.get("uptime"))}
def _gast_punkt(geraet_id: str, gast: dict, t: float) -> dict:
laeuft = gast.get("status") == "running"
rx, tx = _zaehlen(geraet_id, t, gast.get("netin"), gast.get("netout"), gast.get("uptime"), laeuft)
if not laeuft:
return {"cpu": None, "ram": None, "ram_used": None, "ram_total": _ganz(gast.get("maxmem")), "platte": None,
"netz_rx": None, "netz_tx": None, "uptime_s": _ganz(gast.get("uptime"))}
# Proxmox rechnet die CPU eines Gasts schon auf seine eigenen Kerne (1,0 = alle voll). Die Belegung der Platte
# kennt es nur bei Containern; bei VMs steht dort immer 0.
disk = gast.get("disk")
platte = _anteil(disk, gast.get("maxdisk")) if gast.get("art") == "lxc" and messreihen.ist_zahl(disk) and disk else None
return {"cpu": _prozent(gast.get("cpu")),
"ram": _anteil(gast.get("mem"), gast.get("maxmem")),
"ram_used": _ganz(gast.get("mem")), "ram_total": _ganz(gast.get("maxmem")),
"platte": platte, "netz_rx": rx, "netz_tx": tx, "uptime_s": _ganz(gast.get("uptime"))}
def annehmen(daten: dict, jetzt: float | None = None) -> int:
"""Einen Minutenpunkt des Ausführers speichern. Rückgabe: für wie viele Geräte. Kaputte Einträge fallen still weg."""
jetzt = time.time() if jetzt is None else jetzt
zeit = daten.get("zeit")
t = float(zeit) if messreihen.ist_zahl(zeit) and abs(float(zeit) - jetzt) <= ZEIT_TOLERANZ_S else jetzt
punkte: list[tuple[str, dict]] = []
with _lock:
if isinstance(daten.get("host"), dict):
punkte.append((HOST, _host_punkt(daten["host"], t)))
gaeste = daten.get("gaeste") if isinstance(daten.get("gaeste"), list) else []
geraete = {}
for gast in gaeste:
if not isinstance(gast, dict) or inventar.EIGENES_ETIKETT in (gast.get("etiketten") or []):
continue
if not (gid := gast_id(gast)):
continue
punkte.append((gid, _gast_punkt(gid, gast, t)))
geraete[gid] = str(gast.get("name") or gid)
if gaeste:
_zustand["geraete"] = geraete
for quelle, werte in punkte:
messreihen.schreiben(quelle, werte, t)
return len(punkte)
# --- Lesen ------------------------------------------------------------------------------------------
def _geraete(von: float, bis: float) -> list[dict]:
"""Die Geräte wie im Inventar: der Host und die Gäste aus dem letzten Bericht (Namen aus apps.py), dazu Gäste, die
erst die Messwerte kennen. Ohne beides: alle Quellen mit Dateien im Zeitraum."""
eingang = kanal.bericht()
geraete: dict[str, dict] = {}
eigene: set[str] = set()
if eingang:
geraete[HOST] = {"id": HOST, "name": "Proxmox-Host", "art": "proxmox-host"}
for gast in (eingang or {}).get("bericht", {}).get("gaeste") or []:
if not (gid := gast_id(gast)):
continue
if inventar.EIGENES_ETIKETT in (gast.get("etiketten") or []):
eigene.add(gid)
continue
app = apps.app_fuer((gast.get("app") or {}).get("kennung"), gast.get("name"))
geraete[gid] = {"id": gid, "name": app.name if app else str(gast.get("name") or gid), "art": _art(gid)}
with _lock:
gemeldet = dict(_zustand["geraete"])
host_gemeldet = HOST in _zustand["zaehler"]
if host_gemeldet:
geraete.setdefault(HOST, {"id": HOST, "name": "Proxmox-Host", "art": "proxmox-host"})
for gid, name in gemeldet.items():
if gid not in geraete and gid not in eigene:
app = apps.app_fuer(None, name)
geraete[gid] = {"id": gid, "name": app.name if app else name, "art": _art(gid)}
if not geraete:
for quelle in messreihen.quellen(von, bis):
if quelle == HOST or _GAST.fullmatch(quelle):
geraete[quelle] = {"id": quelle, "name": "Proxmox-Host" if quelle == HOST else quelle,
"art": _art(quelle)}
# Reihenfolge wie im Inventar: der Host, dann die Gäste nach Nummer.
return sorted(geraete.values(), key=lambda g: (g["id"] != HOST, int(g["id"].split("-")[1]) if g["id"] != HOST else 0))
def abfrage(zeitraum: str, jetzt: float | None = None) -> dict:
"""Für GET /api/homelab/messwerte: {"zeitraum", "schritt_s", "geraete": [{"id", "name", "art", "reihen",
"aktuell"}]}. temp_cpu gibt es als Reihe nur beim Host; in „aktuell“ stehen temp_cpu und load1 bei Gästen auf null."""
dauer, schritt = messreihen.zeitraum_pruefen(zeitraum)
jetzt = time.time() if jetzt is None else jetzt
geraete = []
for geraet in _geraete(jetzt - dauer, jetzt):
host = geraet["id"] == HOST
daten = messreihen.lesen(geraet["id"], zeitraum, HOST_REIHEN if host else GAST_REIHEN, jetzt)
letzter = messreihen.letzter_punkt(geraet["id"], jetzt, AKTUELL_MAX_S) or {}
aktuell = {name: letzter.get(name) for name in AKTUELL}
if not host:
aktuell["temp_cpu"] = aktuell["load1"] = None
geraete.append({**geraet, "reihen": daten["reihen"], "aktuell": aktuell})
return {"zeitraum": zeitraum, "schritt_s": schritt, "geraete": geraete}
# --- Wächter (Rolle homelab, registriert in services/waechter.py) -------------------------------------------
def _gb(anzahl: object) -> str:
return f"{float(anzahl) / 1e9:.1f}".replace(".", ",") if messreihen.ist_zahl(anzahl) else "?"
def pruefe_host(jetzt: float | None = None) -> list:
"""Gelb, wenn der Proxmox-Host zehn Minuten lang über RAM_GELB % Arbeitsspeicher oder TEMP_GELB °C CPU-Temperatur
liegt. Ohne genug Messungen (Ausführer schweigt, gerade erst gestartet) kein Befund — das Schweigen meldet
pruefe_ausfuehrer()."""
from services.waechter import Befund # der Wächter registriert diese Prüfung, also erst hier
jetzt = time.time() if jetzt is None else jetzt
punkte = messreihen.punkte(HOST, jetzt - WACHE_S, jetzt + 1)
befunde = []
ram = [p["ram"] for p in punkte if messreihen.ist_zahl(p.get("ram"))]
if len(ram) >= WACHE_MIN_PUNKTE and min(ram) > RAM_GELB:
letzter = next(p for p in reversed(punkte) if messreihen.ist_zahl(p.get("ram")))
befunde.append(Befund(
id="host-ram", stufe="gelb", titel=f"Proxmox-Host: Arbeitsspeicher seit 10 Minuten über {RAM_GELB:.0f} %",
text=(f"Zuletzt {ram[-1]:.0f} % belegt ({_gb(letzter.get('ram_used'))} von {_gb(letzter.get('ram_total'))} "
"GB). Wird es noch enger, beendet der Kernel Prozesse, auch in den Gästen. Die Messwerte zeigen, "
"welcher Gast wie viel braucht; einem davon weniger Speicher geben oder ihn stoppen."),
quelle=HOST, sofort=True))
temp = [p["temp_cpu"] for p in punkte if messreihen.ist_zahl(p.get("temp_cpu"))]
if len(temp) >= WACHE_MIN_PUNKTE and min(temp) > TEMP_GELB:
befunde.append(Befund(
id="host-temp", stufe="gelb", titel=f"Proxmox-Host: CPU seit 10 Minuten über {TEMP_GELB:.0f} °C",
text=(f"Zuletzt {temp[-1]:.0f} °C. So heiß drosselt sich die CPU, und auf Dauer leidet die Hardware. "
"Lüfter und Luftwege prüfen (Staub) und in den Messwerten nachsehen, welcher Gast die Last macht."),
quelle=HOST, sofort=True))
return befunde
+184
View File
@@ -0,0 +1,184 @@
"""Messwerte der KI-Box mit Verlauf (Monitoring, seit 24.09.2026).
Der Ereignisstrom (/api/stream) zeigt nur den Augenblick. Für den Verlauf schreibt der Steward (Rolle box, eigener
Prozess, übersteht MC2-Neustarts) jede Minute zur halben Minute einen Punkt über kern/messreihen.py (Quelle „box“):
cpu Mittel der Minute in % (Rechnung wie psutil.cpu_percent(interval=None), aber aus eigenen
cpu_times-Ständen: psutil merkt sich den letzten Aufruf je Thread, und die Messung läuft in wechselnden
Threads des Event-Loops)
ram, platte Belegung in % beim Messen (platte = das Modell-Laufwerk, wie im Cockpit)
gpu Mittel der Proben alle PROBE_S Sekunden in %: gpu_busy_percent zeigt nur den Augenblick, eine Probe je
Minute träfe die kurzen Antworten der Modelle kaum
temp_cpu/_gpu Mittel derselben Proben in °C
netz_rx/_tx Bytes/s über die Minute, alle Schnittstellen außer lo
tokens Prompt- plus Antwort-Tokens pro Minute (Differenz der Gesamtzähler aus services.token_stats)
Ein Zähler, der kleiner wird (Neustart des Gateways, neue Token-Datei), ergibt für diese Minute keine Rate (null).
GET /api/messwerte (routers/messwerte.py) liefert daraus die Reihen für 1h, 24h und 7d und den letzten Punkt.
"""
import asyncio
import logging
import os
import time
from collections.abc import Callable
import psutil
from config import MODELS_DIR
from kern import messreihen
from services import system, token_stats
log = logging.getLogger(__name__)
QUELLE = "box"
REIHEN = ("cpu", "ram", "gpu", "temp_cpu", "temp_gpu", "platte", "netz_rx", "netz_tx", "tokens")
# Ein Trocken- oder Probelauf neben dem echten Steward schreibt nicht mit (sonst stünden zwei Punkte je Minute da).
ENABLED = (os.environ.get("MC_MESSWERTE_ENABLED", "1") != "0" and os.environ.get("MC_WAECHTER_TROCKEN", "") != "1"
and os.environ.get("MC_PROBELAUF", "") != "1")
PROBE_S = float(os.environ.get("MC_MESSWERTE_PROBE_S", "5"))
PHASE_S = 30 # gemessen wird zur halben Minute: jeder 60-s-Schritt bekommt genau einen Punkt
AKTUELL_MAX_S = 180 # älter ist der letzte Punkt nicht „aktuell“ (der Steward schreibt gerade nicht)
# --- Rohwerte ------------------------------------------------------------------------------------
def _cpu_zeiten() -> tuple[float, float] | None:
"""(beschäftigt, gesamt) in Sekunden seit dem Start — gezählt wie psutil.cpu_percent (guest steckt in user)."""
try:
z = psutil.cpu_times()
except Exception:
return None
gesamt = sum(z) - getattr(z, "guest", 0.0) - getattr(z, "guest_nice", 0.0)
return gesamt - z.idle - getattr(z, "iowait", 0.0), gesamt
def cpu_prozent(vorher: tuple[float, float] | None, jetzt: tuple[float, float] | None) -> float | None:
if not vorher or not jetzt or jetzt[1] - vorher[1] <= 0:
return None
anteil = (jetzt[0] - vorher[0]) / (jetzt[1] - vorher[1])
return round(min(100.0, max(0.0, anteil * 100)), 1)
def _netz() -> tuple[int, int] | None:
"""Empfangene und gesendete Bytes aller Schnittstellen außer lo (Gesamtzähler)."""
try:
zaehler = psutil.net_io_counters(pernic=True)
except Exception:
return None
return (sum(z.bytes_recv for name, z in zaehler.items() if name != "lo"),
sum(z.bytes_sent for name, z in zaehler.items() if name != "lo"))
def _tokens() -> int | None:
"""Prompt- plus Antwort-Tokens seit Beginn der Zählung (schreibt der Gateway-Prozess)."""
try:
stand = token_stats.get_stats()
return int(stand.get("prompt_tokens") or 0) + int(stand.get("completion_tokens") or 0)
except Exception:
return None
def _platte() -> float | None:
try:
return round(psutil.disk_usage(str(MODELS_DIR) if MODELS_DIR.exists() else os.getcwd()).percent, 1)
except Exception:
return None
def _mittel(werte: list[float]) -> float | None:
return round(sum(werte) / len(werte), 1) if werte else None
def _runden(wert: float | None, stellen: int | None = None) -> float | int | None:
return None if wert is None else round(wert, stellen)
# --- Die Minute --------------------------------------------------------------------------------------
class Messer:
"""Was zwischen zwei Minutenpunkten mitläuft: die Stände der Zähler und die Proben."""
def __init__(self, uhr: Callable[[], float] = time.monotonic) -> None:
self._uhr = uhr
self._seit = uhr()
self._cpu = _cpu_zeiten()
self._netz = _netz()
self._tokens = _tokens()
self._proben: dict[str, list[float]] = {"gpu": [], "temp_cpu": [], "temp_gpu": []}
def probe(self) -> None:
"""GPU-Last und Temperaturen einmal ablesen (sysfs, Bruchteile einer Millisekunde)."""
gpu = system._gpu_sysfs() or {}
temp = system._temps() or {}
for name, wert in (("gpu", gpu.get("busy_percent")), ("temp_cpu", temp.get("cpu")),
("temp_gpu", temp.get("gpu"))):
if messreihen.ist_zahl(wert):
self._proben[name].append(float(wert))
def punkt(self) -> dict[str, float | int | None]:
"""Der Punkt dieser Minute; danach beginnt die nächste."""
self.probe()
jetzt = self._uhr()
dt = jetzt - self._seit
cpu, netz, tokens = _cpu_zeiten(), _netz(), _tokens()
try:
ram = round(psutil.virtual_memory().percent, 1)
except Exception:
ram = None
tok_s = messreihen.rate(self._tokens, tokens, dt)
werte = {
"cpu": cpu_prozent(self._cpu, cpu),
"ram": ram,
"gpu": _mittel(self._proben["gpu"]),
"temp_cpu": _mittel(self._proben["temp_cpu"]),
"temp_gpu": _mittel(self._proben["temp_gpu"]),
"platte": _platte(),
"netz_rx": _runden(messreihen.rate(self._netz[0], netz[0], dt)) if self._netz and netz else None,
"netz_tx": _runden(messreihen.rate(self._netz[1], netz[1], dt)) if self._netz and netz else None,
"tokens": None if tok_s is None else round(tok_s * 60, 1),
}
self._seit, self._cpu, self._netz, self._tokens = jetzt, cpu, netz, tokens
for proben in self._proben.values():
proben.clear()
return werte
def naechste_messung(jetzt: float) -> float:
"""Die nächste halbe Minute, mindestens eine Sekunde entfernt."""
ziel = jetzt - jetzt % 60 + PHASE_S
return ziel if ziel > jetzt + 1 else ziel + 60
async def messwerte_loop() -> None:
"""Im mc2-steward (Rolle box): Proben alle PROBE_S Sekunden, ein Punkt je Minute. Fehler kosten einen Punkt, nie
die Schleife."""
messer = await asyncio.to_thread(Messer)
naechste = naechste_messung(time.time())
log.info("messwerte: aktiv (ein Punkt je Minute nach %s, Proben alle %ss)", messreihen.ordner(), PROBE_S)
while True:
await asyncio.sleep(max(0.2, min(PROBE_S, naechste - time.time())))
try:
if time.time() >= naechste:
werte = await asyncio.to_thread(messer.punkt)
await asyncio.to_thread(messreihen.schreiben, QUELLE, werte)
naechste = naechste_messung(time.time())
else:
await asyncio.to_thread(messer.probe)
except Exception:
log.warning("messwerte: Messung fehlgeschlagen", exc_info=True)
naechste = naechste_messung(time.time())
# --- Lesen (MC2-Prozess) -------------------------------------------------------------------------------
ZEITRAEUME = messreihen.ZEITRAEUME
def abfrage(zeitraum: str, jetzt: float | None = None) -> dict:
"""Für GET /api/messwerte: {"zeitraum", "schritt_s", "reihen": {…}, "aktuell": {…}}. aktuell ist der letzte
Minutenpunkt, solange er höchstens AKTUELL_MAX_S alt ist, sonst lauter null."""
jetzt = time.time() if jetzt is None else jetzt
daten = messreihen.lesen(QUELLE, zeitraum, REIHEN, jetzt)
letzter = messreihen.letzter_punkt(QUELLE, jetzt, AKTUELL_MAX_S) or {}
return {**daten, "aktuell": {name: letzter.get(name) for name in REIHEN}}
+2
View File
@@ -597,6 +597,8 @@ PRUEFUNGEN = {
"homelab": (pruefe_platte, pruefe_partner, pruefe_ausfuehrer, pruefe_gaeste, pruefe_gast_platten),
}[ROLLE]
PRUEFUNGEN += (__import__("services.homelab.pflege").homelab.pflege.pruefe_paketlisten,) if ROLLE == "homelab" else ()
# Messwerte des Proxmox-Hosts (seit 24.09.2026): zehn Minuten über 95 % RAM oder 90 °C = gelb.
PRUEFUNGEN += (__import__("services.homelab.messwerte").homelab.messwerte.pruefe_host,) if ROLLE == "homelab" else ()
BETREFF_PROBLEM, BETREFF_OK = {"box": ("[Box-Problem]", "[Box wieder ok]"),
"homelab": ("[Homelab-Problem]", "[Homelab wieder ok]")}[ROLLE]