Welle 1 · Vorhersage „voll in etwa N Tagen“: Pool, NAS, Systemplatte, Container, VMs und die Platte der KI-Box
Aus dem Verlauf der Woche (Tagesmittel, Schutz gegen einmalige Sprünge, schneller werdendes Volllaufen zählt sofort): gelb unter 14, rot unter 3 Tagen. Der Wächter meldet es, auch wenn der Füllstand selbst noch unauffällig ist; Speicherkarte, Geräte-Tabelle, Instrumente der Übersicht und die KI-Box-Seite zeigen es an. Jeder Bericht des Ausführers schreibt dafür die Belegungen in die Messreihe „speicher“. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
34360c9644
commit
f7a9b8ad50
@@ -0,0 +1,158 @@
|
||||
"""Füllstands-Vorhersage „voll in etwa N Tagen“ (Ausbauplan Welle 1, seit 25.09.2026).
|
||||
|
||||
Die Schwellen bei 80 und 90 % melden sich erst, wenn es schon eng ist. Die Vorhersage schaut auf das Tempo: Wächst
|
||||
eine Belegung weiter wie zuletzt, wann ist sie voll? Gelb unter GELB_TAGE, rot unter ROT_TAGE Tagen — so bleibt Zeit,
|
||||
Platz zu schaffen, bevor etwas stillsteht.
|
||||
|
||||
Grundlage sind die 30-min-Mittel der letzten sieben Tage aus kern/messreihen.py (Belegung in %). Daraus werden
|
||||
Tagesmittel über 24-Stunden-Fenster (das jüngste endet jetzt); ein Fenster zählt ab sechs Stunden mit Messungen.
|
||||
Die Rate in Prozentpunkten am Tag:
|
||||
• Normalfall: die Steigung der Ausgleichsgeraden durch die Tagesmittel der Woche.
|
||||
• Das Wachstum stockt — der letzte Tag wuchs um weniger als ein Viertel der Wochenrate, etwa nach einem großen
|
||||
Download, der einmal kam: Dann gilt der letzte Tag (mindestens 0). Ein einzelner Sprung hält so keinen Alarm.
|
||||
• Es wird schneller — die letzten beiden Tage wuchsen beide stärker als die Woche, und in der Woche wurde nichts
|
||||
freigeräumt: Dann gilt ihr Mittel. Ein Protokoll, das gerade vollläuft, wartet so nicht eine Woche.
|
||||
Unter MIN_RATE gilt die Belegung als gleichbleibend. Ohne die beiden jüngsten Fenster und ein drittes (also rund zwei
|
||||
Tage Verlauf) oder ohne Messung in den letzten zwei Stunden gibt es keine Vorhersage.
|
||||
|
||||
„Voll“ ist nicht überall 100 %: Proxmox rechnet bei Containern und beim Host die für root reservierten Blöcke von
|
||||
ext4 (5 %) als belegt — dort ist für Dienste schon bei rund 95 % Schluss (AdGuard am 24.09.2026). Das gibt der
|
||||
Aufrufer mit (voll_bei).
|
||||
"""
|
||||
|
||||
import math
|
||||
import threading
|
||||
import time
|
||||
|
||||
from kern import messreihen
|
||||
|
||||
TAG_S = 86400
|
||||
FENSTER = 7 # Tagesfenster in der Woche
|
||||
MIN_WERTE = 12 # 30-min-Mittel je Fenster: sechs Stunden mit Messungen
|
||||
FRISCH_S = 2 * 3600 # so alt darf die jüngste Messung höchstens sein
|
||||
MIN_RATE = 0.05 # Prozentpunkte am Tag; darunter „gleichbleibend“
|
||||
STOCKT = 0.25 # der letzte Tag wuchs um weniger als dieser Anteil der Wochenrate
|
||||
FREIGERAEUMT = -0.5 # ein Tag, der um mehr als so viele Prozentpunkte sank, hat freigeräumt
|
||||
GELB_TAGE, ROT_TAGE = 14, 3
|
||||
MERKEN_S = 600 # je Reihe höchstens alle zehn Minuten neu rechnen (der Wächter fragt jede Minute)
|
||||
VOLL_EXT4 = 95.0 # Container und Proxmox-Host: siehe oben
|
||||
|
||||
_lock = threading.Lock()
|
||||
_merk: dict[tuple[str, str, float], tuple[float, dict]] = {}
|
||||
|
||||
|
||||
def tagesmittel(reihe: list, jetzt: float) -> dict[int, float]:
|
||||
"""Fenster k (0 = die letzten 24 Stunden, 1 = die 24 davor …) → Mittel der Werte darin. Nur Fenster mit
|
||||
mindestens MIN_WERTE Werten. reihe: [[t, wert], …] wie messreihen.lesen() sie liefert."""
|
||||
summen: dict[int, list[float]] = {}
|
||||
for t, wert in reihe:
|
||||
if not messreihen.ist_zahl(wert):
|
||||
continue
|
||||
k = int((jetzt - t) // TAG_S)
|
||||
if 0 <= k < FENSTER:
|
||||
summe = summen.setdefault(k, [0.0, 0])
|
||||
summe[0] += wert
|
||||
summe[1] += 1
|
||||
return {k: s[0] / s[1] for k, s in summen.items() if s[1] >= MIN_WERTE}
|
||||
|
||||
|
||||
def _steigung(punkte: list[tuple[float, float]]) -> float:
|
||||
"""Steigung der Ausgleichsgeraden (kleinste Quadrate)."""
|
||||
mx = sum(x for x, _ in punkte) / len(punkte)
|
||||
my = sum(y for _, y in punkte) / len(punkte)
|
||||
sxx = sum((x - mx) ** 2 for x, _ in punkte)
|
||||
return sum((x - mx) * (y - my) for x, y in punkte) / sxx if sxx else 0.0
|
||||
|
||||
|
||||
def vorhersage(reihe: list, jetzt: float, voll_bei: float = 100.0) -> dict:
|
||||
"""{"tage": Tage bis voll oder None, "rate": Prozentpunkte am Tag oder None, "anteil": jüngstes Mittel in % oder
|
||||
None, "grund": None | "verlauf" (zu wenig Verlauf) | "gleich" (wächst nicht)}."""
|
||||
werte = [(t, w) for t, w in reihe if messreihen.ist_zahl(w)]
|
||||
if not werte or jetzt - werte[-1][0] > FRISCH_S:
|
||||
return {"tage": None, "rate": None, "anteil": None, "grund": "verlauf"}
|
||||
anteil = float(werte[-1][1])
|
||||
mittel = tagesmittel(werte, jetzt)
|
||||
if 0 not in mittel or 1 not in mittel or len(mittel) < 3:
|
||||
return {"tage": None, "rate": None, "anteil": anteil, "grund": "verlauf"}
|
||||
woche = _steigung([(-k, m) for k, m in mittel.items()])
|
||||
zuletzt = mittel[0] - mittel[1]
|
||||
davor = mittel[1] - mittel[2] if 2 in mittel else None
|
||||
schritte = [mittel[k] - mittel[k + 1] for k in range(FENSTER - 1) if k in mittel and k + 1 in mittel]
|
||||
rate = woche
|
||||
if zuletzt < STOCKT * woche:
|
||||
rate = max(zuletzt, 0.0)
|
||||
elif davor is not None and zuletzt > woche and davor > woche and min(schritte) > FREIGERAEUMT:
|
||||
rate = (zuletzt + davor) / 2
|
||||
if rate < MIN_RATE:
|
||||
return {"tage": None, "rate": round(rate, 2), "anteil": anteil, "grund": "gleich"}
|
||||
return {"tage": round(max(0.0, (voll_bei - anteil) / rate), 1), "rate": round(rate, 2), "anteil": anteil,
|
||||
"grund": None}
|
||||
|
||||
|
||||
def aus_messreihe(quelle: str, name: str, voll_bei: float = 100.0, jetzt: float | None = None) -> dict:
|
||||
"""Die Vorhersage für eine Reihe aus kern/messreihen.py. Ohne jetzt (Betrieb) je Reihe höchstens alle MERKEN_S
|
||||
neu gerechnet; mit jetzt (Tests) immer frisch."""
|
||||
betrieb = jetzt is None
|
||||
jetzt = time.time() if jetzt is None else jetzt
|
||||
schluessel = (quelle, name, voll_bei)
|
||||
if betrieb:
|
||||
with _lock:
|
||||
gemerkt = _merk.get(schluessel)
|
||||
if gemerkt and 0 <= jetzt - gemerkt[0] < MERKEN_S:
|
||||
return gemerkt[1]
|
||||
ergebnis = vorhersage(messreihen.lesen(quelle, "7d", [name], jetzt)["reihen"][name], jetzt, voll_bei)
|
||||
if betrieb:
|
||||
with _lock:
|
||||
_merk[schluessel] = (jetzt, ergebnis)
|
||||
return ergebnis
|
||||
|
||||
|
||||
def stufe(v: dict | None) -> str:
|
||||
"""„rot“ unter ROT_TAGE, „gelb“ unter GELB_TAGE Tagen bis voll, sonst „ok“."""
|
||||
tage = (v or {}).get("tage")
|
||||
if tage is None:
|
||||
return "ok"
|
||||
return "rot" if tage < ROT_TAGE else "gelb" if tage < GELB_TAGE else "ok"
|
||||
|
||||
|
||||
def in_tagen(tage: float) -> str:
|
||||
"""„in weniger als einem Tag“, „in etwa 9 Tagen“, „in etwa 5 Wochen“ … — für Hinweise."""
|
||||
if tage < 1:
|
||||
return "in weniger als einem Tag"
|
||||
if tage < 1.5:
|
||||
return "in etwa einem Tag"
|
||||
if tage < 21:
|
||||
return f"in etwa {round(tage)} Tagen"
|
||||
if tage < 90:
|
||||
return f"in etwa {round(tage / 7)} Wochen"
|
||||
return f"in etwa {round(tage / 30)} Monaten" if tage < 365 else "frühestens in einem Jahr"
|
||||
|
||||
|
||||
def tempo(v: dict) -> str:
|
||||
"""„Die Belegung wächst um etwa 1,2 % am Tag (jetzt 78 %).“"""
|
||||
rate = f"{v['rate']:.1f}".replace(".", ",")
|
||||
return f"Die Belegung wächst um etwa {rate} % am Tag (jetzt {round(v['anteil'])} %)."
|
||||
|
||||
|
||||
def bis_voll(v: dict | None) -> float:
|
||||
"""Tage bis voll; unendlich, wenn es keine Vorhersage gibt."""
|
||||
return v["tage"] if v and v.get("tage") is not None else math.inf
|
||||
|
||||
|
||||
IM_TEXT_TAGE = 60 # weiter weg sagt ein Hinweis zum Füllstand nicht mehr dazu, wann es voll wäre
|
||||
|
||||
|
||||
def dazu(v: dict | None) -> str:
|
||||
"""„ Bei diesem Tempo voll in etwa 9 Tagen.“ — der Zusatz für Hinweise, die schon der Füllstand auslöst."""
|
||||
tage = bis_voll(v)
|
||||
return f" Bei diesem Tempo voll {in_tagen(tage)}." if tage < IM_TEXT_TAGE else ""
|
||||
|
||||
|
||||
RANG = {"ok": 0, "gelb": 1, "rot": 2}
|
||||
|
||||
|
||||
def schlimmer(stufe_jetzt: str | None, v: dict | None) -> tuple[str, bool]:
|
||||
"""Die ernstere Stufe aus Füllstand (None/„ok“ = unauffällig) und Vorhersage — und ob die Vorhersage sie bestimmt
|
||||
(dann gehört „voll in etwa N Tagen“ in den Titel)."""
|
||||
jetzt, kuenftig = stufe_jetzt or "ok", stufe(v)
|
||||
return (kuenftig, True) if RANG[kuenftig] > RANG.get(jetzt, 0) else (jetzt, False)
|
||||
Reference in New Issue
Block a user