PV-Batterie clever nutzen bei dynamischen Strompreisen

Wie du besonders in den Wintermonaten mit Home Assistant bares Geld sparen kannst

1. Warum dynamische Strompreise immer wichtiger werden

Die Energiepreise schwanken heutzutage stark. Mit dynamischen Tarifen kann man zeitweise sehr günstigen Strom beziehen, aber eben auch zu teuren Spitzenzeiten viel zahlen. Wer eine PV-Anlage mit Batterie besitzt, kann diese Schwankungen geschickt ausnutzen: Lade die Batterie bei niedrigen Preisen und entlade sie, wenn der Strom teuer ist – so sparst du bares Geld, besonders in den Wintermonaten, wenn die eigene PV-Leistung meist nicht ausreicht.

2. Wie funktioniert das Zusammenspiel von PV-Batterie und dynamischen Strompreisen?

Photovoltaik-Erzeugung: Deine PV-Anlage produziert tagsüber – insbesondere im Sommer – Strom, den du teilweise direkt verbrauchst und überschüssig ins Stromnetz einspeist oder in die Batterie speicherst. Je nach Dimensionierung von PV und Batterie wird die Batterie in den Wintermonaten typischerweise aber kaum geladen.

Dynamischer Stromtarif: Anbieter wie etwa Tibber veröffentlichen stündliche Preise für den kommenden Tag. So kannst du sehen, wann Strom besonders günstig (z. B. nachts oder mittags) oder teuer (z. B. morgens oder in den frühen Abendstunden) ist. Siehe den Screenshot aus der Tibber App mit einem typischen Preisverlauf.

Batterie-Strategie:

Laden: Nutze die günstigen Zeitfenster, um deine Batterie (zusätzlich zur eigenen PV-Produktion) zu füllen.

Entladen: Stell sicher, dass die Batterie in teuren Zeitfenstern zur Verfügung steht, um deinen Haushaltsbedarf zu decken. So musst du keinen teuren Netzstrom kaufen.

Gerade in den Wintermonaten, wenn die PV-Erzeugung geringer ausfällt, bewährt sich diese Strategie: Du kannst gezielt bei niedrigen Preisen in den dunklen Stunden (z. B. nachts) deinen Akku laden und tagsüber den Bezug teuren Netzstroms minimieren.

Tibber Preisbeispiel mit hohen und niedrigen Preisen

3. Setup in Home Assistant: Schritt-für-Schritt-Anleitung

Um dein Energiemanagement mit Home Assistant zu automatisieren, brauchst du:

  1. Home Assistant (z. B. auf einem Raspberry Pi oder einem Mini-PC installiert).
  2. PV- und Batterie-Integration: Stelle sicher, dass du die Entitäten für deine PV-Anlage (Wechselrichter) und deine Batterie (Ladezustand, usw.) in Home Assistant eingebunden hast. Ebenfalls muss dein Wechselrichter dies unterstützen. Ich nutze einen GoodWe GW5048D-ES mit dieser Integration: GoodWe solar inverter for Home Assistant (experimental). Damit kann ich den „Inverter operation mode“ direkt in HomeAssistant ändern. Zum Laden der Batterie nutze ich den Modus „Eco charge mode“ und zum Endladen den Standard-Modus_ „general mode“. Je nach Wechselrichter unterscheiden sich hier die Möglichkeiten.
  3. Dynamische Strompreise:
    • Ich nutze Tibber, die Integration kannst du direkt in Home Assistant konfigurieren. Andere Dienste wie Awattar haben ebenfalls Community-Integrationen.
  4. Helper in Home Assistant:
    • input_datetime.batterie_ladestart und input_datetime.batterie_ladestop für die Ladezeit.
    • input_datetime.batterie_entladestart und input_datetime.batterie_entladestop für die Entladezeit.
    • input_text.tibber_strom_daily_update als Text-Helfer, um eine tägliche Zusammenfassung anzuzeigen.

3.1 Grundlegende Einrichtung der Helper und Template Sensoren

Die Helper dienen dazu, die besten Lade- und Entladezeiten zu speichern und die Templatesensoren brauchen wir, um diese Zeiten später in einer Automation zu nutzen.

Füge in deiner configuration.yaml (oder über das UI) Folgendes hinzu (Beispiel):

input_datetime:
  batterie_ladestart:
    name: "Batterie Ladestart"
    has_date: true
    has_time: true
  batterie_ladestop:
    name: "Batterie Ladestopp"
    has_date: true
    has_time: true
  batterie_entladestart:
    name: "Batterie Entladestart"
    has_date: true
    has_time: true
  batterie_entladestop:
    name: "Batterie Entladestopp"
    has_date: true
    has_time: true

input_text:
  tibber_strom_daily_update:
    name: "Tibber Strom Daily Update"
    max: 1024
    multiline: true

template:
  - binary_sensor:
      # 1) Batterie-Lade-Sensor
      - name: "Batterie Laden aktiv"
        unique_id: "binary_sensor.batterie_laden_aktiv"
        state: >
          {% set start_str = states('input_datetime.batterie_ladestart') %}
          {% set stop_str  = states('input_datetime.batterie_ladestop') %}
          {% if start_str in ['','unknown','unavailable'] or stop_str in ['','unknown','unavailable'] %}
            false
          {% else %}
            {% set start_dt = (start_str | as_datetime) | as_local %}
            {% set stop_dt  = (stop_str  | as_datetime) | as_local %}
            {% set now_local = now() | as_local %}
            {{ now_local >= start_dt and now_local < stop_dt }}
          {% endif %}

      # 2) Batterie-Entlade-Sensor
      - name: "Batterie Entladen aktiv"
        unique_id: "binary_sensor.batterie_entladen_aktiv"
        state: >
          {% set start_str = states('input_datetime.batterie_entladestart') %}
          {% set stop_str  = states('input_datetime.batterie_entladestop') %}
          {% if start_str in ['','unknown','unavailable'] or stop_str in ['','unknown','unavailable'] %}
            false
          {% else %}
            {% set start_dt = (start_str | as_datetime) | as_local %}
            {% set stop_dt  = (stop_str  | as_datetime) | as_local %}
            {% set now_local = now() | as_local %}
            {{ now_local >= start_dt and now_local < stop_dt }}
          {% endif %}

Damit kannst du später automatisiert Lade- und Entladezeiten setzen und einen Text-Helfer anzeigen.

Für diese Werte kann man schonmal eine Entity-Karte im Dashboard anlegen:

Die beiden Binary Sensoren werten die Zeitfenster für günstigen und teuren Strom aus.

Wenn wir uns aktuell in dem „Strom günstig“ Zeitfenster befinden, wird „Batterie Laden aktiv“, auf „ON“ gesetzt.
Ebenso wird „Batterie Entladen aktiv“ auf „ON“ gesetzt, wenn wir uns in dem teuren Zeitfenster befinden.

Diese Sensoren können wir dann in einer Automation verwenden und den Wechselrichter entsprechend schalten.

3.2 Skript für dynamische Zeitplanung

Erstelle über EinstellungenAutomationen & SkripteSkripte (oder in scripts.yaml) ein Skript, das:

  • Die Preisdaten (z. B. von Tibber) abruft
  • Ein günstiges Zeitfenster für das Laden deiner Batterie berechnet (z. B. 5 Stunden am Stück)
  • Ein teures Zeitfenster für das Entladen ermittelt (z. B. min. 4h, max. 12h)
  • Entsprechend die input_datetime-Helper für Lade- und Entladefenster setzt
  • Eine Benachrichtigung generiert und auch in den Text-Helper (input_text.tibber_strom_daily_update) schreibt.

So sieht das Skript bei mir aus, die Variablen usw. können nach Bedarf angepasst werden.

alias: PV Batterie Zeitplanung mit Tibber
mode: single
sequence:
  ###################################################################
  # 1) Tibber-Preise abrufen (für nächsten Tag)
  ###################################################################
  - data:
      start: "{{ now().isoformat() }}"
      end: "{{ (now() + timedelta(days=1)).isoformat() }}"
    response_variable: tibber_data
    action: tibber.get_prices

  ###################################################################
  # 2) Variablen definieren
  #
  #    - cheap_hours: Anzahl Stunden für das "günstige" Zeitfenster.
  #    - expensive_hours_min / expensive_hours_max: erlaubter Bereich
  #      in Stunden für das "teure" Zeitfenster (z. B. 4..12).
  #
  #    - price_diff_threshold, price_ceiling, price_unload_threshold
  #      steuern das Lade-/Entladeverhalten.
  #
  #    - Die restliche Logik (Vergleich, Input Datetimes etc.) bleibt gleich.
  ###################################################################
  - variables:
      cheap_hours: 5           # <-- Günstiges Fenster: z. B. 5 Stunden(So lange braucht man Wechselrichter, um die Batterie vollzuladen
      expensive_hours_min: 4   # <-- Teures Fenster: minimal 4 Stunden
      expensive_hours_max: 12  # <-- Teures Fenster: maximal 12 Stunden

      price_diff_threshold: 0.1 # <-- Das Laden und Entladen der Batterie lohnt sich nur, wenn die Preisdifferenz zwischen günstigem
                                # und teurem Zeitfenster mindestens 10 Cent beträgt 

      price_ceiling: 0.25       # <-- Lade die Batterie nur, wenn das günstige Zeitfenster unter 25 Cent liegt
      price_unload_threshold: 0.33 # <-- Entlade die Batterie nur, wenn das teure Zeitfenster über 33 Cent liegt

      sorted_prices: "{{ tibber_data.prices.westen | sort(attribute='start_time') }}"
      highest_price: "{{ sorted_prices | max(attribute='price') }}"

      ################################################################
      # 2a) Günstiges Zeitfenster (cheap_hours)
      #
      #    Wir prüfen alle Blöcke à cheap_hours (z. B. 5),
      #    suchen die minimalste Summe. Daraus berechnen wir
      #    den Durchschnitt (sum / cheap_hours).
      ################################################################
      best_cheap_window: >-
        {% set ns = namespace(total=9999999, start='', stop='') %}
        {% set n = cheap_hours|int %}
        {# Hier durchlaufen wir sorted_prices in n-Stunden-Blöcken #}
        {% for i in range(0, sorted_prices|length - (n - 1)) %}
          {% set block = sorted_prices[i : i+n] %}
          {% set block_price = block|map(attribute='price')|sum %}
          {% if block_price < ns.total %}
            {% set ns.total = block_price %}
            {% set ns.start = block[0].start_time %}
            {% set ns.stop  = block[-1].start_time %}
          {% endif %}
        {% endfor %}
        {"start": "{{ ns.start }}", "stop": "{{ ns.stop }}", "sum": "{{ ns.total }}"}

      # Start/Stop aus dem JSON
      best_load_start: >
        {% if best_cheap_window.start != '' %}
          {{ best_cheap_window.start }}
        {% else %}
          {{ '' }}
        {% endif %}
      best_load_stop_plus1: >
        {% if best_cheap_window.stop != '' %}
          {{ (as_datetime(best_cheap_window.stop) + timedelta(hours=1)) | string }}
        {% else %}
          {{ '' }}
        {% endif %}

      # Durchschnittspreis = sum / cheap_hours
      average_cheap_price: >
        {% if best_cheap_window.sum != '' %}
          {{ (best_cheap_window.sum|float / (cheap_hours|int))|round(3) }}
        {% else %}
          0
        {% endif %}

      ################################################################
      # 2b) Teuerstes Zeitfenster (expensive_hours_min..expensive_hours_max)
      #
      #    Prüfen alle Blocklängen zwischen expensive_hours_min und
      #    expensive_hours_max (z. B. 4..12), um das teuerste
      #    Durchschnittspreis-Fenster zu finden.
      ################################################################
      worst_flex_window: >-
        {% set ns = namespace(best_avg=0, start='', stop='', best_sum=0) %}
        {# Minimale und maximale Länge #}
        {% set min_len = expensive_hours_min|int %}
        {% set max_len = expensive_hours_max|int %}

        {% for i in range(sorted_prices|length) %}
          {% for length in range(min_len, max_len + 1) %}
            {% if i+length <= sorted_prices|length %}
              {% set block = sorted_prices[i : i+length] %}
              {% set block_price = block|map(attribute='price')|sum %}
              {% set block_avg   = block_price / (block|length) %}
              {% if block_avg > ns.best_avg %}
                {% set ns.best_avg = block_avg %}
                {% set ns.best_sum = block_price %}
                {% set ns.start    = block[0].start_time %}
                {% set ns.stop     = block[-1].start_time %}
              {% endif %}
            {% endif %}
          {% endfor %}
        {% endfor %}
        {"start":"{{ ns.start }}","stop":"{{ ns.stop }}","sum":"{{ ns.best_sum }}","avg":"{{ ns.best_avg }}"}

      unload_start: >
        {% if worst_flex_window.start != '' %}
          {{ worst_flex_window.start }}
        {% else %}
          {{ '' }}
        {% endif %}
      unload_stop_plus1: >
        {% if worst_flex_window.stop != '' %}
          {{ (as_datetime(worst_flex_window.stop) + timedelta(hours=1)) | string }}
        {% else %}
          {{ '' }}
        {% endif %}
      average_worst_price: >
        {% if worst_flex_window.avg != '' %}
          {{ worst_flex_window.avg|float|round(3) }}
        {% else %}
          0
        {% endif %}

      ################################################################
      # 2c) Lade-/Entlade-Bedingungen
      ################################################################
      can_load: >-
        {% set avg_c = average_cheap_price|float %}
        {% if (highest_price.price - avg_c) >= price_diff_threshold or (avg_c <= price_ceiling) %}
          true
        {% else %}
          false
        {% endif %}
      can_unload: >
        {% if (average_worst_price|float) > price_unload_threshold %}
          true
        {% else %}
          false
        {% endif %}

  ###################################################################
  # 3) Ladefenster (nur wenn can_load == true)
  ###################################################################
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ can_load|lower == 'true' }}"
        sequence:
          - condition: template
            value_template: "{{ best_load_start != '' }}"
          - service: input_datetime.set_datetime
            data:
              entity_id: input_datetime.batterie_ladestart
              datetime: "{{ as_datetime(best_load_start)|as_local|string }}"
          - condition: template
            value_template: "{{ best_load_stop_plus1 != '' }}"
          - service: input_datetime.set_datetime
            data:
              entity_id: input_datetime.batterie_ladestop
              datetime: "{{ as_datetime(best_load_stop_plus1)|as_local|string }}"

  ###################################################################
  # 4) Entladezeiten (nur wenn can_unload == true)
  ###################################################################
  - choose:
      - conditions:
          - condition: template
            value_template: >
              {{ can_unload|lower == 'true' and unload_start != '' and unload_stop_plus1 != '' }}
        sequence:
          - service: input_datetime.set_datetime
            data:
              entity_id: input_datetime.batterie_entladestart
              datetime: "{{ as_datetime(unload_start)|as_local|string }}"
          - service: input_datetime.set_datetime
            data:
              entity_id: input_datetime.batterie_entladestop
              datetime: "{{ as_datetime(unload_stop_plus1)|as_local|string }}"

  ###################################################################
  # 5) Text und Push
  ###################################################################
  - variables:
      daily_message: >-
        {% set ls = as_datetime(best_load_start) | as_local if best_load_start else None %}
        {% set us = as_datetime(unload_start) | as_local if unload_start else None %}
        {% set load_day = '' if not ls else ('heute' if ls.date() == now().date() else 'morgen') %}
        {% set unload_day = '' if not us else ('heute' if us.date() == now().date() else 'morgen') %}
        
        {# Günstiges Fenster => cheap_hours #}
        {% if best_load_start != '' %}
          Günstiger Strom ({{ cheap_hours }}h): {{ load_day }} {{ ls.strftime('%H:%M') }}-{{ (as_datetime(best_load_stop_plus1)|as_local).strftime('%H:%M') }} (Ø {{ average_cheap_price }} €/kWh).
        {% else %}
          Kein günstiger Stromfenster.
        {% endif %}
        
        {# Teures Fenster => expensive_hours_min..expensive_hours_max #}
        {% if unload_start != '' %}
          Teurer Strom ({{ expensive_hours_min }}..{{ expensive_hours_max }}h): {{ unload_day }} {{ us.strftime('%H:%M') }}-{{ (as_datetime(unload_stop_plus1)|as_local).strftime('%H:%M') }} (Ø {{ average_worst_price }} €/kWh).
        {% else %}
          Kein teures Stromfenster.
        {% endif %}
        
        Batterie Laden: {% if can_load|lower=='true' %}JA{% else %}NEIN{% endif %}
        Batterie Entladen: {% if can_unload|lower=='true' %}JA{% else %}NEIN{% endif %}.

  - service: input_text.set_value
    data:
      entity_id: input_text.tibber_strom_daily_update
      value: "{{ daily_message }}"

  - service: notify.notify
    data:
      message: "{{ daily_message }}"

Nach Ausfürung des Skripts wird eine Push Nachricht gesendet, z.B. an ein Handy mit der HomeAssistant App. Ebenso wird ein Text Sensor mit dem Inhalt befüllt. Diesen kann man sich auch im Dashboard in einer Markdown-Karte ausgeben lassen:

3.3 Automatisiere den täglichen Ablauf

Lege eine Automation an, die dieses Skript täglich (z. B. um 18:00 Uhr) startet. So stellst du sicher, dass du jeden Tag die neuesten Preisdaten für den kommenden Tag nutzt.

Hier einmal meine Automation. Achtung diese setzt auch direkt die Zustände für den Wechselrichter. Das muss sicherlich für deinen Use-Case angepasst werden.

alias: Batterie Lade-/Entlade-Steuerung mit Zeitplanung
description: >-
  Steuert das Laden und Entladen der Batterie und führt das Zeitplanungs-Skript
  nur bei Zeit-Trigger aus
triggers:
  - at: "18:00:00"
    id: scripttime
    trigger: time
  - trigger: state
    entity_id:
      - binary_sensor.batterie_laden_aktiv
    from: "off"
    to: "on"
    id: ladestart
  - trigger: state
    entity_id:
      - binary_sensor.batterie_laden_aktiv
    from: "on"
    to: "off"
    id: ladestop
  - trigger: state
    entity_id:
      - binary_sensor.batterie_entladen_aktiv
    from: "off"
    to: "on"
    id: entladestart
  - trigger: state
    entity_id:
      - binary_sensor.batterie_entladen_aktiv
    from: "on"
    to: "off"
    id: entladestop
conditions: []
actions:
  - choose:
      - conditions:
          - condition: trigger
            id: ladestart
        sequence:
          - device_id: 5e1664a0f3348a6cfba7e097ceeac8c9
            domain: select
            entity_id: eab7bf1faf89b926e5f90d37af488280
            type: select_option
            option: eco_charge
      - conditions:
          - condition: trigger
            id: entladestart
        sequence:
          - device_id: 5e1664a0f3348a6cfba7e097ceeac8c9
            domain: select
            entity_id: eab7bf1faf89b926e5f90d37af488280
            type: select_option
            option: general
      - conditions:
          - condition: trigger
            id: scripttime
        sequence:
          - action: script.pv_batterie_zeitplanung
            data: {}
mode: single

4. Vorteile für die Wintermonate

Gerade im Winter, wenn:

  • Die PV-Erträge niedrig sind,
  • Man sich ggf. mehr aus dem Netz bedienen muss,
  • Und Strompreise dank dynamischer Tarife abends oft besonders hoch sind,

kann dieses Setup dir helfen, die Batterie effektiv zu managen. Du lädst in den günstigsten Stunden (z. B. spät abends / nachts) und entlädst in den teuren Zeiten (z. B. morgens oder abends zur Hauptverbrauchszeit). Dadurch sparst du merklich bei den Stromkosten.

5. Fazit

Mit einer PV-Batterie und einem dynamischen Stromtarif erschließt du dir in den Wintermonaten eine hervorragende Sparmöglichkeit. Dank Home Assistant lässt sich das vollautomatisch regeln:

  1. Preise abrufen
  2. Günstiges Zeitfenster (z. B. 5h) bestimmen, Batterie laden
  3. Teures Zeitfenster wählen, Batterie entladen
  4. Tägliche Aktualisierung dank Automation

So kannst du selbst dann Kosten reduzieren, wenn die Sonne rar ist. Viel Spaß beim Einrichten und clever Strom sparen!

Zu guter letzt: Mögliche Erweiterungen & Ausbaustufen

  1. Mehrere Lade- und Entladefenster pro Tag
    • Bei einer kleineren Batteriekapazität kann es sinnvoll sein, den Lade- und Entladeprozess in mehrere kürzere Zeitblöcke aufzuteilen. So wird flexibel auf mehrere günstige bzw. teure Preisfenster am Tag reagiert, anstatt nur ein einziges Zeitfenster zu nutzen.
  2. Einbeziehung von Sonnendaten
    • In den Sommermonaten ist es oft nicht nötig, nachts günstigen Netzstrom zuzukaufen, wenn die Batterie tagsüber ausreichend durch die PV-Anlage gefüllt wird.
    • Per Home-Assistant-Integrationen (z. B. Wetter- oder PV-Ertragsprognosen) lässt sich berechnen, ob der zu erwartende Sonnenstrom am nächsten Tag ausreicht. Nur wenn die Prognose nicht genügt und die Preise günstig sind, startet man den Batteriekauf aus dem Netz.
  3. Weitere Szenarien und Feinjustierungen
    • Noch zielgerichtetere Steuerung von Haushaltsgeräten (z. B. Wärmepumpe, Wallbox, etc.) basierend auf Preis- und PV-Prognosen.
    • Priorisierung bestimmter Geräte, um Lastspitzen zu vermeiden oder die Batterie schon frühzeitig zu entladen, falls besonders günstige Nachtstromtarife anstehen.
    • Kontinuierliche Erweiterungen sind durch die Flexibilität von Home Assistant möglich: Einbinden neuer Sensoren, Preisdaten oder externer Dienste, um das Energiemanagement fortlaufend zu optimieren.

(Bei Fragen oder Anmerkungen: Hinterlasse gerne einen Kommentar. Viel Erfolg bei deinem Energiemanagement!)

Schreibe einen Kommentar