Balkonkraftwerk Nulleinspeisung mit Automation (EZ1-m, Bitshake)

Ich stelle hier meine Nulleinspeisung mit ganz normalen Boardmitteln von HomeAssistant vor. Also ohne NodeRed z.B.
Meine Vorraussetzungen sind:
Bitshake AIR (liest Stromzähler alle 10s aus und sendet per MQTT) Entität “sensor.bitshake_aktueller_verbrauch”
EZ1-m (Wechselrichter mit Original Integration, kein MQTT, auf local gestellt, somit keine Cloud, aber lokale API), dessen Leistungsabgabe mit dieser Entität “number.solar_maximale_leistung” eingestellt werden kann.
Solarspeicher kann dumm sein, alles wird durch den Wechselrichter (WR) und HomeAssistant gesteuert und geregelt.
Die Logik spielt sich in einer Automation ab.

Diese Entität vom Bitshake liefert den Stromverbrauch des Haus-/Whg-Anschlusses. Wenn der Wert negativ wird, bedeutet das, dass eingespeist wird, was eine gute Nulleinspeisung möglichst verhindern soll. Gründe lasse ich bewusst aus diesem Thread.
Was ich auch weglasse: Die Angst, der WR könnte daran sterben, die Angst, die Solarzellen könnten daran sterben. Macht Euch da an anderer Stelle schlau. Ich für meinen Teil habe mich bewusst dafür entschieden, das, was ich hier vorstelle umzusetzen.

data:
  value: >
    {% set verbrauch = states('sensor.bitshake_aktueller_verbrauch') | float %}
    {% set sicherheitsabzug = 10 %} {% set schrittweite = 10 %}

    {% set ziel_leistung = verbrauch %} {% if verbrauch < 100 %}
      {% set ziel_leistung = verbrauch - sicherheitsabzug %}
    {% endif %}

    {% if ziel_leistung < 30 %}
      {% set ziel_leistung = 30.0 %}
    {% endif %} {% if ziel_leistung > 800 %}
      {% set ziel_leistung = 800.0 %}
    {% endif %}

    {% set aktuelle_leistung = states('number.solar_maximale_leistung') | float
    %}

    {% if aktuelle_leistung < ziel_leistung %}
      {{ [aktuelle_leistung + schrittweite, ziel_leistung] | min }}
    {% endif %} {% if aktuelle_leistung > ziel_leistung %}
      {{ [aktuelle_leistung - schrittweite, ziel_leistung] | max }}
    {% endif %} {% if aktuelle_leistung == ziel_leistung %}
      {{ aktuelle_leistung }}
    {% endif %}
target:
  entity_id: number.solar_maximale_leistung
action: number.set_value

Ich habe das alles mit ChatGPT erstellt und bin absolut kein Programmier-Gott. Ich beantworte Eure Fragen aber trotzdem so gut ich kann.
Google ist mein Freund
Simon42 ist mein Freund
ChatGPT ist mein Freund.
:wink:

Viel Erfolg beim Nachbauen

P.S. Das alles ist in Vorbereitung meiner Speicher-Bestellung erfolgt. Ich bekomme in ein paar Wochen 2x Growatt NOAH B2500. Da die bisher keine Integration haben, sollte das “so” klappen. Schön wäre, wenn ich es irgendwann irgendwie schaffe eine Entität von den Speichern in HA zu bekommen, die mir den Füllgrad der Speicher sagen können, dann kann ich die Nulleinspeisung an- und abschalten. Aber zunächst sollte es “so” gehen.

2 „Gefällt mir“

(EDIT): Ich werde das weiter optimieren. Eine PID-Regelung ist bisher gescheitert, aber ein schrittweises Nachlaufen dem geforderten Wert hat schonmal geklappt. Dann verhindert man das Zappeln um den Zielwert. (Die Sprungantwort schwing zu arg, es sollte gedämpft werden). Stay tuned.

Hast du das bewusst so gewählt? Man kann den Lesekopf auch in “Quasi-Echtzeit” auslesen und die Werte via mqtt zur Verfügung stellen.

Hast Du da einen Tipp? Ich habe mich beim bitshake noch nicht tief eingearbeitet. Es sendet über mqtt. Aber das schnellste, was ich als Sendefrequenz einstellen konnte waren 10s. Weniger hat das Ding nicht akzeptiert.

Die Daten werden gefühlt im Sekundentakt auf der Seite des bitshake aktualisiert. Die sogenannte Telemetrieperiode ist die Sende-Frequenz und minimal gehen da leider 10:

Das wird in Tasmota nicht in der “Telemetrieperiode” eingestellt (auch wenn das naheliegend wäre), sondern im Skript in der Festlegung der Nachkommastellen. Das mag sich irre anhören, ist aber so auch in der Doku beschrieben. Ist auch unabhängig vom Modell des Lesekopfs: ist bei Hichi und Bitshake (und anderen) identisch.

In deinem Skript (das du ja für deinen Stromzähler individuell anpassen musst) musst du zu den tatsächlich gewollten Nachkommastellen einfach 16 hinzuaddieren, schon wird die Watt-Leistung in Quasi-Echtzeit ausgegeben.

Bei mir sieht das so aus:

>D
>B
->sensor53 r
>M 1
+1,5,s,16,9600,SGM,4
1,77070100010800ff@1000,Verbrauch,kWh,E_in,3
;1,77070100010801ff@1000,VerbrauchHT,kWh,E_inHT,3
1,77070100020800ff@1000,Einspeisung,kWh,E_out,3
;1,77070100020801ff@1000,EinspeisungHT,kWh,E_outHT,3
1,77070100100700ff@1,akt. Leistung,W,Power,16

Die 16 hinter der Power-Angabe bewirkt, dass die Wattleistung in ganzen Watt ohne Nachkommasstellen ausgegeben wird. Wenn dein Zähler auch Nachkommastellen ausgeben kann, z. B. zwei, dann kannst du auch 18 anstelle von 16 eingeben.

2 „Gefällt mir“

ich hab das auch über chat gpt umgesetzt, schaut so aus
saldo hole ich mir vom zähler. den growatt regle ich über einen esp32.

alias: Growatt Regelung
description: Regelwert in %-Schritten anpassen
triggers:
  - seconds: /1
    trigger: time_pattern
conditions:
  - condition: sun
    before: sunset
    before_offset: "-01:00:00"
    after: sunrise
    after_offset: "00:30:00"
actions:
  - choose:
      - conditions:
          - condition: numeric_state
            entity_id: sensor.saldo
            below: -800
        sequence:
          - data_template:
              entity_id: number.growatt_growatt_max_output_power
              value: >
                {% set current =
                states('number.growatt_growatt_max_output_power') | float %} 
                {%set step = 100 %}  {% set min_percent = 25 %}  {{ [current -
                step, min_percent] | max | round(0) }}
            action: number.set_value
      - conditions:
          - condition: numeric_state
            entity_id: sensor.saldo
            above: -500
        sequence:
          - data_template:
              entity_id: number.growatt_growatt_max_output_power
              value: >
                {% set current =
                states('number.growatt_growatt_max_output_power') | float %} 
                {%set step = 2 %}  {% set max_percent = 100 %}  {{ [current +
                step, max_percent] | min | round(0) }}
            action: number.set_value
mode: single

Das ist ja krass, das ist schneller, als eine Sekunde. Funktioniert, wie Du es beschrieben hast. 16 und 18… Vielen Dank!

Das sieht auch sehr interessant aus. Ich lass das morgen mal laufen. Als Leistungsbegrenzung habe ich Deine Entität “number.growatt_growatt_max_output_power” mit der Leistungsbegrenzung meines WR EZ1-m verknüpft.
Meinst Du damit, dass Du einen Growatt NOAH 2000 mit einem ESP steuerst? Oder einen Growatt-Wechselrichter? Wenn Du einen NOAH damit steuerst würde ich mich über einen Tipp wie das geht (Homepage oder oder) super freuen. Da würde ich dann auch einsteigen.

es ist ein growatt MIN 3000 TL-XE
schau mal hier Growatt Shine WiFi an Homeassistant das habe ich geflashed und in den growatt gesteckt. dann hat man den vollen zugriff auf die regelung.

auf den esp32 das geflashed:

Danke Dir.
Beim Growatt NOAH 2000 scheint es nur den Weg über die China Cloud zu geben.

das schon versucht?

Das ist die beste mir bekannte Lösung, aber meines Wissens ist da immer noch der Cloud Server im Spiel. Vielleicht gibt es da auch mal eine bastel Lösung, die die Cloud faked.

Letzten Endes bin ich doch auch, wie viele, bei NodeRed gelandet. Statt einer PID-Regelung Habe ich für den Moment eine P-Regelung, Die ich noch fine tunen muss. Regeln tut sie schon mal schön.

Im Folgenden habe ich eine Erklärung des Flows mit AI erzeugt. Quellcode für den Flow zum direkten Import und Screenshot kommt am Ende.

Node-RED Flow für Proportionale PV-Nulleinspeisung (ca. 10W Ziel)
Hallo zusammen,
ich möchte euch einen einfachen Node-RED Flow vorstellen, der eure überschüssige PV-Leistung so regelt, dass idealerweise nur ein minimaler Netzbezug von etwa 10W verbleibt. Das Prinzip basiert auf einer proportionalen Regelung.
Wie funktioniert es?
** * “Netzbezug geändert” (trigger-state): Dieser Knoten überwacht den aktuellen Netzbezug (oder die Netzeinspeisung) von eurem Sensor (sensor.bitshake_aktueller_verbrauch). Sobald sich dieser Wert ändert, wird der Flow ausgelöst.**
** * “Rate Limit (5s)” (delay): Um zu verhindern, dass zu viele Anfragen an Home Assistant gesendet werden und das System unnötig belasten, begrenzt dieser Knoten die Ausführung des Flows auf maximal alle 5 Sekunden.**
** * “PV Schalter aktiv?” (api-current-state): Hier wird geprüft, ob ein von euch definierter Schalter (input_boolean.nulleinspeisung_hauptschalter) aktiviert ist. Nur wenn dieser Schalter “on” ist, wird die Regelung aktiv.**
** * “grid_power speichern” (change): Der aktuelle Wert des Netzbezugs/der Einspeisung (der im payload ankommt) wird in der msg.grid_power gespeichert. So können wir später darauf zugreifen.**
** * “Akt. Solar Limit holen” (api-current-state): Dieser Knoten liest den aktuellen Zielwert für die maximale Solarleistung aus einer number-Entität (number.solar_maximale_leistung). Diesen Wert wird der Flow später anpassen.**
** * “Proportionale Regelung” (function): Das Herzstück der Regelung! Dieser Function-Node enthält den JavaScript-Code, der die Anpassung der Solarleistung berechnet:**
** * Er nimmt den aktuellen Netzbezug (msg.grid_power) und das aktuelle Solarleistungs-Limit (msg.payload) entgegen.**
** * Er berechnet die Differenz (error) zwischen dem aktuellen Netzbezug und dem Zielwert von 10W.**
** * Anschließend wird mit einem Proportionalitätsfaktor (Kp) eine Anpassung der Solarleistung (adjustment) berechnet. Wichtig: Der Wert von Kp (aktuell 0.3) bestimmt, wie stark die Regelung auf Änderungen des Netzbezugs reagiert. Ein kleinerer Wert führt zu sanfteren, aber langsameren Anpassungen, während ein größerer Wert schnellere Reaktionen, aber auch ein höheres Risiko von Überschwingen mit sich bringt. Hier müsst ihr ggf. experimentieren, um den optimalen Wert für eure Anlage zu finden.**
** * Das neue Solarleistungs-Limit wird berechnet und auf konfigurierte Minimal- und Maximalwerte (min_solar_limit, max_solar_limit) begrenzt. Diese Werte solltet ihr an eure Wechselrichter-Spezifikationen anpassen.**
** * Um unnötige API-Aufrufe zu vermeiden, wird das neue Limit nur dann weitergegeben, wenn es sich um mindestens 1W vom vorherigen Wert unterscheidet.**
** * “Solar Limit setzen” (api-call-service): Wenn der Function-Node ein neues Solarleistungs-Limit ausgibt, wird dieser Knoten verwendet, um den Wert der number-Entität (number.solar_maximale_leistung) in Home Assistant entsprechend zu setzen. Dies sollte idealerweise euren Wechselrichter oder eine andere Steuerungseinheit ansprechen, die die Solarleistung dynamisch anpassen kann.**
** * “Neues Solar Limit” (debug): Dieser optionale Debug-Knoten gibt das neu berechnete Solarleistungs-Limit im Node-RED Debug-Fenster aus. Das kann bei der Fehlersuche und Optimierung hilfreich sein.**
Wichtige Hinweise:
** * Kp-Faktor: Der Wert des Kp-Faktors im Function-Node “Proportionale Regelung” ist entscheidend für das Regelverhalten. Beginnt am besten mit einem kleinen Wert und erhöht ihn schrittweise, falls die Regelung zu träge ist. Beobachtet dabei, ob es zu starken Schwankungen im Netzbezug kommt.**
** * Min/Max Solarleistung: Stellt sicher, dass die min_solar_limit und max_solar_limit im Function-Node mit den minimal und maximal möglichen Leistungseinstellungen eures Wechselrichters übereinstimmen.**
** * Rate-Limit: Das eingestellte Rate-Limit von 5 Sekunden ist ein guter Kompromiss. Ihr könnt diesen Wert anpassen, aber ein zu niedriges Limit könnte die Systemlast erhöhen.**
** * Voraussetzungen: Dieser Flow setzt voraus, dass ihr einen Sensor habt, der den aktuellen Netzbezug zuverlässig erfasst, und eine Möglichkeit, die maximale Solarleistung eures Wechselrichters über Home Assistant (z.B. über eine number-Entität) zu steuern.**

[
    {
        "id": "7583219a0506bf8d",
        "type": "tab",
        "label": "PV Nulleinspeisung Proportional (10W Ziel)",
        "disabled": false,
        "info": "Regelt die maximale Solarleistung, um einen Netzbezug von ca. 10W zu erreichen.\n\nKp-Faktor im Function-Node \"Proportionale Regelung\" muss ggf. angepasst werden!\nMin/Max Solarleistung ebenfalls dort prüfen.\nRate-Limit aktuell auf 5 Sekunden.",
        "env": []
    },
    {
        "id": "bd65966074bb3675",
        "type": "trigger-state",
        "z": "7583219a0506bf8d",
        "name": "Netzbezug geändert",
        "server": "homeassistant-server",
        "version": 5,
        "inputs": 0,
        "outputs": 1,
        "exposeAsEntityConfig": "",
        "entities": {
            "entity": [
                "sensor.bitshake_aktueller_verbrauch"
            ],
            "substring": [],
            "regex": []
        },
        "debugEnabled": false,
        "constraints": [],
        "customOutputs": [],
        "outputInitially": false,
        "stateType": "num",
        "enableInput": false,
        "x": 150,
        "y": 160,
        "wires": [
            [
                "2e0f4306eaaef93b"
            ]
        ]
    },
    {
        "id": "2e0f4306eaaef93b",
        "type": "delay",
        "z": "7583219a0506bf8d",
        "name": "Rate Limit (5s)",
        "pauseType": "rate",
        "timeout": "5",
        "timeoutUnits": "seconds",
        "rate": "1",
        "nbRateUnits": "5",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": true,
        "allowrate": false,
        "outputs": 1,
        "x": 350,
        "y": 160,
        "wires": [
            [
                "7a332b19643ce197"
            ]
        ]
    },
    {
        "id": "7a332b19643ce197",
        "type": "api-current-state",
        "z": "7583219a0506bf8d",
        "name": "PV Schalter aktiv?",
        "server": "homeassistant-server",
        "version": 3,
        "outputs": 2,
        "halt_if": "on",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "entity_id": "input_boolean.nulleinspeisung_hauptschalter",
        "state_type": "str",
        "blockInputOverrides": false,
        "outputProperties": [
            {
                "property": "payload_original_trigger",
                "propertyType": "msg",
                "value": "",
                "valueType": "triggerId"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_location": "payload",
        "override_payload": "none",
        "entity_location": "data",
        "override_data": "none",
        "x": 560,
        "y": 160,
        "wires": [
            [
                "4528fbf90313a230"
            ],
            []
        ]
    },
    {
        "id": "4528fbf90313a230",
        "type": "change",
        "z": "7583219a0506bf8d",
        "name": "grid_power speichern",
        "rules": [
            {
                "t": "move",
                "p": "payload",
                "pt": "msg",
                "to": "grid_power",
                "tot": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 200,
        "y": 240,
        "wires": [
            [
                "ff910a48a86854ad"
            ]
        ]
    },
    {
        "id": "ff910a48a86854ad",
        "type": "api-current-state",
        "z": "7583219a0506bf8d",
        "name": "Akt. Solar Limit holen",
        "server": "homeassistant-server",
        "version": 3,
        "outputs": 1,
        "halt_if": "",
        "halt_if_type": "str",
        "halt_if_compare": "is",
        "entity_id": "number.solar_maximale_leistung",
        "state_type": "num",
        "blockInputOverrides": false,
        "outputProperties": [
            {
                "property": "payload",
                "propertyType": "msg",
                "value": "",
                "valueType": "entityState"
            },
            {
                "property": "data",
                "propertyType": "msg",
                "value": "",
                "valueType": "entity"
            }
        ],
        "for": "0",
        "forType": "num",
        "forUnits": "minutes",
        "override_topic": false,
        "state_location": "payload",
        "override_payload": "msg",
        "entity_location": "data",
        "override_data": "msg",
        "x": 430,
        "y": 240,
        "wires": [
            [
                "29c076b1b69c1f18"
            ]
        ]
    },
    {
        "id": "29c076b1b69c1f18",
        "type": "function",
        "z": "7583219a0506bf8d",
        "name": "Proportionale Regelung",
        "func": "// Eingehende Nachrichten:\n// msg.grid_power: Aktueller Netzbezug/Einspeisung (Zahl, z.B. 50 für Bezug, -200 für Einspeisung)\n// msg.payload: Aktueller eingestellter Wert von number.solar_maximale_leistung (Zahl)\n\nlet grid_power = msg.grid_power;\n// Sicherstellen, dass current_solar_limit eine Zahl ist, falls der vorherige Node mal was anderes liefert\nlet current_solar_limit = parseFloat(msg.payload);\n\nif (isNaN(grid_power) || isNaN(current_solar_limit)) {\n    node.error(\"Ungültige Eingabewerte für Regelung! grid_power: \" + msg.grid_power + \", current_solar_limit: \" + msg.payload);\n    return null;\n}\n\n// --- Konfiguration der Regelung ---\nconst target_grid_power = 10.0; // Ziel: 10W Netzbezug\nconst Kp = 0.5; // Proportionalitätsfaktor. WICHTIG: Dieser Wert muss ggf. angepasst werden!\n                // Kleinere Werte (z.B. 0.1-0.2) = sanfter, langsamer.\n                // Größere Werte (z.B. 0.5-0.8) = schneller, aber Gefahr von Überschwingen.\nconst min_solar_limit = 30.0;  // Minimale Leistungseinstellung für den Wechselrichter\nconst max_solar_limit = 800.0; // Maximale Leistungseinstellung für den Wechselrichter\n// --- Ende Konfiguration ---\n\n// Fehler: Differenz zwischen Ist-Netzleistung und Soll-Netzleistung\n// error > 0: Netzbezug ist zu hoch (oder Einspeisung zu gering) -> Solarleistung erhöhen\n// error < 0: Einspeisung ist zu hoch (oder Netzbezug zu gering/negativ) -> Solarleistung senken\nlet error = grid_power - target_grid_power;\n\n// Berechnung der nötigen Anpassung der Solarleistung\nlet adjustment = error * Kp;\n\nlet new_solar_limit = current_solar_limit + adjustment;\n\n// Begrenzung auf Min/Max Werte\nif (new_solar_limit > max_solar_limit) {\n    new_solar_limit = max_solar_limit;\n} else if (new_solar_limit < min_solar_limit) {\n    new_solar_limit = min_solar_limit;\n}\n\n// Runden auf Ganzzahlen\new_solar_limit = Math.round(new_solar_limit);\n\n// Nur senden, wenn sich der Wert tatsächlich ändert (mind. 1W Unterschied),\n// um unnötige API-Calls und Systemlast zu vermeiden.\nif (Math.abs(new_solar_limit - Math.round(current_solar_limit)) < 1) {\n    node.status({fill:\"blue\", shape:\"ring\", text:\"Keine Änderung: \" + new_solar_limit + \"W\"});\n    return null; // Stoppt den Flow hier, wenn keine signifikante Änderung\n}\n\nmsg.payload = new_solar_limit;\nnode.status({fill:\"green\", shape:\"dot\", text:\"Setze: \" + new_solar_limit + \"W\"});\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 670,
        "y": 240,
        "wires": [
            [
                "1df797cfc92adc6d",
                "a8f7aa73c0dc1ef3"
            ]
        ]
    },
    {
        "id": "1df797cfc92adc6d",
        "type": "api-call-service",
        "z": "7583219a0506bf8d",
        "name": "Solar Limit setzen",
        "server": "homeassistant-server",
        "version": 7,
        "debugenabled": false,
        "action": "number.set_value",
        "floorId": [],
        "areaId": [],
        "deviceId": [],
        "entityId": [
            "number.solar_maximale_leistung"
        ],
        "labelId": [],
        "data": "{\t   \"value\":msg.payload\t}",
        "dataType": "jsonata",
        "mergeContext": "",
        "mustacheAltTags": false,
        "outputProperties": [],
        "queue": "none",
        "x": 900,
        "y": 240,
        "wires": [
            []
        ]
    },
    {
        "id": "a8f7aa73c0dc1ef3",
        "type": "debug",
        "z": "7583219a0506bf8d",
        "name": "Neues Solar Limit",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 900,
        "y": 300,
        "wires": []
    },
    {
        "id": "homeassistant-server",
        "type": "server",
        "name": "Home Assistant",
        "addon": true
    }
]

so, ich habs geschafft - ohne NodeRed:

alias: PV Nulleinspeisung Proportional (10W Ziel)
description: >
  Regelt die maximale Solarleistung, um einen Netzbezug von ca. 10W zu
  erreichen. Kp-Faktor, Min/Max Solarleistung müssen ggf. angepasst werden.
triggers:
  - seconds: /5
    trigger: time_pattern
conditions:
  - condition: state
    entity_id: input_boolean.nulleinspeisung_hauptschalter
    state: "on"
actions:
  - variables:
      target_grid_power: 10
      kp: 0.8
      min_solar_limit: 30
      max_solar_limit: 800
      grid_power: "{{ states('sensor.bitshake_aktueller_verbrauch') | float(0) }}"
      current_solar_limit: "{{ states('number.solar_maximale_leistung') | float(0) }}"
      error_value: "{{ grid_power - target_grid_power }}"
      adjustment: "{{ error_value * kp }}"
      new_solar_limit_calculated: "{{ current_solar_limit + adjustment }}"
      new_solar_limit_clamped: >-
        {{ [[new_solar_limit_calculated, max_solar_limit] | min,
        min_solar_limit] | max }}
      new_solar_limit_final: "{{ new_solar_limit_clamped | round(0) }}"
      current_solar_limit_rounded: "{{ current_solar_limit | round(0) }}"
  - condition: template
    value_template: "{{ (new_solar_limit_final - current_solar_limit_rounded) | abs >= 1 }}"
  - target:
      entity_id: number.solar_maximale_leistung
    data:
      value: "{{ new_solar_limit_final }}"
    action: number.set_value
mode: single