Tibber, aWATTar, Ostrom — dynamische Stromtarife zeigen dir den Preis für heute und morgen. Das reicht, um grob zu wissen, wann es teuer wird. Aber wenn du ein E-Auto, eine Wärmepumpe oder einen Hausspeicher hast, willst du weiter vorausplanen. Genau da kommt energyforecast.de ins Spiel. Die Plattform prognostiziert EPEX-Spot-Preise per Machine Learning für bis zu 6 Tage, viertelstündlich aktualisiert. Ich habe mir das Ganze sieben Tage lang selbst angeschaut, mit meinen echten Tibber-Preisen abgeglichen und in Home Assistant eingebunden.
Was energyforecast.de eigentlich macht
Der Grundgedanke ist simpel: Tibber und energyforecast basieren beide auf dem EPEX-Spot-Markt. Die Prognose zeigt dir also quasi voraus, wohin dein Tibber-Preis läuft. Tag 1 ist dabei wahnsinnig genau, oft sind das schon echte EPEX-Preise. Ab Tag 3 wird es zur Richtungsprognose. Das ist gut genug, um zu wissen, ob sich das Laden grob lohnt — aber nicht auf die Kommastelle genau.
In der Oberfläche kannst du zwischen dem reinen Börsenpreis und dem Gesamtpreis inkl. Fixkosten und Mehrwertsteuer wechseln. Deine Netzentgelte und Aufschläge trägst du einmal im Profil ein, die Mehrwertsteuer liegt für Deutschland bei 19 %. Unterstützte Marktzonen sind neben DE-LU auch Österreich, Frankreich, Niederlande, Belgien, Polen und Dänemark.
Bei meinem Test über 7 Tage lag die Treffergenauigkeit an manchen Tagen bei über 86 %. Der Prognosefehler war mal 2 Cent, mal knapp 4 Cent pro Kilowattstunde. Kein Weltuntergang — denn es geht darum zu wissen, wann der Strom günstig ist, nicht auf den Bruchteil genau was er kostet.
Was das kostet und was du brauchst
Es gibt ein Free-Modell mit 2 Tagen Vorausschau und 50 API-Anfragen pro Tag. Für die meisten Anwendungsfälle reicht die Pro-Version mit 6 Tagen und 500 Anfragen täglich. Die kostet 14,99 € im Jahr, mit dem Code ALLES im ersten Jahr 11,99 €. Wer sein 40-kWh-Auto wöchentlich zur günstigsten Zeit lädt, spart laut Plattform typischerweise rund 50 € jährlich — die Rechnung ist dann ziemlich einfach.
Für die Integration in Home Assistant brauchst du:
- Home Assistant mit HACS
- Einen dynamischen Stromtarif auf EPEX-Basis (Tibber, aWATTar, Ostrom, Rabot u.a.) — mit Festpreistarif bringt das Ganze nichts
- Einen Account bei energyforecast.de und einen API-Key aus den Account-Einstellungen
- Das EPEX-Spot-Plugin von mampfes aus HACS
Weg 1: Plugin (ohne YAML, für die meisten)
Das EPEX-Spot-Plugin installierst du über HACS unter Integrationen, dann einen Neustart, danach über Einstellungen → Geräte & Dienste → Integration hinzufügen → „EPEX Spot". Als Datenquelle wählst du Energyforecast.de, trägst deinen API-Key ein und wählst die Marktzone DE-LU. Das Plugin liefert dir direkt einen Preis-Sensor mit Prognose-Attributen, günstigste Zeiten, Tagesdurchschnitt und mehr — ganz ohne eine Zeile YAML zu schreiben.
Wenn dir das reicht, bist du hier fertig. Die folgenden Abschnitte zeigen den zweiten Weg mit direktem REST-Sensor, der mehr Kontrolle und eigenes Prognose-Tracking ermöglicht.
Weg 2: REST-Sensor direkt (für eigenes Tracking)
Ich habe den direkten API-Weg gewählt, weil ich die Prognosegenauigkeit selbst messen wollte. Das Ganze lege ich als Package an. Dafür muss in der configuration.yaml stehen:
homeassistant:
packages: !include_dir_named packagesDer gesamte Code kommt dann in eine Datei /config/packages/energyforecast_tracking.yaml. Den API-Key lege ich sicher in der secrets.yaml ab:
ENERGY_FORECAST_TOKEN: "DEIN_API_KEY"Der REST-Sensor holt die Rohdaten alle 2 Stunden — das ist locker genug für das API-Limit von 500 Anfragen pro Tag:
1rest:
2 - resource: https://www.energyforecast.de/api/v2/forecast
3 scan_interval: 7200 # alle 2h, Limit ist 500 Requests/Tag
4 timeout: 30
5 params:
6 token: !secret ENERGY_FORECAST_TOKEN
7 market_zone: DE-LU
8 # fixed_cost_cent: 15 # optional: Netzentgelte/Aufschläge in ct/kWh, sonst Profil-Wert
9 # vat: 19 # optional: MwSt in %, sonst Profil-Wert
10 headers:
11 Accept: application/json
12 sensor:
13 - name: Energyforecast Rohdaten
14 unique_id: energyforecast_rohdaten
15 value_template: "{{ value_json.generated_at }}"
16 device_class: timestamp
17 json_attributes:
18 - data # Viertelstunden: start/end/price_ct_kwh/total_ct_kwh/price_origin
19 - plan
20 - valid_until
21 - market_zoneWichtig: sensor.energyforecast_rohdaten gehört in den Recorder-Ausschluss. Die data-Liste enthält 672 Viertelstunden-Slots und würde die Datenbank sonst ordentlich aufblähen:
recorder:
exclude:
entities:
- sensor.energyforecast_rohdatenKennzahlen als Template-Sensoren
Aus den Rohdaten ziehe ich mir ein paar nützliche Kennzahlen als Template-Sensoren. Tagesdurchschnitt, günstigster und teuerster Preis heute, der Prognoseschnitt für morgen — und fürs Tracking noch den Prognosefehler und die Treffergenauigkeit:
1template:
2 - sensor:
3 - name: "EF Schnitt heute"
4 unique_id: ef_schnitt_heute
5 unit_of_measurement: "ct/kWh"
6 state_class: measurement
7 icon: mdi:cash
8 availability: "{{ state_attr('sensor.energyforecast_rohdaten','data') | default([]) | count > 0 }}"
9 state: >
10 {% set d = state_attr('sensor.energyforecast_rohdaten','data') %}
11 {% set today = now().strftime('%Y-%m-%d') %}
12 {% set v = d | selectattr('start','match', today)
13 | map(attribute='total_ct_kwh') | list %}
14 {{ (v | sum / v | count) | round(2) if v | count > 0 else none }}
15
16 - name: "EF realisiert heute"
17 unique_id: ef_realisiert_heute
18 unit_of_measurement: "ct/kWh"
19 state_class: measurement
20 icon: mdi:check-decagram
21 availability: "{{ state_attr('sensor.energyforecast_rohdaten','data') | default([]) | count > 0 }}"
22 state: >
23 {% set d = state_attr('sensor.energyforecast_rohdaten','data') %}
24 {% set today = now().strftime('%Y-%m-%d') %}
25 {% set v = d | selectattr('start','match', today)
26 | selectattr('price_origin','eq','market')
27 | map(attribute='total_ct_kwh') | list %}
28 {{ (v | sum / v | count) | round(2) if v | count > 0 else none }}
29
30 - name: "EF Prognose morgen Schnitt"
31 unique_id: ef_prognose_morgen_schnitt
32 unit_of_measurement: "ct/kWh"
33 icon: mdi:weather-sunny-alert
34 availability: "{{ state_attr('sensor.energyforecast_rohdaten','data') | default([]) | count > 0 }}"
35 state: >
36 {% set d = state_attr('sensor.energyforecast_rohdaten','data') %}
37 {% set day = (now() + timedelta(days=1)).strftime('%Y-%m-%d') %}
38 {% set v = d | selectattr('start','match', day)
39 | map(attribute='total_ct_kwh') | list %}
40 {{ (v | sum / v | count) | round(2) if v | count > 0 else none }}
41
42 - name: "EF Min heute"
43 unique_id: ef_min_heute
44 unit_of_measurement: "ct/kWh"
45 icon: mdi:arrow-down-bold
46 availability: "{{ state_attr('sensor.energyforecast_rohdaten','data') | default([]) | count > 0 }}"
47 state: >
48 {% set d = state_attr('sensor.energyforecast_rohdaten','data') %}
49 {% set today = now().strftime('%Y-%m-%d') %}
50 {% set v = d | selectattr('start','match', today)
51 | map(attribute='total_ct_kwh') | list %}
52 {{ v | min | round(2) if v | count > 0 else none }}
53
54 - name: "EF Max heute"
55 unique_id: ef_max_heute
56 unit_of_measurement: "ct/kWh"
57 icon: mdi:arrow-up-bold
58 availability: "{{ state_attr('sensor.energyforecast_rohdaten','data') | default([]) | count > 0 }}"
59 state: >
60 {% set d = state_attr('sensor.energyforecast_rohdaten','data') %}
61 {% set today = now().strftime('%Y-%m-%d') %}
62 {% set v = d | selectattr('start','match', today)
63 | map(attribute='total_ct_kwh') | list %}
64 {{ v | max | round(2) if v | count > 0 else none }}
65
66 - name: "EF Prognosefehler heute"
67 unique_id: ef_prognosefehler_heute
68 unit_of_measurement: "ct/kWh"
69 state_class: measurement
70 icon: mdi:target
71 availability: >
72 {{ has_value('sensor.ef_realisiert_heute')
73 and states('input_number.ef_prognose_fuer_heute') | float(-50) > -49 }}
74 state: >
75 {% set ist = states('sensor.ef_realisiert_heute') | float(0) %}
76 {% set prog = states('input_number.ef_prognose_fuer_heute') | float(0) %}
77 {{ (prog - ist) | abs | round(2) }}
78
79 - name: "EF Treffergenauigkeit heute"
80 unique_id: ef_treffergenauigkeit_heute
81 unit_of_measurement: "%"
82 icon: mdi:bullseye-arrow
83 availability: >
84 {{ has_value('sensor.ef_realisiert_heute')
85 and states('input_number.ef_prognose_fuer_heute') | float(-50) > -49 }}
86 state: >
87 {% set ist = states('sensor.ef_realisiert_heute') | float(0) %}
88 {% set prog = states('input_number.ef_prognose_fuer_heute') | float(0) %}
89 {% if (ist | abs) > 0.5 %}
90 {{ [0, (100 - ((prog - ist) | abs / (ist | abs) * 100))] | max | round(1) }}
91 {% else %}
92 {{ none }}
93 {% endif %}Das Feld price_origin im Datenpunkt zeigt dir außerdem, ob du gerade einen echten EPEX-Spotpreis (market) oder eine ML-Prognose (forecast) siehst. Im Dashboard kannst du damit die beiden Kurven farblich trennen.
Der „Jetzt günstig?"-Sensor
Das ist eigentlich der wichtigste Teil für alle Automationen. Ein Binary Sensor prüft alle 5 Minuten, ob die aktuelle Viertelstunde zu den 12 günstigsten der nächsten 24 Stunden gehört — das entspricht einem 3-Stunden-Fenster. Wenn ja, schaltet er auf on und alle verknüpften Automationen können loslegen:
1template:
2 - trigger:
3 - trigger: time_pattern
4 minutes: "/5"
5 - trigger: state
6 entity_id: sensor.energyforecast_rohdaten
7 binary_sensor:
8 - name: "EF jetzt günstig"
9 unique_id: ef_jetzt_guenstig
10 icon: mdi:flash
11 state: >
12 {% set d = state_attr('sensor.energyforecast_rohdaten','data') %}
13 {% if not d %}{{ false }}
14 {% else %}
15 {% set now_ts = as_timestamp(now()) %}
16 {% set ns = namespace(items=[]) %}
17 {% for x in d if now_ts <= as_timestamp(x.start) < now_ts + 86400 %}
18 {% set ns.items = ns.items + [x] %}
19 {% endfor %}
20 {% set cheap = (ns.items | sort(attribute='total_ct_kwh'))[:12] | map(attribute='start') | list %}
21 {% set ns2 = namespace(on=false) %}
22 {% for st in cheap if as_timestamp(st) <= now_ts < as_timestamp(st) + 900 %}
23 {% set ns2.on = true %}
24 {% endfor %}
25 {{ ns2.on }}
26 {% endif %}
27 attributes:
28 fenster_anzahl_slots: "12 (= 3h) der günstigsten in 24h"Dazu kommt noch ein Sensor für den günstigsten Zeitpunkt in den nächsten 24 Stunden:
1template:
2 - sensor:
3 - name: "EF günstigste Zeit 24h"
4 unique_id: ef_guenstigste_zeit_24h
5 device_class: timestamp
6 icon: mdi:clock-star-four-points
7 availability: "{{ state_attr('sensor.energyforecast_rohdaten','data') | default([]) | count > 0 }}"
8 state: >
9 {% set d = state_attr('sensor.energyforecast_rohdaten','data') %}
10 {% set now_ts = as_timestamp(now()) %}
11 {% set ns = namespace(items=[]) %}
12 {% for x in d if now_ts <= as_timestamp(x.start) < now_ts + 86400 %}
13 {% set ns.items = ns.items + [x] %}
14 {% endfor %}
15 {% if ns.items | count > 0 %}
16 {% set best = ns.items | sort(attribute='total_ct_kwh') | first %}
17 {{ as_datetime(best.start) }}
18 {% else %}
19 {{ none }}
20 {% endif %}
21 attributes:
22 preis_ct_kwh: >
23 {% set d = state_attr('sensor.energyforecast_rohdaten','data') %}
24 {% set now_ts = as_timestamp(now()) %}
25 {% set ns = namespace(items=[]) %}
26 {% for x in d if now_ts <= as_timestamp(x.start) < now_ts + 86400 %}
27 {% set ns.items = ns.items + [x] %}
28 {% endfor %}
29 {% if ns.items | count > 0 %}
30 {{ (ns.items | sort(attribute='total_ct_kwh') | first).total_ct_kwh | round(2) }}
31 {% endif %}Prognose-Tracking einrichten
Damit ich wirklich messen kann, wie gut die Prognose ist, brauche ich zwei Input-Number-Helfer und zwei Automationen. Die Logik: Jeden Tag um 11:00 wird der Prognose-Schnitt für morgen gespeichert — zu diesem Zeitpunkt ist das noch echte ML-Prognose, weil EPEX die echten Preise für den nächsten Tag erst gegen 13:00 Uhr veröffentlicht. Um 00:05 wird die gestrige Prognose dann auf „heute" verschoben und mit dem realen Tagesdurchschnitt verglichen.
1input_number:
2 ef_prognose_fuer_morgen:
3 name: EF Prognose für morgen (Schnitt)
4 min: -50
5 max: 200
6 step: 0.01
7 unit_of_measurement: "ct/kWh"
8 icon: mdi:crystal-ball
9 ef_prognose_fuer_heute:
10 name: EF Prognose für heute (gestern erstellt)
11 min: -50
12 max: 200
13 step: 0.01
14 unit_of_measurement: "ct/kWh"
15 icon: mdi:calendar-check
16
17automation:
18 - id: ef_shift_prognose_auf_heute
19 alias: "EF: Prognose auf heute übernehmen (00:05)"
20 description: >
21 Übernimmt frühmorgens die gestern um 11:00 erstellte Prognose für heute,
22 BEVOR sie um 11:00 mit der Prognose für morgen überschrieben wird.
23 mode: single
24 triggers:
25 - trigger: time
26 at: "00:05:00"
27 conditions:
28 - condition: numeric_state
29 entity_id: input_number.ef_prognose_fuer_morgen
30 above: -49
31 actions:
32 - action: input_number.set_value
33 target:
34 entity_id: input_number.ef_prognose_fuer_heute
35 data:
36 value: "{{ states('input_number.ef_prognose_fuer_morgen') | float(0) }}"
37
38 - id: ef_snapshot_prognose_morgen
39 alias: "EF: Prognose für morgen sichern (11:00)"
40 description: >
41 Speichert um 11:00 (vor der EPEX-Veröffentlichung ~13:00) den
42 Prognose-Tagesschnitt für MORGEN. Zu diesem Zeitpunkt ist morgen noch
43 echte ML-Prognose, nicht der bereits feststehende market-Preis.
44 mode: single
45 triggers:
46 - trigger: time
47 at: "11:00:00"
48 conditions:
49 - condition: numeric_state
50 entity_id: sensor.ef_prognose_morgen_schnitt
51 above: -49
52 actions:
53 - action: input_number.set_value
54 target:
55 entity_id: input_number.ef_prognose_fuer_morgen
56 data:
57 value: "{{ states('sensor.ef_prognose_morgen_schnitt') | float(0) }}"Den gleitenden 7-Tage-Durchschnitt des Prognosefehlers (MAE) berechnet ein Statistik-Sensor:
1sensor:
2 - platform: statistics
3 name: "EF MAE 7 Tage"
4 unique_id: ef_mae_7tage
5 entity_id: sensor.ef_prognosefehler_heute
6 state_characteristic: mean
7 max_age:
8 days: 7
9 sampling_size: 100Nach dem Einfügen des gesamten Codes: Entwicklerwerkzeuge → YAML → Konfiguration prüfen → Neu starten. Der Neustart dauert 1-2 Minuten. Damit du nicht zwei Tage auf den ersten Wert wartest, kannst du den Helfer direkt nach dem Neustart einmal manuell befüllen — über Entwicklerwerkzeuge → Aktionen:
action: input_number.set_value
target:
entity_id: input_number.ef_prognose_fuer_morgen
data:
value: "{{ states('sensor.ef_prognose_morgen_schnitt') }}"Ab dann läuft alles automatisch.
Automationen: Verbraucher sinnvoll steuern
Mit dem Binary Sensor binary_sensor.ef_jetzt_gunstig kannst du jetzt Verbraucher ganz einfach in günstige Stromfenster legen. Ein paar Beispiele:
Push-Benachrichtigung mit der günstigsten Zeit morgens:
1automation:
2 - id: ef_push_guenstigste_zeit
3 alias: "EF: Push günstigste Zeit (08:00)"
4 mode: single
5 triggers:
6 - trigger: time
7 at: "08:00:00"
8 actions:
9 - if:
10 - condition: template
11 value_template: "{{ has_value('sensor.ef_gunstigste_zeit_24h') }}"
12 then:
13 - action: notify.mobile_app_DEIN_HANDY
14 data:
15 title: "Günstigster Strom heute"
16 message: >
17 Am günstigsten ab
18 {{ as_timestamp(states('sensor.ef_gunstigste_zeit_24h')) | timestamp_custom('%H:%M') }} Uhr
19 ({{ state_attr('sensor.ef_gunstigste_zeit_24h','preis_ct_kwh') }} ct/kWh).
20 Spanne heute: {{ states('sensor.ef_min_heute') }} bis {{ states('sensor.ef_max_heute') }} ct.Einen Smart Plug bei günstigem Strom schalten:
1automation:
2 - id: ef_smartplug_guenstig
3 alias: "EF: Verbraucher bei günstigem Strom"
4 mode: single
5 triggers:
6 - trigger: state
7 entity_id: binary_sensor.ef_jetzt_gunstig
8 actions:
9 - choose:
10 - conditions:
11 - condition: state
12 entity_id: binary_sensor.ef_jetzt_gunstig
13 state: "on"
14 sequence:
15 - action: switch.turn_on
16 target:
17 entity_id: switch.DEINE_STECKDOSE # z.B. switch.boiler
18 default:
19 - action: switch.turn_off
20 target:
21 entity_id: switch.DEINE_STECKDOSEWärmepumpe mit Warmwasser-Boost im Preistief:
1automation:
2 - id: ef_wp_warmwasser_boost
3 alias: "EF: Wärmepumpe Warmwasser im Tief"
4 mode: single
5 triggers:
6 - trigger: state
7 entity_id: binary_sensor.ef_jetzt_gunstig
8 to: "on"
9 conditions:
10 - condition: time
11 after: "06:00:00"
12 before: "16:00:00"
13 actions:
14 # NIBE/myUplink: select.select_option auf select.DEINE_ANLAGE_more_hot_water, option: "3 hours"
15 # Generisch (water_heater):
16 - action: water_heater.set_operation_mode
17 target:
18 entity_id: water_heater.DEINE_WAERMEPUMPE
19 data:
20 operation_mode: "performance"
21 - delay: "02:00:00"
22 - action: water_heater.set_operation_mode
23 target:
24 entity_id: water_heater.DEINE_WAERMEPUMPE
25 data:
26 operation_mode: "eco"Hausspeicher bei Tiefpreis nachladen:
1automation:
2 - id: ef_speicher_netzladen
3 alias: "EF: Speicher bei Tiefpreis nachladen"
4 mode: single
5 triggers:
6 - trigger: state
7 entity_id: binary_sensor.ef_jetzt_gunstig
8 to: "on"
9 conditions:
10 - condition: numeric_state
11 entity_id: sensor.DEIN_BATTERIE_SOC # Batterie-Ladezustand in %
12 below: 30
13 - condition: numeric_state
14 entity_id: sensor.ef_min_heute
15 below: 20 # nur wenn das Tief wirklich tief ist
16 actions:
17 - action: notify.mobile_app_DEIN_HANDY
18 data:
19 title: "Speicher-Nachladen sinnvoll"
20 message: >
21 Speicher bei {{ states('sensor.DEIN_BATTERIE_SOC') }} %,
22 Strom gerade günstig. Jetzt aus dem Netz nachladen.
23 # Hier den Service deines Wechselrichters zum Netzladen einfügen.
24 # Netzladen funktioniert nur, wenn dein Wechselrichter das unterstützt.E-Auto im günstigen Fenster laden (als Vorlage, Wallbox-Steuerung ist je nach Modell unterschiedlich):
1automation:
2 - id: ef_auto_laden_beispiel
3 alias: "EF: E-Auto im günstigen Fenster laden"
4 mode: single
5 triggers:
6 - trigger: state
7 entity_id: binary_sensor.ef_jetzt_gunstig
8 conditions:
9 - condition: state
10 entity_id: binary_sensor.DEIN_AUTO_STECKER # Kabel steckt
11 state: "on"
12 - condition: numeric_state
13 entity_id: sensor.DEIN_AUTO_SOC # Ladezustand %
14 below: 80
15 actions:
16 - choose:
17 - conditions:
18 - condition: state
19 entity_id: binary_sensor.ef_jetzt_gunstig
20 state: "on"
21 sequence:
22 - action: switch.turn_on
23 target:
24 entity_id: switch.DEINE_WALLBOX_LADEN
25 default:
26 - action: switch.turn_off
27 target:
28 entity_id: switch.DEINE_WALLBOX_LADENEin Hinweis zur Wallbox: Bei Tesla Gen-3 Wall Connector ist die Fleet-API auf 16 A gedeckelt und read-only. Dafür ist EVCC die sauberere Lösung — dazu kommt demnächst ein eigener Beitrag.
Was du nach dem Neustart prüfen solltest
Unter Entwicklerwerkzeuge → Zustände einfach nach energyforecast und ef_ suchen. sensor.energyforecast_rohdaten sollte nach spätestens 30 Sekunden einen Timestamp als State zeigen. Die Template-Sensoren wie ef_schnitt_heute oder ef_min_heute brauchen noch ein paar Minuten, dann sollten dort Werte in ct/kWh stehen. binary_sensor.ef_jetzt_gunstig zeigt on oder off.
Am ersten Tag zeigen ef_prognosefehler, ef_treffergenauigkeit und ef_mae_7_tage noch „unavailable" — das ist gewollt. Die Guards warten auf echte Daten. Erst nach dem ersten 00:05-Shift am nächsten Morgen werden die Treffergenauigkeitswerte berechnet. Und falls du im Recorder-Include-Modus bist: Die ef_-Entities müssen explizit in die Allowlist, sensor.energyforecast_rohdaten dagegen bewusst nicht.
Der Unterschied zwischen günstigstem und teuerstem Strompreis war bei mir an manchen Tagen über 20 Cent pro Kilowattstunde. Das klingt erst mal nach wenig, aber bei einem E-Auto mit 40 kWh Akku macht das eben schon einen zweistelligen Eurobetrag aus — und das nur für eine einzige Ladung.
Nutzt du schon einen dynamischen Stromtarif und willst weiter in die Zukunft planen? Ich bin gespannt, was du dir davon versprichst und ob du vielleicht sogar schon ähnliche Lösungen im Einsatz hast.
