Lokale Einbindung und Steuerung vom Marstek Venus E Batteriespeicher mit Home Assistant

Published: 2026-01-11, Revised: 2026-01-22


marstek_setup


TL;DR In diesem Beitrag beschreibe ich die Integration eines günstigen (~1050€) 5kWh AC-Speichers in eine bestehende 30kWp PV-Anlage mit Open-Source tools. Das Ziel: Null Cloud-Abhängigkeit und volle lokale Kontrolle mittels Modbus TCP und Nulleinspeisungs-Automatik in Home Assistant. Ich zeige die physische Installation mit einer Standard-TV-Wandhalterung, die RS485-Verkabelung und teile die komplette Software-Logik.

Motivation#

In 2020 habe ich die Gunst der Stunde genutzt, um eine 30kWp PV-Anlage zu installieren. In den letzten 4 Jahren erreichte ich damit eine Eigenverbrauchsquote von etwa 48-50%.

Meine PV-Produktion 2021-2025

calculation_history

Da der Preis des Marstek Venus E 2.0 im August 2025 auf rund 1.049 € fiel, sah ich die Gelegenheit, den Eigenverbrauch auf etwa 78% zu steigern. Die Größe des Marstek (5.12 kWh) passte gut zu meinem Verbrauch und zum Ziel, die Zyklenzahl zu maximieren. Die Berechnung der Wirtschaftlichkeit war gut genug für das Experiment. Mein aktueller Strompreis liegt bei 0,32 €/kWh und ich erhalte 0,082 €/kWh Einspeisevergütung (fest für die nächsten 15 Jahre). Nach diesen Zahlen sollte die Investition in etwa sechs Jahren wieder drin sein. Danach schätze ich die Einsparungen auf rund 250 € pro Jahr.

Berechnung für die Wirtschaftlichkeit des Speichers

roi_projection

Mein Hauptaugenmerk war aber nicht die Wirtschaftlichkeit. Ich wollte die Cloud und App des Herstellers komplett vermeiden. Leider ist es sehr schwer vor Anschaffung herauszufinden, ob Hersteller offene Schnittstellen anbieten, um komplett die Apps und Cloud zu vermeiden. Die meisten Hersteller spielen hier Buzzword-Bingo mit schwulstigen Marketing-Broschüren, die verschleiern, welche Abhängigkeiten man sich ins Haus holt. Marstek war der erste Hersteller, zu dem ich ausreichend Informationen in der pv community fand.

  1. Privatsphäre/Sicherheit: Ich möchte keine IoT-Geräte irgendwelcher Anbieter in meinem Netzwerk aktiv haben, die eigenständig kommunizieren – noch dazu, ohne dass ich die Algorithmen kenne (nichts für ungut, @Marstek, ihr seid da schon besser als andere).
  2. Kontrolle: Ich möchte die volle Kontrolle über die Lade-/Entladelogik basierend auf meinen spezifischen Zählerdaten und Kontext, keinen one-fits-all Black-Box-Algorithmus.
  3. Robustheit: Das System soll unabhängig von meiner Internetverbindung funktionieren. Der lokale Zugriff stellt zudem sicher, dass ich den Speicher auch dann noch nutzen kann, wenn der Anbieter pleite geht oder den Dienst einstellt (die Möglichkeit des lokalen Zugriffs war letztendlich mein Hauptkriterium für die Wahl des Marstek Speichers).

Hardware Installation#

Wandmontage#

Die Einheit wiegt etwa 50-60kg. Überraschenderweise ist keine offizielle Wandhalterung dabei. Auf Facebook fand ich einen Hinweis, dass eine Standard-TV-Halterungen funktioniere könnte.

Die One-For-All Solid WM4411 TV-Wandhalterung (32-65 Zoll) ist für bis zu 100kg ausgelegt.

wall_mount

RS485 & Netzwerkverbindung#

Ich verwende die RS485-Schnittstelle, um den Speicher ohne WiFi und ohne die App mit meinem Netzwerk zu verbinden.

Die Pinbelegung auf der Marstek-Seite ist entscheidend.1 Das beiliegende Kabel/Adapter kann unterschiedliche Farben haben, verlasst Euch also auf die Pin-Positionen. Ich verband drei Cat7-Adern via Wago-Klemmen (Set mit WAGO Klemmen 221) mit den offenen Enden des Marstek-Adapterkabels. Das muss abgeschnitten werden. Oder Ihr besorgt euch einen Adapter, der auf den mitgelieferten Stecker passt (war mir zu viel Aufwand).

Zuordnung Verkabelung:

wago_connection

Waveshare Konfiguration:

Der Adapter muss auf den Modus Modbus TCP to RTU eingestellt werden. Dies ermöglicht Home Assistant, Modbus TCP zu reden, während der Waveshare die Übersetzung zu Modbus RTU (seriell) für den Speicher übernimmt.

Waveshare-Einstellungen (Screenshot)

waveshare_settings

Tip

Nachdem ich meinen Waveshare über RS485 an die Batterie angeschlossen hatte, ging die Statusleuchte des Marstek sofort an (zu diesem Zeitpunkt war die Batterie noch nicht an das Netz angeschlossen!). Ich fand das ein gutes Zeichen dafür, dass die RS485-Pinbelegung richtig war.

Home Assistant Konfiguration#

Hinweis: Ein Großteil dieses Know-Hows basiert auf der exzellenten Yaml-Dokumentation von Michael Resch auf Github.2

Architektur#

Der Aufbau folgt dem Prinzip der bestmöglichen Trennung von Aufgaben (Separation of Concerns), sodass einzelne Komponenten später leicht austauschbar sind.

  1. Eingang: Ein Raspberry Pi Zero WH liest den Stromzähler (via IR-Kopf/vzlogger) und pusht Daten an MQTT.
  2. Logik: Home Assistant liest MQTT, berechnet die erforderliche Speicher-Aktion und sendet Befehle via Modbus TCP.
  3. Ausgang: Der Marstek-Speicher passt seine Lade-/Entladerate an.

vzlogger_setup

Modbus Integration#

Zuerst definieren wir die Modbus-Verbindung und die Sensoren/Schalter. Ich habe das zur besseren Wartbarkeit in separate Dateien aufgeteilt.

Der erste Teil der configuration.yaml
modbus:
  - name: Marstek
    type: tcp
    host: 192.168.50.77 # IP of the Waveshare
    port: 502
    timeout: 5
    delay: 1
    sensors: !include marstek_modbus_sensors.yaml
    switches: !include marstek_modbus_switches.yaml

input_number:
  # This helper acts as the "Gas Pedal" for the automation
  marstek_discharging_charging_power:
    name: "Marstek (Dis)Charging Power"
    min: -2500
    max: 2500
    step: 10
    unit_of_measurement: W
    mode: slider
    icon: mdi:battery-charging-medium

mqtt:
  sensor:
    - name: "Stromnetz Leistung (MQTT)"
      unique_id: stromnetz_leistung_mqtt
      state_topic: "vzlogger/data/chn0/agg"
      unit_of_measurement: "W"
      device_class: "power"
      state_class: "measurement"

    # Below values are optional and not needed for battery automation
    # I used these to populate the Home Assistant Energy Dashboard

    # Grid Consumption (Bezug 1.8.0) in kWh
    - name: "Stromnetz Bezug (1.8.0)"
      unique_id: stromnetz_bezug_kwh_mqtt
      state_topic: "vzlogger/data/chn1/agg"
      unit_of_measurement: "kWh"
      device_class: "energy"
      state_class: "total_increasing"
      # vzlogger usually sends Wh, but HA wants kWh. We divide by 1000.
      value_template: "{{ value | float / 1000 }}"

    # Return to Grid (Lieferung 2.8.0) in kWh
    - name: "Stromnetz Einspeisung (2.8.0)"
      unique_id: stromnetz_einspeisung_kwh_mqtt
      state_topic: "vzlogger/data/chn2/agg"
      unit_of_measurement: "kWh"
      device_class: "energy"
      state_class: "total_increasing"
      # vzlogger usually sends Wh, but HA wants kWh. We divide by 1000.
      value_template: "{{ value | float / 1000 }}"

    # 1. Current PV Power (Aktuelle Leistung) in Watt
    # Topic aus deinem Log: solaranzeige/33ktl-a/pv_leistung
    - name: "PV Wechselrichter Leistung"
      unique_id: pv_wechselrichter_leistung
      state_topic: "solaranzeige/33ktl-a/pv_leistung"
      unit_of_measurement: "W"
      device_class: "power"
      state_class: "measurement"

    # 2. PV Production (Gesamt Ertrag) in kWh - Für das Energy Dashboard
    # from Solaranzeige, via MQTT
    # Wert: 145710100 (Wh) -> Divide by 1000
    - name: "PV Wechselrichter Gesamt Ertrag"
      unique_id: pv_wechselrichter_gesamt_kwh
      state_topic: "solaranzeige/33ktl-a/wattstundengesamt"
      unit_of_measurement: "kWh"
      device_class: "energy"
      state_class: "total_increasing"
      value_template: "{{ value | float(0) / 1000 }}"

Bei MQTT ist der kritische Parameter Stromnetz Leistung (MQTT) (wird zu sensor.stromnetz_leistung_mqtt) die aktuelle Leistung am zentralen Hauszähler (Netzbezug bzw. PV-Einspeisung). Ich habe zusätzlich Netz-Gesamtstatistiken (Bezug 1.8.0/Einspeisung 2.8.0) und PV-Wechselrichter-Werte (Leistung und Ertrag) hinzugefügt, um das Energie-Dashboard in Home Assistant unter /energy/overview zu befüllen.

Info

Wenn Ihr meine YAML-Konfigurationen nutzt, achtet auf diese Eigenheit3 von Home Assistant: Normalerweise kann man auf Entitäten über ihre unique_id verweisen. Wenn jedoch der Parameter name gesetzt ist, generiert Home Assistant einen slug aus dem Namen. In diesem Fall muss der Slug verwendet werden. Zum Beispiel wird Marstek Battery SOC in Dashboards zu sensors.marstek_battery_soc und muss auch so referenziert werden. Als Programmierer fand ich das etwas unintuitiv und fehleranfällig. Aber es funktioniert.

Tip

Ihr könnt dazu als Hilfswerkzeug die Liste unter Entwickler Werkzeuge / Zustände (States) nutzen. Sucht nach dem Namen und stellt sicher, dass die Referenzierung mit der .yaml Konfiguration zusammenpasst.

marstek_modbus_sensors.yaml
# --- BATTERIE STATUS ---
- name: "Marstek Battery SOC"
  unique_id: marstek_battery_soc
  address: 32104
  slave: 1
  scan_interval: 30
  input_type: holding
  data_type: uint16
  unit_of_measurement: "%"
  device_class: battery
  state_class: measurement
  scale: 1
  precision: 0

- name: "Marstek Battery Voltage"
  unique_id: marstek_battery_voltage
  address: 32100
  slave: 1
  scan_interval: 30
  input_type: holding
  data_type: uint16
  unit_of_measurement: "V"
  device_class: voltage
  state_class: measurement
  scale: 0.01
  precision: 2

# --- LEISTUNG (Wichtig für Regelung) ---
- name: "Marstek AC Power"
  unique_id: marstek_ac_power
  # Positiv = Entladen, Negativ = Laden
  address: 32202
  slave: 1
  scan_interval: 5  # Schnell, für Regelung!
  input_type: holding
  data_type: int32
  unit_of_measurement: "W"
  device_class: power
  state_class: measurement
  scale: 1
  precision: 0

# --- ENERGIE ZÄHLER (Für Statistik) ---
- name: "Marstek Total Charging Energy"
  unique_id: marstek_total_charging_energy
  address: 33000
  slave: 1
  scan_interval: 60
  input_type: holding
  data_type: uint32
  unit_of_measurement: kWh
  device_class: energy
  state_class: total_increasing
  scale: 0.01
  precision: 1

- name: "Marstek Total Discharging Energy"
  unique_id: marstek_total_discharging_energy
  address: 33002
  slave: 1
  scan_interval: 60
  input_type: holding
  data_type: uint32
  unit_of_measurement: kWh
  device_class: energy
  state_class: total_increasing
  scale: 0.01
  precision: 1

# --- TEMPERATUREN (Sicherheit) ---
- name: "Marstek Internal Temperature"
  unique_id: marstek_internal_temperature
  address: 35000
  slave: 1
  scan_interval: 60
  input_type: holding
  data_type: int16
  unit_of_measurement: "°C"
  device_class: temperature
  state_class: measurement
  scale: 0.1
  precision: 1

- name: "Marstek Max Cell Temperature"
  unique_id: marstek_max_cell_temperature
  address: 35010
  slave: 1
  scan_interval: 60
  input_type: holding
  data_type: int16
  unit_of_measurement: "°C"
  device_class: temperature
  state_class: measurement
  scale: 0.1
  precision: 1

# --- STATUS ---
- name: "Marstek Inverter State"
  unique_id: marstek_inverter_state
  address: 35100
  slave: 1
  scan_interval: 10
  input_type: holding
  data_type: uint16
  # 0:Sleep, 1:Standby, 2:Charge, 3:Discharge, 4:Backup

- name: "Marstek RS485 Control Mode Status"
  unique_id: marstek_rs485_control_mode_status
  address: 42000
  slave: 1
  scan_interval: 10
  input_type: holding
  data_type: uint16

- name: "Marstek BMS Charge Current Limit"
  unique_id: marstek_bms_charge_current_limit
  address: 35111
  slave: 1
  scan_interval: 60
  input_type: holding
  data_type: uint16
  unit_of_measurement: "A"
  scale: 0.1
  precision: 1

- name: "Marstek BMS Discharge Current Limit"
  unique_id: marstek_bms_discharge_current_limit
  address: 35112
  slave: 1
  scan_interval: 60
  input_type: holding
  data_type: uint16
  unit_of_measurement: "A"
  scale: 0.1
  precision: 1

- name: "Marstek DC Power"
  unique_id: marstek_dc_power
  address: 32102
  slave: 1
  scan_interval: 10
  input_type: holding
  data_type: int32  # int32 laut PDF, da vorzeichenbehaftet
  unit_of_measurement: "W"
  scale: 1
  precision: 0

- name: "Marstek Alarm Code"
  unique_id: marstek_alarm_code
  address: 36000
  slave: 1
  scan_interval: 60
  input_type: holding
  data_type: uint16
  # Wert 0 = OK. Alles andere sind Warnungen.

- name: "Marstek Fault Code"
  unique_id: marstek_fault_code
  address: 36100
  slave: 1
  scan_interval: 60
  input_type: holding
  data_type: uint16
  # Wert 0 = OK. Alles andere sind Fehler (Abschaltung).

- name: "Marstek Config Max Charge Power"
  unique_id: marstek_config_max_charge_power
  address: 44002
  slave: 1
  scan_interval: 300 # Sehr selten, ändert sich ja nie
  input_type: holding
  data_type: uint16
  unit_of_measurement: "W"
  scale: 1

- name: "Marstek Config Max Discharge Power"
  unique_id: marstek_config_max_discharge_power
  address: 44003
  slave: 1
  scan_interval: 300
  input_type: holding
  data_type: uint16
  unit_of_measurement: "W"
  scale: 1

- name: "Marstek Config Discharge Cutoff SoC"
  unique_id: marstek_config_discharge_cutoff_soc
  address: 44001
  slave: 1
  scan_interval: 300
  input_type: holding
  data_type: uint16
  unit_of_measurement: "%"
  scale: 0.1
  precision: 1

- name: "Marstek Config Charging Cutoff SoC"
  unique_id: marstek_config_charge_cutoff_soc
  address: 44000
  slave: 1
  scan_interval: 300
  input_type: holding
  data_type: uint16
  unit_of_measurement: "%"
  scale: 0.1
  precision: 1

Als Referenz für die Einrichtung der Sensoren dient die offizielle Register-Dokumentation von Marstek.4 All diese Informationen habe ich durch die vielen Berichte im Photovoltaikforum.com gefunden!5

marstek_modbus_switches.yaml
- name: "Marstek Enable RS485 Control Mode"
  unique_id: marstek_enable_rs485_control_mode
  address: 42000
  slave: 1
  command_on: 21930
  command_off: 21947
  write_type: holding
  verify:
    input_type: holding
    address: 42000
    state_on: 21930
    state_off: 21947

Nachfolgend findet ihr meine vollständige configuration.yaml. Diese sorgt beispielsweise dafür, dass das Logging für Automatisierungs- und Skriptaktionen reduziert wird, da diese sonst eure Aktivitätsübersicht in Home Assistant fluten würden.

Die vollständige configuration.yaml
logbook:
  exclude:
    entities:
      # Deativiere das Loggen ausgewählter Script und Automations Aktionen
      # in Home Assistant Aktivitäts-Übersicht
      - automation.marstek_intelligente_regelung_nulleinspeisung
      - automation.marstek_befehl_an_speicher_senden
      - input_number.marstek_discharging_charging_power
      - script.marstek_set_forcible_charge

# Marstek Battery Integration via Waveshare
modbus:
  - name: Marstek
    type: tcp
    host: 192.168.50.77
    port: 502
    timeout: 5
    delay: 1
    sensors: !include marstek_modbus_sensors.yaml
    switches: !include marstek_modbus_switches.yaml

input_number:
  marstek_discharging_charging_power:
    name: "Marstek (Dis)Charging Power"
    min: -2500
    max: 2500
    step: 10
    unit_of_measurement: W
    mode: slider
    icon: mdi:battery-charging-medium

# Template Sensoren für Energie-Dashboard & Status
template:
  - sensor:
      # Berechnet Ladeleistung (nur positiv)
      - name: "Marstek Charging Power"
        unique_id: marstek_charging_power
        unit_of_measurement: "W"
        device_class: power
        state_class: measurement
        state: >
          {% set p = states('sensor.marstek_ac_power') | float(0) %}
          {{ (p * -1) if p < 0 else 0 }}

      # Berechnet Entladeleistung (nur positiv)
      - name: "Marstek Discharging Power"
        unique_id: marstek_discharging_power
        unit_of_measurement: "W"
        device_class: power
        state_class: measurement
        state: >
          {% set p = states('sensor.marstek_ac_power') | float(0) %}
          {{ p if p > 0 else 0 }}

      - name: "Marstek Ladezyklen (berechnet)"
        unique_id: marstek_cycles_calculated
        icon: mdi:battery-sync
        unit_of_measurement: "Zyklen"
        state_class: measurement
        state: >
          {% set total_discharged = states('sensor.marstek_total_discharging_energy_calculated') | float(0) %}
          {% set battery_capacity = 5.12 %}
          {{ (total_discharged / battery_capacity) | round(2) }}
      - name: "Marstek Efficiency"
        unique_id: marstek_efficiency
        unit_of_measurement: "%"
        icon: mdi:chart-donut
        state: >
          {% set chg = states('sensor.marstek_total_charging_energy_calculated') | float(0) %}
          {% set dis = states('sensor.marstek_total_discharging_energy_calculated') | float(0) %}
          {% if chg > 0 %}
            {{ ((dis / chg) * 100) | round(1) }}
          {% else %}
            0
          {% endif %}

sensor:
  - platform: influxdb
    api_version: 2
    host: influx.my.tld.com
    port: 443
    ssl: true
    token: [redacted]
    organization: "my org"
    bucket: "vzlogger"
    queries_flux:
      - name: "Stromnetz Leistung"
        unique_id: stromnetz_leistung_influxdb
        unit_of_measurement: "W"
        range_start: "-1m"  # <--- override the -15m default
        query: > # V--- query starts with the first filter
          filter(fn: (r) => r["_measurement"] == "mqtt_consumer")
          |> filter(fn: (r) => r["topic"] == "vzlogger/data/chn0/agg")
          |> map(fn: (r) => ({ r with _value: r._value * -1.0 }))
          |> last()
  # Riemann Summenintegrale (kWh aus Watt berechnen)
  # Genauer als die internen Zähler des Marstek.
  - platform: integration
    source: sensor.marstek_charging_power
    name: "Marstek Total Charging Energy (Calculated)"
    unique_id: marstek_total_charging_energy_calculated
    unit_prefix: k
    round: 3
    method: left
  - platform: integration
    source: sensor.marstek_discharging_power
    name: "Marstek Total Discharging Energy (Calculated)"
    unique_id: marstek_total_discharging_energy_calculated
    unit_prefix: k
    round: 3
    method: left
  - platform: filter
    name: "Stromnetz Leistung (Geglättet)"
    unique_id: stromnetz_leistung_smooth_2m
    entity_id: sensor.stromnetz_leistung_mqtt
    filters:
      - filter: time_simple_moving_average
        window_size: "00:02:00"  # Durchschnitt der letzten 120 Sekunden
        precision: 0

# Verbrauchszähler für Statistiken (Täglich, Monatlich, Jährlich)
utility_meter:
  # --- ENTLADEN (Verbrauch aus Batterie) ---
  marstek_discharge_daily:
    source: sensor.marstek_total_discharging_energy_calculated
    name: "Marstek Entladung Heute"
    cycle: daily
  marstek_discharge_monthly:
    source: sensor.marstek_total_discharging_energy_calculated
    name: "Marstek Entladung Monat"
    cycle: monthly
  marstek_discharge_yearly:
    source: sensor.marstek_total_discharging_energy_calculated
    name: "Marstek Entladung Jahr"
    cycle: yearly

  # --- LADEN (Speicherung in Batterie) ---
  marstek_charge_daily:
    source: sensor.marstek_total_charging_energy_calculated
    name: "Marstek Ladung Heute"
    cycle: daily
  marstek_charge_monthly:
    source: sensor.marstek_total_charging_energy_calculated
    name: "Marstek Ladung Monat"
    cycle: monthly
  marstek_charge_yearly:
    source: sensor.marstek_total_charging_energy_calculated
    name: "Marstek Ladung Jahr"
    cycle: yearly

Wie Ihr seht, habe ich auch meine lokale InfluxDB hinzugefügt, um zusätzliche Werte für das Dashboard abzurufen (optional, für die Speicher-Automatisierung aber nicht notwendig).

Logik für Nulleinspeisungs-Automatik#

Als ich mit der Automatisierung startete, dachte ich, das muss doch eigentlich ziemlich einfach sein ("Wenn PV-Überschuss > 0, dann Laden"!). Ist es aber nicht (ganz).

Um meine Entscheidungen bei der Automatisierung zu verstehen, müsst Ihr die spezifischen Voraussetzungen meines Setups kennen:

  1. Ich habe eine überdimensionierte PV-Anlage (30kWp) gepaart mit einem relativ kleinen Speicher (5.12kWh). Mein Haushaltsverbrauch ist mittel bis hoch (~6000kWh/a).
  2. Latenz: Der Stromzähler (vzlogger) meldet via MQTT alle 10 Sekunden. Ich könnte das weiter reduzieren, entschied mich aber dagegen (siehe unten). Der Wechselrichter vom Marstek Speicher braucht ebenfalls ein paar Sekunden zum Hochfahren. Der Versuch, sekündliche Lastspitzen zu jagen, würde in einem Katz-und-Maus-Spiel enden und unnötige Schwingungen verursachen. Kurze Lastspitzen schafft die Batterie eh nicht abzudecken!

Daraufhin habe ich meine Ziele für die Automatisierung so festgelegt:

Bei der Implementation nutze ich die untenstehende Automatisierung (ähnlich einer P-Regelung)6, welche mit einem Jinja2-Template in Home Assistant formatiert ist. Statt ein absolutes Ziel zu berechnen, passt die Automatik das aktuelle Leistungsniveau relativ zum Zählerstand an.

Info

Diese Parameter sind jene, die Ihr an Euren speziellen Fall anpassen wollt. Das ermöglicht euch die Feinjustierung, je nachdem, wie groß oder klein Eure PV-Anlage ist, oder wie eure Verbrauchskurve typischerweise aussieht. Eine solche Feinjustierung ist mit Hersteller-Apps oft nicht möglich.

automations.yaml: marstek_smart_regulation
- alias: "Marstek: Intelligente Regelung (Nulleinspeisung)"
  id: marstek_smart_regulation
  mode: restart
  trigger:
    - platform: time_pattern
      seconds: "/10"
  condition:
    # Automatik muss an sein
    - condition: state
      entity_id: input_boolean.marstek_automatik
      state: "on"
    # Sensor muss da sein
    - condition: not
      conditions:
        - condition: state
          entity_id: sensor.stromnetz_leistung_mqtt
          state: ["unavailable", "unknown"]
  action:
    - action: input_number.set_value
      target:
        entity_id: input_number.marstek_discharging_charging_power
      data:
        value: >
          {# 1. Aktuellen Status holen #}
          {% set netz = states('sensor.stromnetz_leistung_mqtt') | float(0) %}
          {% set aktuell_soll = states('input_number.marstek_discharging_charging_power') | float(0) %}
          {% set soc = states('sensor.marstek_battery_soc') | float(0) %}

          {# 2. Einstellungen #}
          {% set limit_max = 800 %}
          {% set lade_start_grenze = 400 %}

          {# --- DYNAMISCHER ZIELWERT --- #}
          {# Wenn wir bereits laden (>0), wollen wir einen Puffer zur EINSPEISUNG behalten (-100W) #}
          {# Wenn wir entladen oder standby sind, wollen wir Puffer zum BEZUG behalten (+50W) #}

          {% if aktuell_soll > 0 %}
            {# Modus LADEN: Ziel ist -100W (Einspeisung), damit wir nicht aus Versehen Netzstrom laden #}
            {% set ziel_netz = -100 %}
          {% else %}
            {# Modus ENTLADEN/STANDBY: Ziel ist 50W (Bezug), damit wir nicht ins Netz einspeisen #}
            {% set ziel_netz = 50 %}
          {% endif %}

          {# 3. Berechnung der Korrektur #}
          {% set korrektur = (netz - ziel_netz) * 0.5 %}
          {% set neu_soll_raw = aktuell_soll - korrektur %}

          {# 4. Logik-Weiche (Start-Hysterese) #}
          {% if aktuell_soll <= 0 %}
            {# Start-Bedingung: Nur Laden beginnen, wenn wir deutlich unter -400W sind #}
            {% if neu_soll_raw > 0 %}
                {% if netz < (lade_start_grenze * -1) %}
                  {% set neu_soll_final = neu_soll_raw %}
                {% else %}
                  {% set neu_soll_final = 0 %}
                {% endif %}
            {% else %}
                {% set neu_soll_final = neu_soll_raw %}
            {% endif %}
          {% else %}
            {# Wir laden bereits: Regelung normal weiterlaufen lassen #}
            {% set neu_soll_final = neu_soll_raw %}
          {% endif %}

          {# 5. Runden #}
          {# 'int' schneidet bei positiven Zahlen ab (rundet ab) -> Sicher gegen Netzbezug beim Laden #}
          {# 'int' schneidet bei negativen Zahlen ab (rundet Richtung 0) -> Sicher gegen Einspeisung beim Entladen #}
          {# Rechenbeispiel: Soll wäre 440W -> Wird 400W. #}
          {# Die restlichen 40W gehen zusätzlich zu den 100W Puffer ins Netz. #}
          {% set neu_soll_gerundet = (neu_soll_final / 50) | int * 50 %}

          {# 6. Sicherheits-Grenzen #}
          {% if neu_soll_gerundet > 0 and soc >= 100 %}
            0
          {% elif neu_soll_gerundet < 0 and soc <= 20 %}
            0
          {% elif neu_soll_gerundet > limit_max %}
            {{ limit_max }}
          {% elif neu_soll_gerundet < (limit_max * -1) %}
            {{ limit_max * -1 }}
          {% else %}
            {{ neu_soll_gerundet | int }}
          {% endif %}

Da Modbus ein stateful Protokoll ist, behält der Speicher den letzten Befehl bei, den er erhalten hat. Wenn Home Assistant nun abstürzt oder neu startet, während der Speicher mit voller Leistung entlädt, würde er weiter entladen, bis er leer ist, da der Regelkreis keine Updates mehr sendet.

Um diesen Zustand zu verhindern und einen sauberen Start zu gewährleisten, habe ich eine Sicherheitslogik in der automations.yaml hinterlegt:

  1. Graceful Shutdown: Mit dem homeassistant.shutdown Event-Trigger sendet HA unmittelbar vor dem Systemstopp einen harten 0 (Stopp) Befehl an den Speicher.
  2. Clean Startup: Beim Booten (homeassistant.start) wird eine Logiksperre angewendet. Die Automatisierung setzt die Zielleistung auf 0 und deaktiviert den Autopilot-Regler. HA wartet 20 Sekunden, um sicherzustellen, dass die Modbus-Verbindung vollständig steht, sendet einen weiteren Sicherheits-Stopp-Befehl und aktiviert erst dann die Regelschleife wieder. Das verhindert, dass die Regelungslogik auf veraltete Sensordaten reagiert, bevor das System vollständig initialisiert ist.

Nach meinen Tests hat das bisher gut funktioniert (auch bei docker compose down && docker compose up -d).

automations.yaml: zusätzliche Hilfswerkzeuge
- alias: "Marstek: Befehl an Speicher senden"
  id: marstek_send_command
  mode: restart
  trigger:
    - platform: state
      entity_id: input_number.marstek_discharging_charging_power
  action:
    - action: script.turn_on
      target:
        entity_id: script.marstek_set_forcible_charge

# 3. Self-healing (Optional)
- alias: "Marstek: Watchdog (Selbstheilung)"
  id: marstek_watchdog
  trigger:
    - platform: time_pattern
      minutes: "/5"
  condition:
    # Wenn Sollwert und Istwert zu stark abweichen
    - condition: template
      value_template: >
        {{ (states('input_number.marstek_discharging_charging_power') | float(0) - states('sensor.marstek_ac_power') | float(0) * -1) | abs > 100 }}
    # Und wir nicht gerade bei 0 sind
    - condition: numeric_state
      entity_id: input_number.marstek_discharging_charging_power
      above: 50
  action:
    # RS485 Reset
    - action: switch.turn_off
      target: {entity_id: switch.marstek_enable_rs485_control_mode}
    - delay: 5
    - action: switch.turn_on
      target: {entity_id: switch.marstek_enable_rs485_control_mode}

# 4. On Shutdown: Stop the battery
# (Schützt vor ungewolltem Weiterlaufen während Updates)
- alias: "System: Marstek Stopp bei Shutdown"
  id: system_marstek_shutdown_safety
  trigger:
    - platform: homeassistant
      event: shutdown
  action:
    - action: script.turn_on
      target:
        entity_id: script.marstek_stop_system

# 5. On Startup: Restart the battery with a time delay
- alias: "System: Marstek Reset bei Start"
  id: system_marstek_startup_reset
  trigger:
    - platform: homeassistant
      event: start
  action:
    # 1. Sperre: Automatik sofort ausschalten
    - action: input_boolean.turn_off
      target:
        entity_id: input_boolean.marstek_automatik
    # 2. Werte auf Null
    - action: input_number.set_value
      target:
        entity_id: input_number.marstek_discharging_charging_power
      data:
        value: 0
    # 3. Warten
    - delay: "00:00:20"
    # 4. Reset: Sicherer Stopp-Befehl an Speicher senden
    - action: script.turn_on
      target:
        entity_id: script.marstek_stop_system
    # 5. Freigabe: Automatik einschalten -> System übernimmt ab jetzt
    - action: input_boolean.turn_on
      target:
        entity_id: input_boolean.marstek_automatik

Tip

Ich habe außerdem stop_grace_period: 30s zu meiner docker-compose.yml hinzugefügt, um dieser Automatisierung etwas mehr Zeit zu geben, den Speicher in einen sicheren Zustand zu versetzen.


Die Automatisierung triggert ein Skript, das die eigentlichen Register schreibt. Dieser Aufbau abstrahiert die Modbus-Komplexität von der Automatisierungslogik.

scripts.yaml
marstek_set_forcible_charge:
  alias: Marstek Set Forcible Charge
  icon: mdi:battery-charging-40
  sequence:
    - choose:
        # Fall 1: Stopp (Wert nahe 0)
        - conditions:
            - condition: numeric_state
              entity_id: input_number.marstek_discharging_charging_power
              above: -1
              below: 1
          sequence:
            - action: modbus.write_register
              data: {hub: Marstek, address: 42010, slave: 1, value: 0}
            - action: modbus.write_register
              data: {hub: Marstek, address: 42020, slave: 1, value: 0}
            - action: modbus.write_register
              data: {hub: Marstek, address: 42021, slave: 1, value: 0}
        # Fall 2: Entladen (Negativer Wert)
        - conditions:
            - condition: numeric_state
              entity_id: input_number.marstek_discharging_charging_power
              above: -2501
              below: -10
          sequence:
            # Leistung setzen (Absolutwert: aus -500 wird 500)
            - action: modbus.write_register
              data:
                hub: Marstek
                address: 42021
                slave: 1
                value: "{{ states('input_number.marstek_discharging_charging_power') | int | abs }}"
            # Modus Entladen (2)
            - action: modbus.write_register
              data: {hub: Marstek, address: 42010, slave: 1, value: 2}

        # Fall 3: Laden (Positiver Wert)
        - conditions:
            - condition: numeric_state
              entity_id: input_number.marstek_discharging_charging_power
              above: 10
              below: 2501
          sequence:
            # Leistung setzen
            - action: modbus.write_register
              data:
                hub: Marstek
                address: 42020
                slave: 1
                value: "{{ states('input_number.marstek_discharging_charging_power') | int | abs }}"
            # Modus Laden (1)
            - action: modbus.write_register
              data: {hub: Marstek, address: 42010, slave: 1, value: 1}

# Additional Scripts for Dashboard (manual, not needed for the automation)
marstek_start_charging:
  alias: "Marstek: Laden Starten (Manuell)"
  icon: mdi:battery-charging
  sequence:
    - action: modbus.write_register
      data: {hub: Marstek, slave: 1, address: 42000, value: 21930}
    - action: modbus.write_register
      data:
        hub: Marstek
        slave: 1
        address: 42020
        value: "{{ states('input_number.marstek_discharging_charging_power') | int | abs }}"
    - action: modbus.write_register
      data: {hub: Marstek, slave: 1, address: 42010, value: 1}

marstek_start_discharging:
  alias: "Marstek: Entladen Starten (Manuell)"
  icon: mdi:battery-charging-low
  sequence:
    - action: modbus.write_register
      data: {hub: Marstek, slave: 1, address: 42000, value: 21930}
    - action: modbus.write_register
      data:
        hub: Marstek
        slave: 1
        address: 42021
        value: "{{ states('input_number.marstek_discharging_charging_power') | int | abs }}"
    - action: modbus.write_register
      data: {hub: Marstek, slave: 1, address: 42010, value: 2}

marstek_stop_system:
  alias: "Marstek: Stopp (Manuell)"
  icon: mdi:stop-circle-outline
  sequence:
    - action: modbus.write_register
      data: {hub: Marstek, slave: 1, address: 42010, value: 0}

Exkurs: Wie behalte ich die Kontrolle über all diese yaml Dateien?

Wie ihr im Beitrag sehen könnt, konfiguriere ich meinen Home Assistant hauptsächlich über Dateien und weniger über die Benutzeroberfläche. Ich halte nichts von Buttons und Mausklicks, da sie zu anfällig für Fehler sind und sich nur schlecht nachverfolgen lassen. Gleichzeitig kann eine Home-Assistant-Instanz schnell sehr komplex werden. Wenn dann noch so wichtige Dinge wie die Batterieregelung hinzukommen, wird das schnell zur „kritischen Infrastruktur”, die dementsprechend behandelt werden sollte. Der Platz reicht nicht aus, um die gesamte Geschichte zu erzählen. Der wichtigste Tipp aber: Trackt alle Home-Assistant-Konfigurationen in Git. Ich wiederhole: Trackt alle Home-Assistant-Konfigurationen in Git.

Wie gut das funktioniert, hängt auch davon ab, ob ihr eure persistenten Home-Assistant-Daten gut von den „ephemeral base images” getrennt habt. Grundsätzlich folge ich dabei immer demselben System beim Aufsetzen verschiedener Dienste. Home Assistant ist da keine Ausnahme. Für Home Assistant sieht mein Git-Ordner beispielsweise so aus:

Home Assistant file tree

hass@zfs-iotdocker:~$ tree
.
├── .git
├── data
│   └── config
│       ├── automations.yaml
│       ├── blueprints
│       │   ├── automation
│       │   │   └── homeassistant
│       │   │       ├── motion_light.yaml
│       │   │       └── notify_leaving_zone.yaml
│       │   ├── script
│       │   │   └── homeassistant
│       │   │       └── confirmable_notification.yaml
│       │   └── template
│       │       └── homeassistant
│       │           └── inverted_binary_sensor.yaml
│       ├── configuration.yaml
│       ├── core
│       ├── deps
│       ├── home-assistant.log
│       ├── home-assistant.log.1
│       ├── home-assistant.log.fault
│       ├── home-assistant_v2.db
│       ├── home-assistant_v2.db-shm
│       ├── home-assistant_v2.db-wal
│       ├── marstek_modbus_sensors.yaml
│       ├── marstek_modbus_switches.yaml
│       ├── scripts.yaml
│       └── tts
└── docker
    └── docker-compose.yml

12 directories, 16 files

docker-compose.yml
services:
  homeassistant:
    container_name: homeassistant
    image: "ghcr.io/home-assistant/home-assistant:stable"
    volumes:
      - /srv/hass/data/config:/config
      - /etc/localtime:/etc/localtime:ro
    ports:
      - 8123:8123
      - 0.0.0.0:5683:5683/udp
    restart: unless-stopped
    privileged: false
    # network_mode: host
    environment:
      - TZ=Europe/Berlin
    stop_grace_period: 30s

  watchtower:
    image: containrrr/watchtower
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 86400

Dieser Aufbau ermöglicht es, alle Änderungen nachverfolgbar abzulegen und zu dokumentieren. Sonst wisst ihr in einem Monat nicht mehr, was ihr gemacht habt. Gleichzeitig hilft er, die Übersicht zu behalten und automatische Änderungen durch Home Assistant zu überwachen. Dieses System funktioniert seit vielen Jahren äußerst problemfrei – inklusive automatischer Updates. Oftmals schaue ich mir im Nachhinein die Konfigurationen in GitLab an, um darüber nachzudenken. Beim Bearbeiten kann ich dann die Tastenkombination Strg+Shift+F (Alles suchen) in VS Code nutzen, um die Verwendung einer Variable über mehrere YAML-Dateien zu finden und nichts zu vergessen.

dashboard_overview

Dashboard#

Ok, zurück zur Batterie. Ich habe ein Dashboard in Home Assistant erstellt, um den Systemstatus zu überwachen und manuelle Eingriffe zu ermöglichen. Das System läuft standardmäßig im "Autopilot" (Automatisierung aktiv), kann aber zu Test- oder Wartungszwecken auf manuelle Steuerung umgeschaltet werden.

Hauptwerte (Anzeige). Der Wert für die maximale Zelltemperatur (1.5°C) ist wahrscheinlich ein Standardwert. Vielleicht wurde auch das Speicherregister noch nicht getriggert.

dashboard_overview

Dann gibt es einen Bereich für Hardware-Limits und Diagnose:

dashboard_limits

Als Nächstes kommen die Statistiken und Zyklen. Der vielleicht interessanteste Teil hier ist die Anzeige der 'Vollzyklen'. Je mehr Vollzyklen in einem Jahr erreicht werden, desto größer ist die Wirtschaftlichkeit des Speichers. 200 Vollzyklen pro Jahr sind ein erreichbarer Durchschnitt für die meisten. Mit meiner größeren PV-Anlage erwarte ich etwas mehr, vielleicht 250–300 Zyklen (wir werden sehen..). Die technische Dokumentation des Marstek Venus E verspricht 6.000 Ladezyklen bei 80% Entladung, was bedeutet, dass mein Speicher bei 300 Zyklen/Jahr 20 Jahre halten sollte. Ich erwarte jedoch weniger (auch hier: wir werden sehen!).

dashboard_statistics_

Schließlich die manuellen Befehle ("Overrides"), um die Automatisierung auszuschalten und das Entladen oder Laden zu erzwingen. Auch das eher optional.

dashboard_gauges

Dashboard yaml
type: vertical-stack
cards:
  - type: heading
    heading: Marstek Speicher Status
    icon: mdi:battery-high
    heading_style: title
  - type: horizontal-stack
    cards:
      - type: gauge
        entity: sensor.marstek_battery_soc
        name: Ladestand (SoC)
        min: 0
        max: 100
        severity:
          green: 20
          yellow: 10
          red: 0
        needle: true
      - type: gauge
        entity: sensor.marstek_ac_power
        name: Leistung (AC)
        min: -2500
        max: 2500
        needle: true
        severity:
          green: -2500
          yellow: 0
          red: 1
  - type: horizontal-stack
    cards:
      - type: gauge
        entity: sensor.marstek_max_cell_temperature
        name: Max. Zell-Temp
        min: 0
        max: 60
        needle: true
        severity:
          green: 15
          yellow: 45
          red: 55
      - type: tile
        entity: sensor.marstek_internal_temperature
        name: Innen-Temp
        icon: mdi:thermometer
        color: orange
  - type: entities
    title: Live Werte
    entities:
      - entity: sensor.stromnetz_leistung_mqtt
        name: Aktueller Netzbezug (MQTT)
        icon: mdi:transmission-tower
      - entity: sensor.marstek_ac_power
        name: AC Leistung (Zum Haus)
        icon: mdi:current-ac
      - entity: sensor.marstek_dc_power
        name: DC Leistung (Von Batterie)
        icon: mdi:current-dc
      - entity: sensor.marstek_battery_voltage
        name: Batteriespannung
      - entity: sensor.marstek_inverter_state
        name: Inverter Status (1=Stby, 2=Chg, 3=Dis)
      - entity: switch.marstek_enable_rs485_control_mode
        name: RS485 Fernsteuerung aktiv
  - type: entities
    title: Hardware-Limits & Diagnose
    show_header_toggle: false
    entities:
      - entity: sensor.marstek_alarm_code
        name: Alarm Code (0 = OK)
        icon: mdi:alert-outline
      - entity: sensor.marstek_fault_code
        name: Fault Code (0 = OK)
        icon: mdi:alert-octagon-outline
      - type: section
        label: Temporäre BMS Limits (Temperaturbedingt)
      - entity: sensor.marstek_bms_charge_current_limit
        name: Max. Ladestrom (BMS)
      - entity: sensor.marstek_bms_discharge_current_limit
        name: Max. Entladestrom (BMS)
      - type: section
        label: Permanente Config Limits (EEPROM)
      - entity: sensor.marstek_config_max_charge_power
        name: Erlaubte Ladeleistung (System)
        icon: mdi:lock-outline
      - entity: sensor.marstek_config_max_discharge_power
        name: Erlaubte Entladeleistung (System)
        icon: mdi:lock-outline
      - entity: sensor.marstek_config_discharge_cutoff_soc
        name: Notabschaltung bei SoC (System)
        icon: mdi:battery-alert
      - entity: sensor.marstek_config_charging_cutoff_soc
        name: Maximal laden bis SoC (System)
        icon: mdi:battery-alert
  - type: entities
    title: Statistik & Zyklen
    show_header_toggle: false
    entities:
      - entity: sensor.marstek_ladezyklen_berechnet
        name: Vollzyklen (seit Installation)
      - entity: sensor.marstek_efficiency
        name: Wirkungsgrad (RTE)
      - type: section
        label: Gesamtenergie
      - entity: sensor.marstek_total_charging_energy_calculated
        name: Gesamt Geladen
        icon: mdi:battery-arrow-up
      - entity: sensor.marstek_total_discharging_energy_calculated
        name: Gesamt Entladen
        icon: mdi:battery-arrow-down
      - type: section
        label: Heute
      - entity: sensor.marstek_ladung_heute
        name: Geladen Heute
      - entity: sensor.marstek_entladung_heute
        name: Entladen Heute
      - type: section
        label: Dieser Monat
      - entity: sensor.marstek_ladung_monat
        name: Geladen Monat
      - entity: sensor.marstek_entladung_monat
        name: Entladen Monat
      - type: section
        label: Dieses Jahr
      - entity: sensor.marstek_ladung_jahr
        name: Geladen Jahr
      - entity: sensor.marstek_entladung_jahr
        name: Entladen Jahr
  - type: heading
    heading: Manuelle Steuerung
    icon: mdi:controller
  - type: tile
    entity: input_boolean.marstek_automatik
    name: Automatik-Modus (An = Autopilot)
    icon: mdi:robot
    color: accent
  - type: entities
    entities:
      - entity: input_number.marstek_discharging_charging_power
        name: Soll-Leistung (+ Laden / - Entladen)
  - type: horizontal-stack
    cards:
      - type: button
        name: LADEN
        icon: mdi:battery-charging
        tap_action:
          action: call-service
          service: script.marstek_start_charging
        show_name: true
        show_icon: true
        card_mod:
          style: |
            ha-card { background: #1b5e20; color: white; }
      - type: button
        name: STOPP
        icon: mdi:stop-circle-outline
        tap_action:
          action: call-service
          service: script.marstek_stop_system
        show_name: true
        show_icon: true
        card_mod:
          style: |
            ha-card { background: #424242; color: white; }
      - type: button
        name: ENTLADEN
        icon: mdi:battery-charging-low
        tap_action:
          action: call-service
          service: script.marstek_start_discharging
        show_name: true
        show_icon: true
        card_mod:
          style: |
            ha-card { background: #b71c1c; color: white; }

Auswertung#

Für Auswertung und Monitoring bevorzuge ich InfluxDB und Grafana. Fügt den folgenden Code zu Eurer configuration.yaml hinzu, um die Speicherstatistiken nach InfluxDB zu exportieren, damit sie in Grafana visualisiert werden können:

configuration.yaml: InfluxDB
influxdb:
  api_version: 2
  ssl: true # I am using https internally
  host: influx.my.tld.com
  port: 443 # Default port for https
  token: [redacted]
  organization: "my org"
  bucket: "homeassistant"
  exclude:
    entity_globs: "*" # This prevents HA from writing its own data to InfluxDB
  include: # except for these metrics
    entities:
      - sensor.marstek_ac_power         # Lade-/Entladeleistung
      - sensor.marstek_battery_soc      # Ladestand
      - sensor.marstek_battery_voltage  # Spannung

Mein Flux-Query in Grafana sieht dann so aus:

import "experimental"

from(bucket: "homeassistant")
  |> range(
      start: experimental.subDuration(
        d: v.windowPeriod,
        from: v.timeRangeStart
      ),
      stop: v.timeRangeStop
  )
  |> filter(fn: (r) => r["_measurement"] == "W")
  |> filter(fn: (r) => r["_field"] == "value")
  |> filter(fn: (r) => r["entity_id"] == "marstek_ac_power")
  |> aggregateWindow(
      every: v.windowPeriod,
      fn: last,
      createEmpty: true
  )
  |> fill(usePrevious: true)
  |> set(key: "_field", value: "Marstek Batterie")
  |> keep(columns: ["_time", "_value", "_field"])

.. und für die Batterie Ladestandsanzeige:

from(bucket: "homeassistant")
|> range(start: -30d, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_measurement"] == "%")
|> filter(fn: (r) => r["_field"] == "value")
|> filter(fn: (r) => r["entity_id"] == "marstek_battery_soc")
|> last()
|> set(key: "_field", value: "Marstek Battery SoC")
|> keep(columns: ["_time", "_value", "_field"])

Grafana Battery Gauge

Es folgt ein gewöhnlicher Tag im Januar, an dem ich 11.4 kWh produziert habe.

Grafana Evaluation

Wie man sehen kann, begann der Speicher gegen 09:32 zu laden. Es war ein komplett bewölkter Tag im Januar, also denkbar ungünstig. Gegen Mittag gab es dann auch noch einen starken Regenschauer, der mit einem hohen Haushaltsenergiebedarf (2500W, Mittagessen!) zusammenfiel, woraufhin der Speicher kurzzeitig in den Entlademodus wechselte. Er schaltete dann zurück auf Laden bis 14:54, als der Sicherheitsabstand dazu führte, dass der Speicher für etwa 30 Minuten in den Standby ging. Zu diesem Zeitpunkt war der Speicher zu 72% geladen. Um 15:08 wechselte er dann erstmals dauerhaft auf Entladen, was stetig anstieg bis zur Hauptlastzeit am Abend ab 17:00. Der Speicher erreichte gegen 21 Uhr einen SoC von 20% und schaltete zurück in den Standby.

Hier sind noch ein paar Details der oben genannten Zeiträume

Grafana Evaluation

Der Ausschnitt zeigt die oben angesprochene Mittagszeit. Man kann hier die Latenz des gesamten Systems aus Speicher/VZLogger gut beobachten. Dennoch schaffte es der Speicher, die kurzen Perioden des Netzbezugs zu minimieren. An einigen wenigen Stellen reagierte der Speicher nicht schnell genug und entlud kurzzeitig ins Netz (gelbe Linie über Null). Da es jedoch Mittag ist, ist das nicht so schlimm. Es war noch genug Tageslicht übrig, um den Speicher wieder aufzuladen.

Im zweiten Teil (rechte Seite) sehen wir die umgekehrte Situation. Der Speicher begann wieder zu laden. Es gab nur wenige Momente, in denen der Speicher nicht schnell genug reagierte und kurzzeitig aus dem Netz lud (gelbe Linie unter Null). Auch das fand ich akzeptabel.

Später am Abend (Screenshot unten) sieht man hier den Übergang vom Laden zum Entladen. Hier gibt es wirklich nicht viel zu beanstanden.

Grafana Evaluation

  • Die Automatisierung schaffte es, den Speicher über lange Zeiträume auf relativ konstanten Niveaus zu halten.
  • Sehr wenige Spitzen wurden aus dem Netz geladen, was ich völlig akzeptabel fand.
  • Die Entladephase war sogar noch besser, wobei der Speicher nur bei zwei Gelegenheiten ins Netz einspeiste.
An einem anderen Tag: Abends-/Nachtentladung

Hier seht ihr den Verlauf an einem üblichen Tag im Januar, nach Volladung der Batterie um ca. 15:30. Es ist gut sichtbar, dass durch den Entladepuffer nur sehr selten Batteriestrom in das Netz eingespeist wird. Die Batterie erreichte ca. 3 Stunden später, um 01:41, 20% und schaltete ab.

Grafana Evaluation

Info

Da mein Speicher derzeit noch an der Schuko-Steckdose hängt (der eingetragene Elektriker, der das Ding anschließen soll bzw. muss, vertröstet mich seit 6 Monaten!) läd er derzeit noch maximal mit 800W. Sobald dieses Limit aufgehoben ist, gehe ich davon aus, dass der Speicher tagsüber auch üblicherweise voll wird.

Schlussendlich noch zwei Ansichten vom ausgefüllten Energie-Dashboard von Home Assistant. Das bietet ebenfalls einen nützlichen Überblick über die täglichen Statistiken.

Home Assistant Energy Dashboard

Home Assistant Energy Statistics

Fazit#

Das System funktioniert zuverlässig. Die Latenz ist gering genug (~10s Intervall), um die Grundlast effektiv abzudecken. Durch die Nutzung der Nulleinspeise-Logik mit Puffer und Dämpfung konnte ich die Schwingungen des System gut minimieren.

Ich habe so (1) erfolgreich die Cloud des Herstellers vermieden, (2) das Gerät in einem separaten VLAN isoliert (oder einfach offline via direktem RS485-Kabel) und (3) es nahtlos in meine bestehende Home Assistant Umgebung integriert. Kein extra Schaltaktor notwendig!

marstek venus e on wall

Sorry für die Mischung aus Deutsch und Englisch (code)!

Das kleine Projekt hat mir jedenfalls sehr viel Spaß gemacht. Lasst mich gern in den Kommentaren wissen, ob Euch die Informationen hier geholfen haben!

Changelog

2026-01-22

  • Exkurs für Management von Konfigurationen in git hinzugefügt

2026-01-21

  • Konfiguration ergänzt, um Automatisierungen aus den Protokollen auszuschließen, um ein Fluten der Home Assistant-Aktivitätsübersicht zu verhindern
  • Kleine Aktualisierung des Automatisierungscodes, um die Anpassung von Parametern zu vereinfachen und Lade- und Entladepuffer separat zu definieren
  • Screenshot des Home Assistant-Energie-Dashboards hinzugefügt
  • Verbesserte Flux-Abfragen in Grafana

2026-01-17

  • Die obere Ladegrenze von 99% auf 100% geändert, da es bei LiFePO4-Batterien keinen Sinn macht, das letzte Prozent nicht zu laden 7

2026-01-16

  • Automatisierung signifikant aktualisiert, hauptsächlich um Schwingungen zu reduzieren

2026-01-15

  • Erster Beitrag.

  1. Verifizierung der Pinbelegung: forwardme.de 

  2. Inspiration und Register-Dokumentation von Michael Resch: reschcloud GitHub 

  3. Home Assistant Eigenheiten bei Namensgebung und Referenzen 

  4. Marstek Register-Tabelle (pdf) 

  5. Diskussionen und Support: Photovoltaikforum 

  6. Proportionalregler: Wikipedia 

  7. Basierend auf Feedback aus einer freundlichen Diskussion