"Balkon" Anlage: Growatt Noah 2000, APsystems EZ1-M und SUNKET 440W TOPCon Panels

Hallo @noize!

Vielen Dank für dein Feedback und die Nachfrage. Bei vollem Speicher sollte die Ausgangsleistung eigentlich hochgeregelt werden - das ist in der Logik enthalten.

Die wahrscheinlichsten Ursachen:

  1. Entity-Namen sind unterschiedlich: Du hast es schon erwähnt - es könnte sein, dass deine EZ1-M Entitäten anders heißen als die im Flow. Prüfe folgende Entities in deinem Home Assistant:

    • sensor.solar_gesamtleistung
    • number.solar_maximale_leistung
    • sensor.nec_noah_soc
    • sensor.nec_noah_solar_power
  2. Set Value Service: Die Aktion number.set_value ist ein Standard-Service in Home Assistant zum Setzen von Zahlenwerten. Du findest ihn in Entwicklerwerkzeuge > Dienste, dann wähle "number" als Domain und "set_value" als Service.

  3. SOC-Schwellwert: Im Code ist ein Schwellwert von 98% für "vollen Akku" definiert. Wenn dein System den SOC auf 100% begrenzt, aber in der Praxis nie 98% erreicht, würde diese Bedingung nie erfüllt.

Lösungsvorschläge:

  1. Prüfe die Entity-Namen: Schaue in deinem Home Assistant nach den korrekten Entity-IDs und passe die Nodes im Flow entsprechend an.

  2. Aktiviere die Debug-Nodes: Im Flow sind mehrere Debug-Nodes vorhanden (Debug Details, Debug Calculation, Debug Response). Aktiviere sie im Flow-Editor, um zu sehen, was genau passiert und welche Werte übertragen werden.

  3. SOC-Schwellwert anpassen: Du kannst den Schwellwert für "vollen Akku" in der Berechne neue EZ1-M Leistung-Funktion anpassen. Suche nach HIGH_SOC: 98 in den Konstanten und setze ihn auf einen Wert, den dein System tatsächlich erreicht (z.B. 95).

  4. Prüfe den tatsächlichen SOC-Wert: Vergewissere dich, dass der SOC-Wert korrekt aus dem Sensor gelesen wird, indem du ihn in der Home Assistant Oberfläche betrachtest.

  5. Service-Test: Du kannst manuell testen, ob der Service funktioniert, indem du in Entwicklerwerkzeuge > Dienste den Service number.set_value aufrufst und deine EZ1-M Entity-ID sowie einen Wert (z.B. 500) angibst.

Lass mich wissen, ob einer dieser Vorschläge das Problem löst oder ob weitere Anpassungen nötig sind!

Gruß,
aspiro​​​​​​​​​​​​​​​​

Hi Aspiro.
Danke für die schnelle Antwort.

Folgendes konnte ich eruieren:

Die 2 Entis heißen bei mir anders. Entweder übersehe ich etwas aber diese sind in den Flows nicht enthalten?
sensor.nec_noah_soc <-> sensor.noah_2000_soc
sensor.nec_noah_solar_power <-> sensor.noah_2000_solar_power

set value service:
Wenn dein Flow läuft gibt er 175 an HA weiter. Das scheint ansich zu gehen.
Aber Node meint es ist ein node mit issues.
Anbei ein Screen:

Und n och zum reinen Verständnis: ich habe deinen Flow vom Nov 2024 und Node vom 7.März genommen.

Hey @noize ,

Möchtest du mir mal folgende IDs von deinem Setup geben.
Und deinen durchschnittlichen Grundverbrauch? Dieser liegt bei mir bei 175W wegen einem Server.

Dann schreibe ich dir den Flow um.

Hallo aspiro.
Also so wie auf deinem Screenshot sieht der Flow bei mir aber nicht aus.

Stromverbrauch: sensor.shellypro3em_8813bfd98a04_total_active_power
Stromerzeugung: sensor.solar_gesamtleistung (hier bin ich mir nicht ganz sicher ob eine andere Enti besser wäre. diese ist von der EZ1 Integration)
EZ1-M max Leistung: number.solar_maximale_leistung
sensor.noah_2000_soc
sensor.noah_2000_solar_power

Meine Grundlast liegt ca bei 300W wenn niemand zu Hause ist.

Hallo @noize,

ich habe mal meine KI Projekt von Claude drüber laufen lassen.

Anpassung des Node-RED Flows für Nulleinspeisung mit EZ1-M Wechselrichter

Ich habe den Flow für den neuen Benutzer angepasst, der ein ähnliches Setup verwendet, aber mit anderen Sensoren und einer höheren Grundlast arbeitet. Hier sind die wichtigsten Änderungen und eine Erklärung, wie der Flow funktioniert.

[
  {
    "id": "638a1d646d26639d",
    "type": "tab",
    "label": "Nulleinspeisung über EZ1-M (Angepasst für Shelly-Messung)",
    "disabled": false,
    "info": "Flow zur Optimierung der Leistungsregelung des EZ1-M Wechselrichters mit dem Ziel der Nulleinspeisung. Angepasste Version für Shelly-Strommessung und Noah 2000 System.",
    "env": []
  },
  {
    "id": "cdef74971de7b4be",
    "type": "inject",
    "z": "638a1d646d26639d",
    "name": "Trigger Shelly Daten (alle 5s)",
    "props": [
      {
        "p": "payload"
      },
      {
        "p": "topic",
        "vt": "str"
      }
    ],
    "repeat": "5",
    "crontab": "",
    "once": true,
    "onceDelay": "2",
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 170,
    "y": 140,
    "wires": [
      [
        "052b0d588db39343",
        "5db22a464cb16baf"
      ]
    ]
  },
  {
    "id": "5db22a464cb16baf",
    "type": "api-current-state",
    "z": "638a1d646d26639d",
    "name": "Tatsächliche WR-Leistung",
    "server": "97162e72.b6a46",
    "version": 3,
    "outputs": 1,
    "halt_if": "",
    "halt_if_type": "str",
    "entity_id": "sensor.solar_gesamtleistung",
    "state_type": "str",
    "blockInputOverrides": false,
    "outputProperties": [
      {
        "property": "payload",
        "propertyType": "msg",
        "value": "",
        "valueType": "entityState"
      },
      {
        "property": "topic",
        "propertyType": "msg",
        "value": "solar_gesamtleistung",
        "valueType": "str"
      }
    ],
    "for": "",
    "forType": "num",
    "x": 530,
    "y": 260,
    "wires": [
      [
        "61c0854d5f7b155d"
      ]
    ]
  },
  {
    "id": "54e48d72ade4787c",
    "type": "server-state-changed",
    "z": "638a1d646d26639d",
    "name": "Überwache EZ1-M Änderungen",
    "server": "97162e72.b6a46",
    "version": 6,
    "outputs": 1,
    "exposeAsEntityConfig": "",
    "entities": {
      "entity": [
        "number.solar_maximale_leistung"
      ],
      "substring": [],
      "regex": []
    },
    "outputInitially": false,
    "ifState": "",
    "ifStateType": "str",
    "outputOnlyOnStateChange": false,
    "for": "",
    "forType": "num",
    "ignorePrevStateNull": false,
    "ignorePrevStateUnknown": false,
    "ignorePrevStateUnavailable": false,
    "ignoreCurrentStateUnknown": false,
    "ignoreCurrentStateUnavailable": false,
    "outputProperties": [],
    "entityidfilter": "number.solar_maximale_leistung",
    "entityidfiltertype": "exact",
    "outputinitially": true,
    "haltifstate": "",
    "x": 170,
    "y": 320,
    "wires": [
      [
        "fa4fd9c7be954d74"
      ]
    ]
  },
  {
    "id": "a39fdc29c1d5d232",
    "type": "inject",
    "z": "638a1d646d26639d",
    "name": "Trigger Batterie & Solar (alle 60s)",
    "props": [
      {
        "p": "payload"
      },
      {
        "p": "topic",
        "vt": "str"
      }
    ],
    "repeat": "60",
    "crontab": "",
    "once": true,
    "onceDelay": "3",
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 180,
    "y": 420,
    "wires": [
      [
        "705a6b9e1df8a6b8",
        "4900dfb56cfa4fd2"
      ]
    ]
  },
  {
    "id": "f9d41ac384c69ab2",
    "type": "inject",
    "z": "638a1d646d26639d",
    "name": "Prüfe EZ1-M Status regelmäßig (15min)",
    "props": [
      {
        "p": "payload"
      },
      {
        "p": "topic",
        "vt": "str"
      }
    ],
    "repeat": "900",
    "crontab": "",
    "once": true,
    "onceDelay": "5",
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 200,
    "y": 380,
    "wires": [
      [
        "fa4fd9c7be954d74"
      ]
    ]
  },
  {
    "id": "052b0d588db39343",
    "type": "api-current-state",
    "z": "638a1d646d26639d",
    "name": "Shelly Strommessung",
    "server": "97162e72.b6a46",
    "version": 3,
    "outputs": 1,
    "halt_if": "",
    "halt_if_type": "str",
    "entity_id": "sensor.shellypro3em_8813bfd98a04_total_active_power",
    "state_type": "str",
    "blockInputOverrides": false,
    "outputProperties": [
      {
        "property": "payload",
        "propertyType": "msg",
        "value": "",
        "valueType": "entityState"
      },
      {
        "property": "topic",
        "propertyType": "msg",
        "value": "shelly_power",
        "valueType": "str"
      }
    ],
    "for": "",
    "forType": "num",
    "x": 510,
    "y": 140,
    "wires": [
      [
        "61c0854d5f7b155d"
      ]
    ]
  },
  {
    "id": "fa4fd9c7be954d74",
    "type": "api-current-state",
    "z": "638a1d646d26639d",
    "name": "EZ1-M Maximale Leistung",
    "server": "97162e72.b6a46",
    "version": 3,
    "outputs": 1,
    "halt_if": "",
    "halt_if_type": "str",
    "entity_id": "number.solar_maximale_leistung",
    "state_type": "str",
    "blockInputOverrides": false,
    "outputProperties": [
      {
        "property": "payload",
        "propertyType": "msg",
        "value": "",
        "valueType": "entityState"
      },
      {
        "property": "topic",
        "propertyType": "msg",
        "value": "solar_maximale_leistung",
        "valueType": "str"
      }
    ],
    "for": "",
    "forType": "num",
    "x": 540,
    "y": 320,
    "wires": [
      [
        "61c0854d5f7b155d"
      ]
    ]
  },
  {
    "id": "705a6b9e1df8a6b8",
    "type": "api-current-state",
    "z": "638a1d646d26639d",
    "name": "Batterie SOC",
    "server": "97162e72.b6a46",
    "version": 3,
    "outputs": 1,
    "halt_if": "",
    "halt_if_type": "str",
    "entity_id": "sensor.noah_2000_soc",
    "state_type": "str",
    "blockInputOverrides": false,
    "outputProperties": [
      {
        "property": "payload",
        "propertyType": "msg",
        "value": "",
        "valueType": "entityState"
      },
      {
        "property": "topic",
        "propertyType": "msg",
        "value": "batterie_soc",
        "valueType": "str"
      }
    ],
    "for": "",
    "forType": "num",
    "x": 490,
    "y": 380,
    "wires": [
      [
        "61c0854d5f7b155d"
      ]
    ]
  },
  {
    "id": "4900dfb56cfa4fd2",
    "type": "api-current-state",
    "z": "638a1d646d26639d",
    "name": "Solar Power",
    "server": "97162e72.b6a46",
    "version": 3,
    "outputs": 1,
    "halt_if": "",
    "halt_if_type": "str",
    "entity_id": "sensor.noah_2000_solar_power",
    "state_type": "str",
    "blockInputOverrides": false,
    "outputProperties": [
      {
        "property": "payload",
        "propertyType": "msg",
        "value": "",
        "valueType": "entityState"
      },
      {
        "property": "topic",
        "propertyType": "msg",
        "value": "solar_power",
        "valueType": "str"
      }
    ],
    "for": "",
    "forType": "num",
    "x": 490,
    "y": 440,
    "wires": [
      [
        "61c0854d5f7b155d"
      ]
    ]
  },
  {
    "id": "61c0854d5f7b155d",
    "type": "function",
    "z": "638a1d646d26639d",
    "name": "Datenspeicherung und Triggerkontrolle",
    "func": "// Initialisierung des Flow-Kontexts, falls noch nicht vorhanden\nconst storage = flow.get('sensorData') || {\n    shelly_power: null,\n    solar_maximale_leistung: null,\n    solar_gesamtleistung: null,\n    batterie_soc: null,\n    solar_power: null,\n    lastCalculation: 0,\n    updateRequested: false,\n    lastMorningCheck: 0,\n    lastLoadCheck: 0\n};\n\n// Speichern des aktuellen Sensorwerts im Flow-Kontext\nconst topic = msg.topic;\nconst value = msg.payload;\n\n// Aktualisieren des entsprechenden Sensorwerts\nstorage[topic] = value;\n\n// Prüfen auf Vollständigkeit der Daten\nconst allDataAvailable = (\n    storage.shelly_power !== null &&\n    storage.solar_maximale_leistung !== null &&\n    storage.solar_gesamtleistung !== null &&\n    storage.batterie_soc !== null &&\n    storage.solar_power !== null\n);\n\n// Aktuelle Zeit in Millisekunden\nconst now = Date.now();\n\n// Systemkonstanten für bessere Wartbarkeit\nconst CONSTANTS = {\n    // Minimale Aktualisierungszeiten (in ms)\n    UPDATE_INTERVALS: {\n        shelly_power: 5000,         // 5 Sekunden für Stromverbrauch/Einspeisung\n        solar_gesamtleistung: 5000, // 5 Sekunden für tatsächliche Leistung\n        solar_maximale_leistung: 0, // Sofortige Berechnung bei Änderung\n        batterie_soc: 30000,        // 30 Sekunden für SOC (erhöht für Stabilität)\n        solar_power: 30000          // 30 Sekunden für Solar Power (erhöht)\n    },\n    // Allgemeines Mindestintervall zwischen Berechnungen (in ms)\n    GENERAL_MIN_INTERVAL: 5000,    // 5 Sekunden als Minimum zwischen Updates\n    \n    // Lastspitzenerkennung\n    HIGH_LOAD_THRESHOLD: 1000,     // Schwellwert für sehr hohe Last (W)\n    MEDIUM_LOAD_THRESHOLD: 200,    // Schwellwert für mittlere Last (W)\n    \n    // Zeitintervalle\n    MORNING_CHECK_INTERVAL: 300000, // 5 Minuten zwischen Morgenprüfungen\n    LOAD_CHECK_INTERVAL: 4000      // 4 Sekunden zwischen Lastspitzenprüfungen\n};\n\n// Shelly-Wert verarbeiten (kann positiv oder negativ sein)\nconst shellyPower = parseFloat(storage.shelly_power) || 0;\n\n// Positive Werte bedeuten Netzbezug (Verbrauch)\n// Negative Werte bedeuten Einspeisung\nconst netzbezug = shellyPower > 0 ? shellyPower : 0;\nconst einspeisung = shellyPower < 0 ? Math.abs(shellyPower) : 0;\n\n// Spezielle Zeitbedingungen prüfen\nconst currentHour = new Date().getHours();\nconst isEarlyMorning = currentHour >= 5 && currentHour < 10;\nconst lastMorningCheckElapsed = now - storage.lastMorningCheck;\n\n// Lastspitzenerkennung mit definierten Schwellwerten\nlet isHighLoadSituation = false;\nconst maxLeistung = parseFloat(storage.solar_maximale_leistung) || 0;\nconst aktuelleLeistung = parseFloat(storage.solar_gesamtleistung) || 0;\n\n// Verbesserte Lastspitzenerkennung\nif (netzbezug > CONSTANTS.HIGH_LOAD_THRESHOLD || \n    (netzbezug > CONSTANTS.MEDIUM_LOAD_THRESHOLD && aktuelleLeistung > maxLeistung * 0.9)) {\n    isHighLoadSituation = true;\n}\n\n// Bestimmen, ob eine Neuberechnung erfolgen soll\nlet shouldTriggerCalculation = false;\n\n// Prüfen, ob genügend Zeit seit der letzten Berechnung vergangen ist\nconst timePassedSinceLastCalc = now - storage.lastCalculation;\n\n// 1. Morgenprüfung - bei Sonnenaufgang und inaktivem WR\nif (isEarlyMorning && \n    lastMorningCheckElapsed > CONSTANTS.MORNING_CHECK_INTERVAL && \n    parseFloat(storage.solar_maximale_leistung) === 0 && \n    parseFloat(storage.batterie_soc) > 20 && \n    parseFloat(storage.solar_power) > 50) {\n    \n    shouldTriggerCalculation = true;\n    storage.lastMorningCheck = now;\n}\n// 2. Lastspitzenprüfung - häufigere Berechnung bei hoher Last\nelse if (isHighLoadSituation && \n         timePassedSinceLastCalc >= CONSTANTS.LOAD_CHECK_INTERVAL &&\n         !storage.updateRequested) {\n    \n    shouldTriggerCalculation = true;\n}\n// 3. Standardberechnung mit Priorisierung wichtiger Sensoren\nelse if (allDataAvailable && \n         timePassedSinceLastCalc >= CONSTANTS.GENERAL_MIN_INTERVAL && \n         !storage.updateRequested) {\n    \n    // Hauptsensoren haben Priorität (Verbrauch/Einspeisung, tatsächliche Leistung)\n    if (topic === 'shelly_power' || topic === 'solar_gesamtleistung') {\n        shouldTriggerCalculation = true;\n    }\n    // Andere Sensoren nur, wenn ihr spezifisches Intervall erreicht ist\n    else if (CONSTANTS.UPDATE_INTERVALS[topic] === 0 || \n             timePassedSinceLastCalc >= CONSTANTS.UPDATE_INTERVALS[topic]) {\n        shouldTriggerCalculation = true;\n    }\n}\n\n// Speichern des aktualisierten Zustands im Flow-Kontext\nflow.set('sensorData', storage);\n\n// Wenn Berechnung ausgelöst werden soll, alle Daten an die Berechnungsfunktion senden\nif (shouldTriggerCalculation) {\n    // Update-Flag setzen, um parallele Berechnungen zu vermeiden\n    storage.updateRequested = true;\n    flow.set('sensorData', storage);\n    \n    // Zeitstempel der Berechnung aktualisieren\n    storage.lastCalculation = now;\n    \n    // Erstellen der Nachricht mit allen Sensordaten für die Berechnungsfunktion\n    return {\n        payload: {\n            netzbezug: netzbezug,\n            einspeisung: einspeisung,\n            shelly_power: storage.shelly_power,\n            solar_maximale_leistung: storage.solar_maximale_leistung,\n            solar_gesamtleistung: storage.solar_gesamtleistung,\n            batterie_soc: storage.batterie_soc,\n            solar_power: storage.solar_power,\n            triggerSource: topic,\n            triggerTime: now,\n            isEarlyMorning: isEarlyMorning,\n            isHighLoadSituation: isHighLoadSituation\n        }\n    };\n}\n\n// Keine Nachricht zurückgeben, wenn keine Berechnung ausgelöst werden soll\nreturn null;",
    "outputs": 1,
    "timeout": "",
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 930,
    "y": 140,
    "wires": [
      [
        "02638474a170add7"
      ]
    ]
  },
  {
    "id": "02638474a170add7",
    "type": "function",
    "z": "638a1d646d26639d",
    "name": "Berechne neue EZ1-M Leistung",
    "func": "// Systemkonstanten für bessere Wartbarkeit und Lesbarkeit\nconst CONSTANTS = {\n    // Leistungsgrenzen\n    MAX_POWER: 780,                // Maximale Wechselrichterleistung in Watt\n    MIN_POWER: 75,                 // Minimale Wechselrichterleistung in Watt\n    \n    // Betriebsbereiche - angepasst für höhere Grundlast\n    GRUNDLAST_MIN: 75,             // Untere Grenze für Grundlastbereich (W)\n    GRUNDLAST_MAX: 325,            // Obere Grenze für Grundlastbereich (W) - angepasst auf 325W\n    \n    // Schwellwerte\n    HIGH_SOC: 98,                  // Schwellwert für hohen SOC (%)\n    MIN_GRID_EXPORT: 15,           // Minimale Einspeisung für Leistungsreduzierung (W)\n    LOW_GRID_IMPORT: 20,           // Schwellwert für niedrigen Netzbezug (W)\n    MEDIUM_GRID_IMPORT: 50,        // Schwellwert für mittleren Netzbezug (W)\n    HIGH_GRID_IMPORT: 100,         // Schwellwert für hohen Netzbezug (W)\n    VERY_HIGH_GRID_IMPORT: 1000,   // Schwellwert für sehr hohen Netzbezug (W)\n    FAILSAFE_POWER: 200,           // Fallback-Leistung bei fehlenden Daten (W) - angepasst auf 200W\n    \n    // Zeitintervalle für Stabilitätssteuerung\n    LOAD_PEAK_INTERVAL: 3000,      // Intervall für Lastspitzen (ms)\n    NORMAL_UPDATE_INTERVAL: 15000,  // Normales Update-Intervall (ms)\n    STABLE_UPDATE_INTERVAL: 30000,  // Verlängertes Intervall bei Stabilität (ms) - auf 30s reduziert\n    \n    // Stabilitätskonfiguration\n    STABILITY_THRESHOLD: 8,         // Anzahl stabiler Updates für verlängerte Intervalle\n    LARGE_CHANGE_THRESHOLD: 50      // Schwellwert für große Änderungen (W)\n};\n\n// Initialisierung des globalen Kontexts\nif (!global.solarControl) {\n    global.solarControl = {\n        lastPower: 0,                // Zuletzt gesetzte Leistung\n        lastUpdateTime: 0,           // Zeitstempel der letzten Aktualisierung\n        peakLoadStartTime: 0,        // Zeitstempel des Beginns einer Lastspitze\n        stabilityCounter: 0,         // Zähler für stabile Betriebsbedingungen\n        consecutiveStableUpdates: 0  // Zähler für aufeinanderfolgende stabile Updates\n    };\n}\n\n// Hauptfunktion zur Berechnung der neuen Wechselrichterleistung\nfunction calculateNewSolarPower(msg) {\n    // Sensorwerte extrahieren und parsen\n    const energie = parseFloat(msg.payload.netzbezug) || 0;           // Netzbezug (W)\n    const stromerzeugung = parseFloat(msg.payload.einspeisung) || 0;   // Netzeinspeisung (W)\n    const soc = parseFloat(msg.payload.batterie_soc) || 0;            // Batterieladezustand (%)\n    const solarPower = parseFloat(msg.payload.solar_power) || 0;      // Solarleistung (W)\n    const aktuelleLeistung = parseFloat(msg.payload.solar_maximale_leistung) || 0; // Eingestellte WR-Leistung (W)\n    const tatsaechlicheLeistung = parseFloat(msg.payload.solar_gesamtleistung) || 0; // Tatsächliche WR-Leistung (W)\n    const isHighLoadSituation = msg.payload.isHighLoadSituation || false; // Lastspitze Flag\n    \n    // Aktuelle Zeit für Zeitstempelvergleiche\n    const now = Date.now();\n    \n    // Erstinitialisierung bei erstem Lauf\n    if (global.solarControl.lastPower === 0) {\n        global.solarControl.lastPower = aktuelleLeistung;\n    }\n    \n    // Leistungsdefizit berechnen (Differenz zwischen Soll und Ist)\n    const leistungsDefizit = aktuelleLeistung - tatsaechlicheLeistung;\n    \n    // Strategieauswahl und Leistungsberechnung\n    let neueLeistung = aktuelleLeistung;\n    let anpassungsgrund = \"Keine Änderung\";\n    \n    /* ===== ENTSCHEIDUNGSLOGIK IN PRIORITÄTSREIHENFOLGE ===== */\n    \n    // 1. Failsafe: Prüfung auf fehlende Daten (Shelly, Noah, EZ1-M)\n    if (energie === 0 && stromerzeugung === 0 && (solarPower === 0 || tatsaechlicheLeistung === 0)) {\n        neueLeistung = CONSTANTS.FAILSAFE_POWER; // Failsafe: Sichere Grundlast (angepasst auf 200W)\n        anpassungsgrund = `Failsafe aktiviert: Fehlende Daten von Sensoren, setze sichere Grundlast (${CONSTANTS.FAILSAFE_POWER}W)`;\n    }\n    // 2. Prüfung auf Hochlastsituation (sofortige Reaktion erforderlich)\n    else if (isHighLoadSituation) {\n        if (energie > CONSTANTS.VERY_HIGH_GRID_IMPORT) {\n            // Bei extremem Verbrauch sofort auf Maximum\n            neueLeistung = CONSTANTS.MAX_POWER;\n            anpassungsgrund = `Extreme Lastspitze: ${energie}W Netzbezug, maximale WR-Leistung aktiviert`;\n        } else {\n            // Bei hohem Verbrauch und WR nahe Volllast, schrittweise erhöhen\n            neueLeistung = Math.min(aktuelleLeistung + 50, CONSTANTS.MAX_POWER);\n            anpassungsgrund = `Lastspitze: WR bei ${tatsaechlicheLeistung}W, erhöhe auf ${neueLeistung}W`;\n        }\n        global.solarControl.peakLoadStartTime = now;\n    }\n    // 3. Strategie bei vollem Akku (SOC ≥ 98%)\n    else if (soc >= CONSTANTS.HIGH_SOC) {\n        // Ziel: Batterieladen vermeiden und Eigenverbrauch maximieren\n        neueLeistung = Math.min(solarPower + energie, CONSTANTS.MAX_POWER);\n        anpassungsgrund = `SOC ≥ ${CONSTANTS.HIGH_SOC}%, setze Leistung auf Solar-Input + Netzbezug`;\n    }\n    // 4. Ausgleich bei signifikantem Leistungsdefizit\n    else if (leistungsDefizit > 50 && energie > 25) {\n        // WR liefert nicht die gewünschte Leistung, aber Netzbezug besteht\n        neueLeistung = Math.min(aktuelleLeistung + energie + leistungsDefizit * 0.5, CONSTANTS.MAX_POWER);\n        anpassungsgrund = `WR-Leistungsdefizit: ${Math.round(leistungsDefizit)}W, Erhöhung um ${Math.round(energie + leistungsDefizit * 0.5)}W`;\n    }\n    // 5. Standardstrategie für normale Betriebsbedingungen\n    else {\n        // Prüfen, ob wir uns im Grundlastbereich befinden (75-325W - angepasst)\n        const isGrundlastBereich = (aktuelleLeistung >= CONSTANTS.GRUNDLAST_MIN && \n                                   aktuelleLeistung <= CONSTANTS.GRUNDLAST_MAX);\n        \n        // Berücksichtigung der tatsächlichen Leistung\n        // Bei signifikant niedrigerer Ist-Leistung stärkere Anpassung vornehmen\n        const leistungsFaktor = tatsaechlicheLeistung < aktuelleLeistung * 0.9 ? 1.2 : 1.0;\n        \n        // 5.1 Sonderfall: Hoher Netzbezug außerhalb des Grundlastbereichs\n        if (energie > CONSTANTS.MEDIUM_GRID_IMPORT && !isGrundlastBereich) {\n            neueLeistung = aktuelleLeistung + Math.round(energie * 0.8 * leistungsFaktor);\n            anpassungsgrund = `Hoher Netzbezug (${energie}W), schnelle Anpassung mit Faktor ${leistungsFaktor.toFixed(1)}`;\n        }\n        // 5.2 Geringer Netzbezug ohne Einspeisung - stabil, keine Änderung\n        else if (energie > 0 && \n                 energie < CONSTANTS.LOW_GRID_IMPORT && \n                 stromerzeugung < CONSTANTS.MIN_GRID_EXPORT) {\n            // Keine Änderung, stabiler Zustand\n            anpassungsgrund = \"Stabile Bedingungen, keine Anpassung nötig\";\n        }\n        // 5.3 Verbesserte Feinabstimmung im Grundlastbereich (75-325W - angepasst)\n        else if (isGrundlastBereich) {\n            if (energie >= 25 || stromerzeugung >= CONSTANTS.MIN_GRID_EXPORT) {\n                // Netzbezug: Leistung mit gestaffelten Schritten erhöhen\n                if (energie >= 25) {\n                    let schrittGroesse;\n                    \n                    // Deutlich gestaffelte Anpassung je nach Netzbezug\n                    if (energie >= 100) {\n                        // Sehr hoher Netzbezug im Grundlastbereich: Große Schritte\n                        schrittGroesse = Math.min(50, Math.round(energie * 0.5));\n                        anpassungsgrund = `Großer Schritt (${schrittGroesse}W) wegen hohem Netzbezug von ${energie}W`;\n                    } else if (energie >= 50) {\n                        // Mittlerer Netzbezug: Mittlere Schritte\n                        schrittGroesse = Math.min(30, Math.round(energie * 0.4));\n                        anpassungsgrund = `Mittlerer Schritt (${schrittGroesse}W) bei Netzbezug von ${energie}W`;\n                    } else {\n                        // Geringer Netzbezug: Kleine Schritte wie bisher\n                        schrittGroesse = tatsaechlicheLeistung < aktuelleLeistung * 0.9 ? 10 : 5;\n                        anpassungsgrund = `Kleiner Schritt (${schrittGroesse}W) bei geringem Netzbezug`;\n                    }\n                    \n                    // Zusätzlicher Faktor für Fälle mit Leistungsdefizit\n                    if (leistungsDefizit < -15) {\n                        // WR liefert mehr als eingestellt - berücksichtigen bei Anpassung\n                        schrittGroesse = Math.round(schrittGroesse * 1.3);\n                        anpassungsgrund += ` (erhöht wegen Leistungsdefizit von ${Math.round(leistungsDefizit)}W)`;\n                    }\n                    \n                    neueLeistung = aktuelleLeistung + schrittGroesse;\n                }\n                // Einspeisung: Leistung reduzieren (unverändert)\n                else if (stromerzeugung >= CONSTANTS.MIN_GRID_EXPORT) {\n                    neueLeistung = aktuelleLeistung - 5;\n                    anpassungsgrund = `5W-Schritt nach unten (Netzeinspeisung ${stromerzeugung}W)`;\n                }\n            }\n        }\n        // 5.4 Moderate Anpassung bei mittlerem Netzbezug\n        else if (energie >= CONSTANTS.LOW_GRID_IMPORT && energie <= CONSTANTS.MEDIUM_GRID_IMPORT) {\n            neueLeistung = aktuelleLeistung + Math.round(energie * 0.7 * leistungsFaktor);\n            anpassungsgrund = `Moderater Netzbezug (${energie}W), angepasste Erhöhung um ${Math.round(energie * 0.7 * leistungsFaktor)}W`;\n        }\n        // 5.5 Direkte Anpassung bei höherem Netzbezug\n        else if (energie > CONSTANTS.MEDIUM_GRID_IMPORT) {\n            neueLeistung = aktuelleLeistung + Math.round(energie * leistungsFaktor);\n            anpassungsgrund = `Direkte Anpassung bei Netzbezug (${energie}W) um ${Math.round(energie * leistungsFaktor)}W`;\n        }\n        // 5.6 Anpassung bei höherer Grundleistung und moderatem Verbrauch\n        else if (aktuelleLeistung > CONSTANTS.GRUNDLAST_MAX && energie >= 25) {\n            neueLeistung = aktuelleLeistung + Math.round(energie * leistungsFaktor);\n            anpassungsgrund = `Erhöhung bei ${aktuelleLeistung}W Grundleistung und ${energie}W Netzbezug`;\n        }\n        // 5.7 Reduzierung bei Netzeinspeisung\n        else if (stromerzeugung >= CONSTANTS.MIN_GRID_EXPORT) {\n            // Minimum 100W beibehalten\n            const reduktion = Math.min(stromerzeugung, aktuelleLeistung - 100);\n            neueLeistung = Math.max(aktuelleLeistung - reduktion, 100);\n            anpassungsgrund = `Reduziere Leistung um ${Math.round(reduktion)}W wegen Netzeinspeisung (${stromerzeugung}W)`;\n        }\n    }\n\n    // IMMER auf 5W-Schritte runden für konsistente Steuerung\n    neueLeistung = Math.round(neueLeistung / 5) * 5;\n    \n    // Begrenzung auf erlaubten Betriebsbereich\n    neueLeistung = Math.max(CONSTANTS.MIN_POWER, Math.min(CONSTANTS.MAX_POWER, neueLeistung));\n    \n    // Bestimmen, ob ein Update gesendet werden soll (verbesserte Stabilität)\n    let sendUpdate = false;\n    const lastUpdateTime = global.solarControl.lastUpdateTime || 0;\n    const timeSinceLastUpdate = now - lastUpdateTime;\n    \n    if (neueLeistung !== aktuelleLeistung) {\n        // 1. Sofortige Reaktion bei Lastspitzen oder großen Änderungen\n        if (isHighLoadSituation || Math.abs(neueLeistung - aktuelleLeistung) > CONSTANTS.LARGE_CHANGE_THRESHOLD) {\n            sendUpdate = true;\n            global.solarControl.stabilityCounter = 0; // Stabilitätszähler zurücksetzen\n            global.solarControl.consecutiveStableUpdates = 0;\n        }\n        // 2. Bei stabilen Bedingungen längere Intervalle zwischen Updates\n        else if (global.solarControl.stabilityCounter >= CONSTANTS.STABILITY_THRESHOLD) {\n            // Nach mehreren stabilen Intervallen seltener anpassen\n            if (timeSinceLastUpdate >= CONSTANTS.STABLE_UPDATE_INTERVAL) {\n                sendUpdate = true;\n                global.solarControl.consecutiveStableUpdates++;\n                // Stabilitätszähler nicht vollständig zurücksetzen bei kleinen Änderungen\n                global.solarControl.stabilityCounter = Math.max(global.solarControl.stabilityCounter - 2, 0);\n            }\n        }\n        // 3. Normale Aktualisierung nach Mindestwartezeit\n        else if (timeSinceLastUpdate >= CONSTANTS.NORMAL_UPDATE_INTERVAL) {\n            sendUpdate = true;\n            global.solarControl.stabilityCounter++;\n            global.solarControl.consecutiveStableUpdates = 0;\n        }\n    } else {\n        // Bei \"keine Änderung\" Stabilitätszähler erhöhen\n        global.solarControl.stabilityCounter++;\n    }\n    \n    // Status aktualisieren, wenn Update gesendet wird\n    if (sendUpdate) {\n        global.solarControl.lastPower = neueLeistung;\n        global.solarControl.lastUpdateTime = now;\n    }\n    \n    // Nach der Berechnung das Update-Flag zurücksetzen\n    const storage = flow.get('sensorData') || {};\n    storage.updateRequested = false;\n    flow.set('sensorData', storage);\n\n    // Rückgabe des Ergebnisses mit Zusatzinformationen für Debugging\n    return {\n        payload: neueLeistung,                   // Berechnete Sollleistung für den WR\n        entity_id: 'number.solar_maximale_leistung', // Ziel-Entity für HA\n        sendUpdate: sendUpdate,                   // Flag für API-Aufruf\n        debugInfo: {                             // Debug-Informationen\n            energie,\n            stromerzeugung,\n            soc,\n            solarPower,\n            aktuelleLeistung,\n            tatsaechlicheLeistung,\n            leistungsDefizit,\n            neueLeistung,\n            anpassungsgrund,\n            anpassungsmenge: neueLeistung - aktuelleLeistung,\n            updateSent: sendUpdate,\n            isHighLoadSituation,\n            timeSinceLastUpdate,\n            stabilityCounter: global.solarControl.stabilityCounter,\n            consecutiveStableUpdates: global.solarControl.consecutiveStableUpdates,\n            isGrundlastBereich: (aktuelleLeistung >= CONSTANTS.GRUNDLAST_MIN && \n                              aktuelleLeistung <= CONSTANTS.GRUNDLAST_MAX)\n        }\n    };\n}\n\n// Hauptfunktion aufrufen und Ergebnis zurückgeben\nreturn calculateNewSolarPower(msg);",
    "outputs": 1,
    "timeout": "",
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 910,
    "y": 200,
    "wires": [
      [
        "d31c244d73a5dd73",
        "a2d8a7b2a494e125",
        "0015b5d7cfce55f6"
      ]
    ]
  },
  {
    "id": "d31c244d73a5dd73",
    "type": "switch",
    "z": "638a1d646d26639d",
    "name": "Nur bei Änderung",
    "property": "sendUpdate",
    "propertyType": "msg",
    "rules": [
      {
        "t": "true"
      }
    ],
    "checkall": "true",
    "repair": false,
    "outputs": 1,
    "x": 870,
    "y": 260,
    "wires": [
      [
        "41180399ac416d12"
      ]
    ]
  },
  {
    "id": "41180399ac416d12",
    "type": "api-call-service",
    "z": "638a1d646d26639d",
    "name": "Setze EZ1-M Leistung",
    "server": "97162e72.b6a46",
    "version": 7,
    "debugenabled": false,
    "action": "number.set_value",
    "floorId": [],
    "areaId": [],
    "deviceId": [],
    "entityId": [],
    "labelId": [],
    "data": "{    \"entity_id\": entity_id,    \"value\": payload}",
    "dataType": "jsonata",
    "mergeContext": "",
    "mustacheAltTags": false,
    "outputProperties": [],
    "queue": "none",
    "blockInputOverrides": false,
    "domain": "number",
    "service": "set_value",
    "x": 880,
    "y": 320,
    "wires": [
      [
        "3aa713db0fa0101c"
      ]
    ]
  },
  {
    "id": "0015b5d7cfce55f6",
    "type": "debug",
    "z": "638a1d646d26639d",
    "name": "Debug Calculation",
    "active": false,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "payload",
    "targetType": "msg",
    "statusVal": "",
    "statusType": "auto",
    "x": 1270,
    "y": 200,
    "wires": []
  },
  {
    "id": "3aa713db0fa0101c",
    "type": "debug",
    "z": "638a1d646d26639d",
    "name": "Debug Response",
    "active": false,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "payload",
    "targetType": "msg",
    "statusVal": "",
    "statusType": "auto",
    "x": 1270,
    "y": 260,
    "wires": []
  },
  {
    "id": "a2d8a7b2a494e125",
    "type": "debug",
    "z": "638a1d646d26639d",
    "name": "Debug Details",
    "active": false,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "debugInfo",
    "targetType": "msg",
    "statusVal": "",
    "statusType": "auto",
    "x": 1260,
    "y": 140,
    "wires": []
  },
  {
    "id": "97162e72.b6a46",
    "type": "server",
    "name": "Home Assistant",
    "addon": true,
    "rejectUnauthorizedCerts": true,
    "ha_boolean": "",
    "connectionDelay": false,
    "cacheJson": false,
    "heartbeat": false,
    "heartbeatInterval": "",
    "statusSeparator": "",
    "enableGlobalContextStore": false
  }
]

Erklärung der Anpassungen

Ich habe den Flow an das neue Setup angepasst. Hier sind die wichtigsten Änderungen und was sie bedeuten:

1. Sensornamen angepasst

  • Tibber-Sensoren wurden durch Shelly und Noah 2000 Sensoren ersetzt:
    • sensor.shellypro3em_8813bfd98a04_total_active_power (für Stromverbrauch/Einspeisung)
    • sensor.noah_2000_soc (Batterieladezustand)
    • sensor.noah_2000_solar_power (Solarleistung)

2. Umgang mit Shelly-Messwerten

Der fundamentale Unterschied zwischen Tibber und Shelly ist, dass Tibber separate Sensoren für Verbrauch und Einspeisung hat, während Shelly einen einzigen Wert liefert, der positiv (bei Netzbezug) oder negativ (bei Einspeisung) sein kann.

Im ersten Funktionsknoten Datenspeicherung und Triggerkontrolle habe ich daher folgende Umrechnung eingefügt:

// Shelly-Wert verarbeiten (kann positiv oder negativ sein)
const shellyPower = parseFloat(storage.shelly_power) || 0;

// Positive Werte bedeuten Netzbezug (Verbrauch)
// Negative Werte bedeuten Einspeisung
const netzbezug = shellyPower > 0 ? shellyPower : 0;
const einspeisung = shellyPower < 0 ? Math.abs(shellyPower) : 0;

Diese Werte werden dann an den zweiten Funktionsknoten weitergegeben, wo sie wie zuvor verwendet werden können.

3. Anpassung der Grundlast-Parameter

Da die Grundlast des neuen Nutzers höher ist (ca. 300W statt ursprünglich ca. 175W), habe ich die entsprechenden Parameter angepasst:

// Betriebsbereiche - angepasst für höhere Grundlast
GRUNDLAST_MIN: 75,             // Untere Grenze für Grundlastbereich (W)
GRUNDLAST_MAX: 325,            // Obere Grenze für Grundlastbereich (W) - angepasst auf 325W

Außerdem wurde die Failsafe-Leistung (Notfallwert) erhöht:
FAILSAFE_POWER: 200, // Fallback-Leistung bei fehlenden Daten (W) - angepasst auf 200W

4. Entfernung der Strompreis-Logik

Da der neue Nutzer keinen Tibber-Strompreis hat, wurden die entsprechenden Knoten und Logik entfernt:

  • Der stündliche Trigger für Strompreise wurde entfernt
  • Der Strompreis-Parameter wurde aus der Berechnungsfunktion entfernt

So funktioniert der Flow

Der Flow folgt diesem Ablauf:

  1. Datenerfassung: Alle 5 Sekunden werden die Werte vom Shelly-Leistungsmesser und dem tatsächlichen Wechselrichter-Output gelesen. Alle 60 Sekunden werden Batterie- und Solardaten aktualisiert.
  2. Datenspeicherung und Trigger-Logik: In der ersten Funktion werden die Daten gesammelt und es wird entschieden, wann eine neue Berechnung ausgelöst werden soll. Besonders wichtig ist hier die Umwandlung des Shelly-Werts in getrennte Werte für Netzbezug und -einspeisung.
  3. Leistungsberechnung: Die zweite Funktion berechnet, wie die Leistung des EZ1-M Wechselrichters angepasst werden sollte. Dafür werden verschiedene Szenarien nach Priorität durchlaufen:
  • Failsafe bei fehlenden Daten
  • Sofortige Reaktion bei Lastspitzen
  • Spezialbehandlung bei vollem Akku
  • Ausgleich bei Leistungsdefizit des Wechselrichters
  • Standardstrategie mit feinen Anpassungen je nach Netzbezug und -einspeisung
  1. Stabilisierung: Um unnötige Schwankungen zu vermeiden, wird eine Aktualisierung nur gesendet, wenn bestimmte Bedingungen erfüllt sind. Bei stabilen Bedingungen werden Updates seltener gesendet.
  2. Aktualisierung: Wenn eine Änderung erforderlich ist, wird die neue Leistung an den EZ1-M Wechselrichter gesendet.

Tipps zur weiteren Optimierung

Hier sind einige Tipps, wie der neue Nutzer den Flow für seine Bedürfnisse optimieren kann:

  1. Grundlastparameter anpassen: Der Bereich GRUNDLAST_MIN und GRUNDLAST_MAX sollte an die tatsächliche Grundlast angepasst werden. Wenn die Grundlast bei 300W liegt, könnte ein Bereich von etwa 75-350W sinnvoll sein.
  2. Schwellwerte anpassen: Die Schwellwerte für niedrigen, mittleren und hohen Netzbezug können je nach individuellen Verbrauchsmustern angepasst werden.
  3. Debug aktivieren: Die Debug-Knoten sind standardmäßig deaktiviert. Für die Fehlersuche und Feinabstimmung empfiehlt es sich, diese zu aktivieren, um die Berechnungen nachvollziehen zu können.
  4. Anpassungsgeschwindigkeit: Die Parameter NORMAL_UPDATE_INTERVAL und STABLE_UPDATE_INTERVAL steuern, wie oft Updates gesendet werden. Bei zu häufigen Anpassungen können diese Werte erhöht werden.
  5. Lastspitzenerkennung: Die Schwellwerte HIGH_LOAD_THRESHOLD und MEDIUM_LOAD_THRESHOLD bestimmen, wann eine Lastspitze erkannt wird. Diese können je nach Haushaltsgröße angepasst werden.
  6. Statistik und Protokollierung: Um den Flow langfristig zu optimieren, empfiehlt es sich, die Werte und Entscheidungen in einer Datenbank oder Textdatei zu protokollieren. So können Muster erkannt und die Parameter gezielt angepasst werden.

Beste Grüße
aspiro

Gratuliere zu der tollen Lösung und der ausführlichen Darlegung! Plane ein ähnliches, noch kleineres "Nulleinspeisungsprojekt" unter Nutzung eines vorhandenen EZ1-M Systems und wäre dankbar für Antworten auf folgende Fragen:

  1. Ist ein Schaltplan (oder -skizze) für das System verfügbar? Wo?
  2. Bei mir wird die "Reserve"-Last für die Nacht durch Kühlschränke gebildet. Gibt es Erfahrungen, wie Systeme mit dem EZ1-M und Noah 2000 auf die Einschaltstromspitzen der Kühlschränke reagieren?
  3. Die teuerste Komponente in der diskutierten Lösung ist der Growatt Noah 2000. Gibt es Alternativen? Für den Einstieg würde ich gern einen gut erhaltenen 12 V 80Ah Akku benutzen.
    Beste Grüße
    gre

Hallo!

Vielen Dank für deine Anfrage zu dem Nulleinspeisungsprojekt. Hier sind meine Antworten zu deinen Fragen:

  1. Aktuell ist kein detaillierter Schaltplan verfügbar. Die Grundkonfiguration ist recht einfach: 4 Solarmodule, jeweils 2 parallel an je einen MPPT des Noah 2000 angeschlossen, und der Noah ist mit dem Wechselrichter verbunden. Für diese grundlegende Konfiguration ist ein komplexer Schaltplan in der Tat nicht zwingend erforderlich.
  2. Was die Einschaltstromspitzen der Kühlschränke betrifft: Diese stellen für den EZ1-M und Noah 2000 normalerweise kein Problem dar. Die Einschaltstromspitzen sind kurzzeitig. Im schlimmsten fall reagiert der EZ1-M für 3 Sekunden zu hoch.
  3. Es gibt durchaus Alternativen zum Growatt Noah 2000.
    Beachte aber, dass ein 12V 80Ah Akku nur etwa 960Wh Kapazität bietet, was je nach deinem Energiebedarf relativ schnell erschöpft sein könnte. Für ein Nulleinspeisungsprojekt solltest du die Batteriekapazität an deinen tatsächlichen Verbrauch anpassen.

@aspiro Danke für die Antwort. Zur Bemerkung 3.: Es handelt sich um einen low cost Einstiegsversuch. Die Überlegung ist, den Tagesüberschuss aus den Pausen der Kühlschränke nachts zu nutzen.
Noch eine Frage: Funktionieren die im Projekt genutzten Tibber-Abfragen auch ohne Tibber-Strombezugsvertrag? (Ich verfüge über einen mit der App gut funktionierenden Tibber-Adapter).

Wenn diese Werte in HA einfließen können, dann funktioniert es.

@aspiro, es ist ja gerade meine Frage, ob die benötigten Werte ohne Tibber-Strom-bezugsvertrag mit dem HA gelesen werden können. Die Tibber App liefert mir ohne Strombezugsvertrag die momentanen Leistungen (Verbrauch und Einspeisung) und den akkumulierten Tagesverbrauch. Diese Werte konnte ich auch in einer VC-anwendung lesen.
Da ich noch keine HA-Erfahrung habe, wäre ich eine url für den schnellen Einstieg in HA dankbar.

Wenn du die Daten in der Anwendung von Tibber siehst, kannst du sie in HA eingeben. Dafür gibt es eine API. Zur Anleitung. Ich hatte dieses Video als Einstieg Youtube EZ1-M Home Assistant. Dann habe ich angefangen. Home Assistant als VM usw. Man muss sich da reinarbeiten.