Causal, predictive watering for GARDENA & FYTA. — Know why. Water right.
Ein Dashboard, das vorhersagt, wann Pflanzen Wasser brauchen — und wie viel — statt nach starrem Zeitplan zu giessen. Es liest Bodenfeuchte (GARDENA + FYTA) und Wetterdaten, prognostiziert den Feuchteverlauf per ML und leitet daraus eine begruendete Giessdauer mit Sicherheitsreserve ab. Laeuft produktiv am Garten des Autors.
(Deutschsprachiges Projekt; englische Kurzfassung oben in der Tagline.)
- ML-Feuchteprognose — Delta-Regressor mit Quantil-Baendern (q10/q50/q90) fuer 6h/12h/24h, SHAP-Erklaerbarkeit pro Zone.
- Kausale Giess-Empfehlung — nicht "unter Schwelle = giessen", sondern Welkepunkt + Trocknungsrate -> Reserve-Tage -> konkrete Giessdauer.
- Automatische Kalibrierung — Feldkapazitaet und Wirkungsrate (pp/min) je Zone aus der eigenen Historie, statt geratener Konstanten.
- Bewaesserung ausfuehren — automatisch (Opt-in pro Zone) oder manuell, inkl. Pre-Soak gegen hydrophobe Substrate.
- Sicherheit — Watchdog, Notfall-Stopp, Tages-Budget, Regen-Guard, Leck-Detektion.
- Multi-Standort — Garten, Balkon, Indoor; Regner, Ventile, Mikrodrip, Solar-Pumpe, Giesskanne.
- Betrieb — Wasser-Bilanz, CSV-Export, Wochen-Report, Push bei Anomalien, Auto-Retrain mit Deploy-Gate.
- GARDENA Smart System mit Developer-API-Zugang (Client-ID + Secret) — Pflicht.
- FYTA-Pflanzensensoren — optional, fuer Topf- und Indoor-Zonen.
- Python 3.10+, Node.js (fuer den Frontend-Build).
- Der Hintergrund-Service nutzt
launchdund ist damit auf macOS zugeschnitten; das Backend selbst laeuft plattformunabhaengig.
Ehrlich vorab, damit die Erwartung stimmt:
- Persoenliches Produktivsystem, kein Produkt. Gewachsen an einem konkreten Garten — nicht als generische Loesung getestet.
- Der Giess-Historie-Backfill nutzt einen inoffiziellen GARDENA-Endpoint, der sich jederzeit aendern kann (dafuer gibt es einen Endpoint-Health-Check).
- Prognosen sind probabilistisch. Die Welkepunkt-Reserve ist ein Sicherheitspuffer, keine Garantie gegen Trockenstress.
- Doku, Code und Konfig-Schluessel sind deutsch.
Quickstart · Nutzung · Konfiguration · Troubleshooting · Development · Architektur
# Einmalig: Abhaengigkeiten installieren
python3 -m venv .venv && source .venv/bin/activate
pip install -r backend/requirements.txt
cd frontend && npm install && cd ..
# Konfig anlegen (Vorlage: config/default.example.yaml)
cp config/default.example.yaml config/default.yaml
# Standorte, Zonen, Geo-Koordinaten und Sensor-Zuordnung an eigenes Setup anpassen.
# .env anlegen (Vorlage: .env.example)
cp .env.example .env
# Pflicht: GARDENA_CLIENT_ID, GARDENA_CLIENT_SECRET (Developer-API)
# Empfohlen: GARDENA_CUSTOMER_EMAIL, GARDENA_CUSTOMER_PASSWORD (fuer DHS-Backfill)
# Empfohlen: FYTA_ACCESS_TOKEN (fuer FYTA-Pflanzensensoren)
# Starten
./start.sh # Interaktiv mit Browser
./start.sh --build # Frontend neu bauen und interaktiv starten
./start.sh --dev # Backend + Vite Dev-Server
# Service-Betrieb im Hintergrund
./service.sh install # Service installieren und starten
./service.sh status # Service/API pruefen
./service.sh logs # Logs live ansehen
./service.sh restart # Nach Backend-Aenderungen
./service.sh restart --build # Nach Frontend-Aenderungen
./service.sh stop # Service stoppen
# Logs auch per Finder-Doppelklick auf logs.command (statt ./service.sh logs)Dashboard: http://127.0.0.1:8090
Uebersicht
- StatusLeiste (oben): alle Zonen nach OK / Giessen bald / Kritisch / Nass. Klick springt zur Zone.
- ZonenKarten: pro Zone Feuchte-Wert + Historie-Chart (24h/48h/7d/30d). Jede Karte enthaelt:
- "Warum"-Panel: Status der letzten Entscheidung inline + Drawer mit Historie und Begruendungen (z.B. "Feuchte ausreichend", "Regen erwartet").
- Schwellen-Vorschlag: kleine 💡-Zeile "Vorschlag X/Y % (aktuell X/Y %, Pflanze A-B %)", wenn aus 30 Tagen Historie + Pflanzen-Optimum ein abweichender Wert ableitbar ist. Rein informativ — deine Schwellen bleiben unveraendert, bis du sie selbst anpasst. Tooltip erklaert Quelle (empirisch vs. optimum-dominiert).
- Wasser-Bilanz: Zugefuehrt / Verdunstet / Bilanz in Litern, umschaltbar 24h/7d/30d, mit Quellenanzeige (Archiv / Forecast / gemischt; "indikativ"-Badge bei Forecast).
- ML-Kurzinfo: Feuchte-Vorhersage fuer 6h/12h/24h. Tooltip zeigt q10–q90-Spanne.
- "Erklaerung (Top-5 Features)": aufklappbar pro Zone. Zeigt SHAP-Beitraege der wichtigsten Features pro Horizont als farbige Balken (gruen = treibt Prognose hoch, orange = runter). Laedt on-demand — das Dashboard-Polling bleibt schlank.
- Chart-Band: Recharts-Area zwischen q10 und q90 unter der ML-Prognose-Linie zeigt die Unsicherheit visuell — enges Band = sicher, breites Band = nur Trend.
- Manuell-Giessen: Button fuer Schlauch-Bewaesserung (Dauer + optional Liter + optional Zeitstempel).
- Wetterkarte: Temperatur + Niederschlag pro Standort.
Ops
- KPI-Zeile: Shadow-Empfehlungen, Bewaesserungen heute, Blocker, Wetter-/Sensor-Warnungen.
- Tag-Events: alle Ventil-Events heute mit Quelle (Live / Gardena-App / manuell / Sensor-Heuristik) und Ausloeser-Badge. Ungeklaerte Heuristik-Events koennen per Klick klassifiziert oder geloescht werden.
- Timeline: Entscheidungen, Wetter-Events, Sensor-Warnungen chronologisch.
- Notfall-Stopp: roter Button im Header, wenn ein Ventil aktiv ist.
Historie
- Zeitraum-Filter (24h/7d/30d), Zone, Blocker-Typ, Limit 100-5000.
- Tabelle aller Entscheidungen mit Zeit, Zone, Scope, Aktion, Dauer, Blocker, Begruendung.
- "Als CSV exportieren": Download der aktuellen Filter-Auswahl als CSV (fuer Excel / Pandas / Langzeit-Auswertung).
Gieß-Historie
- Pro Zone: was das System getan hat (im Unterschied zum Entscheidungs-Log der Historie) — wann, wie lange, wie (Einzel-Lauf vs. Pre-Soak inkl. Phasen Vorwaessern/Hauptdose), Ausloeser (Automatik/Manuell).
- Pre-Soaks (Puls + Pause + Hauptdose) erscheinen als ein Lauf (Backend-Marker
lauf_gruppe, keine zeitliche Heuristik). Ignorierte / Cross-Spray-Laeufe sind ausgegraut (zaehlen nicht als echte Zonen-Bewaesserung). - Zonen-Auswahl + 7/14/30-Tage-Fenster.
Uebersicht v2
Drei-Schichten-Refactor der Zonen-Karte als additiver Tab neben "Uebersicht" und "Uebersicht (neu)". Bestehende Tabs bleiben byte-stabil — Rollback durch Tab-Wechsel, keine Datei-Tausch-Operation noetig.
Jede Karte hat drei Schichten:
- Glance (Sekunden-Blick): Header mit Zonen-Name,
KANAL X · STRATEGIEals Sub-Titel, Badges (AUTOMATIK / Sensor-Warnungen / Quellen-MarkerG+F), grosse Feuchte-Prozent rechts mit→ Trend pp/h, Schwellen-Bar mit Welkepunkt-Marker + Korridor + Optimum-Band + Sub-Sensor-Markern + Ist-Punkt. - Action (Was-tun-Schicht): zeigt aktuelle Empfehlung mit
strukturierter Detail-Zeile
praeventiv: 22 min · ML: 41 min · Rate 0.13 pp/minund Reserve-TagWohl-Min in ~14h. Lauf-Banner fuer aktive Bewaesserung dimmt die Empfehlung automatisch (Hinweis "MANUELL AKTIV — EMPFEHLUNG PAUSIERT"). Stop-Button in derselben Box. - Inspect (Tief-Dive, auf-/zuklappbar): Feuchte-Verlauf-Chart
mit ML-Prognose-Band (24h/48h/7d/30d toggle), Modell-Drift-Ampel
mit Ratio inline (
6h 3.9 pp 1.9xmacht Schwellen-Logik direkt sichtbar), FYTA-KPI-Block (Feuchte/Licht/Temperatur/Salz mit Optimum-Baendern), Wasser-Bilanz (24h/7d/30d), Top-5-ML-Features ("Erklaerung"-Toggle), Multi-Sensor-Diagnose-Tabelle (nur bei Multi-Sensor-Zonen wie Waldblumenhain). Expand-State wird pro Zone persistiert (localStorage: zk-v2-expanded-{zone_id}).
Sortierung pro Standort: kritisch → giessen → warn → ok. Bei
Gleichstand niedrigste Feuchte zuerst. Outdoor-Reihenfolge fest
: Bambus → Waldblumen → Magerwiese → Hecke.
Lauf-Banner mit Optimistic-Update: nach Live starten /
Stop-Klick erscheint der Banner sofort (kein 5-30 s-Polling-Lag),
weil der Frontend-State direkt nach erfolgreichem POST gesetzt
wird; Polling synchronisiert sich beim naechsten Cycle.
Performance: ein Bulk-Endpoint /api/dashboard-snapshot
ersetzt pro Karte 3 Per-Karte-Polls (Messwerte + ML + Empfehlung).
Beim Tab-Wechsel 1 HTTP-Call statt 42, Polling-Cadence 60 s. Inspect-
Sub-Komponenten (Recharts, FYTA-KPI, ML-SHAP, Wasser-Bilanz, Drift-
Ampel) sind via React.lazy aus dem Initial-Bundle (~200 KB gzip)
heraus — laden erst beim Aufklappen.
Statt nur "X % unter Schwelle = bewaessern" liefert das System eine kausale Erzaehlung: aktueller Sensor + Welkepunkt + Decay (ET0+Regen) -> Reserve-Tage -> Empfehlungs-Typ + konkrete Dauer. Sichtbar in jeder ZonenKarte:
"Sensor 55 %, Welkepunkt 50 %. Ohne Giessen morgen frueh 50 %, uebermorgen 45 %. 12 min jetzt -> deckt 3 Tage."
Vier Empfehlungs-Typen: akut (Welke <= 1.5 Tage), praeventiv (Reserve unter sicherheits_tage), wohlfuehl_grenze (Sensor unter optimum_min, sanfter Hinweis), kein_bedarf. Bei laengerer Abwesenheit via ?sicherheits_tage=7 hochsetzen.
Vier Bewaesserungs-Strategien pro Zone — bewaesserungs_strategie: korridor | haeufig_klein | selten_gross | konstant_niedrig:
- korridor (Default): Welkepunkt-basiert + sanfter
wohlfuehl_grenze-Hinweis, backwards-kompatibel. - haeufig_klein: Wohlfuehl-Min als primaerer Trigger, oft + wenig (Topfpflanzen, frisch gesetzte Stauden).
- selten_gross: nur
akutoderkein_bedarf, Ziel = Feldkapazitaet (Tiefwurzler wie Bambus, etablierte Buesche). - konstant_niedrig: Trigger nur bei Welkepunkt + 5 pp Reserve (Sukkulenten, Magerwiese).
Mehrfach-Takt: bei max_dauer_sekunden ausgereizt + weiterhin Bedarf empfiehlt das System "X min jetzt + Y min in Z h" als zwei Dosen statt eines unrealistisch langen Stosses.
Saisonale Feuchte-Regimes: pro Zone optionale Regime-Liste mit MM-DD-Range. Aktives Regime ueberschreibt optimum_*, feuchte_schwelle_*, feuchte_kritisch, welkepunkt. Sinnvoll fuer Magerwiese-Sommer-Trockenphase, Anwachs-Phase frischer Pflanzungen, Sukkulenten-Winterruhe.
ML-Vergleich strategie-aware: das ML-Modell rechnet auf das gleiche Ziel wie das kausale Modell — bei haeufig_klein auf optimum_max, bei selten_gross auf Feldkapazitaet, bei korridor auf feuchte_schwelle_min. Vorher kapitulierte ML mit "sensor_ueber_ziel" sobald der Sensor genau an feuchte_schwelle_min lag, obwohl das kausale Modell weiter bewaessern wuerde. Zusatz: ml_status_grund im API-Response erklaert dem Frontend in Klartext, warum kein ML-Wert da ist (sensor_ueber_ziel / kein_bedarf / modell_fehlt / inverse_kein_wert / inferenz_fehler / konfig_aus / service_aus / keine_messung / zone_monitoring / zone_unbekannt / ok). Das Dashboard zeigt entweder den Vergleichswert oder einen klaren Hinweis-Block ("kein Vorschlag (Sensor schon am Ziel)"), keine stille Luecke mehr.
Live-Manuell-Bewaesserung direkt ueber das Dashboard. Optional mit Pre-Soak-Sequenz: kurze Vorwaesserung (z. B. 5 min) -> Pause (z. B. 30 min, lasst das Wasser einsickern) -> Hauptdose (z. B. 60 min). Nuetzlich auf Sandboden, der bei direkter Volldosis zu Tiefensickerung neigt. Stop-Button auch fuer extern (Gardena-App) gestartete Bewaesserungen. Pre-Soak-Sequenzen ueberleben Backend-Restart. Default-Werte fuer Vor-/Pause-Dauer kommen aus pre_soak_min/pre_soak_pause_min der Zone.
Pro Zone konfigurierbar, ob der "Gegossen"-Button Sekunden- oder Liter-/ml-Optionen zeigt. Default sekunden (Schlauch); fuer FYTA-Topfpflanzen mit Giesskanne ist ml intuitiver:
- zone_id: zitrus
logging_einheit: ml
logging_optionen_ml: [100, 250, 500, 1000]Backend rechnet aus liter eine Pseudo-dauer_sekunden (mind. 1 s, damit kleine Dosen wie 50 ml nicht aus dauer-basierten ML-Features fallen) und schreibt beides ins ventil_ereignis. Bilanz nutzt das liter-Feld kanonisch.
Fuer FYTA-Topfpflanzen mit Gardena-AquaBloom-Solar-Pumpe (kein Smart-Ventil, also keine WebSocket-Events) schreibt das Backend synthetische Bewaesserungs-Events in ventil_ereignis, basierend auf der eingestellten Pumpen-Frequenz und -Dauer. Das verschafft ML, Bilanz und Wirkungsrate echte Datenpunkte ohne taegliches Mitloggen.
Konfig pro Zone (Beispiel zitrus + kasten_4):
aquabloom_pumpen_dauer_sekunden: 600 # 10 min pro Puls
aquabloom_pumpen_intervall_stunden: 48 # alle 48 h
aquabloom_anker_zeitstempel: "2026-05-09T08:00:00"
aquabloom_tropfer_anzahl: 1 # Tropfer in dieser Zone
aquabloom_tropfer_liter_pro_stunde: 0.5 # Gardena-Spec
aquabloom_aktiv_ab: "05-01"
aquabloom_aktiv_bis: "10-01"Modell: liter = dauer_s/3600 × tropfer_anzahl × tropfer_l_h pro Puls (1 Tropfer × 0.5 L/h × 600 s ≈ 0.083 L). Job laeuft 1×/h, holt fehlende Pulse seit dem letzten DB-Eintrag bzw. Konfig-Anker (max(letzter_db, konfig) — bei Frequenzwechsel den Anker auf den letzten realen Pumpenpuls setzen). ML-Behandlung: Wirkungsrate-Median ignoriert AquaBloom (zu kleine Pulse), Response-Features nehmen nur Rows mit delta_6h > 1.0 ins Training (sonst dominieren delta≈0-Datenpunkte das Modell).
Pro Zone wird die echte Wirkungsrate (delta_pp / dauer_min) automatisch aus historischen SCHLIESSEN-Events gelernt — kein Hardcode mehr. 5-stufige Aufloesungs-Kette: manueller Konfig-Override -> Kalibrierungs-Median (n>=3 in 90 Tagen) -> Default 1.0. Plus log-Decay-Korrektur fuer Tiefensickerung bei langen Dosen (Bambus-Mikrodrip alpha~ -0.6, Sprinkler-Sand schwaecher) und Plateau-Modell fuer Saettigungs-Effekte. Garbage-Filter verhindert dass Stale-CLOSED-Phantom-Dauer den Median korrumpiert.
Stuendlicher Snapshot der kausalen Empfehlung pro Zone, automatische Eval gegen den realen Sensor-Wert nach 6 und 24 h. Datengrundlage fuer den Vertrauens-Aufbau vor ventilsteuerung_aktiv: true. Endpoint /api/empfehlungs-audit?zone_id=&tage= mit Stats. UI-Rueckklassifikation alter Events ueber PATCH /api/ventil-ereignis/{id}?paar=true mit Audit-Trail.
Proaktive iMessage-Pushes bei System-Anomalien — fuer Vergesslichkeits- und Urlaubs-Faelle, in denen der tagliche Dashboard-Check ausfaellt.
Heute zwei Trigger:
- Empfehlung 3 Tage in Folge "akut" fuer dieselbe Zone — moeglicher stiller Sensor-Defekt oder echter Trockenstress, den der User uebersehen hat (Pre-Mortem Akt 1).
- Husqvarna-Soft-Ban-Frueherkennung: juengste Gardena-Sensor-Messung aelter als 90 min — Cadence-Einbruch wie am 23.04.2026, der spaeter zum 67-h-Block fuehrte. Erstinstall (nie eine Messung) wird bewusst nicht gepusht, damit das Setup ruhig bleibt.
Aktivierung: watchdog.aktiv: true in config/default.yaml + Empfaenger via watchdog.empfaenger oder env IMESSAGE_EMPFAENGER. Throttle 24 h pro Trigger-Klasse + Zone (in DB persistiert, ueberlebt Restart). Defaults konservativ — Push ist Zweit-Linie, nicht Erst-Linie.
Trigger C — Endpoint-Schema-Drift: wenn DHS oder FYTA-Login laenger als 24 h einen Nicht-OK-Status haben (Schema umbenannt, Auth dauerhaft kaputt), sendet der Watchdog einen Push. Der EndpointHealthJob (Default aktiv: true) macht 1x/Tag einen leichtgewichtigen Probe-Call gegen beide Endpoints und persistiert das Ergebnis in endpoint_health. Inoffizielle Schnittstellen werden so frueh erkannt, wenn sie kippen.
Sonntags 20:00 bekommt der konfigurierte iMessage-Empfaenger eine Zusammenfassung der vergangenen Woche:
- Bewaesserungen pro Zone (Anzahl + Liter)
- Regen + Verdunstung pro Standort
- ML-Drift (MAE pro Horizont)
- Aktive Warnungen
Aktivierung: wochen_report.aktiv: true in config/default.yaml. Empfaenger: entweder explizit via wochen_report.empfaenger oder Fallback auf den ersten Zonen-Benachrichtigungs-Empfaenger.
Opt-In, dreistufig: Eine Zone wird nur dann autonom bewaessert, wenn alle drei zutreffen:
ventilsteuerung_aktiv: true(global, Master-Switch; Defaultfalse= Log-Only, protokolliert nur, wann/wie lange es bewaessern wuerde).modus: automatik(Zone, Shadow-Entscheidungslogik aktiv).auto_loop_opt_in: true(Zone; Defaultfalse).
Damit laesst sich der Auto-Loop pro Strang scharfschalten: das globale Flag aktivieren, aber nur die gewuenschten Zonen per auto_loop_opt_in freigeben — andere automatik-Zonen bleiben Shadow (loggen weiter, feuern nicht). Manuelle Endpoints + Pre-Soak sind unabhaengig (Hand-am-Ruder).
Auto-Pre-Soak: Zonen mit pre_soak_modus: immer werden vom Auto-Loop als Pre-Soak gegossen (Puls + Pause + Hauptdose) statt als Einzellauf — Pflicht fuer Sprinkler-Zonen. Die Sequenz laeuft als loop-getriebene State-Machine (kein asyncio.sleep-Task): der wake-sichere Entscheidungsloop treibt die Phasen per Wall-Clock, sodass die Hauptdose auch nach einem Laptop-Sleep in der Pause zuverlaessig nachgezogen wird.
Zonen mit gleichem ventil_kanal teilen einen Wasserkreis. Der Start richtet sich nach der trockensten bzw. groessten-Defizit-Zone, nicht mehr nach dem Durchschnitt. Eine bereits sehr nasse Nachbarzone blockiert den Start nicht; sie wirkt nur als Max-Stop, wenn der Kanal bereits aktiv ist.
Sicherheitsmechanismen:
- Watchdog-Timer: schliesst Ventil nach
max_dauer_sekunden + 30s. - Max-Stop pro Kanal: aktive Kanaele werden gestoppt, sobald eine Kanal-Zone
feuchte_schwelle_max + 10ueberschreitet. - Gardena-Server-Timeout (3600s) als zweites Sicherheitsnetz.
- Notfall-Stopp via Dashboard oder
POST /api/notfall-stopp. - Startup-Recovery: schliesst offene Ventile nach Neustart, setzt Live-Manuell-Bewaesserungen + Pre-Soak-Sequenzen ueber Restart hinweg fort.
- Sensor-Ausreisser-Filter (MAD): einzelne Spikes kippen keine Entscheidung.
- Leck-Detektor: warnt wenn Bewaesserung keine Wirkung zeigt oder Sensor ueber 48 h konstant bleibt.
- Festklemm-Sensor-Block: bei offener
SENSOR_EINGEFROREN-Warnung returnt_robuste_feuchteNone-> Empfehlung faellt auf KEINE_MESSUNG-Pfad statt in eine wochenlange Akut-Spirale. - Wirkungsrate-Garbage-Filter: Stale-CLOSED-Phantom-Dauer (z. B. 119 min statt nominal 90 min nach py-smart-gardena-Reconnect) wird vor Aufnahme in den Kalibrierungs-Median rausgefiltert -- Hard-Cap 120 min + Per-Zone-Cap fuer AUTOMATIK.
- ML-Saettigungs-Cap: drei Stufen gegen Mean-Reversion bei Saturierung + Regen — eng (>95 %/>5 mm), mittel (>85 %/>7 mm), weit (>75 %/>10 mm).
- Single-Instance-Lock: Lock-File unter
~/Library/Application Support/de.xindaan.pflanzen-dashboard/bewaesserung.pid-- ueberlebt macOS-Sleep-Wake (vorher /tmp, das raeumt macOS auf). - Hahn-Cluster + Durchfluss-Budget: mehrere Zonen am gleichen Wasserhahn teilen sich den Druck. Konfig pro Zone
hahn_cluster+verbrauch_lpm+ optionalexklusiv: truefuer druckabhaengige Sprinkler. Pro Clustermax_durchfluss_lpm= echtes Hahn-Maximum. Pre-flight Lock prueft (a) Exklusivitaet: jede markierte Zone blockt Mitstart unabhaengig vom Volumen, (b) Volumen:summe(verbrauch_lpm) <= max_durchfluss_lpm. Reject mitHAHN_BELEGT+ Klartext-Begruendung. Auto-Loop sortiert die Bedarfs-Kandidaten nach Akut-Score (Sensor - feuchte_kritisch), damit kritische Zonen Vorrang haben. Wirkungsrate-Auto-Kalibrierung verwirft Datenpunkte, deren Eval-Fenster mit einem Geschwister-Lauf am gleichen Hahn ueberlappt (Druck-Konkurrenz verfaelscht Sensor-Antwort). Default-Konfig leer = altes Verhalten unveraendert (Backward-Compat). - Shadow-Pause-Fallback: verhindert Spam wiederholter Empfehlungen im Opt-In-Mode.
Alle API-Endpoints (Ausnahme: /api/health) brauchen einen API-Key
im Header X-Api-Key. Zwei Rollen:
- read: alle GETs (Dashboard-Daten, Status, Historie).
- control: zusätzlich alle mutierenden Endpoints (Giessen, Notfall- Stopp, Ventil-Steuerung, Pre-Soak, Ereignis-Korrektur). Token-Bucket- Ratelimit 5/min/Key. Read ist auf control implizit enthalten.
Schluessel anlegen / verwalten:
# Neuen Schluessel erzeugen (Klartext wird NUR EINMAL angezeigt)
python -m bewaesserung.api_auth schluessel-erstellen --rolle control --label iphone-andre
# Alle Schluessel listen
python -m bewaesserung.api_auth liste
# Schluessel entfernen
python -m bewaesserung.api_auth loeschen --label iphone-andreDie Hashes liegen unter
~/Library/Application Support/de.xindaan.pflanzen-dashboard/api_keys.json
(Mode 0600). scrypt-gehasht, der Klartext landet nirgends auf Disk.
Web-Frontend: liest den Key aus localStorage.pflanzen_api_key
(per Hand setzen) bzw. fallback aus Vite-Env-Var VITE_API_KEY.
Ablage in frontend/.env.local (gitignored ueber *.local-Pattern,
greift sowohl in vite dev als auch in vite build).
Wichtig: NICHT .env.development.local -- das wird vom vite build
(Production-Mode) ignoriert, der Key landet dann nicht im Bundle. Ohne
Key (weder localStorage noch Env) → 401.
iPhone-App: Key landet im Keychain.
/api/health bleibt absichtlich public-minimal ({"ok": true, "zeitstempel": ...}), damit service.sh und externe Liveness-Probes
weiter laufen. Versions-/Konfig-Details liegen unter
/api/health/detail (read-geschuetzt).
Auth-Faustregel (Details s. oben): /api/health ist public, alle
weiteren GETs brauchen einen read-Key, POST/PATCH/DELETE brauchen
control. Vollstaendige Route-zu-Rolle-Matrix in
backend/src/bewaesserung/api_auth.py:ROUTE_ROLLEN.
| Endpunkt | Methode | Beschreibung |
|---|---|---|
/api/zonen |
GET | Alle Zonen mit aktuellem Zustand |
/api/zonen/{id}/messwerte |
GET | Messwert-Verlauf (?stunden=48) |
/api/zonen/{id}/bilanz |
GET | Wasser-Bilanz (?fenster=24h|7d|30d) |
/api/zonen/{id}/ereignisse |
GET | Ventil-Events einer Zone |
/api/zonen/{id}/giess-historie |
GET | Giess-Laeufe einer Zone, gruppiert (?tage=N) — Pre-Soaks als ein Lauf |
/api/ventil-ereignisse |
GET | Ventil-Events gefiltert (?von&bis&zone_id) |
/api/ventil-ereignis/{id} |
PATCH | Ausloeser / Zeit / Liter korrigieren |
/api/ventil-ereignis/{id} |
DELETE | Event entfernen (War-Regen / Fehlalarm) |
/api/prognose |
GET | Bewaesserungs-Prognosen |
/api/standorte |
GET | Konfigurierte Wetter-Standorte mit Koordinaten |
/api/wetter |
GET | Wettervorhersage fuer alle Standorte (Multi-Standort-Dashboard) |
/api/wetter/{standort} |
GET | Wettervorhersage pro Standort |
/api/entscheidungen |
GET | Entscheidungs-Log (?zone_id&limit&format=csv|json) — CSV-Export fuer Historie-Tab/Excel |
/api/ventil-status |
GET | Aktive Bewaesserungen |
/api/notfall-stopp |
POST | Alle Ventile sofort schliessen |
/api/giessen |
POST | Manuelles Giessen loggen (Zeit/Liter optional) |
/api/ops/summary |
GET | KPI-Zeile (Shadow, Bewaesserungen, Blocker, ...) |
/api/ops/timeline |
GET | Entscheidungen + Events chronologisch |
/api/schwellen-vorschlag |
GET | Datengetriebene Schwellen-Vorschlaege pro Zone |
/api/kalibrierung/{id} |
GET | Feldkapazitaet + Welkepunkt-Proxy pro Zone |
/api/health |
GET | Liveness-Check (public-minimal: {ok, zeitstempel}) |
/api/health/detail |
GET | Versions-/Konfig-Status (read-geschuetzt) |
/api/ml/status |
GET | ML-Modell-Status + Retrain-Infos (letzter Lauf, Gate-Faktor) |
/api/ml/vorhersage/{id} |
GET | ML-Feuchte-Vorhersage (6h/12h/24h) inkl. q10/q90; ?details=top_features liefert SHAP-Beitraege |
/api/ml/drift |
GET | Rollender MAE pro Horizont (?zone_id&fenster=7d|30d), Ampel rot wenn mae > 1.5 × baseline |
/api/ml/drift/log |
GET | Letzte N Drift-Log-Zeilen pro Horizont (Prognose vs. Ist + q10/q90) |
/api/ml/dauer-drift |
GET | 6h-Sensor-Ist-Delta-Drift fuer ML-Bewaesserungs-Response-Modell pro Zone |
/api/zonen/{id}/empfehlung-jetzt |
GET | Dry-Run-Giess-Empfehlung mit kausaler Erzaehlung + ML-Drift-Ampel |
/api/empfehlungs-audit |
GET | Audit-Trail Empfehlung vs. realer Sensor-Wert nach 6/24h (?zone_id&tage) |
/api/ventil-ereignis/{id}?paar=true |
PATCH | Korrektur eines Ventil-Event-Paars (OEFFNEN+SCHLIESSEN) inkl. Audit-Spalte |
/api/manuell/start |
POST | Live-Manuell-Bewaesserung ueber Dashboard, optional mit Pre-Soak-Sequenz |
config/default.yaml — zentrale Konfiguration:
- ventilsteuerung_aktiv:
false(Log-Only) odertrue(aktive Steuerung) — globaler Master-Switch (siehe Ventilsteuerung: Scharfschalten ist dreistufig). - Zonen: Schwellen, Budget, bevorzugte Zeiten, Modus,
flaeche_m2,anteil_kanal(Anteil am gemeinsamen Kanal-Wasser). Optional pro Zone:auto_loop_opt_in— gibt diese Zone fuer den autonomen Auto-Loop frei. Greift nur zusammen mit globalemventilsteuerung_aktiv: true+modus: automatik. So laesst sich der Auto-Loop pro Strang scharfschalten (z. B. nur Bambus), waehrend andereautomatik-Zonen Shadow bleiben.pre_soak_modus—nie= Auto-Loop giesst als Einzellauf;immer= als Pre-Soak (Puls + Pause + Hauptdose, mitpre_soak_min/pre_soak_pause_min). Pflicht fuer Sprinkler-Zonen (Einzellauf benetzt nur die Oberflaeche). Greift wieauto_loop_opt_innur im scharfen Auto-Loop.optimum_feuchte_min/optimum_feuchte_max— Pflanzen-Optimum-Range. Dominiert Schwellen-Vorschlag, wenn empirische Historie darunter liegt. Waldblumenhain 40/60, Bambuswald 60/75.welkepunkt— manueller Override fuer den Stress-Punkt. Wenn nicht gesetzt, wird der Welkepunkt zur Laufzeit aus einer 5-stufigen Aufloesungs-Kette ermittelt: (0) aktivesfeuchte_regime[*].welkepunkt→ (1)zone.welkepunkt(manuell) → (2)feldkapazitaet_messung-Kalibrierungs-Median bei n≥3 → (3) p10 der Tagesminima der letzten 90 Tage minus 3 pp Sicherheitsmarge → (4)feuchte_kritisch-Fallback (Regime-aware). Die Quelle wird ans Frontend durchgereicht ("aus Regime", "aus Sensor-Historie", "aus Kalibrierungs-Median", etc.). Saisonale Anpassung passiert primaer ueber den Decay (ET0+Regen); fuer hartharte Phasen-Wechsel (Magerwiese, Sukkulenten) das Regime nutzen. Empfehlung: erstmal nicht manuell setzen — Aufloesungs-Kette macht den Job sauber.sicherheits_tage— wieviele Tage Reserve ueber Welkepunkt eine Bewaesserungs-Empfehlung schaffen soll. Default 3 deckt durchschnittliches Wochenende. Bei laengeren Abwesenheiten via API-Param?sicherheits_tage=7ueberschreibbar.versickerungs_karenz_stunden— wie lange nach einem Ground-Truth-Bewaesserungsende die Sensor-Heuristik keinen Phantom-Event mehr erzeugt. Sandboden mit Sprinkler braucht 6 (Waldblumenhain), Mikrodrip 3 (Default).bewaesserungs_strategie—korridor/haeufig_klein/selten_gross/konstant_niedrig. Steuert die Trigger-Logik der kausalen Empfehlung (siehe oben).delta_pp_pro_minute— Pro-Zone-Wirkungsrate (Sensor-Anstieg pro Bewaesserungs-Minute). Wenn None: Auto-Kalibrierung ausfeldkapazitaet_messung typ='wirkungsrate'. Realistische Werte: Sprinkler-Sand ~0.17, Mikrodrip-Bambus ~0.4. Konfig-Override hat Vorrang vor Kalibrierungs-Median.feuchte_regime— saisonale/phasen-abhaengige Override-Liste, siehe unten.
- FYTA: Pflanzen-IDs und Zone-Zuordnung. Polling holt alle Datenpunkte aus dem
list-measurements-Range. Beim Service-Start laeuft zusaetzlich ein Lueckenfuellungs-Hook, der die letzten 7 Tage automatisch nachzieht und mit existierenden Eintraegen via(zone_id, zeitstempel, geraet_id)dedupliziert. Restart-Luecken durch Crashes/Auth-Lag/DB-Lock-Konflikte werden so automatisch geschlossen. Status sichtbar in der CLI-Console[Backfill-Hook] fertig: X importiert, Y duplikate, Z Tage. - Wetter: Standorte mit Koordinaten.
- bilanz: Durchflussrate pro Kanal + manueller Schlauch (L/min).
- schwellen_adaption: ET0-abhaengige Schwellen-Anhebung bei Hitze.
- ml_ausschluss_fenster: Zeitraeume, die beim ML-Training und beim Schwellen-Vorschlag ignoriert werden (z.B. Sensor-Umzug, nicht-eingeschlemmte Initial-Phase). Aktuelles Beispiel:
bambuswald_yogaraum2026-04-06 bis -07 (94 × 0.0 % haetten sonst das 10-Perzentil verzerrt). - ml.retrain: Auto-Retrain —
aktiv,intervall_tage,trainings_fenster_tage,gate_faktor,folds,quantile,monotone_constraints,start_verzoegerung_minuten(Default 30 — verhindert dass der erste Retrain direkt nach Service-Start die CPU blockiert und Dashboard-Polls verzoegert).monotone_constraints: trueist Opt-In und constrained nurniederschlag_summe_*monoton steigend. Nach jedem erfolgreichen Training wird das Gatemae_neu < gate_faktor * mae_altpro Horizont geprueft; nur bei Bestehen aller drei werden die neuen Modelle deployed, sonst Rollback. - kalibrierung: Feldkapazitaet + Welkepunkt-Proxy + Wirkungsrate —
aktiv,intervall_stunden(Default 1, idempotent),regen_min_mm,plateau_max_delta,welkepunkt_min_tage,saison_monate. Erkennt Feldkapazitaet per Regen-Peak + 12-24h-Plateau, Welkepunkt-Proxy per Sensor-Minimum nach Trockenphase (Saison Mai-Sept), Wirkungsrate aus SCHLIESSEN-Events mit Garbage-Filter. - plant_optimum_intervall_stunden: FYTA-API-Abruf-Intervall fuer
plant.measurements.moisture.values.min_good/max_good. Default 24h, Cache in Tabelleplant_optimum. - wochen_report:
aktiv,empfaenger(optional — Fallback auf ersten Zonen-Benachrichtigungs-Empfaenger), Wochentag/Uhrzeit. Sonntags 20:00 iMessage-Zusammenfassung. - watchdog:
aktiv,empfaenger(optional — Fallback aufIMESSAGE_EMPFAENGERenv),intervall_minuten,throttle_stunden,akut_in_folge_tage,husqvarna_max_alter_minuten(Default 90 — juengste Gardena-Messung aelter = Block-Push). Aktuell Default ON indefault.yaml. - feuchte_regime pro Zone: saisonale oder phasen-abhaengige Schwellen-Overrides. Liste von Eintraegen mit
von_mm_dd/bis_mm_dd(MM-DD-Range, Wraparound ueber Jahreswechsel erlaubt) plus optionalen Wertenoptimum_min/max,feuchte_schwelle_min/max,feuchte_kritisch,name,grund. Felder mitnullwerden vom Zone-Default uebernommen. Sinnvoll fuer Magerwiesen mit Sommer-Trockenphase, Sukkulenten/Zitrus mit Winter-Ruhe oder Anwachs-Phasen. Beispiel:feuchte_regime:- {name: anwachs, von_mm_dd: "04-01", bis_mm_dd: "06-30", optimum_min: 40, optimum_max: 60, grund: "Aussaat-Phase"}- {name: sommer_trocken, von_mm_dd: "07-01", bis_mm_dd: "09-30", feuchte_schwelle_min: 12, feuchte_kritisch: 5, optimum_min: 15, optimum_max: 25, grund: "Trockenphase gegen Konkurrenzgraeser"} - backup: DB-Backup-Job — Intervall, Retention, Verzeichnis, Monatlich-Schalter. Off-Mac-Spiegel: optionales
spiegel_verzeichnisfuer Off-Mac-Backup (z. B. iCloud Drive). Mac-Disk-Defekt wuerde sonst alle lokalen Backups gleichzeitig mitnehmen. Tilde-Expansion + Spiegel-Rotation wie lokales Verzeichnis. Spiegel-Fehler (z. B. iCloud offline) sind isoliert geloggt und retried beim naechsten Tick. - endpoint_health: Schema-Probe-Calls fuer DHS + FYTA.
aktiv(Default true),intervall_stunden(Default 24). Pre-Mortem-Akt-4-Schluss: erkennt stille Schema-Drift inoffizieller Endpoints, Watchdog-Trigger C feuert iMessage bei > 24 h-Fehler. - ml_bewaesserungs_response: Pro-Zone-Modell fuer empfohlene Giess-Dauer.
aktiv(Shadow-Logging),wirksam(scharf statt Heuristik),min_events, Drift-Eval-Endpoint/api/ml/dauer-drift. - bewaesserungs_response_retrain: Auto-Retrain fuer Response-Modelle, eigenes Gate.
- sensor_namen — Klartextnamen pro Sensor-Geraet-ID fuer
die SensorListeDiagnose-Tabelle (nur sichtbar bei Multi-Sensor-Zonen
wie Waldblumenhain). FYTA-Namen werden automatisch aus
fyta.pflanzen[].namevor-belegt (Schluesselfyta_<fyta_id>), manuelle Eintraege ueberschreiben. Gardena-UUIDs muessen hier gepflegt werden, sonst zeigt das Frontend die gekuerzte ID als Fallback. Beispiel:
sensor_namen:
"176a357e-394b-4951-a6f5-e6730516d9fb": "Waldblumenhain (Gardena)"
"fyta_129884": "Waldblumen A (FYTA Terra 11 cm)"
Umgebungsvariablen (.env, siehe .env.example):
GARDENA_CLIENT_ID,GARDENA_CLIENT_SECRET— Developer-API (Pflicht fuer Live-Steuerung).GARDENA_CUSTOMER_EMAIL,GARDENA_CUSTOMER_PASSWORD— Customer-Login fuer DHS-Backfill (inoffiziell, aber stabil genutzt; kein Fallback fuer verlorene WebSocket-Events).FYTA_EMAIL,FYTA_PASSWORD— Email + Passwort des FYTA-Accounts. Backend holt sich automatisch ein Access-Token, refresht bei 401/403 und cached inbackend/daten/fyta_customer_token.json. Kein manuelles Token-Extrahieren mehr noetig.FYTA_ACCESS_TOKEN(Legacy) — manuell aus web.fyta.de LocalStorage extrahiertes Bearer-Token. Bleibt als Fallback aktiv, solangeFYTA_EMAIL/FYTA_PASSWORDleer sind. Bei neuen Installs nicht mehr setzen.IMESSAGE_EMPFAENGER— fuer Push-Benachrichtigungen.WETTER_BREITE,WETTER_LAENGE— Default-Standort (einzeln ueberschreibbar inconfig/default.yaml).
- Dashboard leer: Backend laeuft? →
curl http://127.0.0.1:8090/api/zonen - Keine FYTA-Daten: Token abgelaufen? → neuen Token in
.enveintragen. - Gardena nicht verbunden: Firewall/VPN? Gardena braucht WebSocket-Verbindung.
- DHS-Backfill inaktiv:
GARDENA_CUSTOMER_*in.envsetzen, neu starten. - "ML-Vorhersage deaktiviert" im Log: Modelle liegen nicht unter
backend/daten/ml/. Neu trainieren (siehe Development) oder Rollback ausml_archiv/<datum>/. - Kein q10/q90 im Chart: Service laedt nur Punkt-Modelle (nicht Quantile). Entweder Quantile-Retrain laufen lassen (
ml.cli trainiere --quantile) oder auf den naechsten Auto-Retrain warten (wennml.retrain.aktiv: true). - Dashboard startet 5-10 Min langsam: Der erste Retrain lief direkt nach Service-Start und blockierte den Event-Loop mit LightGBM. Dagegen greift
ml.retrain.start_verzoegerung_minuten: 30— wenn dein Config-Wert niedriger ist, anheben. - Schwellen-Vorschlag unsinnig niedrig (z.B. 5 %): Der Sensor hat Messwerte aus einer Nicht-Boden-Phase geliefert (Einschlemmen, Umzug). Loesung: Zeitraum in
ml_ausschluss_fensteraufnehmen — Perzentile werden dann um diese Periode herum berechnet. Beispielbambuswald_yogaraum2026-04-06/07. - "Backend nicht erreichbar" waehrend des Retrains (historisch): Ab 2026-04-19 laeuft das Training in
asyncio.to_thread, der Event-Loop bleibt frei — kein Freeze mehr. - "Bewaesserungen heute: 0" trotz Aktivitaet: die KPI zaehlt nur heute seit 00:00 lokal. Gestern-Events (z.B. Einschlemmen am Abend) zaehlen nicht.
- WebSocket-Event fehlt: Heartbeat-Zeile
ws_events=...zeigt Counter — siehedocs/t-0055-websocket-diagnose.md. - Watchdog-Fehlalarm
Husqvarna-Soft-Ban: tritt auf, wennhusqvarna_max_alter_minutenzu eng gesetzt ist. Realdaten zeigen ~3-8 Messungen/h gesamt (alle Sensoren), pro Sensor ~1/h. Default 90 min Schwelle ist sicher; bei stabilem Wetter koennen einzelne Sensoren auch >60 min schweigen. - Watchdog wiederholt nach Fehlalarm 24 h still: Throttle-Eintrag in
watchdog_eventmuss geloescht werden:sqlite3 backend/daten/bewaesserung.db "DELETE FROM watchdog_event WHERE typ='husqvarna_block';"(mit gestopptem Backend). Gilt analog fuer andere Trigger-Klassen. database is locked-Lockup: ueblicherweise zweistart.sh-Instanzen parallel oder ein vergessenersqlite3CLI-Prozess. Lock-Datei pruefen:cat ~/Library/Application\ Support/de.xindaan.pflanzen-dashboard/bewaesserung.pid. Mit gestopptem Backend ggf. loeschen + neu starten.- Endpoint-Health-Push fuer
gardena_dhsoderfyta: ein echter Schema-Drift oder Auth-Bruch in der inoffiziellen API.endpoint_health-Tabelle zeigt Details:sqlite3 backend/daten/bewaesserung.db "SELECT * FROM endpoint_health;"
# Backend-Tests (vollstaendige Suite)
cd backend && PYTHONPATH=src ../.venv/bin/pytest -q
# Frontend Lint + Build
cd frontend && npx eslint src/ && npx vite build
# FYTA-Daten nachziehen (Luecken fuellen)
cd backend && PYTHONPATH=src ../.venv/bin/python3 -m bewaesserung.fyta_backfill --von 2026-02-22 --bis 2026-04-06
# Wetter-Archiv manuell nachziehen (Open-Meteo ERA5)
cd backend && PYTHONPATH=src ../.venv/bin/python3 -m bewaesserung.wetter_archiv --von 2026-02-01 --bis 2026-04-12
# Luftfeuchte-Backfill fuer VPD-Feature
cd backend && PYTHONPATH=src ../.venv/bin/python3 -m bewaesserung.wetter_archiv \
--luftfeuchte-backfill --von 2026-04-09 --bis 2026-04-13cd backend
# Punkt-Modelle trainieren (3 Modelle fuer 6h/12h/24h)
PYTHONPATH=src ../.venv/bin/python3 -m bewaesserung.ml.cli trainiere \
--von 2026-02-20 --bis 2026-04-19 --folds 3
# Quantile-Modelle trainieren
PYTHONPATH=src ../.venv/bin/python3 -m bewaesserung.ml.cli trainiere --quantile \
--von 2026-02-20 --bis 2026-04-19 --folds 3
# Dry-Run / Gate-Check — trainiert ins *_gatecheck/, vergleicht MAE,
# modifiziert Live-Modelle NICHT. Nuetzlich fuer Validierung vor `aktiv: true`.
PYTHONPATH=src ../.venv/bin/python3 -m bewaesserung.ml.cli trainiere \
--quantile --gate-check --von 2026-02-20 --bis 2026-04-19 --folds 3
# Monotone-Regen-A/B — trainiert Default und Monotone in
# Temp-Verzeichnisse, modifiziert Live-Modelle NICHT.
PYTHONPATH=src ../.venv/bin/python3 -m bewaesserung.ml.cli trainiere \
--quantile --monotone-ab --von 2026-02-20 --bis 2026-04-19 --folds 3
# Gate-Check mit aktivierten Monotone-Constraints.
PYTHONPATH=src ../.venv/bin/python3 -m bewaesserung.ml.cli trainiere \
--quantile --gate-check --monotone-constraints \
--von 2026-02-20 --bis 2026-04-19 --folds 3
# Baseline-Vorhersage evaluieren (Regel-basierter Trend + Wetter)
PYTHONPATH=src ../.venv/bin/python3 -m bewaesserung.ml.cli baseline \
--von 2026-04-01 --bis 2026-04-19Nur wenn Auto-Retrain (ml.retrain.aktiv: true) aus ist oder eine spezielle
Konstellation einen Sofort-Deploy braucht:
- Vorheriges Live-Verzeichnis archivieren:
mv backend/daten/ml backend/daten/ml_archiv_$(date +%F) - Neues Trainingsverzeichnis an die Live-Stelle verschieben:
mv daten/ml_neu backend/daten/ml - Service neu starten (
./service.sh restartoder Ctrl+C +./start.sh). - Verify: Startlog zeigt
3 Punkt-Modell(e) + 6 Quantile-Modell(e) geladen.
Im Regelfall uebernimmt der MlRetrainJob genau das: Training in _tmp/,
Gate-Pruefung, atomarer Deploy nach _archiv/<datum>/ bei Bestehen.
Der Service legt taeglich einen Snapshot der SQLite-DB an (native
SQLite-Backup-API, integrity-verifiziert, atomar ueber os.replace):
backend/daten/backup/
taeglich/ bewaesserung_YYYY-MM-DD.db (letzte 14 Tage)
monatlich/ bewaesserung_YYYY-MM.db (erster je Monat, dauerhaft)
Off-Mac-Spiegel: mit backup.spiegel_verzeichnis (z. B.
iCloud Drive) wird jeder Snapshot zusaetzlich dort abgelegt. Mac-Disk-
Defekt wuerde sonst alle lokalen Backups gleichzeitig mitnehmen. Spiegel-
Fehler (iCloud offline) sind isoliert geloggt und beim naechsten Tick
retried.
Konfiguration: config/default.yaml, Block backup: (Intervall, Retention,
Verzeichnis, Spiegel, Abschalter). Wiederherstellung siehe
docs/backup_wiederherstellung.md.
backend/src/bewaesserung/
main.py — Einstiegspunkt, verdrahtet alle Jobs, Entscheidungs-Loop, Single-Instance-Lock
entscheidung.py — Entscheidungslogik + Min-Start/Max-Stop pro Kanal + MAD-Filter + 4 Bewaesserungs-Strategien + kausale Empfehlung mit Welkepunkt-Reserve + FeuchteRegime-Override
ventil_sicherung.py — Safety-Wrapper (Watchdog, Notfall-Stopp, Retry-Limit)
pre_soak.py — Pre-Soak-Sequenz (Vorwasser + Pause + Hauptdose) mit Restart-Recovery
gardena_client.py — Gardena Smart System API (OAuth, WebSocket) + Diagnose-Metriken
gardena_customer_auth.py — Husqvarna-IAM Customer-Login fuer smart.gardena.com
gardena_web_backfill.py — DHS-Backfill (smart.gardena.com/v1/dhs) als Haupt-Event-Quelle
sensor_dhs_backfill.py — Sensor-Bodenfeuchte/-Temperatur via DHS preset=sensor2
fyta_client.py — FYTA Pflanzensensor-API (Polling) + Auto-Login
wetter.py — Open-Meteo Wettervorhersagen
wetter_archiv.py — Open-Meteo Archive (ERA5-Ground-Truth) Job
wetter_ereignisse.py — Frost / Hitze / Starkregen-Erkennung
bilanz.py — Wasser-Bilanz pro Zone (Zugefuehrt / Verdunstet / Bilanz)
leck_detektor.py — Bewaesserung-ohne-Wirkung / Sensor-eingefroren (saisonal Mai-Sept)
sensor_backfill.py — Sensor-Heuristik-Fallback (Feuchte-Spruenge -> Ventil-Events) inkl. Aktiv-Lauf-Schutz
sensor_health.py — Ausfall- und Batterie-Warnungen
speicher.py — SQLite-Zeitreihenspeicher + Migrationen + WAL-Mode (Tabellen u.a. `plant_optimum`, `feldkapazitaet_messung`, `empfehlungs_audit`, `pre_soak_state`, `live_lauf_state`, `watchdog_event`, `endpoint_health`)
backup.py — DB-Backup-Rotation taeglich/monatlich + Off-Mac-Spiegel
api_server.py — FastAPI REST-Server
sensordaten.py — Sensor-Callback-Verarbeitung + temperature-only-Beat-Filter
benachrichtigung.py — iMessage-Versand (osascript)
watchdog.py — Proaktives iMessage-Push bei Anomalien (akut-3-Tage / Husqvarna-Stille / Endpoint-Schema-Drift)
endpoint_health.py — Schema-Probe-Calls fuer DHS + FYTA, persistiert Status
schwellen_vorschlag.py — Datengetriebene Schwellen-Vorschlaege + Regime-Override
plant_optimum_job.py — FYTA-API-Abruf `min_good/max_good`, 24h-Cache
kalibrierung.py — Feldkapazitaet + Welkepunkt-Proxy + Wirkungsrate-Auto-Kalibrierung mit Garbage-Filter
report.py — Wochen-Report-Generator + iMessage-Job (ISO-Wochen-Idempotenz)
empfehlungs_audit_job.py — Snapshot Empfehlung + Eval gegen realen Sensor-Wert nach 6/24h
ml/
features.py — Feature Engineering (~65 Features inkl. VPD, Bilanz-Delta, ziel_delta_Xh)
training.py — Walk-Forward-CV, Delta-Regressor, Band-Scale, Sample-Weighting + Monotone-Constraints
vorhersage.py — Laden + Live-Inferenz, q10/q90-Band, Saettigungs-Cap (3 Stufen)
evaluation.py — MAE/RMSE/R²/Pinball-Loss/Abdeckungsrate/Baseline
drift_job.py — Persistiert Live-Prognosen, gleicht mit Ist ab + Catchup-Loop
retrain_job.py — Woechentlicher Auto-Retrain mit Deploy-Gate
response_features.py — Pro-Zone-Response-Features fuer Bewaesserungs-Dauer-Modell
response_training.py — Forward-Quantile + Inverse-Punkt-Modell pro Zone
response_vorhersage.py — Live-Inference fuer empfohlene Giess-Dauer (Shadow + scharf)
response_retrain_job.py — Auto-Retrain fuer Response-Modelle mit Drift-Gate
cli.py — CLI fuer Training / Baseline / Gate-Check / Monotone-A/B / Response
konfig.py — YAML-Config-Loader
frontend/src/
App.tsx — Layout + Tab-Routing
komponenten/
ZonenKarte.tsx — Pro-Zone-Hauptkarte
EntscheidungsErklaerung.tsx — "Warum"-Panel pro Zone
WasserBilanz.tsx — KPI-Block Zugefuehrt/Verdunstet/Bilanz
FeuchteAnzeige.tsx — Feuchte-Wert-Darstellung + Schwellen-Indikator
StatusLeiste.tsx — Zonen-Klassifikation oben
WetterKarte.tsx — Temperatur + Niederschlag pro Standort
ManuellesGiessen.tsx — Modal fuer Schlauch-Bewaesserung
MLStatusBadge.tsx — Modell-Status-Badge (geladen / fehlt / veraltet)
MLAttributierung.tsx — On-Demand-SHAP-Beitraege pro Horizont
HistorieTab.tsx — Entscheidungs-Historie mit Filter + CSV-Export
OpsTab.tsx — Ops-Review-Seite
OpsFilter.tsx — Filter-Leiste fuer Ops-Timeline (Typ / Zone / Zeit)
OpsKpiZeile.tsx — KPI-Karten (Shadow, Bewaesserungen, Blocker, ...)
OpsTimeline.tsx — Chronologie-Timeline
OpsTimelineEintrag.tsx — Einzel-Eintrag der Ops-Timeline (Severity, Icon, Aktion)
TagEvents.tsx — Heutige Ventil-Events + Klassifikations-Buttons
EntscheidungsLog.tsx — Letzte 30 Entscheidungen, einklappbar
FehlerGrenze.tsx — ErrorBoundary um Tabs + ZonenKarten [Review-Fix F16]
config/default.yaml — Zonen, Schwellen, Standorte, Bilanz, Schwellen-Adaption,
Kalibrierung, Plant-Optimum, Wochen-Report, ML-Ausschluss-Fenster
docs/ — Technische Notizen (WebSocket-Diagnose, Design-Prompts,
Recherche-Synthesen, Feldkapazitaets-Check, Phase-4-Report)
Die Feuchte-Prognose basiert auf einem LightGBM-Stack mit 65 Features (Sensor-Lags, Rolling-Averages, Wetter, Bewaesserung, VPD, Bilanz-Delta).
| Task | Inhalt |
|---|---|
| VPD | relative_humidity_2m aus Open-Meteo + Magnus-Formel → vpd_mittel_{6,12,24}h als Transpirations-Feature. Archive-Backfill via wetter_archiv --luftfeuchte-backfill. |
| Drift-Log | Jede Live-Inferenz landet in ml_vorhersage_log mit prognose_ziel_zeit + Modell-Version. Stuendlicher Job evaluiert gegen Sensor-Messung ±30 min zum Ziel. /api/ml/drift liefert rollende MAE pro Horizont mit Ampel (rot wenn > 1.5 × baseline). |
| Bilanz-Delta | bilanz_liter_{6,12,24}h + bilanz_diff_24h = feuchte_diff - bilanz_liter/flaeche_m2 als Residual fuer Versickerung und Wurzel-Aufnahme (NaN fuer Zonen ohne flaeche_m2). |
| Quantile Regression | Pro Horizont drei Modelle (q10/q50/q90, Pinball-Loss). aktuell_{H}h.lgbm zeigt auf q50 fuer Backwards-Compat. Frontend rendert Unsicherheits-Band zwischen q10 und q90. Bei Crossing bleibt q50 stabil, nur das Band wird korrigiert. |
| Auto-Retrain | Woechentlicher In-Service-Retrain, Training in <live>_tmp/. Gate mae_neu < gate_faktor × mae_alt fuer alle drei Horizonte — Pass → atomarer Deploy, alte Modelle nach <live>_archiv/<datum>/; Ablehnung → tmp loeschen, Live unberuehrt. Opt-In via ml.retrain.aktiv. start_verzoegerung_minuten (Default 30) verhindert Dashboard-Lags beim Service-Start. |
| Saettigungs-Cap (3 Stufen) | Post-Processing gegen Mean-Reversion bei Saettigung + Regen. (Eng) >95 %/>5 mm -> aktuell-2, (Mittel) >85 %/>7 mm -> aktuell-4, (Weit) >75 %/>10 mm -> aktuell-6. Mittel/Weit ergaenzt 2026-05-04 nach Pre-Mortem-Akt 5: 6h-MAE war gemildert, 12h+24h noch nicht. |
| Sample-Weighting | Training gewichtet Regen- und Saettigungs-Samples 3x. Greift erst mit neuer Trainingsrunde und wirkt allmaehlich. |
| Delta-Regressor | Kernfix der Regen-Ignoranz: Modelle lernen ziel_delta_Xh = ziel - aktuell statt absolutes Feuchteniveau. Inferenz rechnet aktuell + delta_q50 und clippt erst danach auf 0-100 %. Eliminiert den 44 %-Mean-Reversion-Anker, der beim absoluten Regressor bei Saettigung (100 %) eine Prognose zurueck Richtung Trainings-Mittelwert erzwang. Dry-Run-Ergebnis: 6h MAE −30 %, 12h −19 %, 24h −26 %. |
| Band-Scale | Nach jedem Training wird die empirische q10/q90-Abdeckung auf Holdout gemessen und ein Skalierfaktor pro Horizont in band_scale_{h}h.json gespeichert. Inferenz skaliert die Bandbreite damit, Zielkorridor 75-85 %. Vermeidet 27-Modell-Explosion (= eigene Quantile pro Klasse). |
| Monotone-A/B | Opt-In-Constraints: +1 fuer niederschlag_summe_*, 0 fuer alle anderen Features. CLI --monotone-ab vergleicht Default vs. Monotone mit Gesamt-MAE und Regen-Slice-MAE, ohne Live-Modelle zu veraendern. |
| Feldkapazitaets-/Welkepunkt-Kalibrierung | 6h-Job sucht Regen-Peaks (mindestens regen_mindestmenge_mm) und pruefd 24h spaeter, ob die Sensor-Feuchte ein Plateau zeigt — das ist die zonen-spezifische Feldkapazitaet. Welkepunkt-Proxy: Sensor-Minimum nach Trockenphase in Saison Mai-Sept. Ergebnisse in Tabelle feldkapazitaet_messung, per /api/kalibrierung/{zone_id} abrufbar. Liefert die Grundlage fuer spaetere physikalische Schwellen-Kalibrierung. |
Neben der reinen Prognose liefert das Dashboard mehrere Hilfen fuer den User, die Entscheidungen nachzuvollziehen und zu verbessern:
- SHAP-Panel: aufklappbare Zeile pro Zone zeigt die Top-5-Feature-
Beitraege pro Horizont via LightGBM
pred_contrib=True. Laedt lazy — kein Dashboard-Polling-Overhead. Gruene Balken treiben Prognose hoch, orange runter. - Schwellen-Vorschlag: aus 30 Tagen Sensor-Historie werden 10-/90-
Perzentile mit ±5 %-Puffer berechnet, gefiltert durch
ml_ausschluss_fenster. Liefert einen Vorschlag, ohne die Config zu aendern — Entscheidung bleibt beim User. - Optimum-Dominanz: wenn eine Zone
optimum_feuchte_min/maxhat (aus Config oder FYTA-Cache), wird ein zu niedriger empirischer Vorschlag nach oben korrigiert (quelle=optimum_dominiert). Rationale: wenn der User historisch zu wenig gegossen hat, darf der Vorschlag nicht diese schlechte Gewohnheit zementieren. - FYTA-Optimum-Cache: 24h-Job liest
plant.measurements.moisture.values.min_good/max_goodpro FYTA-Pflanze und legt sie in Tabelleplant_optimumab. Wird als Fallback genutzt, wenn keine Config-Werte gesetzt sind. - Historie-Tab + CSV-Export: vollstaendige Entscheidungs-Historie mit Filter (Zeitraum, Zone, Blocker). Export als CSV fuer Excel / Pandas.
Bewaesserungs-Events landen in ventil_ereignis aus mehreren Quellen, mit Prioritaet:
- Live-WebSocket (
ventil_id= UUID): Gardena-Cloud pusht Events via py-smart-gardena — Ground truth. - User-manuell (
ventil_id='manuell'): ueberPOST /api/giessenoder das Dashboard-Modal. - Gardena-Web-DHS (
ventil_id='gardena_web'): periodischer Pull des inoffiziellensmart.gardena.com/v1/dhs-Endpoints (faengt Offline-Phasen und WebSocket-Verluste ab). - Sensor-Heuristik (
ventil_id='sensor_heuristik'): Fallback wenn DHS ausfaellt; erkennt Events ueber Feuchte-Spruenge, markiert sie alsausloser=UNBEKANNTfuer Nutzer-Klassifikation.
Dedup-Fenster: pro (zone_id, zeitstempel ±60s) nur ein Event-Paar; niedrigere Prioritaet skip.
Stack: Python 3.13 + asyncio, FastAPI, aiosqlite, LightGBM, httpx, structlog | React 19, Recharts, Vite, TypeScript
MIT — siehe LICENSE.