The optimizer treated the end of `optimization.horizon_hours` as the end of the world: energy left in the battery there was worth a single configured price per kWh, so it either dumped the battery into the last hours or hoarded it, depending on that one number. The horizon is now two spans. `horizon_hours` still receives every control command. The new `optimization.tail_horizon_hours` (default 48 h) is a pure lookahead that never produces a command. In AUTO terminal-value mode a deterministic dynamic program solves that tail backwards on a 101-point SoC grid using the production battery and inverter models - SoC bounds, power caps, conversion losses, configured charge and export rates, direct-marketing permission and LCOS on delivered DC energy - and the existing AUTO proxy supplies the continuation value at the tail end. Genetic fitness reads the resulting curve. `tail_horizon_hours: 0` restores the plain proxy at the control end, FIXED is unchanged. The forecast budget is reported, never enforced by refusal: a tail that does not fit is shortened to what the forecast covers and reported as `effective_tail_hours`, and a control horizon that does not fit is warned about at configuration time and rejected by the optimizer at run time, which knows which series ran out. `prediction.hours` defaults to 72 so the new defaults fit out of the box; existing shorter configurations keep starting. Control arrays and warm-start genomes now begin at the run timestamp rather than midnight, flagged by `controls_start_at_now` so the adapters still read older solutions. `forecast_interval_seconds` declares the resolution of shortened native quarter-hour inputs. Required forecasts are no longer silently replaced by demo providers. A missing PV, price, load, feed-in or weather forecast used to rewrite the configured provider and retry, so a run could quietly optimize against invented data. Missing values now stay missing, and provider values are held only within their own source interval instead of being extended indefinitely. Also fixes a config update that could leave EOS half-updated: the merged candidate is validated before the singleton is reinitialized. Four provider tests that hard-coded the old 48 h prediction default are rewritten to derive their expectations from the configured horizon.
12 KiB
Tail-Optimierung in Grafana debuggen
Ziel sind drei vorhandene, zeitlich übereinanderliegende Panels:
- Strompreis – welcher wirtschaftliche Anreiz besteht?
- Steuerplan – welche Betriebsart wurde gewählt?
- Batterie-SoC – was bewirkt die Entscheidung?
Die ersten 24 Stunden sind der echte Steuerplan. Danach folgt der intern optimierte Tail. Der Tail ist ausschließlich Diagnose und wird nicht als Steuerbefehl ausgegeben.
Im Grafana Query Editor alle Namen ohne Backslashes eingeben. Richtig ist
eos_tail_plan, nichteos\_tail\_plan. Auch vor$__timeFilterund$__timeGroupAliassteht kein Backslash.
1. Prüfen, ob die neue EOS-Version läuft
EOS neu starten und einmal /optimize ausführen. In der vollständigen Antwort
muss terminal_value.tail_plan vorhanden und gefüllt sein.
Bei 15-Minuten-Intervallen enthält es normalerweise 192 Einträge bei 48 Stunden
Tail oder 190 Einträge bei einem auf 47,5 Stunden gekürzten Tail. Fehlt das Feld
oder ist es [], läuft noch die alte EOS-Version. Dann kann Node-RED nichts für
Grafana speichern.
2. MariaDB-Tabelle einmalig anlegen
Das SQL aus nodered_tail_plan_schema.sql
einmal in der Datenbank sensor ausführen. Danach prüfen:
SHOW TABLES LIKE 'eos_tail_plan';
Es muss eine Zeile mit eos_tail_plan erscheinen.
3. Nur einen Node-RED-Node ändern
- Den Function-Node Tail + Terminalwert speichern öffnen.
- Ausschließlich dessen Funktionsinhalt ersetzen.
- Den Inhalt aus
nodered_tail_plan_function.jsverwenden. - Done und danach Deploy drücken.
- Die EOS-Optimierung erneut ausführen.
4. Vor Grafana prüfen, ob Node-RED Daten geschrieben hat
Diese Abfrage direkt in Grafana Explore ausführen. Format: Table.
SELECT
COUNT(*) AS "Tail-Zeilen",
MAX(run_ts) AS "Letzter Lauf",
MIN(timestamp) AS "Tail beginnt",
MAX(timestamp) AS "Letzter Tail-Slot"
FROM eos_tail_plan;
Erwartet werden 192 Zeilen für einen vollständigen Lauf beziehungsweise 190
Zeilen für 47,5 Stunden. Steht dort 0, liegt das Problem noch bei EOS oder
Node-RED. Dann kann keine Grafana-Zeitabfrage Daten zeigen.
Zur Kontrolle der Inhalte:
SELECT
run_ts,
timestamp,
slot,
action,
soc_start_pct,
soc_end_pct,
import_price_euro_kwh
FROM eos_tail_plan
ORDER BY run_ts DESC, slot
LIMIT 10;
5. Dashboard-Zeitbereich einstellen
Oben rechts:
From: now-2h
To: now+72h
Panels mit Ist-Daten wie Tesla, Wärmepumpen und Temperaturen behalten ihre kurzen Panel-Zeitbereiche.
6. Panel „Strompreis“ erweitern
Im vorhandenen Strompreis-Panel eine Query hinzufügen. Format: Time series.
SELECT
$__timeGroupAlias(timestamp,$__interval),
AVG(import_price_euro_kwh) AS "Preis Tail – nur Bewertung"
FROM eos_tail_plan
WHERE
$__timeFilter(timestamp)
AND run_ts = (SELECT MAX(run_ts) FROM eos_tail_plan)
GROUP BY 1
ORDER BY 1;
Für die Tail-Linie Farbe Gelb, Line style Dashes, Line width 2 und Fill
opacity 0 einstellen. Die Werte sind bereits in €/kWh. Kein weiteres
* 1000 anwenden.
7. Panel „Steuerplan“ erweitern
Die vorhandenen Zeilen Disch, Spuel, AC, DC und DV sind einzelne
0/1-Steuersignale des ausführbaren 24-h-Plans. Der Tail hat dagegen genau eine
gewählte Betriebsart pro Slot. Deshalb wird er als eine zusätzliche
Text-Zeile dargestellt und nicht auf die vorhandenen 0/1-Zeilen verteilt.
Im State-Timeline-Panel eine neue Query hinzufügen. Format: Table.
SELECT
timestamp AS time,
CASE action
WHEN 'HOLD' THEN 'Halten'
WHEN 'PV_CHARGE_ONLY' THEN 'Nur PV laden'
WHEN 'SELF_CONSUMPTION' THEN 'Haus aus PV/Akku'
WHEN 'DISCHARGE_ONLY' THEN 'Akku entladen'
WHEN 'GRID_CHARGE' THEN 'Akku aus Netz laden'
WHEN 'BATTERY_EXPORT' THEN 'Akku ins Netz verkaufen'
ELSE 'Unbekannt'
END AS "Tail – gedachte Entscheidung"
FROM eos_tail_plan
WHERE
$__timeFilter(timestamp)
AND run_ts = (SELECT MAX(run_ts) FROM eos_tail_plan)
ORDER BY timestamp;
Merge equal consecutive values einschalten und Show values auf Always
setzen. Für das Feld Tail – gedachte Entscheidung Value mappings mit Farben
anlegen: Netzladen blau, Batterieverkauf orange, Haus aus PV/Akku grün,
Nur-PV-Laden türkis, Entladen gelb und Halten grau. Weil die Abfrage bereits
verständliche Texte zurückgibt, dienen die Mappings nur noch der Farbe.
Die Tail-Zeile bleibt während der ersten 24 Stunden absichtlich leer. Sie
beginnt erst an der Grenze zum Tail. In den Standard options No value leeren,
damit Grafana für diesen Abschnitt nicht -1 im Tooltip anzeigt.
8. Panel „Batterie SoC / Current“ erweitern
Neue Query, Format Time series:
SELECT
$__timeGroupAlias(timestamp,$__interval),
AVG(soc_end_pct) AS "SoC Tail – nur Bewertung"
FROM eos_tail_plan
WHERE
$__timeFilter(timestamp)
AND run_ts = (SELECT MAX(run_ts) FROM eos_tail_plan)
GROUP BY 1
ORDER BY 1;
Override für SoC Tail – nur Bewertung: Einheit Percent (0-100), dieselbe
Achse wie der bisherige Prognose-SoC, gestrichelte hellblaue Linie, Breite 2,
Punkte aus.
9. Panel „Bezug & Einspeisung“ erweitern
Zuerst die Batterieleistung ergänzen:
SELECT
$__timeGroupAlias(p.timestamp,$__interval),
AVG(
(p.battery_charge_wh - p.battery_discharge_wh)
/ NULLIF(v.data, 0)
) AS "Akku Tail (+ Laden / - Entladen)"
FROM eos_tail_plan p
JOIN eos_terminal_value v
ON v.run_ts = p.run_ts
AND v.topic = 'tail_slot_hours'
WHERE
$__timeFilter(p.timestamp)
AND p.run_ts = (SELECT MAX(run_ts) FROM eos_tail_plan)
GROUP BY 1
ORDER BY 1;
Diese Reihe beschreibt den Akku, nicht den Netzanschluss. Positive Werte sind Ladung, negative Werte Entladung. Vorhandene PV kann auch direkt die Last decken oder ins Netz fließen und erzeugt deshalb nicht zwingend einen positiven Batteriewert.
Tail-Netzbezug, Format Time series:
SELECT
$__timeGroupAlias(p.timestamp,$__interval),
AVG(p.grid_import_wh / NULLIF(v.data, 0)) AS "Bezug Tail"
FROM eos_tail_plan p
JOIN eos_terminal_value v
ON v.run_ts = p.run_ts
AND v.topic = 'tail_slot_hours'
WHERE
$__timeFilter(p.timestamp)
AND p.run_ts = (SELECT MAX(run_ts) FROM eos_tail_plan)
GROUP BY 1
ORDER BY 1;
Tail-Einspeisung, Format Time series:
SELECT
$__timeGroupAlias(p.timestamp,$__interval),
-AVG(p.grid_export_wh / NULLIF(v.data, 0)) AS "Einspeisung Tail"
FROM eos_tail_plan p
JOIN eos_terminal_value v
ON v.run_ts = p.run_ts
AND v.topic = 'tail_slot_hours'
WHERE
$__timeFilter(p.timestamp)
AND p.run_ts = (SELECT MAX(run_ts) FROM eos_tail_plan)
GROUP BY 1
ORDER BY 1;
Beide Tail-Reihen gestrichelt darstellen. Positive Werte sind Bezug, negative Werte Einspeisung.
10. Grenzen der drei Bereiche markieren
Unter Dashboard settings → Annotations → Add annotation query:
SELECT
MIN(timestamp) AS time,
'Tail beginnt – ab hier nur Bewertung' AS text
FROM eos_tail_plan
WHERE run_ts = (SELECT MAX(run_ts) FROM eos_tail_plan)
UNION ALL
SELECT
DATE_ADD(MAX(timestamp), INTERVAL 15 MINUTE) AS time,
'Prognoseende – danach greift der Restwert' AS text
FROM eos_tail_plan
WHERE run_ts = (SELECT MAX(run_ts) FROM eos_tail_plan);
Die erste senkrechte Linie trennt die reale Steuerung vom Tail. Die zweite Linie markiert den Terminalpunkt.
Unter Dashboard settings → General → Graph tooltip zusätzlich Shared crosshair wählen. Beim Überfahren eines Zeitpunkts steht der Cursor dann in
Strompreis, Steuerplan, SoC, PV sowie Bezug/Einspeisung an derselben Stelle.
Das macht einzelne Entscheidungen wesentlich leichter nachvollziehbar.
11. Kleines Panel „Wie eindeutig war die Entscheidung?“
Den bisherigen zeitunabhängigen Terminalwert-Kurvenplot durch ein kleines
Time series-Panel ersetzen:
SELECT
timestamp AS time,
decision_margin_euro * 100 AS "Vorsprung vor Alternative"
FROM eos_tail_plan
WHERE
$__timeFilter(timestamp)
AND run_ts = (SELECT MAX(run_ts) FROM eos_tail_plan)
ORDER BY timestamp;
Einheit: Currency → Cent, Minimum 0, Linienbreite 2, Punkte Auto.
Der Wert vergleicht die gewählte Betriebsart einschließlich ihrer späteren
Folgen mit der besten anders benannten Betriebsart. Nahe 0 ct war die Wahl
fast gleichwertig und kann schon durch kleine Prognoseänderungen umspringen.
Ein größerer Wert bedeutet eine robuste Entscheidung.
12. Verdächtige Entscheidungen prüfen
Für einzelne Lade- oder Entladeereignisse ein temporäres Table-Panel anlegen:
SELECT
timestamp AS "Zeit",
CASE action
WHEN 'GRID_CHARGE' THEN 'Netzladen'
WHEN 'BATTERY_EXPORT' THEN 'Batterieverkauf'
WHEN 'SELF_CONSUMPTION' THEN 'Normalbetrieb'
WHEN 'PV_CHARGE_ONLY' THEN 'PV-Laden'
WHEN 'DISCHARGE_ONLY' THEN 'Entladen'
ELSE 'Halten'
END AS "Gewählt",
alternative_action AS "Beste andere Betriebsart",
ROUND(import_price_euro_kwh, 3) AS "Preis €/kWh",
ROUND(soc_start_pct, 1) AS "SoC vorher %",
ROUND(soc_end_pct, 1) AS "SoC nachher %",
ROUND(battery_charge_wh, 0) AS "Akku geladen Wh",
ROUND(battery_discharge_wh, 0) AS "Akku entladen Wh",
ROUND(grid_import_wh, 0) AS "Netzbezug Wh",
ROUND(grid_export_wh, 0) AS "Einspeisung Wh",
ROUND(decision_margin_euro * 100, 3) AS "Vorteil ct"
FROM eos_tail_plan
WHERE run_ts = (SELECT MAX(run_ts) FROM eos_tail_plan)
ORDER BY timestamp;
Vorteil ct deutlich positiv bedeutet, dass die Aktion einschließlich der
späteren Slots besser als die beste andere Betriebsart war. Ein Wert nahe null
zeigt einen nahezu gleichwertigen und damit instabilen Entschluss. Fällt der
SoC bei positiver Einspeisung, wurde Energie verkauft. Fällt er ohne
Einspeisung bei vorhandener Last, versorgt die Batterie das Haus. Fällt der SoC
ohne battery_discharge_wh, besteht ein Fehler in Pfad oder Zeitzuordnung.
13. Empfohlene kompakte Debug-Ansicht
Für das Verständnis reichen fünf übereinander ausgerichtete Zeitreihen:
Strompreis– wirtschaftlicher Anreiz.PV und Last– verfügbare und benötigte Energie.Steuerplan– fünf ausführbare 0/1-Zeilen plus eine Tail-Textzeile.Batterie-SoC– Wirkung auf den Energiespeicher.Bezug & Einspeisung– Wirkung am Netzanschluss.
Daneben oder darunter genügt das kleine Panel Wie eindeutig war die Entscheidung?. Die Informationstabelle kann auf 24 h Steuerung, 48 h Tail,
Prognose vollständig und Prognoseende verkürzt werden. Der alte Plot der
Terminalwert-Kurve über Batterie-kWh ist für die tägliche Fehlersuche entbehrlich:
Er ist keine Zeitprognose, sondern nur die interne Bewertung möglicher
Restladungen am Prognoseende.
14. Keine erfundene PV-Prognose hinter dem Provider-Ende verwenden
PVForecastForecastSolar liefert im geprüften Lauf echte Werte nur bis zum
Abend des Folgetags. Der Endpoint /v1/prediction/list interpoliert trotzdem
bis zum angefragten Ende und erzeugt dadurch kleine scheinbare PV-Werte. Diese
Werte dürfen nicht als echter 72-h-Forecast in den Tail gelangen.
In Node-RED genau zwei Function-Nodes ändern:
- Prediction URL: PV 72h durch den Inhalt von
nodered_pv_series_url_function.jsersetzen. - Den direkt hinter PV read liegenden bisherigen Node rename durch den
Inhalt von
nodered_pv_series_to_slots_function.jsersetzen und in PV series -> echte 15-min-Slots umbenennen.
Diese Variante interpoliert nur zwischen tatsächlich vorhandenen
Provider-Zeitpunkten. Hinter dem letzten echten Wert liefert sie null. EOS
verkürzt den Tail dann sichtbar, statt mit einer erfundenen Rest-PV zu rechnen.
Für einen vollständigen 48-h-Tail muss der PV-Provider mindestens 72 Stunden
ab jetzt liefern. Dafür pvforecast.provider beispielsweise auf
PVForecastAkkudoktor oder auf den mit API-Zugang konfigurierten
PVForecastSolcast umstellen. Mit PVForecastForecastSolar ist ein vollständiger
rollender 72-h-PV-Horizont nicht gewährleistet.