mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-14 01:51:56 +00:00
363 lines
12 KiB
Markdown
363 lines
12 KiB
Markdown
# Tail-Optimierung in Grafana debuggen
|
||||
|
|
|
|||
|
|
Ziel sind drei vorhandene, zeitlich übereinanderliegende Panels:
|
|||
|
|
|
|||
|
|
1. **Strompreis** – welcher wirtschaftliche Anreiz besteht?
|
|||
|
|
2. **Steuerplan** – welche Betriebsart wurde gewählt?
|
|||
|
|
3. **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`, nicht `eos\_tail\_plan`. Auch vor `$__timeFilter` und
|
|||
|
|
> `$__timeGroupAlias` steht 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`](nodered_tail_plan_schema.sql)
|
|||
|
|
einmal in der Datenbank `sensor` ausführen. Danach prüfen:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
SHOW TABLES LIKE 'eos_tail_plan';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Es muss eine Zeile mit `eos_tail_plan` erscheinen.
|
|||
|
|
|
|||
|
|
## 3. Nur einen Node-RED-Node ändern
|
|||
|
|
|
|||
|
|
1. Den Function-Node **Tail + Terminalwert speichern** öffnen.
|
|||
|
|
2. Ausschließlich dessen Funktionsinhalt ersetzen.
|
|||
|
|
3. Den Inhalt aus
|
|||
|
|
[`nodered_tail_plan_function.js`](nodered_tail_plan_function.js) verwenden.
|
|||
|
|
4. **Done** und danach **Deploy** drücken.
|
|||
|
|
5. 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`.
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
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`.
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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`.
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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`:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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`:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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`:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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**:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
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:
|
|||
|
|
|
|||
|
|
1. `Strompreis` – wirtschaftlicher Anreiz.
|
|||
|
|
2. `PV und Last` – verfügbare und benötigte Energie.
|
|||
|
|
3. `Steuerplan` – fünf ausführbare 0/1-Zeilen plus eine Tail-Textzeile.
|
|||
|
|
4. `Batterie-SoC` – Wirkung auf den Energiespeicher.
|
|||
|
|
5. `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:
|
|||
|
|
|
|||
|
|
1. **Prediction URL: PV 72h** durch den Inhalt von
|
|||
|
|
[`nodered_pv_series_url_function.js`](nodered_pv_series_url_function.js)
|
|||
|
|
ersetzen.
|
|||
|
|
2. Den direkt hinter **PV read** liegenden bisherigen Node **rename** durch den
|
|||
|
|
Inhalt von
|
|||
|
|
[`nodered_pv_series_to_slots_function.js`](nodered_pv_series_to_slots_function.js)
|
|||
|
|
ersetzen 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.
|