- kern/ausreisser.py: 168 Fächer je Reihe (CPU, GPU, Speicher, Temperaturen, Netz), erst genau, ab acht Werten gleitend; auffällig ab 4 Streuungen und Mindestabstand, erst mit drei Wochen Erfahrung; Ausreißer gekappt gelernt - services/ausreisser.py: Wächter beider Rollen, gelber Hinweis nach zwei auffälligen Stunden mit Normalwert und Wochenstunde im Satz; erster Lauf lernt aus den aufbewahrten acht Tagen - Monitoring: Zeile mit Lernwoche bzw. was gerade auffällt; GET /api/ausreisser, /api/homelab/ausreisser Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
3.2 KiB
Python
69 lines
3.2 KiB
Python
"""Ausreißer ohne feste Schwellen — die Prüfung des Wächters (Ausbauplan „Später“, Punkt 22, seit 26.09.2026).
|
|
|
|
Beide Rollen: Die KI-Box lernt ihre eigene Messreihe („box“), der Homelab-Teil die des Proxmox-Hosts („pve“) und jedes
|
|
Gasts („ct-104“, „vm-106“). Das Lernen und Vergleichen steht in kern/ausreisser.py; hier kommen Gerätenamen und die
|
|
Sätze der Hinweise dazu. Der Wächter ruft pruefe_ausreisser() jede Minute; gerechnet wird nur, wenn eine Stunde neu
|
|
abgeschlossen ist. Hinweise sind gelb — etwas Ungewöhnliches, kein Ausfall; „24 h stumm“ passt, wenn es gewollt ist.
|
|
"""
|
|
|
|
import time
|
|
|
|
from kern import ausreisser as kern_ausreisser
|
|
from kern import messreihen
|
|
from kern.einstellungen import einstellungen
|
|
|
|
TITEL = {"temp_cpu": "ist seit {n} Stunden ungewöhnlich warm", "temp_gpu": "ist seit {n} Stunden ungewöhnlich warm",
|
|
"netz_rx": "hat seit {n} Stunden ungewöhnlich viel {was}", "netz_tx": "hat seit {n} Stunden ungewöhnlich viel {was}"}
|
|
|
|
|
|
def quellen(jetzt: float) -> list[str]:
|
|
if einstellungen().rolle == "box":
|
|
return ["box"]
|
|
return [q for q in messreihen.quellen(jetzt - 2 * 86400, jetzt + 1) if q == "pve" or q.startswith(("ct-", "vm-"))]
|
|
|
|
|
|
def geraetename(quelle: str) -> str:
|
|
if quelle == "box":
|
|
return "Die KI-Box"
|
|
if quelle == "pve":
|
|
return "Der Proxmox-Host"
|
|
try:
|
|
from services.homelab import apps, kanal
|
|
for g in ((kanal.bericht() or {}).get("bericht") or {}).get("gaeste") or []:
|
|
if f"{'ct' if g.get('art') == 'lxc' else 'vm'}-{g.get('vmid')}" == quelle:
|
|
app = apps.app_fuer((g.get("app") or {}).get("kennung"), g.get("name"))
|
|
return app.name if app else str(g.get("name") or quelle)
|
|
except Exception:
|
|
pass
|
|
return quelle
|
|
|
|
|
|
def satz(a: dict) -> tuple[str, str]:
|
|
anzeige, einheit, _, _ = kern_ausreisser.GROESSEN[a["name"]]
|
|
muster = TITEL.get(a["name"], "braucht seit {n} Stunden ungewöhnlich viel {was}")
|
|
titel = f"{geraetename(a['quelle'])} {muster.format(n=a['folge'], was=anzeige)}"
|
|
text = (f"{kern_ausreisser.zahl_text(a['wert'], einheit)} statt sonst etwa "
|
|
f"{kern_ausreisser.zahl_text(a['normal'], einheit)} {kern_ausreisser.fach_text(a['fach'])}. Ist das gewollt "
|
|
"(ein Update, eine Sicherung, ein Test), schalte den Hinweis stumm; sonst im Monitoring nachsehen.")
|
|
return titel, text
|
|
|
|
|
|
def pruefe_ausreisser(jetzt: float | None = None) -> list:
|
|
from services.waechter import Befund
|
|
|
|
jetzt = time.time() if jetzt is None else jetzt
|
|
daten = kern_ausreisser.lernen_und_pruefen(quellen(jetzt), jetzt)
|
|
befunde = []
|
|
for a in kern_ausreisser.auffaelligkeiten(daten):
|
|
titel, text = satz(a)
|
|
befunde.append(Befund(id=f"ausreisser:{a['quelle']}:{a['name']}", stufe="gelb", titel=titel, text=text,
|
|
quelle=a["quelle"], sofort=True))
|
|
return befunde
|
|
|
|
|
|
def lage() -> dict:
|
|
"""Für GET /api/ausreisser bzw. /api/homelab/ausreisser: Lernstand und was gerade auffällt."""
|
|
daten = kern_ausreisser.lesen()
|
|
return {**kern_ausreisser.lernstand(daten), "min_wochen": kern_ausreisser.MIN_WOCHEN,
|
|
"auffaellig": [{**a, "titel": satz(a)[0]} for a in kern_ausreisser.auffaelligkeiten(daten)]}
|