Lokale Einbindung und Steuerung vom Marstek Venus E Batteriespeicher mit Home Assistant
Published: 2026-01-11, Revised: 2026-01-22

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

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

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.
- 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).
- 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.
- 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.
- Kosten: ~13-20 EUR.
- Passform: Passt perfekt.
- Montage: Ich habe die ursprünglich für die Räder des Speichers vorgesehenen Schrauben genutzt, um die Halterung an der Rückseite der Marstek-Einheit zu befestigen. Die vorgebohrten Schraubenlöcher sahen erst etwas klein aus, aber das Gestell stellte sich als absolut solide heraus. Unten verwendete ich zusätzlich zwei Winkelverbinder als Abstandshalter (2,5cm), um den Speicher parallel zur Wand auszurichten.
- Anbringung: Man kann das Ding allein heben, aber stellt sicher, dass jemand dabei ist, insbesondere wenn Ihr (wie ich) auf einen wackeligen Tisch steigt!

RS485 & Netzwerkverbindung#
Ich verwende die RS485-Schnittstelle, um den Speicher ohne WiFi und ohne die App mit meinem Netzwerk zu verbinden.
- Adapter:
Waveshare 20978 RS485 TO ETH (B) - Kabel: Ein Standard
Cat7Patchkabel (geopfert).
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:
- A: Gelb (Pin 5)
- B: Rot (Pin 4)
- GND: Schwarz (Pin 3)

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.
- Baud Rate:
115200 - Data Size:
8 - Parity:
None - Stop Bits:
1 - Work Mode:
TCP Server - Protocol:
Modbus TCP to RTU
Waveshare-Einstellungen (Screenshot)

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

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:
- Ich habe eine überdimensionierte PV-Anlage (
30kWp) gepaart mit einem relativ kleinen Speicher (5.12kWh). Mein Haushaltsverbrauch ist mittel bis hoch (~6000kWh/a). - 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:
- Prio 1: Vermeidung von Netzladung: Jede kWh, die aus dem Netz geladen und später entladen wird, leidet unter einen Umwandlungsverlust von
~20%. Daher habe ich eine relativ hohe Ladeschwelle gesetzt (400WÜberschuss). Das stellt sicher, dass morgens knapper Solarstrom direkt in den Hausverbrauch geht. Ich habe mittags genug Überschuss, um den kleinen Speicher schnell zu füllen, es besteht also früh keine Eile. - Prio 2: Vermeidung von Speichereinspeisung: Speicherstrom ins Netz einzuspeisen ist ein finanzieller Verlust. Da der Speicher ohnehin zu klein ist, um die ganze Nacht abzudecken, priorisiere ich Sicherheit vor Abdeckung. Daher entschied ich mich für einen sicheren Entladepuffer (
~50-100WNetzbezug). Wenn sich ein Gerät ausschaltet, hat der Speicher genug Zeit herunterzufahren, ohne während der Latenzzeit versehentlich ins Netz einzuspeisen. - Langlebigkeit: Statt schnell wechselnder Zustände bevorzuge ich einen möglichst konstanten Leistungspegel der möglichst lange gehalten wird. Ich habe das mit einen Dämpfungsfaktor und einer Stufen-Logik (Rundung auf
100W) umgesetzt. Das erzeugt eine Treppenkurve (statt einer zackigen Linie), was wahrscheinlich die Effizienz des Wechselrichters verbessert und den Verschleiß reduziert. Keine Ahnung, ob das stimmt oder wie wichtig es ist – lasst es mich also in den Kommentaren wissen, wenn ihr Elektro- oder Wechselrichter-Experten seid!
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.
ziel_netz: Hier nutzen wir eine Weiche. Beim Entladen zielen wir auf mindestens50WNetzbezug (Sicherheit vor Einspeisung). Beim Laden wechseln wir jedoch auf mindestes-100W(immer etwas Einspeisung). Dieser "negative Puffer" sorgt dafür, dass beim Einschalten von Verbrauchern (z.B. Licht, TV) die Lastkurve nicht sofort in den Netzbezug rutscht, sondern erst der Puffer aufgezehrt wird.lade_start_grenze: Eine Hysterese-Einstellung. Der Speicher beginnt erst zu laden, wenn der Solarüberschuss400Wübersteigt. Sobald das Laden jedoch begonnen hat, darf die Leistung unter diese Schwelle fallen (z.B. bei durchziehenden Wolken), ohne sofort zu stoppen. Das sorgt für einen ruhigeren Betrieb.korrektur: Die Logik berechnet die Differenz zwischen dem tatsächlichen Zählerstand und dem dynamischen Ziel (50W,-100W). Sie multipliziert dies mit einem Dämpfungsfaktor (0.5), um die relative Anpassung zu berechnen. Das verhindert, dass das System überreagiert und schwingt.limit_max: Ein hartes Limit für die Lade-/Entladeleistung (z.B. aktuell auf800Wfür Schuko-Anschluss gesetzt; ich werde das höhersetzen, sobald mein Speicher festverdrahtet ist. Maximal ist2500Wmöglich).soc: Überwacht den Ladezustand (State of Charge). Stoppt das Laden bei100%6 und das Entladen bei20%(um die Lebensdauer der Batterie zu verlängern). Diese Zustände überschreiben die Leistungsberechnung.
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:
- Graceful Shutdown: Mit dem
homeassistant.shutdownEvent-Trigger sendet HA unmittelbar vor dem Systemstopp einen harten0(Stopp) Befehl an den Speicher. - Clean Startup: Beim Booten (
homeassistant.start) wird eine Logiksperre angewendet. Die Automatisierung setzt die Zielleistung auf0und deaktiviert den Autopilot-Regler. HA wartet20 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#
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.

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

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!).

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

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"])

Es folgt ein gewöhnlicher Tag im Januar, an dem ich 11.4 kWh produziert habe.
Rot: Speicherlast (Laden/Entladen), Daten von Home Assistant via InfluxDB.Orange: Netzlast (Einspeisung/Bezug), Daten vom VzLogger via MQTTCyan: Gesamte PV-Wechselrichter-Produktion, Daten von meinem Huawei-Wechselrichter, abgefragt via Modbus TCP (Greetings @Solaranzeige!)

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

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.

- 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.

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.


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!

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.
-
Verifizierung der Pinbelegung: forwardme.de ↩
-
Inspiration und Register-Dokumentation von Michael Resch: reschcloud GitHub ↩
-
Home Assistant Eigenheiten bei Namensgebung und Referenzen ↩
-
Marstek Register-Tabelle (pdf) ↩
-
Diskussionen und Support: Photovoltaikforum ↩
-
Basierend auf Feedback aus einer freundlichen Diskussion ↩