mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-09 16:06:40 +00:00
feat(optimization): split the control horizon from the forecast tail
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.
This commit is contained in:
@@ -236,6 +236,7 @@
|
||||
]
|
||||
},
|
||||
"optimization": {
|
||||
"tail_horizon_hours": 48,
|
||||
"horizon_hours": 24,
|
||||
"interval": 3600,
|
||||
"algorithm": "GENETIC",
|
||||
@@ -253,7 +254,7 @@
|
||||
}
|
||||
},
|
||||
"prediction": {
|
||||
"hours": 48,
|
||||
"hours": 72,
|
||||
"historic_hours": 48
|
||||
},
|
||||
"pvforecast": {
|
||||
@@ -263,7 +264,8 @@
|
||||
"PVForecastVrm": null,
|
||||
"PVForecastPVNode": null,
|
||||
"PVForecastForecastSolar": null,
|
||||
"PVForecastSolcast": null
|
||||
"PVForecastSolcast": null,
|
||||
"PVForecastAkkudoktorLocal": null
|
||||
},
|
||||
"planes": [
|
||||
{
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
| horizon_hours | `EOS_OPTIMIZATION__HORIZON_HOURS` | `int` | `rw` | `24` | The general time window within which the energy optimization goal shall be achieved [h]. Defaults to 24 hours. |
|
||||
| interval | `EOS_OPTIMIZATION__INTERVAL` | `int` | `rw` | `3600` | The optimization interval (slot length) [sec]. The genetic optimizer supports 3600 (1 hour) and 900 (15 min); other values fall back to 3600. Defaults to 3600 seconds (1 hour). |
|
||||
| keys | | `list[str]` | `ro` | `N/A` | The keys of the solution. |
|
||||
| tail_horizon_hours | `EOS_OPTIMIZATION__TAIL_HORIZON_HOURS` | `int` | `rw` | `48` | Forecast lookahead after the control horizon [h]. No tail commands are issued. Set 0 to disable. |
|
||||
| terminal_value_euro_per_kwh | `EOS_OPTIMIZATION__TERMINAL_VALUE_EURO_PER_KWH` | `float` | `rw` | `0.0` | Value assigned to usable battery energy remaining at the end of the optimization horizon [EUR/kWh]. This terminal value is independent of the battery LCOS. Only used with terminal_value_mode = FIXED. Defaults to 0 EUR/kWh. |
|
||||
| terminal_value_mode | `EOS_OPTIMIZATION__TERMINAL_VALUE_MODE` | `<enum 'TerminalValueMode'>` | `rw` | `AUTO` | How to value the energy left in the battery at the end of the optimization horizon. AUTO derives a concave value curve from the trailing horizon window and needs no configuration; FIXED uses 'terminal_value_euro_per_kwh'. Defaults to AUTO. |
|
||||
| terminal_value_window_hours | `EOS_OPTIMIZATION__TERMINAL_VALUE_WINDOW_HOURS` | `int` | `rw` | `24` | Length of the trailing horizon window the AUTO terminal value curve is derived from [h]. One day covers a full load and PV cycle. Defaults to 24 hours. |
|
||||
| terminal_value_mode | `EOS_OPTIMIZATION__TERMINAL_VALUE_MODE` | `<enum 'TerminalValueMode'>` | `rw` | `AUTO` | How to value the energy left in the battery at the end of the control horizon. AUTO solves the forecast tail with an AUTO continuation proxy at its end (or only the proxy if tail is zero); FIXED uses 'terminal_value_euro_per_kwh'. Defaults to AUTO. |
|
||||
| terminal_value_window_hours | `EOS_OPTIMIZATION__TERMINAL_VALUE_WINDOW_HOURS` | `int` | `rw` | `24` | Length of the trailing window at the effective tail end the AUTO continuation curve is derived from [h]. One day covers a full load and PV cycle. Defaults to 24 hours. |
|
||||
| visualize_pdf | `EOS_OPTIMIZATION__VISUALIZE_PDF` | `bool` | `rw` | `True` | Generate the PDF visualization after each optimization run. Disable for headless setups (e.g. Node-RED integration) to save several seconds per run. Defaults to True. |
|
||||
:::
|
||||
<!-- pyml enable line-length -->
|
||||
@@ -28,6 +29,7 @@
|
||||
```json
|
||||
{
|
||||
"optimization": {
|
||||
"tail_horizon_hours": 48,
|
||||
"horizon_hours": 24,
|
||||
"interval": 3600,
|
||||
"algorithm": "GENETIC",
|
||||
@@ -56,6 +58,7 @@
|
||||
```json
|
||||
{
|
||||
"optimization": {
|
||||
"tail_horizon_hours": 48,
|
||||
"horizon_hours": 24,
|
||||
"interval": 3600,
|
||||
"algorithm": "GENETIC",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
| Name | Environment Variable | Type | Read-Only | Default | Description |
|
||||
| ---- | -------------------- | ---- | --------- | ------- | ----------- |
|
||||
| historic_hours | `EOS_PREDICTION__HISTORIC_HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the past for historical predictions data |
|
||||
| hours | `EOS_PREDICTION__HOURS` | `Optional[int]` | `rw` | `48` | Number of hours into the future for predictions |
|
||||
| hours | `EOS_PREDICTION__HOURS` | `Optional[int]` | `rw` | `72` | Number of hours into the future for predictions |
|
||||
:::
|
||||
<!-- pyml enable line-length -->
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
```json
|
||||
{
|
||||
"prediction": {
|
||||
"hours": 48,
|
||||
"hours": 72,
|
||||
"historic_hours": 48
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Akkudoktor-EOS
|
||||
|
||||
**Version**: `v0.3.0.dev2609040861878062`
|
||||
**Version**: `v0.3.0.dev2609090521492067`
|
||||
|
||||
<!-- pyml disable line-length -->
|
||||
**Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.
|
||||
|
||||
@@ -108,3 +108,5 @@ Some of the `configuration keys` have default values by definition. For most of
|
||||
:relative-docs: ..
|
||||
:relative-images:
|
||||
```
|
||||
|
||||
See [Control horizon and battery lookahead](optimization_horizons.md) for horizon validation, forecast availability and control-array indexing.
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,57 @@
|
||||
const SLOT_MINUTES = 15;
|
||||
const PREDICTION_HOURS = 72;
|
||||
const SLOT_MS = SLOT_MINUTES * 60000;
|
||||
const raw = msg.payload && msg.payload.data;
|
||||
if (!raw || typeof raw !== "object") {
|
||||
node.warn("PV-Rohprognose fehlt oder hat kein data-Objekt.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const points = Object.entries(raw)
|
||||
.map(([timestamp, value]) => [new Date(timestamp).getTime(), Number(value)])
|
||||
.filter(([timestamp, value]) => Number.isFinite(timestamp) && Number.isFinite(value))
|
||||
.sort((a, b) => a[0] - b[0]);
|
||||
if (points.length < 2) {
|
||||
node.warn("PV-Rohprognose enthält weniger als zwei gültige Punkte.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const start = new Date(now);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const slotStart = new Date(now);
|
||||
slotStart.setSeconds(0, 0);
|
||||
slotStart.setMinutes(Math.floor(slotStart.getMinutes() / SLOT_MINUTES) * SLOT_MINUTES);
|
||||
const endMs = slotStart.getTime() + PREDICTION_HOURS * 3600000;
|
||||
const values = [];
|
||||
let right = 1;
|
||||
for (let timestamp = start.getTime(); timestamp < endMs; timestamp += SLOT_MS) {
|
||||
if (timestamp < points[0][0]) {
|
||||
values.push(0);
|
||||
continue;
|
||||
}
|
||||
if (timestamp > points[points.length - 1][0]) {
|
||||
// Absichtlich null: Function 3 und EOS verkürzen damit den Tail, statt
|
||||
// einen erfundenen linearen PV-Rest als echten Forecast zu behandeln.
|
||||
values.push(null);
|
||||
continue;
|
||||
}
|
||||
while (right < points.length && points[right][0] < timestamp) right++;
|
||||
const b = points[Math.min(right, points.length - 1)];
|
||||
const a = points[Math.max(right - 1, 0)];
|
||||
if (timestamp === b[0] || a[0] === b[0]) {
|
||||
values.push(b[1]);
|
||||
} else {
|
||||
const fraction = (timestamp - a[0]) / (b[0] - a[0]);
|
||||
values.push(a[1] + fraction * (b[1] - a[1]));
|
||||
}
|
||||
}
|
||||
|
||||
msg.topic = "pv_forecast";
|
||||
msg.payload = values;
|
||||
node.status({
|
||||
fill: points[points.length - 1][0] >= endMs - SLOT_MS ? "green" : "yellow",
|
||||
shape: "dot",
|
||||
text: `PV echt bis ${new Date(points[points.length - 1][0]).toLocaleString()}`
|
||||
});
|
||||
return msg;
|
||||
@@ -0,0 +1,27 @@
|
||||
const BASE_URL = "http://192.168.1.151:8503";
|
||||
const PREDICTION_HOURS = 72;
|
||||
const SLOT_MINUTES = 15;
|
||||
const now = new Date();
|
||||
const start = new Date(now);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const slotStart = new Date(now);
|
||||
slotStart.setSeconds(0, 0);
|
||||
slotStart.setMinutes(Math.floor(slotStart.getMinutes() / SLOT_MINUTES) * SLOT_MINUTES);
|
||||
const end = new Date(slotStart.getTime() + PREDICTION_HOURS * 3600000);
|
||||
|
||||
function iso(date) {
|
||||
const pad = n => String(Math.trunc(Math.abs(n))).padStart(2, "0");
|
||||
const offset = -date.getTimezoneOffset();
|
||||
const sign = offset >= 0 ? "+" : "-";
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
|
||||
`T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +
|
||||
`${sign}${pad(offset / 60)}:${pad(offset % 60)}`;
|
||||
}
|
||||
|
||||
// /list interpolates über das echte Forecast-Ende hinaus. /series liefert nur
|
||||
// die tatsächlich vom Provider vorhandenen Zeitpunkte.
|
||||
msg.method = "GET";
|
||||
msg.url = `${BASE_URL}/v1/prediction/series?key=pvforecast_ac_power` +
|
||||
`&start_datetime=${encodeURIComponent(iso(start))}` +
|
||||
`&end_datetime=${encodeURIComponent(iso(end))}`;
|
||||
return msg;
|
||||
@@ -0,0 +1,124 @@
|
||||
// Tail und anschließenden Terminalwert der Optimierung -> MariaDB
|
||||
const tv = msg.payload && msg.payload.terminal_value;
|
||||
if (!tv) {
|
||||
node.warn("Kein terminal_value im Payload - läuft EOS noch auf dem alten Stand?");
|
||||
return null;
|
||||
}
|
||||
|
||||
const curve = tv.curve;
|
||||
const continuation = tv.continuation_curve;
|
||||
const diag = tv.tail_diagnostics || {};
|
||||
const d = new Date();
|
||||
const p2 = v => ("0" + v).slice(-2);
|
||||
const runTs = `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} ` +
|
||||
`${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`;
|
||||
const num = v => (v === null || v === undefined || isNaN(v)) ? "NULL" : Number(v).toFixed(8);
|
||||
const str = v => "'" + String(v === null || v === undefined ? "" : v)
|
||||
.replace(/'/g, "''").slice(0, 250) + "'";
|
||||
|
||||
function marginalAt(c, energyWh) {
|
||||
if (!c || !Array.isArray(c.energy_wh) || c.energy_wh.length < 2) return null;
|
||||
for (let i = 1; i < c.energy_wh.length; i++) {
|
||||
if (energyWh <= c.energy_wh[i]) return c.marginal_euro_per_kwh[i - 1];
|
||||
}
|
||||
return c.marginal_euro_per_kwh[c.marginal_euro_per_kwh.length - 1] ?? null;
|
||||
}
|
||||
|
||||
const res = msg.payload.result || {};
|
||||
const priceNow = Array.isArray(res.Electricity_price) && res.Electricity_price.length
|
||||
? res.Electricity_price[0] * 100000 : null;
|
||||
const feedInNow = Array.isArray(res.Feed_in_tariff) && res.Feed_in_tariff.length
|
||||
? res.Feed_in_tariff[0] * 100000 : null;
|
||||
const marginalNow = marginalAt(curve, tv.battery_energy_wh);
|
||||
const continuationMarginalNow = marginalAt(continuation, tv.battery_energy_wh);
|
||||
|
||||
const scalars = {
|
||||
credited_euro: tv.credited_euro,
|
||||
tail_operating_euro: tv.tail_operating_euro,
|
||||
continuation_value_euro: tv.continuation_value_euro,
|
||||
battery_energy_wh: tv.battery_energy_wh,
|
||||
control_horizon_hours: tv.control_horizon_hours,
|
||||
requested_tail_hours: tv.requested_tail_hours,
|
||||
effective_tail_hours: tv.effective_tail_hours,
|
||||
tail_end_hour: tv.tail_end_hour,
|
||||
marginal_now_ct_kwh: marginalNow === null ? null : marginalNow * 100,
|
||||
continuation_marginal_now_ct_kwh:
|
||||
continuationMarginalNow === null ? null : continuationMarginalNow * 100,
|
||||
price_now_ct_kwh: priceNow,
|
||||
feed_in_now_ct_kwh: feedInNow,
|
||||
tail_slots: diag.slots,
|
||||
tail_slot_hours: diag.slot_hours,
|
||||
tail_soc_grid_points: diag.soc_grid_points,
|
||||
tail_min_import_ct_kwh: diag.min_import_price_euro_per_kwh * 100,
|
||||
tail_max_import_ct_kwh: diag.max_import_price_euro_per_kwh * 100,
|
||||
tail_min_feed_in_ct_kwh: diag.min_feed_in_tariff_euro_per_kwh * 100,
|
||||
tail_max_feed_in_ct_kwh: diag.max_feed_in_tariff_euro_per_kwh * 100,
|
||||
tail_negative_price_slots: diag.negative_import_price_slots,
|
||||
tail_positive_export_slots: diag.positive_battery_export_slots,
|
||||
mode_tail: tv.mode === "TAIL" ? 1 : 0,
|
||||
mode_auto: (tv.mode === "TAIL" || tv.mode === "AUTO") ? 1 : 0
|
||||
};
|
||||
|
||||
let sql = "START TRANSACTION;\n";
|
||||
sql += `DELETE FROM eos_terminal_value WHERE run_ts = '${runTs}';\n`;
|
||||
for (const topic in scalars) {
|
||||
const info = topic === "mode_tail" || topic === "mode_auto"
|
||||
? str(`${tv.mode}; continuation=${tv.continuation_mode}; ${tv.reason || "vollständiger Forecast"}`)
|
||||
: "NULL";
|
||||
sql += `INSERT INTO eos_terminal_value (run_ts, topic, data, info) VALUES ` +
|
||||
`('${runTs}', '${topic}', ${num(scalars[topic])}, ${info});\n`;
|
||||
}
|
||||
|
||||
if (curve && Array.isArray(curve.energy_wh) && curve.energy_wh.length) {
|
||||
sql += `DELETE FROM eos_terminal_value_curve WHERE run_ts = '${runTs}';\n`;
|
||||
const rows = curve.energy_wh.map((wh, i) => {
|
||||
const marginal = i < curve.marginal_euro_per_kwh.length
|
||||
? curve.marginal_euro_per_kwh[i] * 100 : null;
|
||||
const operating = Array.isArray(curve.operating_value_euro)
|
||||
? curve.operating_value_euro[i] : null;
|
||||
const continuationValue = Array.isArray(curve.continuation_value_euro)
|
||||
? curve.continuation_value_euro[i] : null;
|
||||
return `('${runTs}', ${i}, ${num(wh)}, ${num(curve.value_euro[i])}, ` +
|
||||
`${num(marginal)}, ${num(operating)}, ${num(continuationValue)})`;
|
||||
});
|
||||
sql += "INSERT INTO eos_terminal_value_curve " +
|
||||
"(run_ts, point_idx, energy_wh, value_euro, marginal_ct_kwh, " +
|
||||
"tail_operating_euro, continuation_value_euro) VALUES\n" + rows.join(",\n") + ";\n";
|
||||
}
|
||||
|
||||
const tailPlan = Array.isArray(tv.tail_plan) ? tv.tail_plan : [];
|
||||
if (tailPlan.length) {
|
||||
const slotHours = Number(diag.slot_hours) || 0.25;
|
||||
const slotMs = slotHours * 3600000;
|
||||
const planStart = new Date(Math.floor(d.getTime() / slotMs) * slotMs);
|
||||
sql += `DELETE FROM eos_tail_plan WHERE run_ts = '${runTs}';\n`;
|
||||
const rows = tailPlan.map((slot, i) => {
|
||||
const ts = new Date(planStart.getTime() + Number(slot.hour_from_start) * 3600000);
|
||||
const tsSql = `${ts.getFullYear()}-${p2(ts.getMonth() + 1)}-${p2(ts.getDate())} ` +
|
||||
`${p2(ts.getHours())}:${p2(ts.getMinutes())}:${p2(ts.getSeconds())}`;
|
||||
return `('${runTs}', '${tsSql}', ${i}, ${str(slot.action)}, ` +
|
||||
`${str(slot.alternative_action)}, ${num(slot.decision_margin_euro)}, ` +
|
||||
`${num(slot.soc_start_percentage)}, ${num(slot.soc_end_percentage)}, ` +
|
||||
`${num(slot.pv_wh)}, ${num(slot.load_wh)}, ${num(slot.grid_import_wh)}, ` +
|
||||
`${num(slot.grid_export_wh)}, ${num(slot.battery_charge_wh)}, ` +
|
||||
`${num(slot.battery_discharge_wh)}, ${num(slot.import_price_euro_per_kwh)}, ` +
|
||||
`${num(slot.feed_in_tariff_euro_per_kwh)}, ${num(slot.slot_value_euro)}, ` +
|
||||
`${num(slot.remaining_value_euro)}, ${num(slot.ac_charge_factor)}, ` +
|
||||
`${num(slot.dc_charge_allowed)}, ${num(slot.discharge_allowed)}, ` +
|
||||
`${num(slot.battery_grid_export_factor)})`;
|
||||
});
|
||||
sql += "INSERT INTO eos_tail_plan " +
|
||||
"(run_ts, timestamp, slot, action, alternative_action, decision_margin_euro, " +
|
||||
"soc_start_pct, soc_end_pct, pv_wh, load_wh, grid_import_wh, grid_export_wh, " +
|
||||
"battery_charge_wh, battery_discharge_wh, import_price_euro_kwh, " +
|
||||
"feed_in_tariff_euro_kwh, slot_value_euro, remaining_value_euro, " +
|
||||
"ac_charge_factor, dc_charge_allowed, discharge_allowed, " +
|
||||
"battery_grid_export_factor) VALUES\n" + rows.join(",\n") + ";\n";
|
||||
}
|
||||
|
||||
sql += "DELETE FROM eos_terminal_value_curve WHERE run_ts < NOW() - INTERVAL 14 DAY;\n";
|
||||
sql += "DELETE FROM eos_terminal_value WHERE run_ts < NOW() - INTERVAL 90 DAY;\n";
|
||||
sql += "DELETE FROM eos_tail_plan WHERE run_ts < NOW() - INTERVAL 14 DAY;\n";
|
||||
sql += "COMMIT;";
|
||||
msg.topic = msg.payload = sql;
|
||||
return msg;
|
||||
@@ -0,0 +1,27 @@
|
||||
CREATE TABLE IF NOT EXISTS eos_tail_plan (
|
||||
run_ts DATETIME NOT NULL,
|
||||
timestamp DATETIME NOT NULL,
|
||||
slot SMALLINT NOT NULL,
|
||||
action VARCHAR(32) NOT NULL,
|
||||
alternative_action VARCHAR(32) NULL,
|
||||
decision_margin_euro DOUBLE NULL,
|
||||
soc_start_pct DOUBLE NULL,
|
||||
soc_end_pct DOUBLE NULL,
|
||||
pv_wh DOUBLE NULL,
|
||||
load_wh DOUBLE NULL,
|
||||
grid_import_wh DOUBLE NULL,
|
||||
grid_export_wh DOUBLE NULL,
|
||||
battery_charge_wh DOUBLE NULL,
|
||||
battery_discharge_wh DOUBLE NULL,
|
||||
import_price_euro_kwh DOUBLE NULL,
|
||||
feed_in_tariff_euro_kwh DOUBLE NULL,
|
||||
slot_value_euro DOUBLE NULL,
|
||||
remaining_value_euro DOUBLE NULL,
|
||||
ac_charge_factor DOUBLE NULL,
|
||||
dc_charge_allowed TINYINT NULL,
|
||||
discharge_allowed TINYINT NULL,
|
||||
battery_grid_export_factor DOUBLE NULL,
|
||||
PRIMARY KEY (run_ts, slot),
|
||||
KEY ix_eos_tail_plan_timestamp (timestamp),
|
||||
KEY ix_eos_tail_plan_run (run_ts)
|
||||
);
|
||||
@@ -0,0 +1,133 @@
|
||||
# Control horizon and battery lookahead
|
||||
|
||||
The genetic optimizer issues controls only for `optimization.horizon_hours`,
|
||||
measured from the run start. The default is 24 hours. Battery and EV genomes,
|
||||
appliance schedules, simulation costs and returned control arrays all stop at
|
||||
that boundary. A larger prediction horizon does not add control genes.
|
||||
|
||||
```json
|
||||
{
|
||||
"optimization": {
|
||||
"horizon_hours": 24,
|
||||
"tail_horizon_hours": 48,
|
||||
"terminal_value_mode": "AUTO"
|
||||
},
|
||||
"prediction": {"hours": 72}
|
||||
}
|
||||
```
|
||||
|
||||
The forecast budget is never enforced by rejecting the configuration.
|
||||
`prediction.hours` also serves callers that do not optimize at all, so a budget
|
||||
that cannot serve the optimization horizons is reported rather than refused.
|
||||
|
||||
A `prediction.hours` below `horizon_hours + tail_horizon_hours` shortens the
|
||||
tail to `prediction.hours - horizon_hours`, logged once at INFO and reported per
|
||||
run as `effective_tail_hours`. A `prediction.hours` below `horizon_hours` is
|
||||
logged as a warning at configuration time and rejected by the optimizer itself
|
||||
when a run starts, naming the forecast series that ends too early. EOS never
|
||||
rewrites the settings; raise `prediction.hours` to use the requested horizons in
|
||||
full.
|
||||
|
||||
## Tail value and continuation
|
||||
|
||||
In AUTO mode, a deterministic dynamic program evaluates the battery state at
|
||||
the end of the control horizon against the following forecast intervals. It
|
||||
uses a 101-point SOC grid and interpolates between states. Each transition uses
|
||||
the production battery and inverter models, including SOC bounds, power caps,
|
||||
conversion losses, configured charging/export rates, PV, load, direct-marketing
|
||||
permission and LCOS on delivered DC energy.
|
||||
|
||||
The value curve is computed once per run. Genetic fitness subtracts its
|
||||
interpolated value from the simulated control cost. The older AC break-even
|
||||
penalty is disabled in TAIL mode because it does not account for future tail
|
||||
opportunities. Other feasibility penalties, including EV targets, still apply.
|
||||
|
||||
The curve may decrease with SOC: free capacity can earn money at negative
|
||||
prices. Even an empty battery can have nonzero value. Values include net tail
|
||||
cash flows and the continuation credit; this constant baseline does not affect
|
||||
which control plan wins within a run. Tail actions are never returned.
|
||||
|
||||
The existing AUTO proxy supplies the continuation value at the **effective tail
|
||||
end**. Its trailing window is controlled by `terminal_value_window_hours`. A
|
||||
window with no valuable load or export opportunities conservatively has zero
|
||||
continuation credit. With `tail_horizon_hours: 0`, AUTO uses the existing proxy
|
||||
directly at the control end. FIXED preserves the scalar terminal credit directly
|
||||
at the control end and does not solve a tail.
|
||||
|
||||
This is a deterministic approximation on a discretized SOC grid, using the
|
||||
configured action levels. It does not model forecast uncertainty or schedule
|
||||
additional EV/appliance activity beyond the control horizon.
|
||||
|
||||
## Forecast availability and API indexing
|
||||
|
||||
The four required series are load, PV, import price and feed-in tariff. Every
|
||||
control interval must contain a finite value. Missing control data rejects the
|
||||
run. The tail stops at the first missing value in any required series, logs a
|
||||
warning and moves continuation to that point. No missing price becomes zero.
|
||||
Provider interval values are held within their source interval, never extended
|
||||
indefinitely beyond the last observed forecast timestamp.
|
||||
|
||||
Legacy API forecast arrays still begin at midnight of the run's start day.
|
||||
They must therefore include the elapsed prefix plus the desired forecast
|
||||
coverage from now. The prefix is removed before optimization. Declare
|
||||
`forecast_interval_seconds: 900` for shortened native quarter-hour inputs;
|
||||
hourly inputs use `3600`. Fully sized native arrays remain auto-detected for
|
||||
compatibility. Scalar feed-in tariffs represent an explicitly constant tariff.
|
||||
|
||||
New solutions set `controls_start_at_now: true`: index zero of every returned
|
||||
control array and warm-start genome corresponds to the run timestamp. The
|
||||
generic solution and plan adapters still understand older, midnight-indexed
|
||||
solutions where this flag is absent. Incompatible old genome lengths are
|
||||
discarded by warm-start validation.
|
||||
|
||||
`terminal_value` reports the mode (`TAIL`, `AUTO`, or `FIXED`), usable remaining
|
||||
AC energy, control hours, requested/effective tail hours, continuation mode and
|
||||
any degradation reason. `tail_end_hour` is elapsed hours from the run start.
|
||||
FIXED mode reports zero effective tail hours.
|
||||
|
||||
The credited amount is explicitly decomposed:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "TAIL",
|
||||
"battery_energy_wh": 5230,
|
||||
"credited_euro": 1.84,
|
||||
"tail_operating_euro": 1.21,
|
||||
"continuation_value_euro": 0.63,
|
||||
"control_horizon_hours": 24,
|
||||
"requested_tail_hours": 48,
|
||||
"effective_tail_hours": 48,
|
||||
"tail_end_hour": 72,
|
||||
"continuation_mode": "AUTO",
|
||||
"curve": {
|
||||
"energy_wh": [],
|
||||
"value_euro": [],
|
||||
"operating_value_euro": [],
|
||||
"continuation_value_euro": [],
|
||||
"marginal_euro_per_kwh": []
|
||||
},
|
||||
"continuation_curve": {
|
||||
"energy_wh": [],
|
||||
"value_euro": [],
|
||||
"marginal_euro_per_kwh": []
|
||||
},
|
||||
"tail_diagnostics": {
|
||||
"slots": 192,
|
||||
"slot_hours": 0.25,
|
||||
"soc_grid_points": 101,
|
||||
"min_import_price_euro_per_kwh": -0.08,
|
||||
"max_import_price_euro_per_kwh": 0.34,
|
||||
"min_feed_in_tariff_euro_per_kwh": 0.0,
|
||||
"max_feed_in_tariff_euro_per_kwh": 0.29,
|
||||
"negative_import_price_slots": 4,
|
||||
"positive_battery_export_slots": 160
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`credited_euro` always equals `tail_operating_euro +
|
||||
continuation_value_euro`. The combined `curve` is the function read by genetic
|
||||
fitness. It contains the same decomposition at every SOC breakpoint.
|
||||
`continuation_curve` is the proxy at the effective tail end before the dynamic
|
||||
program folds the tail backwards onto it. Tail diagnostics summarize the actual
|
||||
forecast segment; they do not contain executable actions.
|
||||
@@ -33,6 +33,7 @@ develop/update.md
|
||||
develop/revert.md
|
||||
akkudoktoreos/adapter/adapterhomeassistant.md
|
||||
akkudoktoreos/adapter/adapternodered.md
|
||||
akkudoktoreos/grafana_tail_debugging.md
|
||||
|
||||
```
|
||||
|
||||
@@ -43,6 +44,7 @@ akkudoktoreos/adapter/adapternodered.md
|
||||
akkudoktoreos/architecture.md
|
||||
akkudoktoreos/configuration.md
|
||||
akkudoktoreos/configtimewindow.md
|
||||
akkudoktoreos/optimization_horizons.md
|
||||
akkudoktoreos/optimpost.md
|
||||
akkudoktoreos/optimauto.md
|
||||
akkudoktoreos/resource.md
|
||||
|
||||
Reference in New Issue
Block a user