Zum Inhalt springen
Tobias Schulz
Smart Home

Zendure SolarFlow im Energie-Dashboard

Werbung Diese Seite enthält Werbe-/Affiliate-Links, erkennbar am Einkaufswagen-Symbol. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen.

Wenn zwei Sensoren denselben Stromfluss messen und leicht voneinander abweichen, ist das selten ein Defekt. Es sind meistens zwei richtige Antworten auf zwei leicht unterschiedliche Fragen.

Erinnerst du dich an mein Versprechen aus dem SolarFlow-Lokal-Beitrag? Ganz am Ende hatte ich angekündigt, dass ich darüber schreibe, wie ich die ganzen Messwerte aus dem SolarFlow für das Home Assistant Energie-Dashboard passend mache. Knapp ein Jahr ist das jetzt her, höchste Zeit also 😄.

Info

Alle Beiträge rund um Zendure SolarFlow lokal findest du in der Zendure-Übersicht.

Ich hätte den Beitrag schon eher schreiben können, aber ich wollte erst eine Sache wirklich verstanden haben: warum mir mein Energie-Dashboard so lange Werte angezeigt hat, die fast stimmten - aber eben nur fast. Die Antwort hat mit der Physik des Setups zu tun.

Die Herausforderung: zwei Welten, zwei Messlogiken #

Stell dir folgende Situation vor:

  • Mein Shelly Pro3EM am Hauptzähler meldet auf allen drei Phasen je 100 Watt Bezug. In Summe also 300 Watt aus dem Netz.
  • Der Shelly Outdoor an der PV-Steckdose (also direkt am Ausgang des SolarFlow zum Haushalt) meldet 30 Watt, die gerade ins Haus eingespeist werden.
  • Mein SolarFlow Hub 2000 sagt dagegen, sein Home Output sei in diesem Moment 25 Watt.

Sieh genauer hin 🤨.

Die Shellys teilen mir also mit: insgesamt verbraucht der Haushalt gerade 330 Watt (300 vom Netz + 30 von der PV-Steckdose). SolarFlow sagt: 325 Watt (300 + 25). Klingt nach Pingelei, oder? Ist aber systematisch und summiert sich über den Tag zu spürbaren Differenzen im Energie-Dashboard.

Info

Beide Geräte messen korrekt. Sie messen nur nicht dasselbe und auch nicht an derselben Stelle. Mehrere Effekte überlagern sich, die in der Summe genau diese kleinen Abweichungen erzeugen:

  1. Verschiedene Messpunkte im System. Der SolarFlow erfasst seine Werte intern, vor der Steckdose. Der Shelly Outdoor ist selbst die Steckdose und misst, was tatsächlich im Haus ankommt. Dazwischen liegen Wechselrichter-Wirkungsgrad, Kabelweg und ein paar Watt Standby-Verbrauch des SolarFlow selbst.
  2. Verschiedene Genauigkeits- und Zertifizierungs-Klassen. Der Shelly Pro3EM ist ein präziser Wechselstromzähler, gebaut, um den Stromfluss am Hausanschluss möglichst genau zu erfassen. SolarFlow ist primär ein Speicher-Controller. Seine Messung muss genau genug sein, um vernünftig zu steuern, sie ist aber nicht für eine kWh-genaue Bilanz ausgelegt.
  3. Verschiedene Abtast-Frequenzen. SolarFlow publiziert seine Werte alle paar Sekunden. Die Shellys messen feiner und mitteln anders. Bei schnellen Lastwechseln entstehen daraus geringfügig unterschiedliche Momentanwerte.

Niemand misst hier falsch. Beide haben recht, in ihrem jeweiligen Kontext. Unsere Aufgabe als Smart-Home-Enthusiasten ist es, die beiden Messlogiken so zu harmonisieren, dass am Ende ein konsistentes Bild entsteht.

Daraus ist mein Ansatz entstanden: ich nehme die Shelly-Werte als Bezugsgröße, weil sie näher am Hausanschluss messen und damit das Bild liefern, das auch der Energieversorger sieht. Die SolarFlow-Werte nutze ich für die Verhältnisse im System (Solar gegen Batterie, Panel 1 gegen Panel 2), weil sie genau dafür gebaut sind. Aus beidem zusammen entsteht ein stimmiges Energie-Dashboard. Genau das macht eine kleine Schicht in Node-RED, die ich in diesem Beitrag mit dir gemeinsam zerlege.

Architektur im Überblick #

Bevor ich in den Code einsteige, hier einmal das Schaubild:

flowchart TD
    subgraph quellen["Quellen"]
        sf["SolarFlow via MQTT<br/>(sensor.solarflow_*)"]
        p3em["Shelly Pro3EM<br/>(3 Phasen)"]
        pv["Shelly Outdoor<br/>PV-Steckdose"]
        keller["Shelly Plug S<br/>Keller-Geräte"]
    end

    subgraph normalisierung["Node-RED Normalisierung"]
        compute["compute-Function<br/>7 Layer Mathematik"]
    end

    subgraph homeem["home_em-Schicht (Watt)"]
        power["sensor.home_em_*_power<br/>(via WebSocket-API)"]
    end

    subgraph energy["home_em-Schicht (kWh)"]
        riemann["sensor.home_em_*_energy<br/>(Riemann-Sum-Helfer)"]
    end

    subgraph dashboard["Home Assistant"]
        ed["Energie-Dashboard"]
    end

    sf --> compute
    p3em --> compute
    pv --> compute
    keller --> compute
    compute --> power
    power --> riemann
    riemann --> ed

Vier Quellen oben, eine Berechnungs-Schicht in der Mitte, zwei abstrakte Schichten danach, das Energie-Dashboard am Ende. Genau diese Trennung ist der Schlüssel - und sie ist auch der Grund, warum ich überhaupt eine eigene home_em-Schicht eingeführt habe statt einfach die rohen Sensoren ins Dashboard zu hängen.

Info

home_em steht für Home Energy Management. Mein Namensschema ist home_em_<klasse>_<funktion>_<einheit>, wobei s für Speicher/Solar-Seite, p für Provider/Netz, a für Abrechnung steht. Auto-Complete in der UI bietet damit immer die richtige Gruppe an.

Warum eine eigene home_em-Schicht? #

Du stellst dir möglicherweise die Frage: warum nicht einfach die rohen SolarFlow- und Shelly-Sensoren direkt ins Energie-Dashboard hängen? Da habe ich aufgrund unzähliger Datenbank-Migrationen der Longterm-Statistics von Home Assistant ein paar ausgeprägte Meinungen zu 😄:

  1. Einheitliches Naming. Ich will in jedem Dashboard, jeder Automation und jeder Karte denselben Namensraum sehen, egal welcher Hersteller dahinter sitzt. Wenn ich morgen den Speicher tausche, lasse ich die home_em_*-Schicht stehen und ändere nur darunter.
  2. Robuste Verfügbarkeits-Behandlung. Sobald ein Roh-Sensor unavailable liefert, würde sich das ungebremst durch alle abgeleiteten Werte ziehen. Mit Number.isFinite()-Checks und 0-Defaults im Node-RED Flow landet im Zweifel null Watt in der Berechnung, statt eine ganze Kette zu kippen.
  3. Konsistente Mathematik. Genau das, worum es in diesem Beitrag geht: die SolarFlow-Werte werden auf den Shelly-Bezug abgeglichen, damit die Bilanz am Ende des Tages aufgeht.
  4. Erweiterbarkeit. Wenn ich morgen ein zweites Balkonkraftwerk dazustelle, einen weiteren Großverbraucher mit eigenem Shelly versehe oder den SolarFlow durch ein anderes Modell ersetze, hänge ich die neue Quelle einfach an die compute-Function unten dran. Die home_em-Schicht darüber bleibt unverändert. Das Dashboard merkt von dem Umbau gar nichts. Stichwort: abstrakte Schicht als Versicherung gegen Hardware-Wechsel.

Genau Punkt 4 ist mir mit der Zeit am wichtigsten geworden. Es ist die Versicherungsprämie, die ich für die Mehrarbeit beim Aufbau bezahle - und sie hat sich schon mehrfach ausgezahlt, wenn ich Hardware umgesteckt habe.

Die Berechnung im Detail #

Jetzt zur eigentlichen Mathematik. Das Herzstück ist eine compute-Function in Node-RED, die in sieben Layern alle abgeleiteten Werte zentral berechnet. Sie liest die rohen Sensor-Werte aus einem Input-Objekt und erzeugt ein konsistentes Result-Objekt.

Dieses Objekt wird laufend mit den aktuellen Daten versorgt und bildet die Grundlage der mathematischen Berechnungen.

{
  "sh_pv_socket": -141,
  "sh_keller_server": 24.29,
  "sh_keller_dryer": 0,
  "sh_keller_wama": 0,
  "sh_3em_a": 83.72,
  "sh_3em_b": 0,
  "sh_3em_c": -95.9,
  "sf_solar_input": 408,
  "sf_solar_panel_1": 150,
  "sf_solar_panel_2": 258,
  "sf_battery_input": 0,
  "sf_battery_output": 239,
  "sf_home_output": 145
}

Einmal definiert, welche Sensoren genau ich abbilden möchte: das Objekt enthält die Ausgabe der auf das Input-Objekt angewendeten Berechnungen.

{
  "socket_import": 141,
  "grid_import": 12.11,
  "grid_export": 0,
  "grid_consume": 153.11,
  "battery_import": 239,
  "battery_export": 0,
  "battery_flow": -239,
  "solar_production": 380,
  "solar_consume": 141,
  "solar_panel_1": 139.71,
  "solar_panel_2": 240.29
}

Die Layer-Reihenfolge ist nicht zufällig. Sie bildet die Abhängigkeitskette ab: spätere Layer brauchen Werte aus früheren. Wer das durcheinanderwürfelt, bekommt überraschende Nullen 😁.

Layer 1: Socket-Import als Bezugsgröße #

// pv_socket ist negativ, wenn Strom vom SolarFlow ins Haus fließt
// (Shelly Outdoor misst Verbrauchsrichtung)
r.socket_import = round(pv_socket < 0 ? -pv_socket : 0);

Der Shelly Outdoor an der PV-Steckdose ist mein Bezugspunkt. Er misst, was hinter der Steckdose tatsächlich ins Haus geht - und nichts anderes. Das Vorzeichen ist eine Konvention der Shellys: positive Werte bedeuten Verbrauch durch das Gerät, negative bedeuten Einspeisung durch das Gerät. Da der SolarFlow einspeist, ist pv_socket negativ. Den Absolutwert nehme ich als socket_import.

Dieser Wert ist die Bezugsgröße, an der sich alles andere ausrichtet.

Layer 2: Grid-Bilanz aus Pro3EM und Keller-Verbrauchern #

const sum3em = k_server + k_dryer + k_wama + p3em_a + p3em_b + p3em_c;
r.grid_export = round(sum3em < 0 ? -sum3em : 0);
r.grid_import = round(sum3em >= 0 ? sum3em : 0);

Hier summiere ich alle Verbrauchsquellen, die hinter dem Hauptzähler hängen: die drei Phasen des Pro3EM plus die einzeln per Shelly Plug S gemessenen Großverbraucher im Keller (Server, Trockner, Waschmaschine). Das Ergebnis ist der saldierte Netz-Fluss:

  • Positiver Saldo bedeutet Bezug aus dem Netz.
  • Negativer Saldo bedeutet Einspeisung ans Netz.

Mit dieser Aufteilung entstehen zwei strikt nicht-negative Sensoren: einer, der nur dann Werte zeigt, wenn wir Strom beziehen, und einer, der nur dann Werte zeigt, wenn wir einspeisen. Das Energie-Dashboard verlangt diese Trennung, weil es daraus die beiden Pfeile der Sankey-Bilanz baut und mit gemischten Vorzeichen nichts anfangen kann.

Layer 3: Solar-Anteil am Socket-Import #

let solar_net = sf_solar - sf_batt_out;

// Sonderfall: Batterie entlädt mehr als die Solar liefert
if (sf_batt_out >= sf_solar && sf_home_out > 0 && sf_batt_in === 0) {
    solar_net = sf_solar - (sf_batt_out - sf_home_out);
}

const sfb = solar_net + sf_batt_in;
r.solar_consume = round(sfb > 0 ? (solar_net / sfb) * r.socket_import : 0);

Jetzt wird es interessant. Mein socket_import aus Layer 1 ist eine Gesamtsumme - er sagt mir nicht, welcher Anteil davon vom Solar kommt und welcher von der Batterie. Genau das wird hier berechnet, und zwar proportional.

Die Idee: die SolarFlow-Werte haben in absoluten Zahlen kleine Abweichungen vom Shelly-Bezug, aber das Verhältnis zwischen Solar-Anteil und Batterie-Anteil ist intern stimmig. Genau dafür ist der Speicher-Controller gebaut. Daher rechne ich:

solar_consume = (solar_net / (solar_net + batt_in)) * socket_import

Der Sonderfall ist die unangenehme Stelle. Wenn die Batterie mehr entlädt als die Solar produziert (typischerweise abends, wenn die Sonne weg ist), und gleichzeitig Strom ans Haus fließt, ist die naive Berechnung sf_solar - sf_batt_out negativ. Das ist mathematisch korrekt, hier aber nicht gewollt: die echte Solar-Energie kommt nicht aus der Batterie. Daher die Korrektur über den sf_home_out-Wert.

Tipp

Solche Sonderfälle sind genau der Grund, warum ich diese Schicht überhaupt baue. Wer die rohen SolarFlow-Werte unbearbeitet ins Dashboard kippt, sieht in genau dieser Situation Abweichungen vom Shelly-Bezug, die erst auffallen, wenn die Bilanz am Monatsende nicht aufgeht.

Layer 4: Batterie-Bilanz proportional zum Solar-Anteil #

r.battery_export = round(sf_batt_in > 0
    ? Math.max(0, r.socket_import - r.solar_consume)
    : 0);
r.battery_import = round(sf_batt_in > 0
    ? (sf_batt_out / sf_batt_in) * r.battery_export
    : sf_batt_out);
r.battery_flow = round(r.battery_export - r.battery_import);

Hier wird der Rest des socket_import der Batterie zugeschrieben. Was nicht Solar war, war Batterie. Auch hier wieder: die Proportionen stammen vom SolarFlow, die Absolutwerte kommen über den Shelly.

battery_flow ist der Saldo aus Export und Import. Positiv bedeutet entladen, negativ bedeutet laden. Praktisch für Visualisierungen, die nur einen Wert wollen.

Layer 5: Solar-Produktion als Summe aller Ausgänge #

const stb = sf_batt_out + solar_net;
r.solar_production = round(stb > 0 ? (r.battery_import + r.solar_consume) : 0);

Was die Solaranlage insgesamt produziert, ist das, was sie ans Haus liefert plus das, was sie in die Batterie lädt. Beides ist in den vorherigen Layern bereits auf den Shelly-Bezug normalisiert worden, also wird hier nur noch addiert.

Layer 6: Aufteilung auf die einzelnen Panels #

r.solar_panel_1 = round(
    (r.solar_production > 0 && sf_solar > 0)
        ? r.solar_production * sf_panel_1 / sf_solar
        : 0
);
r.solar_panel_2 = round(Math.max(0, r.solar_production - r.solar_panel_1));

Mein Balkonkraftwerk hat zwei Solarstränge in unterschiedlicher Ausrichtung (Süd-Süd-Ost und Süd-Süd-West). Die solar_production aus Layer 5 wird proportional zur SolarFlow-internen Aufteilung gesplittet. Der zweite Wert ist die Restmenge, damit die Summe garantiert wieder die Produktion ergibt.

Layer 7: Hausverbrauch über das Netz #

if (r.grid_export > 0 && (r.socket_import - r.grid_export) < 0) {
    r.grid_consume = 0;
} else if (r.grid_export > 0) {
    r.grid_consume = round(r.socket_import - r.grid_export);
} else {
    r.grid_consume = round(r.socket_import + r.grid_import);
}

Der Hausverbrauch folgt einer einfachen Energiebilanz: was an Strom ins Haus reinkommt, minus was wieder rausgeht. Im Code wird das in drei Fälle aufgeteilt, sortiert vom Sonderfall zum Normalfall:

  1. Sonderfall durch Messversatz. Wenn wir gerade einspeisen (grid_export > 0) und die PV-Steckdose dabei rechnerisch weniger ans Haus liefert, als ins Netz geht, würde socket_import - grid_export negativ. Das ist physikalisch unmöglich und entsteht durch kurze Zeitversätze zwischen Pro3EM und Shelly Outdoor. Statt eine negative Zahl ans Dashboard durchzulassen, fixieren wir auf 0.
  2. Normaler Einspeisungs-Fall. Wir speisen ein und die PV-Steckdose deckt den Eigenverbrauch problemlos ab. Der Hausverbrauch ist dann der Teil der PV-Lieferung, der nicht ans Netz weitergereicht wird: socket_import - grid_export.
  3. Normaler Bezugs-Fall. Wir beziehen aus dem Netz, grid_export ist 0. Was wir verbrauchen, ist die Summe aus PV-Beitrag und Netzbezug: socket_import + grid_import.

Das Ergebnis ist eine konsistente Energiebilanz, die mit dem Shelly-Bezug aufgeht.

Wie der Flow strukturiert ist #

Damit die Mathematik nicht bei jedem einzelnen Sensor-Update läuft, ist der Flow als State-Eventbus aufgebaut:

flowchart TD
    sensors["13 State-Change Nodes<br/>(alle Quellen)"] --> state["state-Function<br/>schreibt flow.sensor_raw"]
    state --> trigger["Trigger-Node<br/>(250 ms Debounce)"]
    trigger --> compute["compute-Function<br/>(7 Layer Mathematik)"]
    compute --> tracking["change-tracking<br/>(nur geänderte Werte raus)"]
    tracking --> switchnode["Switch-Node<br/>(verteilt nach Topic)"]
    switchnode --> hasensors["11 HA-Sensor-Nodes<br/>(home_em_*_power)"]

Drei Bausteine, die den Unterschied machen:

  1. State-Bus. Statt 13 Funktionen parallel laufen zu lassen, schreibt jeder State-Change in ein einziges Flow-Objekt sensor_raw. Die compute-Function hat damit immer den vollständigen Snapshot.
  2. 250-ms-Debounce. Wenn fünf Sensoren in derselben Sekunde feuern, soll die Berechnung einmal laufen, nicht fünfmal. Der Trigger-Node sammelt die Tick-Events ein und setzt maximal alle 250 ms eine Nachricht ab.
  3. Change-Tracking. Nur geänderte Werte werden an die HA-Sensoren weitergereicht. Das hält den Datenverkehr zur Home Assistant WebSocket-Verbindung schlank und die Historie in der HA-Datenbank sauber.

Die Ausgabe geht über ha-sensor-Nodes der Node-RED Home Assistant WebSocket Integration. Damit definiere ich pro Sensor genau einmal device_class: power, unit_of_measurement: W und state_class: measurement - und die Sensoren erscheinen automatisch in Home Assistant, sauber benannt, mit dem home_em_*-Präfix.

Von Watt zu Kilowattstunden #

Damit hat das Energie-Dashboard zwar saubere Power-Werte, aber noch keine Kilowattstunden. Die holen wir uns mit dem eingebauten Riemann-Sum-Helfer von Home Assistant, der aus jedem Power-Sensor einen Energie-Sensor macht.

Statt das händisch in YAML zu pflegen, lege ich die Helfer bequem über die UI an: Einstellungen → Geräte und Dienste → Helfer → Helfer erstellen → Integralsensor. Pro Power-Sensor einmal anlegen, als Methode Linke Riemannsche Summe, Präfix k für kWh und eine Genauigkeit von 3 setzen. Die Details und Optionen sind sehr gut in der offiziellen Doku beschrieben.

Das Ergebnis sind elf neue Sensoren mit device_class: energy, unit_of_measurement: kWh und state_class: total - genau das, was das Energie-Dashboard auswählbar macht.

Im Energie-Dashboard einbinden #

Letzter Schritt, erfreulich kurz: Einstellungen → Energie. Drei Kategorien anlegen:

KategorieSensoren
Stromnetzsensor.home_em_p_grid_import_energy (Verbrauch), sensor.home_em_p_grid_export_energy (Einspeisung)
Solarmodulesensor.home_em_s_solar_panel_1_import_energy (SSW), sensor.home_em_s_solar_panel_2_import_energy (SSO)
Batteriespeichersensor.home_em_s_battery_import_energy, sensor.home_em_s_battery_export_energy

Sobald du auf Speichern klickst, beginnt das Energie-Dashboard mit dem Sammeln. Die Sankey-Visualisierung wird in den ersten Stunden noch karg sein - das ist normal. Nach ein paar Tagen hast du ein vollständiges Bild deines Stromhaushalts, und vor allem: ein Bild, das mit deinem Stromzähler übereinstimmt 😎.

Sankey-Fluss im Home-Assistant-Energie-Dashboard mit Netz, Solar und Speicher

Stolpersteine #

Reihenfolge der Layer ist nicht verhandelbar. Wer in der compute-Function Layer 3 vor Layer 2 berechnet, bekommt überall Nullen. Die Abhängigkeitskette muss strikt eingehalten werden.

Debounce-Wert mit Bedacht wählen. Bei mir feuern die rohen Sensoren teils mehrfach pro Sekunde. Ohne Debounce würde die compute-Function entsprechend oft laufen, obwohl sich faktisch kaum etwas ändert. Mit 250 Millisekunden begrenze ich sie auf höchstens vier Durchläufe pro Sekunde, was im Alltag ein guter Kompromiss zwischen Reaktionsfreude und CPU-Schonung ist. Wer auf 1000 hochdreht, bekommt spürbare Latenz im Dashboard, weil Lastwechsel erst nach einer Sekunde durchgereicht werden.

ha-sensor-Nodes kennen nur einen State. Wenn dein Flow nach einem Neustart von Node-RED nicht sofort auslöst, bleibt der Sensor bei seinem letzten Wert. Mein Inject-Node mit force: true setzt nach jedem Deploy einmal alle Werte zwangsweise neu - kleine Geste, große Wirkung.

Shelly Pro3EM versus Wechselrichter-Werte. Die Differenz zwischen Pro3EM und Wechselrichter ist nicht null. Das ist normal und liegt an Verkabelungspfaden und Messpositionen. Bis zu 2 oder 3 Prozent sind unauffällig, alles darüber lohnt einen Blick auf den Anschluss.

Unterm Strich #

Das, was sich erst nach einer Riesen-Frickelei anhört, ist im Kern eine einzige Idee: die Shellys liefern den Bezug zum Hausanschluss, die SolarFlow liefert die Proportionen, und die home_em-Schicht in der Mitte harmonisiert beides zu einem stimmigen Bild, das am Ende mit dem Stromzähler aufgeht.

Der eigentliche Gewinn liegt für mich in der Erweiterbarkeit. Wenn ich morgen ein zweites Balkonkraftwerk dazustelle, einen weiteren Großverbraucher mit eigenem Shelly versehe oder den SolarFlow durch ein anderes Modell ersetze, hänge ich die neue Quelle einfach an die compute-Function unten dran.

Der Rest läuft 😎.


Das Titel-/Hintergrundbild stammt von Benjamin Jopen auf Unsplash.

Hat dir der Beitrag gefallen?

Mitgeredet

Reaktionen

 < 1k Aufrufe ·  1 Like

Weiterlesen

Verwandte Beiträge