"""Füllstands-Vorhersage „voll in etwa N Tagen“ (kern/prognose.py, Ausbauplan Welle 1, 25.09.2026): die Rechnung auf künstlichen Verläufen (stetig, ein Sprung, plötzlich schneller, zu wenig Verlauf) und wie Speicherkarte und Wächter sie nutzen.""" from types import SimpleNamespace import pytest from kern import einstellungen as einstellungen_mod from kern import messreihen, prognose JETZT = 1_790_340_000.0 # 25.09.2026 ~14:40 TAG = 86400 def _reihe(wert_bei, tage: float = 7, alter_ab: float = 0) -> list[list]: """30-min-Raster wie messreihen.lesen("7d"): [[t, wert], …], ältester zuerst; wert_bei(alter in Tagen).""" schritte = int(tage * TAG // 1800) return [[JETZT - alter_ab - i * 1800, wert_bei((alter_ab + i * 1800) / TAG)] for i in reversed(range(schritte))] def test_stetiges_wachstum(): v = prognose.vorhersage(_reihe(lambda alter: 57 - alter), JETZT) # 1 % am Tag, jetzt 57 % assert v == {"tage": 43.0, "rate": 1.0, "anteil": 57.0, "grund": None} assert prognose.stufe(v) == "ok" assert prognose.vorhersage(_reihe(lambda alter: 57 - alter), JETZT, voll_bei=95)["tage"] == 38.0 def test_ein_einmaliger_sprung_haelt_keinen_alarm(): """Ein großer Download vor zweieinhalb Tagen, seitdem nichts: Die Woche steigt, der letzte Tag nicht.""" v = prognose.vorhersage(_reihe(lambda alter: 50 if alter < 2.5 else 40), JETZT) assert (v["grund"], v["tage"], v["rate"]) == ("gleich", None, 0.0) def test_ploetzlich_schneller_wartet_nicht_eine_woche(): """Fünf Tage gleich, dann 5 % am Tag: Die Woche allein sähe rund 1 % am Tag (voll in 30 Tagen).""" v = prognose.vorhersage(_reihe(lambda alter: 60 + 5 * max(0.0, 2 - alter)), JETZT) assert v["rate"] == pytest.approx(3.75, abs=0.05) and v["anteil"] == 70.0 and v["tage"] == pytest.approx(8, abs=0.2) assert prognose.stufe(v) == "gelb" def test_freigeraeumt_in_der_woche_zaehlt_die_woche(): """Sicherungsziel: jeden Tag +1 %, vor dreieinhalb Tagen hat das Aufräumen 6 % freigegeben — netto kaum Zuwachs. Die letzten beiden Tage allein (je +1 %) sollen nicht „schneller“ heißen.""" v = prognose.vorhersage(_reihe(lambda alter: 80 - alter - (0 if alter < 3.5 else -6)), JETZT) assert v["rate"] < 1.0 def test_ohne_genug_verlauf_keine_vorhersage(): assert prognose.vorhersage([], JETZT)["grund"] == "verlauf" kurz = prognose.vorhersage(_reihe(lambda alter: 50 - alter, tage=1.5), JETZT) assert (kurz["grund"], kurz["anteil"]) == ("verlauf", 50.0) alt = prognose.vorhersage(_reihe(lambda alter: 50 - alter, alter_ab=3 * 3600), JETZT) # seit 3 h nichts assert (alt["grund"], alt["anteil"]) == ("verlauf", None) luecken = [[t, w if i % 5 == 0 else None] for i, (t, w) in enumerate(_reihe(lambda alter: 50 - alter))] assert prognose.vorhersage(luecken, JETZT)["grund"] == "verlauf" # je Tag keine sechs Stunden mit Messungen def test_texte_und_stufen(): assert [prognose.in_tagen(t) for t in (0.4, 1.2, 9.4, 35, 200, 400)] == [ "in weniger als einem Tag", "in etwa einem Tag", "in etwa 9 Tagen", "in etwa 5 Wochen", "in etwa 7 Monaten", "frühestens in einem Jahr"] v = {"tage": 2.5, "rate": 1.25, "anteil": 91.9, "grund": None} assert prognose.tempo(v) == "Die Belegung wächst um etwa 1,2 % am Tag (jetzt 92 %)." assert prognose.schlimmer("gelb", v) == ("rot", True) and prognose.schlimmer("rot", v) == ("rot", False) assert prognose.schlimmer(None, {"tage": None}) == ("ok", False) assert prognose.dazu({"tage": 30}) == " Bei diesem Tempo voll in etwa 4 Wochen." and prognose.dazu(None) == "" assert prognose.dazu({"tage": 80}) == "" # --- Mit echten Dateien: Verlauf aus den Berichten ------------------------------------------------------------- @pytest.fixture def daten(tmp_path, monkeypatch): monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path)) einstellungen_mod.einstellungen.cache_clear() yield tmp_path einstellungen_mod.einstellungen.cache_clear() def _bericht(pool: float, vm: float) -> dict: return {"host": {"pools": [{"speicher": "local-lvm", "belegt": int(pool * 1000), "gesamt": 1000, "meta_anteil": 0.01}], "netzlaufwerke": [{"ziel": "/mnt/qnap-backup", "platz": {"belegt": 400, "gesamt": 1000}}, {"ziel": "/mnt/weg", "zustand": "haengt", "platz": None}], "sicherungsziele": {"pbs-qnap": {"belegt": 400, "gesamt": 1000}}}, "gaeste": [{"vmid": 106, "art": "qemu", "platte": {"belegt": int(vm * 1000), "gesamt": 1000}}, {"vmid": 102, "art": "lxc", "platte": {"belegt": 1, "gesamt": 4}}]} def test_berichte_schreiben_den_verlauf_und_die_vorhersage_liest_ihn(daten): from services.homelab import speicher for stunde in range(3 * 24, -1, -1): # drei Tage lang stündlich ein Bericht tage = stunde / 24 speicher.verlauf_merken(_bericht(pool=0.70 - 0.02 * tage, vm=0.5), JETZT - stunde * 3600) letzte = messreihen.letzter_punkt(speicher.VERLAUF, JETZT) assert {k: v for k, v in letzte.items() if k != "t"} == { "pool:local-lvm": 70.0, "pool-meta:local-lvm": 1.0, "nas:/mnt/qnap-backup": 40.0, "ziel:pbs-qnap": 40.0, "vm-106": 50.0} v, meta = speicher.pool_vorhersage("local-lvm", JETZT) assert meta is False and v["rate"] == pytest.approx(2.0, abs=0.05) and v["tage"] == pytest.approx(15, abs=0.5) assert speicher.platten_vorhersage("vm-106", JETZT)["grund"] == "gleich" assert speicher.platten_vorhersage("ct-102", JETZT)["grund"] == "verlauf" # Minutenwerte fehlen hier assert speicher.platten_reihe("pve") == ("pve", "platte", 95.0) and speicher.platten_reihe("nas") is None speicher.verlauf_merken({"host": "kaputt"}, JETZT) # kostet nur diesen Punkt def test_speicherkarte_und_waechter_mit_vorhersage(monkeypatch): from services.homelab import speicher from test_homelab_speicher import _bericht as echter_bericht monkeypatch.setattr(speicher, "antwortet", lambda s, p: True) monkeypatch.setattr(speicher, "_bericht", lambda: echter_bericht()) bald = {"tage": 5.2, "rate": 2.0, "anteil": 60.0, "grund": None} spaeter = {"tage": 30.0, "rate": 0.3, "anteil": 91.0, "grund": None} kurven = {"pool:local-lvm": bald, "nas:/mnt/qnap-backup": spaeter, "ziel:pbs-qnap": spaeter} monkeypatch.setattr(speicher, "_vorhersage", lambda name, jetzt: kurven.get(name, {"tage": None, "grund": "verlauf"})) monkeypatch.setattr(speicher, "platten_vorhersage", lambda gid, jetzt=None: {"tage": 2.0, "rate": 5.0, "anteil": 85.0}) lage = speicher.lage() assert lage["pools"][0]["vorhersage"] == bald and lage["nas"][0]["vorhersage"] == spaeter assert lage["systemplatte"] is None and lage["sicherungen"]["ziele"][0]["vorhersage"] == spaeter befunde = {b.id: b for b in speicher.pruefe_speicher(1_790_330_000.0)} pool = befunde["pool:local-lvm"] assert (pool.stufe, pool.titel) == ("gelb", "Speicherpool local-lvm in etwa 5 Tagen voll") assert pool.text.startswith("Die Belegung wächst um etwa 2,0 % am Tag (jetzt 60 %). Noch ") assert "nas:/mnt/qnap-backup" not in befunde # 39 % und 30 Tage: nichts zu melden # Der Füllstand löst aus, die Vorhersage steht dabei; die Systemplatte läuft in zwei Tagen voll (rot). b = echter_bericht(nas_anteil=0.91) b["host"]["systemplatte"] = {"belegt": 40, "gesamt": 100} monkeypatch.setattr(speicher, "_bericht", lambda: b) befunde = {x.id: x for x in speicher.pruefe_speicher(1_790_330_000.0)} nas = befunde["nas:/mnt/qnap-backup"] assert (nas.stufe, nas.titel) == ("rot", "NAS (192.168.178.62) zu 91 % voll") assert "frei. Bei diesem Tempo voll in etwa 4 Wochen. Die Sicherungen" in nas.text host = befunde["host-platte"] assert (host.stufe, host.titel) == ("rot", "Systemplatte des Proxmox-Hosts in etwa 2 Tagen voll") def test_waechter_gastplatten_und_box_platte(monkeypatch): from services import waechter from services.homelab import kanal, speicher bericht = {"gaeste": [{"vmid": 101, "art": "lxc", "name": "npmplus", "platte": {"belegt": 1, "gesamt": 4}}, {"vmid": 106, "art": "qemu", "name": "arcane", "platte": {"belegt": 90, "gesamt": 100}}]} monkeypatch.setattr(kanal, "bericht", lambda: {"bericht": bericht}) kurven = {"ct-101": {"tage": 1.2, "rate": 60.0, "anteil": 25.0}, "vm-106": {"tage": 40.0, "rate": 0.1, "anteil": 90.0}} monkeypatch.setattr(speicher, "platten_vorhersage", lambda gid, jetzt=None: kurven[gid]) befunde = {b.id: b for b in waechter.pruefe_gast_platten()} ct = befunde["gast-platte:101"] assert (ct.stufe, ct.quelle) == ("rot", "ct-101") and ct.titel.endswith(" in etwa einem Tag voll") assert ct.text.startswith("Die Belegung wächst um etwa 60,0 % am Tag (jetzt 25 %). 0,0 von 0,0 GB belegt") vm = befunde["gast-platte:106"] assert vm.stufe == "rot" and vm.titel.endswith(" zu 90 % voll") and "(VM 106). Bei diesem Tempo voll in etwa 6 Wochen. Platz" in vm.text # KI-Box: Füllstand unauffällig, aber in 10 Tagen voll → eigener gelber Hinweis. monkeypatch.setattr(waechter, "ROLLE", "box") monkeypatch.setattr(waechter.psutil, "disk_usage", lambda pfad: SimpleNamespace(percent=62.0)) monkeypatch.setattr(waechter.prognose, "aus_messreihe", lambda quelle, name, voll_bei=100.0, jetzt=None: {"tage": 10.4, "rate": 3.6, "anteil": 62.0}) (tempo,) = waechter.pruefe_platte() assert (tempo.id, tempo.stufe, tempo.titel) == ("platte:tempo", "gelb", "Die Platte ist in etwa 10 Tagen voll") monkeypatch.setattr(waechter.psutil, "disk_usage", lambda pfad: SimpleNamespace(percent=85.0)) assert [b.id for b in waechter.pruefe_platte()] == ["platte"] # gelb ist gelb: kein zweiter Hinweis