Skip to content
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Skripten / Logik
  4. JavaScript
  5. Zendure SolarFlow2400 AC (EVCC, Tibber und PV-Forecast)

NEWS

  • Jahresrückblick 2025 – unser neuer Blogbeitrag ist online! ✨
    BluefoxB
    Bluefox
    15
    1
    845

  • Neuer Blogbeitrag: Monatsrückblick - Dezember 2025 🎄
    BluefoxB
    Bluefox
    13
    1
    681

  • Weihnachtsangebot 2025! 🎄
    BluefoxB
    Bluefox
    25
    1
    1.9k

Zendure SolarFlow2400 AC (EVCC, Tibber und PV-Forecast)

Geplant Angeheftet Gesperrt Verschoben JavaScript
56 Beiträge 6 Kommentatoren 1.8k Aufrufe 6 Watching
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • R Offline
    R Offline
    Rand
    schrieb am zuletzt editiert von Rand
    #35

    @schimi
    So, erste Versuche mit dem Skript,

    1. Eine kleine Verbesserung (zu faul an 10 Stellen zu ändern):
    var basePath= "mqtt.0";
    var baseID= "ID";
    
    const IDs = {
        // --- HAUPTZÄHLER (Pflicht) ------------------------------------------------------
        // Muss ein Number-Wert sein. Positiv = Netzbezug (Kauf), Negativ = Einspeisung (Verkauf).
        netz: "Zaehler, 
            
        // --- ZENDURE MQTT (Pflicht) -----------------------------------------------------
        // Diese Pfade finden Sie im MQTT-Adapter unter Ihrem Zendure-Topic.
        acMode: `${basePath}.Zendure.select.${baseID}.acMode`,              // Liest Modus
        acModeSet: `${basePath}.Zendure.select.${baseID}.acMode.set`,      // Schaltet Modus
        currentInput: `${basePath}.Zendure.number.${baseID}.inputLimit`,   // Liest Ladelimit
        inputSet: `${basePath}.Zendure.number.${baseID}.inputLimit.set`,   // Setzt Ladelimit
        currentOutput: `{basePath}.Zendure.number.${baseID}.outputLimit`, // Liest Entladelimit
        outputSet: `${basePath}.Zendure.number.${baseID}.outputLimit.set`, // Setzt Entladelimit
        soc: `${basePath}.Zendure.sensor.${baseID}.electricLevel`,         // Akkustand in %
    
    1. Multiple Konverter sind noch nicht unterstützt?
      Hab die ja um die 2400W max Lade und Entladeleistung zu umgehen, da ist dann eine Nacheinander Lösung doof, das sollte schon parallel laufen
      (Wobei ich noch schauen muss wie das läuft da ich noch keinen Elektriker da hatte unterschreibt das ich es sauber auf 2 Phasen einspeise) - mag ggf eine Fehlersituation sein die auftaucht - wobei ja pro Konverter 2400 gehen sollte und das System die Gesamt Upload Rate von 4800 nicht sieht...

    2. Muss mal logging aktivieren damit ich nachvollziehen kann was es gerade tut - es schwankt gerade immer um den Eigenverbrauch rum, zu diesig...

    3. Hat jemand eigentlich ein Möglichkeit gefunden die Dinger sauber auszuschalten wenn man sie nicht braucht (Akku auf minimum)? Die idle Entladung ist ja nicht gerade wenig (1% ohne Heizung), da wäre es sicherlich besser die Dinger einfach aus zu machen, aber stromlos funktionier irgendwie nur manchmal...
      Hab da Shelly's Sv3 dran gepackt, die könnte ich schön einbinden/einschalten wenn Solar den Eigenverbrauch übersteigt...

    1 Antwort Letzte Antwort
    0
    • S Online
      S Online
      Schimi
      schrieb am zuletzt editiert von
      #36

      @rand

      1. Das ist eine sehr gute Idee (habe ich angepasst)
      2. ne, ich hab nur einen, somit wäre das testen recht schwierig

      keine anung ob es so funktioniert.

      Funktionsweise: Ist Batterie A leer (0%), liefert sie 0W. Der PI-Regler merkt "Oh, reicht nicht" und erhöht die globale Anforderung, wodurch Batterie B&C mehr liefern müssen. Das System regelt sich also selbst aus.

      //-------------------------------------------------------------------------------------
      // ### ioBroker Skript: Multi-Batteriesteuerung (Zendure) mit PI-Regler ###
      //
      // v4.2 - 14.12.2025
      // - FEATURE: Soft-Start Rampe implementiert (Leistung steigt sanft, fällt schnell).
      // - LOGIC:   Unterstützt 3 Hubs + Watchdog (ignoriert Offline-Geräte).
      // - CLEAN:   Keine visuellen Datenpunkte, reiner Regelcode.
      //
      //-------------------------------------------------------------------------------------
      
      // ====================================================================================
      // 1. KONFIGURATION
      // ====================================================================================
      
      // --- ZENDURE GERÄTE -----------------------------------------------------------------
      const ZENDURE_DEVICES = [
         { 
             id: "HOAXXXXXXXXX",          // ID Hub 1
             mqttPath: "mqtt.0.Zendure1",     // Instanz Hub 1
             active: true 
         },
         { 
             id: "HOB2XXXXXXXXXXX",          // ID Hub 2
             mqttPath: "mqtt.0.Zendure2",     // Instanz Hub 2
             active: true 
         },
         { 
             id: "HOC3YYYYYYYYYYY",          // ID Hub 3
             mqttPath: "mqtt.0.Zendure3",     // Instanz Hub 3
             active: true 
         }
      ];
      
      // ------------------------------------------------------------------------------------
      
      const CONFIG = {
         // --- FUNKTIONSUMFANG ---
         ENABLE_TIBBER: true,             // [true/false] Tibber Preis-Logik nutzen
         ENABLE_EVCC: true,               // [true/false] EVCC Auto-Logik nutzen
         ENABLE_PV_FORECAST: true,        // [true/false] Solarprognose nutzen
      
         // --- LOGGING ---
         INFO_LOGS: true,                 // [true] Wichtige Infos im Log anzeigen
         DEBUG: false,                    // [false] Technische Details (nur zur Fehlersuche)
      
         // --- BATTERIE & SYSTEM (PRO GERÄT!) ---
         BATTERY_CAPACITY_KWH: 8.64,      // Akkugröße PRO TURM in kWh
         MAX_CHARGE_W_PER_DEVICE: 2400,   // Max Ladeleistung PRO GERÄT
         MAX_DISCHARGE_W_PER_DEVICE: 2400,// Max Entladeleistung PRO GERÄT
         
         PV_FORECAST_SAFETY_FACTOR: 2.0,  // Sicherheitsfaktor für Wetterprognose
      
         // --- SICHERHEIT & RAMPE ---
         CONNECTION_TIMEOUT_MS: 300000,   // 5 Min. Wenn Daten älter sind -> Hub offline.
         RAMP_STEP_W: 50,                 // Soft-Start: Max. 50W Änderung pro Intervall (nur beim Erhöhen)
      
         // --- PI-REGLER (GLOBAL) ---
         KP: 0.7,                         // P-Anteil
         KI: 0.08,                        // I-Anteil
         DEADBAND_W: 15,                  // Totzone
         TARGET_W: 0,                     // Zielwert
         
         INTEGRAL_MAX: 30000,             // Interner Speicher Max
         INTEGRAL_MIN: -30000,            // Interner Speicher Min
      
         // --- ZEIT ---
         REGEL_INTERVALL_MS: 2000,        // Regelung alle 2 Sekunden
         AC_MODE_COOLDOWN_MS: 5000,       // 5 Sek. Pause bei Moduswechsel
      };
      
      
      // ====================================================================================
      // 2. DATENPUNKTE (IDs)
      // ====================================================================================
      
      const IDs = {
         // --- HAUPTZÄHLER (Pflicht) ---
         netz: "shelly.0.SHEM-3#8CAAB5619A05#1.Total.InstantPower", 
         
         // --- EXTERNE LOGIK ---
         evccModus:   "0_userdata.0.zendure.EVCC_Modus",                        
         tibberLaden: "0_userdata.0.Tibber.01_Automatisierungs-Kanaele.Batterie_laden", 
         
         // --- WETTER ---
         pvForecastRest:  "solarprognose.0.forecast.00.energy_from_now",      
         pvForecastAcc:   "solarprognose.0.forecast.00.accuracy",               
         pvForecastTotal: "solarprognose.0.forecast.00.energy",               
         pvForecastSoFar: "solarprognose.0.forecast.00.energy_now"            
      };
      
      // Generiere dynamische IDs für alle Hubs
      let HUB_IDS = [];
      
      function generateHubIDs() {
         HUB_IDS = [];
         ZENDURE_DEVICES.forEach((device, index) => {
             if (!device.active) return;
             const base = device.mqttPath;
             const id = device.id;
             
             HUB_IDS.push({
                 index: index,
                 id: id,
                 acMode:        `${base}.select.${id}.acMode`,
                 acModeSet:     `${base}.select.${id}.acMode.set`,
                 currentInput:  `${base}.number.${id}.inputLimit`,
                 inputSet:      `${base}.number.${id}.inputLimit.set`,
                 currentOutput: `${base}.number.${id}.outputLimit`,
                 outputSet:     `${base}.number.${id}.outputLimit.set`,
                 soc:           `${base}.sensor.${id}.electricLevel`,
                 // Interner Status
                 lastSwitch:    0,
                 lastWatts:     0,    // Für die Rampe
                 isOnline:      true 
             });
         });
         if (CONFIG.DEBUG) log(`[Init] ${HUB_IDS.length} Hubs konfiguriert.`, 'info');
      }
      
      // ====================================================================================
      // AB HIER NICHTS MEHR ÄNDERN (Code-Logik)
      // ====================================================================================
      
      const AC_MODES = {
         INPUT: "Input mode",    
         OUTPUT: "Output mode"   
      };
      
      let integral = 0.0;                    
      
      // --- 4. HILFSFUNKTIONEN ---
      
      function logInfo(message) {
         if (CONFIG.INFO_LOGS) log(`[Info] ${message}`, 'info');
      }
      
      function logDebug(message) {
         if (CONFIG.DEBUG) log(`[Debug] ${message}`, 'info');
      }
      
      function getStateSafe(id) {
         if (!id) return 0; 
         try {
             if (!existsState(id)) return 0;
             const val = getState(id).val;
             if (val === null || val === undefined) return 0;
             const num = parseFloat(val);
             return isNaN(num) ? 0 : num;
         } catch (e) {
             return 0;
         }
      }
      
      function getStateString(id) {
         if (!id) return ""; 
         try {
             if (!existsState(id)) return "";
             const val = getState(id).val;
             return val ? val.toString() : "";
         } catch (e) {
             return "";
         }
      }
      
      /**
      * Prüft MQTT Zeitstempel (Watchdog).
      */
      function checkHubConnectivity() {
         const now = Date.now();
         let onlineCount = 0;
      
         HUB_IDS.forEach(hub => {
             let isOnline = false;
             try {
                 if (existsState(hub.soc)) {
                     const state = getState(hub.soc);
                     const age = now - state.ts;
                     if (age < CONFIG.CONNECTION_TIMEOUT_MS) {
                         isOnline = true;
                     } else {
                         if (hub.isOnline) {
                             logInfo(`WARNUNG: Hub ${hub.id} ist OFFLINE! Keine Daten seit ${(age/1000/60).toFixed(1)} Min.`);
                         }
                     }
                 }
             } catch(e) { isOnline = false; }
      
             hub.isOnline = isOnline;
             if (isOnline) onlineCount++;
         });
      
         return onlineCount;
      }
      
      // --- 5. INITIALISIERUNG ---
      
      function initialize() {
         logInfo("--- Multi-Batteriesteuerung (v4.2) gestartet ---");
         generateHubIDs();
         
         if (HUB_IDS.length === 0) {
             log("KEINE aktiven Hubs konfiguriert! Skript stoppt.", 'warn');
             return;
         }
      
         logInfo(`Konfiguriert: ${HUB_IDS.length} Hubs.`);
         logInfo(`Feature Konfig: Watchdog=${CONFIG.CONNECTION_TIMEOUT_MS/1000}s, Rampe=${CONFIG.RAMP_STEP_W}W`);
      
         integral = 0;
         const now = Date.now();
         HUB_IDS.forEach(h => {
             h.lastSwitch = now;
             h.lastWatts = 0;
         });
      
         if (CONFIG.REGEL_INTERVALL_MS < 1000) CONFIG.REGEL_INTERVALL_MS = 1000;
         setInterval(mainControlLoop, CONFIG.REGEL_INTERVALL_MS); 
         
         checkOverridesAndAct(); 
      }
      
      // --- 6. TRIGGER ---
      
      if (CONFIG.ENABLE_EVCC && IDs.evccModus) {
         on({ id: IDs.evccModus, change: "any" }, (obj) => {
             logInfo(`Trigger: EVCC Modus auf ${obj.state.val} geändert.`);
             checkOverridesAndAct();
         });
      }
      
      if (CONFIG.ENABLE_TIBBER && IDs.tibberLaden) {
         on({ id: IDs.tibberLaden, change: "any" }, (obj) => {
             logInfo(`Trigger: Tibber Laden auf ${obj.state.val} geändert.`);
             checkOverridesAndAct();
         });
      }
      
      if (CONFIG.ENABLE_TIBBER && CONFIG.ENABLE_PV_FORECAST) {
         if (IDs.pvForecastRest) on({ id: IDs.pvForecastRest, change: "any" }, () => checkOverridesAndAct());
         if (IDs.pvForecastAcc)  on({ id: IDs.pvForecastAcc, change: "any" }, () => checkOverridesAndAct());
      }
      
      // --- 7. LOGIK & REGELUNG ---
      
      function checkOverridesAndAct() {
         checkHubConnectivity(); 
         const onlineHubs = HUB_IDS.filter(h => h.isOnline);
      
         if (onlineHubs.length === 0) return false;
      
         // 7.1 Tibber Logik
         if (CONFIG.ENABLE_TIBBER && IDs.tibberLaden) {
             let tibberActive = false;
             try {
                 tibberActive = getState(IDs.tibberLaden).val === true;
             } catch(e) { tibberActive = false; }
             
             if (tibberActive) {
                 let chargeAllowed = false;
                 if (isNight()) {
                     chargeAllowed = true;
                 } else {
                     if (CONFIG.ENABLE_PV_FORECAST) {
                         if (checkPvForecastAllowsDayChargeGlobal(onlineHubs)) {
                             chargeAllowed = true;
                         }
                     }
                 }
                 
                 if (chargeAllowed) {
                     applyPowerToHubs(onlineHubs, AC_MODES.INPUT, CONFIG.MAX_CHARGE_W_PER_DEVICE);
                     integral = CONFIG.INTEGRAL_MIN; 
                     return true; 
                 }
             }
         }
      
         // 7.2 EVCC Logik
         if (CONFIG.ENABLE_EVCC && IDs.evccModus) {
             const evccMode = getStateSafe(IDs.evccModus); 
             if (evccMode === 2) {
                 applyPowerToHubs(onlineHubs, AC_MODES.OUTPUT, 0); 
                 integral = CONFIG.INTEGRAL_MAX; 
                 return true; 
             } else if (evccMode === 3) {
                 applyPowerToHubs(onlineHubs, AC_MODES.INPUT, CONFIG.MAX_CHARGE_W_PER_DEVICE);
                 integral = CONFIG.INTEGRAL_MIN; 
                 return true; 
             }
         }
      
         return false;
      }
      
      function mainControlLoop() {
         try {
             if (checkOverridesAndAct()) return;
      
             const onlineCount = checkHubConnectivity();
             if (onlineCount === 0) return;
      
             const dt = CONFIG.REGEL_INTERVALL_MS / 1000.0; 
             const gridPower = getStateSafe(IDs.netz); 
             
             if (!IDs.netz) return;
      
             let error = CONFIG.TARGET_W - gridPower;
      
             // Deadband
             if (Math.abs(gridPower) <= CONFIG.DEADBAND_W) {
                 error = 0.0;
             }
      
             // I-Anteil
             integral += error * dt;
             integral = Math.max(CONFIG.INTEGRAL_MIN, Math.min(CONFIG.INTEGRAL_MAX, integral));
      
             // P-Anteil
             const pShare = CONFIG.KP * error;
             const iShare = CONFIG.KI * integral;
      
             let totalOutputPower = pShare + iShare;
      
             // --- SPLIT LOGIC ---
             let powerPerDevice = totalOutputPower / onlineCount;
      
             // Clamping (Limitierung)
             if (powerPerDevice > 0) { 
                 powerPerDevice = Math.min(powerPerDevice, CONFIG.MAX_CHARGE_W_PER_DEVICE);
             } else if (powerPerDevice < 0) { 
                 powerPerDevice = Math.max(powerPerDevice, -CONFIG.MAX_DISCHARGE_W_PER_DEVICE);
             }
      
             const onlineHubs = HUB_IDS.filter(h => h.isOnline);
             applyPowerToHubs(onlineHubs, null, powerPerDevice);
      
         } catch (e) {
             log(`Error in MainLoop: ${e}`, 'error');
         }
      }
      
      // --- 8. HELPER ---
      
      function applyPowerToHubs(hubList, modeOverride, targetPower) {
         hubList.forEach(hub => {
             setHubPower(hub, modeOverride, targetPower);
         });
      }
      
      function setHubPower(hubConfig, modeOverride, power) {
         if (!hubConfig.acModeSet) return;
      
         let targetMode = AC_MODES.OUTPUT;
         let targetVal = 0;
      
         // 1. Ziel-Modus bestimmen
         if (modeOverride) {
             targetMode = modeOverride;
             targetVal = Math.abs(power);
         } else {
             power = Math.round(power);
             targetVal = Math.abs(power);
             targetMode = (power > 0) ? AC_MODES.INPUT : AC_MODES.OUTPUT;
         }
      
         const currentMode = getStateString(hubConfig.acMode);
         
         // 2. RAMPE IMPLEMENTIERUNG
         // Wir rampen den Wert nur, wenn wir im gleichen Modus bleiben.
         // Bei Moduswechsel wird der Rampen-Speicher zurückgesetzt.
         
         if (currentMode !== targetMode) {
             // Bei Moduswechsel: Zielwert sofort akzeptieren (startet meist bei 0 oder niedrig)
             // und Rampe Reset
             hubConfig.lastWatts = 0; 
         } else {
             // Im gleichen Modus: Soft Start prüfen
             const lastVal = hubConfig.lastWatts;
             const diff = targetVal - lastVal;
      
             // Rampe nur anwenden, wenn wir die LEISTUNG ERHÖHEN (diff > 0).
             // Wenn wir reduzieren (diff < 0), machen wir das sofort (Sicherheit/Netzbezug vermeiden).
             if (diff > CONFIG.RAMP_STEP_W) {
                 targetVal = lastVal + CONFIG.RAMP_STEP_W;
                 // Wenn wir rampen, loggen wir das kurz im Debug, damit man es sieht
                 logDebug(`[${hubConfig.id}] Rampe aktiv: ${lastVal}W -> ${targetVal}W (Ziel war ${Math.abs(power)}W)`);
             }
         }
         
         // Wert für nächsten Durchlauf speichern
         hubConfig.lastWatts = targetVal;
      
      
         // 3. Moduswechsel und Senden
         if (currentMode !== targetMode) {
             const now = Date.now();
             
             if (now - hubConfig.lastSwitch < CONFIG.AC_MODE_COOLDOWN_MS) {
                 const currentSetID = (currentMode === AC_MODES.INPUT) ? hubConfig.inputSet : hubConfig.outputSet;
                 if (getStateSafe(currentSetID) !== 0) setState(currentSetID, "0", false);
                 return;
             }
      
             logInfo(`[${hubConfig.id}] Wechsel: ${currentMode} -> ${targetMode} (${targetVal}W)`);
             hubConfig.lastSwitch = now;
             
             setState(hubConfig.acModeSet, targetMode, false);
             
             if (targetMode === AC_MODES.INPUT) {
                 setState(hubConfig.outputSet, "0", false);
                 setState(hubConfig.inputSet, String(targetVal), false);
             } else {
                 setState(hubConfig.inputSet, "0", false);
                 setState(hubConfig.outputSet, String(targetVal), false);
             }
      
         } else {
             // Gleicher Modus, nur Wert senden
             const targetStr = String(targetVal);
             if (targetMode === AC_MODES.INPUT) {
                 if (getState(hubConfig.inputSet).val != targetStr) setState(hubConfig.inputSet, targetStr, false);
             } else {
                 if (getState(hubConfig.outputSet).val != targetStr) setState(hubConfig.outputSet, targetStr, false);
             }
         }
      }
      
      /**
      * Forecast Berechnung basierend auf den Online-Hubs.
      */
      function checkPvForecastAllowsDayChargeGlobal(activeHubs) {
         try {
             let totalSoC = 0;
             let count = 0;
             
             activeHubs.forEach(h => {
                 totalSoC += getStateSafe(h.soc);
                 count++;
             });
             if (count === 0) return false;
             
             const avgSoc = totalSoC / count;
             const forecastRest = getStateSafe(IDs.pvForecastRest);
             let accuracy = getStateSafe(IDs.pvForecastAcc);
             if (!IDs.pvForecastAcc || accuracy <= 0) accuracy = 50; 
             
             const effectiveRest = forecastRest * (accuracy / 100.0);
             const totalCapacity = CONFIG.BATTERY_CAPACITY_KWH * count; 
             const missingCap = totalCapacity * (100 - avgSoc) / 100.0;
             const threshold = missingCap * CONFIG.PV_FORECAST_SAFETY_FACTOR;
      
             return effectiveRest < threshold;
         } catch (e) {
             return false; 
         }
      }
      
      function isNight() {
         try {
             const sunrise = getAstroDate('sunrise');
             const sunset = getAstroDate('sunset');
             const now = new Date();
             if (!sunrise || !sunset) return false; 
             return (now > sunset || now < sunrise);
         } catch (e) {
             return false;
         }
      }
      
      // --- Start ---
      initialize();
      

      1 Antwort Letzte Antwort
      1
      • R Offline
        R Offline
        Rand
        schrieb am zuletzt editiert von
        #37

        @schimi

        Das widerspricht aber der Idee das man 2/3 Geräte hat um die maximale Einspeise/Abgabe Leistung zu erhöhen. Eigentlich müssten beide/alle 3 immer als Gruppe geregelt werden (also 50%/33%) der Gesamt Ladung/Ausgabe pro Gerät.
        Eine gruppenbasierte Aufteilung sollte auch "trocken" relativ gut entwickelbar sein, hast halt ne Gruppe mit einem Gerät die dann 100%/1 der Eingabe/Ausgabe-Anforderung abdeckt.

        Theoretisch mag es auch Leute mit mehr als 3 Geräten geben (was ja technisch kein Problem ist solange sie an unterschiedlichen Sicherungen hängen), aber das wäre denke ich erstmal nur ein relativer kleiner Teil.

        Andere Frage: warum ist denn SetSmartmode nicht bei Dir mit drin? Ich meine ja man kann auch das andere Skript nehmen, aber du pollst / setzt ja eh schon regelmäßig da wäre das doch kein Thema...

        Und hast Du mal den idle Stromverbrauch betrachtet? Hatte ja im Adapterthread über Shelly's gesprochen aber habe natürlich keine Ahnung ob das gut oder schlecht für den Akku ist, wenn man versucht, ihn per Stromweg auszuschalten - und wann er denn wirklich aus geht (minSoc?) Hab Zendure gefragt aber noch keine Rückmeldung bekommen...

        1 Antwort Letzte Antwort
        0
        • S Online
          S Online
          Schimi
          schrieb am zuletzt editiert von
          #38

          schau mal in den Spoiler... das müsste funktionieren...

          ich will dieses Script so einfach wie moglich halten...

          ich lasse das von maxclaudi auch seperat laufen, eine Fehlerauelle weniger und wenn er was optimiert, kann man es direkt nutzen.

          Ich habe noch was "größeres" in der Pipeline (ist gerade im test).... das konnte entweder total speziell auf mich zugeschnitten oder richtig gut fur alle sein, sobald es veröffentlicht ist.

          Würde dann mit dem Zendure ZENKO konkurieren....

          Die aktuellen tests sind schonmal positiv

          1 Antwort Letzte Antwort
          0
          • R Offline
            R Offline
            Rand
            schrieb am zuletzt editiert von Rand
            #39

            Ok, dann muss ich mal schauen wie man das das eigentlich Set macht...

            Re neue Version - Beide Zendures schreiben in den gleichen MQTT Zweig (mqtt.0.Zendure.sensor.HOXXX) und unterscheiden sich dann nur bzgl ID

             // --- HARDWARE LIMITS (ZENDURE HUB) ----------------------------------------------
                MAX_CHARGE_W: 2400,              // Maximale Ladeleistung (Hub 2000 = 1200W, Hub 2000 Dual = 2400W)
                MAX_DISCHARGE_W: 2400,           // Maximale Entladeleistung ins Hausnetz
            

            Ist das pro Gerät oder Total?
            Hardware limit klingt nach Gerät, "Entladeleistungs ins Hausnetz" nach Gesamt;)

            S 1 Antwort Letzte Antwort
            0
            • R Rand

              Ok, dann muss ich mal schauen wie man das das eigentlich Set macht...

              Re neue Version - Beide Zendures schreiben in den gleichen MQTT Zweig (mqtt.0.Zendure.sensor.HOXXX) und unterscheiden sich dann nur bzgl ID

               // --- HARDWARE LIMITS (ZENDURE HUB) ----------------------------------------------
                  MAX_CHARGE_W: 2400,              // Maximale Ladeleistung (Hub 2000 = 1200W, Hub 2000 Dual = 2400W)
                  MAX_DISCHARGE_W: 2400,           // Maximale Entladeleistung ins Hausnetz
              

              Ist das pro Gerät oder Total?
              Hardware limit klingt nach Gerät, "Entladeleistungs ins Hausnetz" nach Gesamt;)

              S Online
              S Online
              Schimi
              schrieb am zuletzt editiert von
              #40

              @Rand wo steht das?

              Bei mir im Script finde ich das nicht ;-)

              https://forum.iobroker.net/post/1314031

              1 Antwort Letzte Antwort
              0
              • R Offline
                R Offline
                Rand
                schrieb am zuletzt editiert von
                #41

                Lol, aber stimmt. Habe ich in der alten Version geschaut;)

                Hat soweit ganz gut funktioniert heute, etwas hibbelig um den Break even/ Ladepunkt herum, vlt weil er dann ja auch schon auf 2 aufteilt und dann auch kleine Lichtschwankungen (diesig) schnell unter die Bezugsgrenze fallen.

                1 Antwort Letzte Antwort
                1
                • R Offline
                  R Offline
                  Rand
                  schrieb am zuletzt editiert von
                  #42

                  @schimi
                  Kann ich mit dem Script auch nachladen aus der Steckdose triggern?

                  Sonne ist gerade nicht viel und wie ich gerade gesehen habe hat der extrem hohe idle Verbrauch die Dinger fast leer gesaugt :(

                  image.png

                  Würde sie ja ausschalten aber es funktioniert nicht (6s Button laut Anleitung, aber peiept nur lang und bleibt an).

                  Bin kurz davor die Dinger zurückzuschicken wg der Idle Last ... 200W am Tag?????
                  Ist das bei Euch auch so? Vlt ist es auch nur so schlimm weil die Dinger regelmäßig gepollt werden aber ein embedded webserver sollte nicht so viel verbrauchen :(

                  S 1 Antwort Letzte Antwort
                  0
                  • R Rand

                    @schimi
                    Kann ich mit dem Script auch nachladen aus der Steckdose triggern?

                    Sonne ist gerade nicht viel und wie ich gerade gesehen habe hat der extrem hohe idle Verbrauch die Dinger fast leer gesaugt :(

                    image.png

                    Würde sie ja ausschalten aber es funktioniert nicht (6s Button laut Anleitung, aber peiept nur lang und bleibt an).

                    Bin kurz davor die Dinger zurückzuschicken wg der Idle Last ... 200W am Tag?????
                    Ist das bei Euch auch so? Vlt ist es auch nur so schlimm weil die Dinger regelmäßig gepollt werden aber ein embedded webserver sollte nicht so viel verbrauchen :(

                    S Online
                    S Online
                    Schimi
                    schrieb am zuletzt editiert von Schimi
                    #43

                    @Rand ja kannst du.... über den entsprechenden EVCC Datenpunkt

                    Bin gerade am Handy und kann nicht nachschauen aber da muss dann eine 1 oder 2 rein (eins ist sperre und eins ist forciertes laden)

                    ansonsten lädt sich der 2400 AC von selbst nach bevor er tiefenentladen ist.
                    Habe das bei mir schon ausprobiert und deswegen nicht zusätzlich eingebaut

                    edit
                    habe meinen Idle Verbrauch nie gemessen, finde am 200 viel... würde sagen dass es bei mir nicht soviel ist (reines Gefühl)

                    1 Antwort Letzte Antwort
                    0
                    • R Offline
                      R Offline
                      Rand
                      schrieb am zuletzt editiert von Rand
                      #44

                      Hab EVCC auf true gesetzt (und das Skript äuft auch durch den Ramp Up Prozess) aber wie es aussieht sind die Dinger schon wieder offline.

                      Da resettet sich bei mir die Netzwerkconfig und sie reden nur noch mit dem Cloudserver und nicht meinem MQTT Broker bis die ich die NW konfig neu mache (einmal mit identischen Settings rekonfigurieren)
                      Hab ich erwähnt das ich leicht genervt bin von den Dingern;)?

                      Schauen wir mal ob sie bei mir auch selbstständig nachladen... das wäre ja ok.

                      1 Antwort Letzte Antwort
                      0
                      • R Offline
                        R Offline
                        Rand
                        schrieb am zuletzt editiert von
                        #45

                        Nachgeladen hatten sie nicht (oder zu mindestens nicht bis sie bei 2% waren)...

                        Der Eigenstromverbrauch ist auch nicht so hoch wie gedacht, zu mindestens nicht dauerhaft - hab mir mal die Tage davor angeschaut, da war es 1% (bei 5.76 kWh =5760W =>57,6W) in ~20h, also ~70W/d bzw ~3W/h

                        Auch das ist nicht wenig, aber sicherlich besser als die 200W die am 24. gezogen wurden.

                        image.png

                        1 Antwort Letzte Antwort
                        0
                        • S Online
                          S Online
                          Schimi
                          schrieb am zuletzt editiert von
                          #46

                          hast du den 2400AC oder einen Hyper? nicht das es wir da aneinander vorbei reden...

                          1 Antwort Letzte Antwort
                          0
                          • L Offline
                            L Offline
                            lesiflo
                            Most Active
                            schrieb am zuletzt editiert von lesiflo
                            #47

                            Hi, schon mal daran gedacht, dass die Selbstentladung bzw.
                            der Eigenstromverbrauch bei den kalten Temperaturen durch die Akkuheizung kommen könnte?

                            1 Antwort Letzte Antwort
                            0
                            • R Offline
                              R Offline
                              Rand
                              schrieb am zuletzt editiert von
                              #48

                              Sind die 2400AC, und ja vlt verbrauchen die anders...

                              @lesiflo - hmm eigentlich hätte ich nicht damit gerechnet das sie heizen müssen, weder im Büro noch im Keller ist es so wirklich kalt - stellt sich die Frage was die Minimum Temperatur ist die sie halten wollen wenn sie nicht laden ? Nehme an das weiss man nicht, aber ich kann ja mal fragen, hab dazu ja einen Case offen (nicht das ich wirklich Antworten erwarte)

                              1 Antwort Letzte Antwort
                              0
                              • L Offline
                                L Offline
                                lesiflo
                                Most Active
                                schrieb am zuletzt editiert von
                                #49

                                Ok, wenn sie nicht draußen stehen kann es wohl kaum die Heizung sein. Meine Laden immer von alleine wenn der SOC auf 0 fällt. Sie stehen aber auch in der Garage.

                                1 Antwort Letzte Antwort
                                0
                                • S Online
                                  S Online
                                  Schimi
                                  schrieb am zuletzt editiert von
                                  #50

                                  hast du zufällig die "smarte steckdose" aktiviert? die braucht um die 35Watt.... auch wenn nichts angedchlossen ist....

                                  1 Antwort Letzte Antwort
                                  0
                                  • R Offline
                                    R Offline
                                    Rand
                                    schrieb am zuletzt editiert von
                                    #51

                                    Ne - ich wollte das mal anschalten aber da wollte er dann Cloud Login Credentials haben und da meine Shellies alle nicht in der Cloud sind hatte sich das gleich wieder erledigt.
                                    Und aktull lässt sich ja eh nichts mehr einstellen da sie nur noch vom Skript kontrolliert werden.

                                    Mit den 3W/h (70W/Tag) kann ich auch leben (auch wenn nicht wirklich gut, wäre besser wenn man die Dinger abschalten könnte), aber der eine Tag mit den 200W war halt blöd, das würde die ganze Rechnung kaputt machen (die eh schon nicht sonderlich gut ausfällt dank der doppelten Spannungskonvertierung, aber das wusste ich ja vorher). 70W sind ~8€/Jahr, erhöht die Amortisationsgschwelle auf 10J um 5%

                                    @lesiflo Auf 0 gefallen sind sie bei mir nicht, vieleicht hätten sie dann geladen, danke für die Info:)

                                    1 Antwort Letzte Antwort
                                    0
                                    • L Offline
                                      L Offline
                                      lesiflo
                                      Most Active
                                      schrieb am zuletzt editiert von lesiflo
                                      #52

                                      Hi, falls Interesse besteht habe ich hier mal versucht den evcc Optimizer für Zendure zu nutzen.

                                      S 1 Antwort Letzte Antwort
                                      0
                                      • L lesiflo

                                        Hi, falls Interesse besteht habe ich hier mal versucht den evcc Optimizer für Zendure zu nutzen.

                                        S Online
                                        S Online
                                        Schimi
                                        schrieb am zuletzt editiert von Schimi
                                        #53

                                        @lesiflo Ich bin tatsächlich an sowas in der Art (mit noch mehr variablen) dran...

                                        es scheint gut zu funktionieren, ich "befürchte" aber langsam das es "zu viel aufwand" für die knapp 8,x kWh ist.
                                        wenn mein anbieter nicht im Q1 schafft Modul 3 anzubieten, spiele ich eh mit dem Gedanken Tibber zu verlassen (bin letztes Jahr nen halben cent unter einem fix-Preis gelandet, wenn man bedenkt mit welchem aufwand.....) und dann würde mir die dumme "PV Strom speichern und bei bedarf ausgeben" Logik vollkommen reichen.

                                        aber hier mal was ich bisher habe... Vielleicht wäre es Sinvoll das in einem Seperaten Theard auszulagern? (was meint ihr?)?

                                        PS: wenn jemand eine Version wieder für mehrere Geräte braucht, kurz beschied geben dann erweitre ich es....


                                        Es handelt sich um eine All-in-One Lösung, die eine Nulleinspeisung mit einer wirtschaftlichen Optimierung verbindet.

                                        🚀 Was macht das Skript?

                                        Das Skript übernimmt die komplette Kontrolle über den acMode (Input/Output) und die Leistungslimits des Zendure Hubs.

                                        Dynamische Nulleinspeisung (PI-Regler):
                                        Es regelt den Netzbezug auf 0 Watt (oder einen eingestellten Zielwert). Ein PI-Regler sorgt dafür, dass Lastspitzen schnell ausgeglichen werden, ohne dass das System nervös schwingt (konfigurierbare Hysterese & Totzone).

                                        Tibber-Integration (Preis-Optimierung):

                                        • Günstige Phasen: Wenn der Strompreis sehr niedrig ist, wird der Akku aus dem Netz geladen ("Force Charge").

                                        • Profit Guard: Wenn der Strompreis niedrig ist (aber nicht niedrig genug zum Laden), wird das Entladen gesperrt, um den wertvollen Speicherstrom nicht "billig" zu verschwenden. Entladen wird nur, wenn der Netzstrom teurer ist als der gespeicherte Strom inkl. Verlusten.

                                        EVCC-Integration (Wallbox-Koordination):
                                        Das Skript reagiert auf einen eigenen Datenpunkt EVCC_Modus, der von EVCC gesteuert werden kann:

                                        • 1 (Normal): Akku regelt normal den Hausverbrauch.

                                        • 2 (Min+PV): Entladesperre. Das Auto lädt mit PV-Überschuss, der Hausakku soll sich dabei nicht ins Auto entleeren.

                                        • 3 (Fast Charge): Ladezwang. Das Auto lädt schnell, der Hausakku lädt ebenfalls mit voller Leistung aus dem Netz (Bypass).

                                        Intelligente Bedarfs-Prognose ("KI"):
                                        Das Skript lernt! Es analysiert die Verbrauchsdaten der letzten 21 Tage (via SQL-Adapter) und erstellt ein Verbrauchsprofil für jeden Wochentag.

                                        Heizungs-Aufschlag: Es prüft die Wettervorhersage (daswetter). Ist es morgen kälter als im Durchschnitt, wird automatisch mehr Energie im Akku reserviert (für die Wärmepumpe).

                                        PV-Prognose: Es prüft (pvforecast), wie viel Sonne heute noch kommt. Wenn mittags genug Sonne erwartet wird, wird der Akku morgens nicht unnötig voll geladen.

                                        ⚙️ Funktionsweise & Logik (Prioritäten)

                                        Das Skript entscheidet nach einer strikten Kaskade, was zu tun ist:

                                        PRIO 1: FORCE CHARGE (Ladezwang)

                                        • Auslöser: Tibber-Preis extrem tief ODER EVCC Modus "Schnellladen".

                                        • Aktion: Bypass des Reglers -> Laden mit maximaler Leistung (z.B. 2400W).

                                        PRIO 2: DISCHARGE LOCK (Entladesperre)

                                        • Auslöser: EVCC Modus "Min+PV" ODER Strompreis zu niedrig (Profit Guard).

                                        • Aktion: Entladen wird auf 0W begrenzt. Laden bei PV-Überschuss ist weiterhin erlaubt (geregelt durch PI).

                                        PRIO 3: NORMALBETRIEB

                                        • Aktion: Der PI-Regler versucht, den Netzbezug auf 0W zu halten.

                                        📋 Voraussetzungen & Benötigte Adapter

                                        Damit das Skript läuft, müssen folgende Adapter installiert und konfiguriert sein:

                                        • Javascript/Blockly: Zum Ausführen des Skripts.

                                        • MQTT-Client: Zur Verbindung mit dem Zendure Hub.

                                        • SQL: Zwingend erforderlich für die Historien-Daten (Verbrauch lernen).

                                        • TibberLink: Für die aktuellen Strompreise.

                                        • DasWetter: Für Temperatur-Forecasts (Heizbedarf).

                                        • PV Forecast: Für Ertragsprognosen.

                                        • Shelly (oder anderer Smartmeter): Für den aktuellen Hausverbrauch (Total.InstantPower).

                                        Benötigte Datenpunkte & Logging

                                        Im oberen Bereich des Skripts (const IDs = { ... }) müssen die Pfade angepasst werden.

                                        Wichtig: Folgende Datenpunkte müssen per SQL-Adapter geloggt werden (Aufbewahrung min. 21 Tage), damit die KI funktioniert:

                                        • Netzbezug Gesamt (kWh)

                                        • Netzeinspeisung Gesamt (kWh)

                                        • PV Erzeugung Heute (kWh)

                                        • Wallbox Verbrauch (kWh) - optional

                                        • Heizung/WW Verbrauch (kWh) - optional, verbessert die Prognose bei Wärmepumpen

                                        💻 Installation

                                        Neues JS-Skript im ioBroker anlegen.

                                        Code einfügen.

                                        Im Bereich // --- 1. KONFIGURATION --- die Parameter anpassen (Akku-Größe, Limits, Tibber-Preisgrenze).

                                        Im Bereich // --- 2. DATENPUNKTE (IDs) --- eure eigenen Objekt-IDs eintragen.

                                        Skript starten.

                                        Beim ersten Start benötigt das Skript einige Sekunden, um die Historie aus der SQL-Datenbank zu laden und das Verbrauchsprofil zu berechnen.

                                        Hinweis: Nutzung auf eigene Gefahr. Prüft bitte, ob die MQTT-Topics bei euch übereinstimmen.

                                        //-------------------------------------------------------------------------------------
                                        // ### ioBroker Skript: Batteriesteuerung (Zendure) mit PI-Regler & Tibber-KI ###
                                        //
                                        // **v5.5.11 - LOGGING INFO UPDATE (04.01.2026)**
                                        // - DOCU:     Kommentare hinzugefügt, welche Datenpunkte zwingend ein SQL-Logging 
                                        //             benötigen und wie lange die Daten vorgehalten werden müssen.
                                        //             (Grundlage für die KI-Historien-Analyse).
                                        //-------------------------------------------------------------------------------------
                                        
                                        // --- 1. KONFIGURATION ---
                                        
                                        const CONFIG = {
                                           // MQTT Pfade (Anpassen an eigene Instanz)
                                           MQTT_BASE_PATH: "mqtt.2.Zendure",  
                                           DEVICE_ID:      "HOXXXXXXXXXX", 
                                        
                                           // Feature-Flags (Zum schnellen Deaktivieren von Teilbereichen)
                                           ENABLE_TIBBER: true,              // Aktiviert Preis-Analysen
                                           ENABLE_EVCC: true,                // Aktiviert Wallbox-Logik
                                           ENABLE_PV_FORECAST: true,         // Berücksichtigt Solar-Vorhersage
                                           ENABLE_PREDICTIVE_CHARGE: true,   // Berechnet nötige Lademenge für die Nacht
                                           ENABLE_WEATHER_FORECAST: true,    // Nutzt Temperatur für Heizbedarf-Prognose
                                        
                                           // Logging & Debugging
                                           INFO_LOGS: true,                  // Zeigt Status-Wechsel im Log
                                           DEBUG: false,                     // Zeigt PI-Interna (P/I-Anteile, Fehler) im Log
                                           
                                           // System-Instanzen
                                           SQL_INSTANCE_ID: 'sql.0',         
                                        
                                           // Hardware-Grenzen & Parameter
                                           BATTERY_CAPACITY_KWH: 8.64,       // Gesamtkapazität der Batterie
                                           MAX_CHARGE_W: 2400,               // Hardware-Limit Laden
                                           MAX_DISCHARGE_W: 2400,            // Hardware-Limit Entladen
                                           MIN_SOC_PERCENT: 5,               // Tiefentladeschutz (Software-seitig)
                                           
                                           // Effizienz (Wichtig für Wirtschaftlichkeits-Berechnung)
                                           // Revert auf Standardwerte (SQL-Analyse war verzerrt durch hohen SoC)
                                           CHARGE_EFFICIENCY: 0.915,         // Wirkungsgrad AC->Akku
                                           DISCHARGE_EFFICIENCY: 0.915,      // Wirkungsgrad Akku->AC
                                           // -> Roundtrip ca. 83.7%
                                        
                                           // Tibber / Wirtschaftlichkeit
                                           MIN_PRICE_FLOOR_CT_KWH: 23,       // "Schmerzgrenze": Unter diesem Preis lohnt Entladen meist nicht (Netzbezug günstiger)
                                           HISTORY_DAYS: 21,                 // Lerndauer für das Verbrauchsprofil (bestimmt Logging-Dauer!)
                                           
                                           // Wärmepumpen-Logik (KI)
                                           MAX_HEATING_RESERVE_KWH: 6.0,     // Max. Reserve für Heizung
                                           HEATING_FACTOR: 0.5,              // kWh Mehrverbrauch pro Grad kälter
                                        
                                           // Regelungs-Parameter (PI-Controller)
                                           REGEL_INTERVALL_MS: 2000,         // Wie oft geregelt wird
                                           AC_MODE_COOLDOWN_MS: 60000,       // Schutzzeit gegen hektisches Umschalten (Input<->Output)
                                           
                                           HYSTERESIS_SWITCH_W: 200,         // Hysterese beim Umschalten Lade/Entlademodus
                                           DEADBAND_W: 30,                   // Toleranzbereich um 0W (keine Regelung nötig)
                                        
                                           KP: 0.70,                         // Proportional-Anteil (Reaktionsschnelle)
                                           KI: 0.08,                         // Integral-Anteil (Genauigkeit/Ausgleich bleibender Fehler)
                                           
                                           INTEGRAL_MAX: 30000,              // Windup-Schutz (Max Speicher des Integrals)
                                           INTEGRAL_MIN: -30000,              
                                           TARGET_W: 0                       // Zielwert am Netzanschlusspunkt
                                        };
                                        
                                        // --- 2. DATENPUNKTE (IDs) ---
                                        // Hier werden alle externen Datenpunkte gemappt.
                                        // WICHTIG: "EVCC_Modus" Definition beachten!
                                        
                                        function zendurePath(type, endpoint) {
                                           return `${CONFIG.MQTT_BASE_PATH}.${type}.${CONFIG.DEVICE_ID}.${endpoint}`;
                                        }
                                        
                                        const IDs = {
                                           // --- Hardware & Echtzeit (Zendure & Shelly) ---
                                           netz: "shelly.0.SHEM-3#8CAAB5619A05#1.Total.InstantPower", // Aktueller Hausverbrauch (Positiv=Bezug, Negativ=Einspeisung)
                                           
                                           // Zendure Hub Steuerung
                                           acMode: zendurePath("select", "acMode"),           // Aktueller Modus (lesen)
                                           acModeSet: zendurePath("select", "acMode.set"),    // Modus setzen (Input/Output)
                                           currentInput: zendurePath("number", "inputLimit"), // Aktuelles Ladelimit (lesen)
                                           inputSet: zendurePath("number", "inputLimit.set"), // Ladelimit setzen
                                           currentOutput: zendurePath("number", "outputLimit"),// Aktuelles Entladelimit (lesen)
                                           outputSet: zendurePath("number", "outputLimit.set"),// Entladelimit setzen
                                           soc: zendurePath("sensor", "electricLevel"),       // Akku-Stand in %
                                           minSoc: zendurePath("number", "minSoc"),           // MinSoC Einstellung am Hub
                                        
                                           // --- Energie-Historie (Zählerstände für KI-Analyse) ---
                                           // [LOGGING]: SQL-Logging aktivieren! Mind. 21 Tage (CONFIG.HISTORY_DAYS) Vorhaltezeit.
                                           // Werden genutzt, um den Durchschnittsverbrauch zu lernen
                                           batTotalEnergy: "shelly.0.shellyplugsg3#d0cf13daf7f0#1.Relay0.Energy",
                                           batDischargeTotal: "shelly.0.shellyplugsg3#d0cf13daf7f0#1.Relay0.ReturnedEnergy",
                                        
                                           // --- Last-Analyse & Umgebungsfaktoren ---
                                           // [LOGGING]: SQL-Logging aktivieren! Mind. 21 Tage Vorhaltezeit.
                                           temperature: "0_userdata.0.zendure.KI.Temperatur_Mittelwert",
                                           heatLoadTotal: "0_userdata.0.zendure.KI.Heizung_Leistung_Tag",
                                           wwLoadTotal: "0_userdata.0.zendure.KI.WW_Leistung_Tag",
                                           wbTotalKWh: "0_userdata.0.zendure.KI.Wallboxen_Verbrauch",
                                           
                                           // --- Wettervorhersage (DasWetter Adapter) ---
                                           forecastMinTempToday: "daswetter.0.location_1.ForecastDaily.Day_1.Temperature_Min",
                                           forecastMinTempTomorrow: "daswetter.0.location_1.ForecastDaily.Day_2.Temperature_Min",
                                        
                                           // --- PV-Anlage Daten ---
                                           // [LOGGING]: SQL-Logging aktivieren! Mind. 21 Tage Vorhaltezeit.
                                           pvTodayKWh: "0_userdata.0.PV-Anlage.Erzeugt_heute", // Genutzt für PV-Accuracy Berechnung
                                        
                                           // --- Netz-Historie ---
                                           // [LOGGING]: SQL-Logging aktivieren! Mind. 21 Tage Vorhaltezeit.
                                           netImportTotalKWh: "tibberlink.0.LocalPulse.0.Import_total",
                                           netExportTotalKWh: "tibberlink.0.LocalPulse.0.Export_total",
                                        
                                           // --- Tibber & Preise ---
                                           tibberPricesToday: "tibberlink.0.Homes.f7xxxxxxxxxxxx.PricesToday.json",
                                           tibberPricesTomorrow: "tibberlink.0.Homes.f7xxxxxxxxxxxx.PricesTomorrow.json",
                                        
                                           // --- Externe Steuerung & Prognose ---
                                           
                                           // WICHTIG: EVCC Modus Definition
                                           // 1 = Normalbetrieb (Regler darf arbeiten, Entladen erlaubt)
                                           // 2 = Entladesperre (Entladen verboten, Laden bei PV-Überschuss erlaubt)
                                           // 3 = Force Charge (Ladebefehl: Akku wird mit MAX_CHARGE_W geladen, egal ob Überschuss)
                                           evccModus: "0_userdata.0.zendure.EVCC_Modus", 
                                           
                                           consumptionProfile: "0_userdata.0.zendure.KI.ConsumptionProfile", // Cache für das gelernte Profil
                                           vacationMode: "0_userdata.0.Sonstiges.UrlaubsModus_An", // true = Urlaubsmodus (weniger Grundlast)
                                        
                                           // PV-Prognosen (Adapter pvforecast)
                                           pvForecastRest: "pvforecast.0.summary.energy.nowUntilEndOfDay",
                                           pvForecastNow: "pvforecast.0.summary.energy.now",
                                           pvForecastTomorrow: "pvforecast.0.summary.energy.tomorrow"
                                        };
                                        
                                        const AC_MODES = { INPUT: "Input mode", OUTPUT: "Output mode" };
                                        
                                        // --- 3. GLOBALE VARIABLEN ---
                                        let integral = 0.0;             // Speicher für den I-Anteil des Reglers
                                        let lastAcModeSwitch = 0;       // Timestamp des letzten Moduswechsels (Cooldown)
                                        let currentStatusText = "Initialisierung"; 
                                        let isFirstRun = true;          
                                        
                                        // KI & Strategie Variablen
                                        let predictedChargeKWh = 0;     // Berechneter Bedarf für die Nacht
                                        let tibberPriceData = [];       // Rohdaten der Preise
                                        let cheapestIntervals = [];     // Liste der Zeitstempel für "Force Charge"
                                        let expensiveIntervals = [];    // Liste der Zeitstempel für "Reserve halten"
                                        let historicalConsumptionProfile = {}; // Gelerntes Verhalten (Wochentag/Uhrzeit)
                                        let globalReserveKWh = 0;       // Aktuell nötige Reserve im Akku
                                        let dynamicPriceReference = 0;  // Berechneter Referenzpreis (Durchschnitt/Floor)
                                        
                                        
                                        // --- 4. LOGGING & UTILS ---
                                        function logInfo(message) { if (CONFIG.INFO_LOGS) { log(`[Info] ${message}`, 'info'); } }
                                        function logDebug(message) { if (CONFIG.DEBUG) { log(`[Debug] ${message}`, 'info'); } }
                                        function logWarn(message) { log(`[Warn] ${message}`, 'warn'); }
                                        
                                        function setStatus(text) {
                                           if (currentStatusText !== text) {
                                               currentStatusText = text;
                                               logInfo(`[Status] ${text}`);
                                           }
                                        }
                                        
                                        // Wrapper für asynchrone sendTo Befehle (SQL)
                                        function sendToAsync(instance, command, message) {
                                           return new Promise((resolve, reject) => {
                                               sendTo(instance, command, message, function (result) {
                                                   if (result.error) reject(new Error(result.error));
                                                   else resolve(result);
                                               });
                                           });
                                        }
                                        
                                        // Berechnet wie zuverlässig die PV-Prognose heute war (0.0 - 1.0)
                                        function calculatePVAccuracy(today, forecastNow) {
                                           if (!forecastNow || forecastNow < 0.1) return 1.0; 
                                           const deviation = Math.abs(today - forecastNow);
                                           const acc = Math.max(0.0, 1.0 - (deviation / forecastNow));
                                           if (CONFIG.DEBUG) logDebug(`[KI-Check] PV Accuracy: ${(acc*100).toFixed(1)}% (Ist: ${today.toFixed(1)}kWh / Soll: ${forecastNow.toFixed(1)}kWh)`);
                                           return acc;
                                        }
                                        
                                        function logStrategyDetails(intervalTimestamps, type, logLevel) {
                                           if (logLevel === 'info' && !CONFIG.INFO_LOGS) return;
                                           if (logLevel === 'debug' && !CONFIG.DEBUG) return;
                                           if (intervalTimestamps.length === 0) {
                                               if (type === "Lade") {
                                                   const bep = calculateBreakevenPrice();
                                                   logInfo(`[Strategie] Keine Ladeintervalle unter ${bep.toFixed(2)} ct/kWh gefunden oder benötigt.`);
                                               }
                                               return;
                                           }
                                           let output = `[Strategie] Geplante **${type}**-Zeiten (${intervalTimestamps.length}x):\n`;
                                           const relevantIntervals = tibberPriceData.filter(i => intervalTimestamps.includes(i.start.getTime()));
                                           relevantIntervals.sort((a,b) => a.start.getTime() - b.start.getTime());
                                           relevantIntervals.forEach(i => { output += `- ${i.start.toLocaleString()} (${i.price.toFixed(2)} ct/kWh)\n`; });
                                           if (logLevel === 'info') logInfo(output); else logDebug(output);
                                        }
                                        
                                        
                                        // --- 5. KI-LOGIK & ANALYSE ---
                                        
                                        // Holt historische Deltas (Verbrauch/Erzeugung) aus SQL
                                        async function getHistoryDeltas(id, scaleFactor = 1.0) {
                                           if (!id || !existsState(id)) return []; 
                                           const end = Date.now();
                                           const start = end - (CONFIG.HISTORY_DAYS * 24 * 3600 * 1000);
                                           try {
                                               const result = await sendToAsync(CONFIG.SQL_INSTANCE_ID, 'getHistory', {
                                                   id: id,
                                                   options: { start: start, end: end, aggregate: 'none', limit: 45000, returnNewestEntries: true, removeBorderValues: true, ignoreNull: false, ack: true }
                                               });
                                               const data = result.result;
                                               if (!data || data.length < 2) return [];
                                               data.sort((a, b) => a.ts - b.ts);
                                               const res = [];
                                               let lastVal = null;
                                               for(let i=0; i<data.length; i++) {
                                                   if (data[i].val === null) continue;
                                                   if (lastVal === null) { lastVal = data[i].val; continue; }
                                                   let delta = (data[i].val - lastVal) * scaleFactor;
                                                   lastVal = data[i].val;
                                                   if (delta < 0 || delta > 120) continue; 
                                                   res.push({ time: data[i].ts, deltaKWh: delta });
                                               }
                                               return res;
                                           } catch(e) { logDebug(`Fehler SQL ${id}: ${e.message}`); return []; }
                                        }
                                        
                                        async function getHistoryStats(id) {
                                           const end = Date.now();
                                           const start = end - (CONFIG.HISTORY_DAYS * 24 * 3600 * 1000);
                                           try {
                                               const result = await sendToAsync(CONFIG.SQL_INSTANCE_ID, 'getHistory', {
                                                   id: id,
                                                   options: { start: start, end: end, aggregate: 'average', count: 1, ack: true }
                                               });
                                               if(result.result && result.result[0]) return result.result[0].val || 0;
                                               return 0;
                                           } catch(e) { return 0; }
                                        }
                                        
                                        // Lädt/Speichert das gelernte Profil in einen Datenpunkt (Caching)
                                        function tryLoadConsumptionProfile() {
                                           if(!IDs.consumptionProfile) return false;
                                           try {
                                               const state = getState(IDs.consumptionProfile);
                                               if (state && state.val) {
                                                   const data = JSON.parse(state.val);
                                                   if (data && Object.keys(data).length > 0) {
                                                       historicalConsumptionProfile = data;
                                                       logInfo("[Cache] Verbrauchsprofil erfolgreich geladen.");
                                                       return true;
                                                   }
                                               }
                                           } catch (e) { logDebug(`[Cache] Konnte Profil nicht laden: ${e.message}`); }
                                           return false;
                                        }
                                        
                                        function saveConsumptionProfile() {
                                           if(!IDs.consumptionProfile) return;
                                           try {
                                               const jsonStr = JSON.stringify(historicalConsumptionProfile);
                                               setState(IDs.consumptionProfile, jsonStr, true);
                                               logInfo("[Cache] Verbrauchsprofil im Datenpunkt gesichert.");
                                           } catch (e) { logWarn(`[Cache] Fehler beim Speichern: ${e.message}`); }
                                        }
                                        
                                        function getBucketKey(timestamp) {
                                           const date = new Date(timestamp);
                                           const wd = date.getDay(); 
                                           const minutes = Math.floor(date.getMinutes() / 15) * 15;
                                           const hm = `${('0'+date.getHours()).slice(-2)}:${('0'+minutes).slice(-2)}`;
                                           return { wd, hm };
                                        }
                                        
                                        function getMedian(values) {
                                           if (values.length === 0) return 0;
                                           values.sort((a, b) => a - b);
                                           const half = Math.floor(values.length / 2);
                                           if (values.length % 2) return values[half];
                                           return (values[half - 1] + values[half]) / 2.0;
                                        }
                                        
                                        // Hauptfunktion zum Lernen des Verbrauchsprofils
                                        async function updateConsumptionProfileFromSQL() {
                                           if(!IDs.netImportTotalKWh || !IDs.batTotalEnergy) return;
                                           logInfo("[KI] Starte SQL-Analyse für Verbrauchsprofil...");
                                           historicalConsumptionProfile = {}; 
                                           
                                           // Daten holen...
                                           const [importD, exportD, pvDeltas, wbD, batTotalD, batDischargeD] = await Promise.all([
                                               getHistoryDeltas(IDs.netImportTotalKWh, 1.0),
                                               getHistoryDeltas(IDs.netExportTotalKWh, 1.0),
                                               getHistoryDeltas(IDs.pvTodayKWh, 1.0),
                                               getHistoryDeltas(IDs.wbTotalKWh, 1.0), 
                                               getHistoryDeltas(IDs.batTotalEnergy, 0.001),     
                                               getHistoryDeltas(IDs.batDischargeTotal, 0.001)   
                                           ]);
                                           
                                           // Daten aggregieren...
                                           const dailyBuckets = {}; 
                                           const dailySums = {}; 
                                        
                                           const addDeltas = (deltas, type) => {
                                               if(!deltas) return;
                                               deltas.forEach(d => {
                                                   const dateObj = new Date(d.time);
                                                   const dateKey = dateObj.toISOString().split('T')[0];
                                                   const { hm } = getBucketKey(d.time); 
                                                   
                                                   if (!dailyBuckets[dateKey]) dailyBuckets[dateKey] = {};
                                                   if (!dailyBuckets[dateKey][hm]) dailyBuckets[dateKey][hm] = { import: 0, export: 0, pv: 0, wb: 0, batIn: 0, batOut: 0, wd: dateObj.getDay() };
                                                   dailyBuckets[dateKey][hm][type] += d.deltaKWh;
                                               });
                                           };
                                           
                                           addDeltas(importD, 'import'); addDeltas(exportD, 'export'); addDeltas(pvDeltas, 'pv');
                                           addDeltas(wbD, 'wb'); addDeltas(batTotalD, 'batIn'); addDeltas(batDischargeD, 'batOut'); 
                                        
                                           // Ausreißer filtern (Urlaubstage etc.)
                                           const consumptionSums = [];
                                           for (const dateKey in dailyBuckets) {
                                               let daySum = 0;
                                               const dayData = dailyBuckets[dateKey];
                                               for (const hm in dayData) {
                                                   const b = dayData[hm];
                                                   const realBatIn = Math.max(0, b.batIn - b.batOut);
                                                   const rawSystem = b.import + b.pv - b.export;
                                                   const wbSum = b.wb || 0; 
                                                   const batEffect = b.batOut - realBatIn;
                                                   let consumption = Math.max(0, rawSystem - wbSum + batEffect);
                                                   daySum += consumption;
                                               }
                                               dailySums[dateKey] = daySum;
                                               consumptionSums.push(daySum);
                                           }
                                           
                                           const medianConsumption = getMedian(consumptionSums);
                                           const outlierThreshold = medianConsumption * 0.4; // Tage < 40% des Medians ignorieren
                                        
                                           if (CONFIG.DEBUG) logDebug(`[KI-Filter] Median-Verbrauch: ${medianConsumption.toFixed(2)} kWh. Filter-Schwelle: < ${outlierThreshold.toFixed(2)} kWh`);
                                        
                                           let ignoredDays = 0;
                                           for (const dateKey in dailyBuckets) {
                                               if (dailySums[dateKey] < outlierThreshold) {
                                                   ignoredDays++;
                                                   continue; 
                                               }
                                        
                                               const dayData = dailyBuckets[dateKey];
                                               for (const hm in dayData) {
                                                   const b = dayData[hm];
                                                   const realBatIn = Math.max(0, b.batIn - b.batOut);
                                                   const rawSystem = b.import + b.pv - b.export;
                                                   const wbSum = b.wb || 0; 
                                                   const batEffect = b.batOut - realBatIn;
                                                   let consumption = Math.max(0, rawSystem - wbSum + batEffect);
                                                   
                                                   const wd = b.wd;
                                                   if (!historicalConsumptionProfile[wd]) historicalConsumptionProfile[wd] = {};
                                                   if (!historicalConsumptionProfile[wd][hm]) historicalConsumptionProfile[wd][hm] = [];
                                                   historicalConsumptionProfile[wd][hm].push(consumption);
                                               }
                                           }
                                           
                                           if (ignoredDays > 0) logInfo(`[KI] ${ignoredDays} Tage wegen ungewöhnlich niedrigem Verbrauch (Urlaub?) aus dem Lernprofil gefiltert.`);
                                           saveConsumptionProfile(); 
                                        }
                                        
                                        function getSumAndDays(deltas) {
                                           if (!deltas || deltas.length === 0) return { sum: 0, days: 0 };
                                           const totalSum = deltas.reduce((sum, d) => sum + d.deltaKWh, 0);
                                           const uniqueDays = new Set(deltas.map(d => new Date(d.time).toISOString().split('T')[0])).size;
                                           return { sum: totalSum, days: Math.max(1, uniqueDays) };
                                        }
                                        
                                        // Berechnet den Ladbedarf für die Nacht basierend auf Historie, Wetter und Urlaub
                                        async function getPredictedChargeNeedKWh() {
                                           if (!CONFIG.ENABLE_PREDICTIVE_CHARGE) return 0;
                                           try {
                                               const [importD, exportD, pvD, wbD, batTotalD, batOutD, heatD, wwD] = await Promise.all([
                                                   getHistoryDeltas(IDs.netImportTotalKWh, 1.0), getHistoryDeltas(IDs.netExportTotalKWh, 1.0),
                                                   getHistoryDeltas(IDs.pvTodayKWh, 1.0), getHistoryDeltas(IDs.wbTotalKWh, 1.0), 
                                                   getHistoryDeltas(IDs.batTotalEnergy, 0.001), getHistoryDeltas(IDs.batDischargeTotal, 0.001),
                                                   getHistoryDeltas(IDs.heatLoadTotal, 1.0), getHistoryDeltas(IDs.wwLoadTotal, 1.0)
                                               ]);
                                        
                                               const days = Math.max(1, getSumAndDays(importD).days);
                                               const avgPV = getSumAndDays(pvD).sum / days;
                                               const avgImp = getSumAndDays(importD).sum / days;
                                               const avgExp = getSumAndDays(exportD).sum / days;
                                               const avgWB = getSumAndDays(wbD).sum / days;
                                               const avgBatOut = getSumAndDays(batOutD).sum / days;
                                               const avgBatIn = Math.max(0, (getSumAndDays(batTotalD).sum / days) - avgBatOut);
                                               const avgHeat = getSumAndDays(heatD).sum / days;
                                               const avgWW = getSumAndDays(wwD).sum / days;
                                        
                                               const totalCons = (avgPV + avgImp - avgExp) - avgWB + (avgBatOut - avgBatIn);
                                               let baseLoad = Math.max(0, totalCons - avgHeat - avgWW);
                                        
                                               // --- VACATION MODE CHECK ---
                                               let isVacation = false;
                                               if (IDs.vacationMode && existsState(IDs.vacationMode)) {
                                                   isVacation = getState(IDs.vacationMode).val || false;
                                               }
                                        
                                               if (isVacation) {
                                                   baseLoad = baseLoad * 0.5; // 50% Reduktion im Urlaub
                                               }
                                        
                                               // Temperatur-Einfluss (Heizbedarf)
                                               let calcTemp = 10, avgHistTemp = 10;
                                               let isUsingForecast = false;
                                               
                                               if (existsState(IDs.temperature)) {
                                                    avgHistTemp = await getHistoryStats(IDs.temperature) || 10;
                                               }
                                        
                                               if (CONFIG.ENABLE_WEATHER_FORECAST && existsState(IDs.forecastMinTempToday)) {
                                                   const minToday = getState(IDs.forecastMinTempToday).val;
                                                   const minTomorrow = getState(IDs.forecastMinTempTomorrow).val;
                                                   if ((typeof minToday === 'number' || typeof minToday === 'string') && 
                                                       (typeof minTomorrow === 'number' || typeof minTomorrow === 'string')) {
                                                       
                                                       const tToday = parseFloat(minToday);
                                                       const tTomorrow = parseFloat(minTomorrow);
                                        
                                                       if (!isNaN(tToday) && !isNaN(tTomorrow)) {
                                                           const currentT = getState(IDs.temperature).val || 10;
                                                           const forecastMin = Math.min(tToday, tTomorrow);
                                                           calcTemp = Math.min(currentT, forecastMin);
                                                           isUsingForecast = true;
                                                       }
                                                   }
                                               }
                                               
                                               if (!isUsingForecast && existsState(IDs.temperature)) {
                                                    calcTemp = getState(IDs.temperature).val || 10;
                                               }
                                        
                                               const tempDelta = avgHistTemp - calcTemp;
                                               let predHeat = avgHeat + avgWW;
                                               if (tempDelta > 0) predHeat += (tempDelta * CONFIG.HEATING_FACTOR);
                                               else predHeat = Math.max(avgWW, predHeat - (Math.abs(tempDelta) * CONFIG.HEATING_FACTOR));
                                               
                                               if (isVacation) {
                                                   predHeat = 0; // Keine Komfort-Reserve im Urlaub
                                               } else {
                                                   predHeat = Math.min(predHeat, CONFIG.MAX_HEATING_RESERVE_KWH);
                                               }
                                               
                                               let tempInfo = `${calcTemp.toFixed(1)}°C`;
                                               if (isUsingForecast) tempInfo += " (Forecast)";
                                               if (isVacation) tempInfo += " [URLAUB: Wärme-Res deaktiviert]";
                                               
                                               logInfo(`[KI-Analyse] Base: ${baseLoad.toFixed(2)}kWh | Wärme: ${predHeat.toFixed(2)}kWh (Kalkulations-Basis: ${tempInfo})`);
                                        
                                               // PV Prognose einbeziehen
                                               const actualPV = getState(IDs.pvTodayKWh).val || 0;
                                               const acc = calculatePVAccuracy(actualPV, getState(IDs.pvForecastNow).val||1);
                                               const effPVToday = actualPV + ((getState(IDs.pvForecastRest).val || 0) * acc);
                                               const safePVMorgen = (getState(IDs.pvForecastTomorrow).val || 0) * Math.max(0, acc - 0.2);
                                        
                                               let need = (baseLoad + predHeat) / CONFIG.DISCHARGE_EFFICIENCY - (effPVToday + (safePVMorgen * 0.5));
                                               let charge = Math.max(0, need) / CONFIG.CHARGE_EFFICIENCY;
                                               
                                               const soc = getState(IDs.soc).val || 0;
                                               return Math.min(charge, CONFIG.BATTERY_CAPACITY_KWH * (100 - soc) / 100.0);
                                           } catch(e) { return 0; }
                                        }
                                        
                                        function calculateDynamicPriceReference() {
                                           if (tibberPriceData.length === 0) { dynamicPriceReference = CONFIG.MIN_PRICE_FLOOR_CT_KWH; return; }
                                           const avg = tibberPriceData.reduce((sum, i) => sum + i.price, 0) / tibberPriceData.length;
                                           dynamicPriceReference = Math.max(avg, CONFIG.MIN_PRICE_FLOOR_CT_KWH);
                                        }
                                        
                                        function calculateBreakevenPrice() {
                                           // Preis, ab dem Laden+Entladen teurer ist als Direktbezug
                                           return dynamicPriceReference * CONFIG.CHARGE_EFFICIENCY * CONFIG.DISCHARGE_EFFICIENCY;
                                        }
                                        
                                        async function loadTibberPriceData() {
                                           if(!IDs.tibberPricesToday) return; 
                                           let today=[], tomorrow=[];
                                           try {
                                               const t = getState(IDs.tibberPricesToday).val; if(t) today = JSON.parse(t);
                                               const m = getState(IDs.tibberPricesTomorrow).val; if(m) tomorrow = JSON.parse(m);
                                           } catch(e) {}
                                           tibberPriceData = [];
                                           [...today, ...tomorrow].forEach(i => {
                                               tibberPriceData.push({ start: new Date(i.startsAt), price: i.total * 100, durationMs: 15 * 60 * 1000 });
                                           });
                                        }
                                        
                                        // Führt die strategische Analyse durch (wird stündlich aufgerufen)
                                        async function runPredictiveAnalysis() {
                                           if (!CONFIG.ENABLE_TIBBER) return;
                                           await loadTibberPriceData();
                                           calculateDynamicPriceReference();
                                           if (Object.keys(historicalConsumptionProfile).length === 0) tryLoadConsumptionProfile();
                                        
                                           predictedChargeKWh = await getPredictedChargeNeedKWh();
                                           
                                           // Strategie-Zeiten berechnen
                                           const now = new Date();
                                           const bep = calculateBreakevenPrice();
                                           const future = tibberPriceData.filter(i => (i.start.getTime() + i.durationMs) > now.getTime());
                                           
                                           // CHEAPEST: Wann laden wir? (Preis < Break-Even)
                                           cheapestIntervals = future.filter(i => i.price < bep).sort((a,b) => a.price - b.price).slice(0, Math.ceil(predictedChargeKWh/((CONFIG.MAX_CHARGE_W/1000)*0.25*CONFIG.CHARGE_EFFICIENCY))).map(i => i.start.getTime());
                                           
                                           // EXPENSIVE: Wann auf jeden Fall entladen/reservieren?
                                           expensiveIntervals = future.filter(i => i.price > dynamicPriceReference).sort((a,b) => b.price - a.price).slice(0, 10).map(i => i.start.getTime());
                                        
                                           logStrategyDetails(cheapestIntervals, "Lade", "info");
                                           logStrategyDetails(expensiveIntervals, "Entlade", "info");
                                        
                                           // Reserve für Hochpreisphasen berechnen
                                           let totalPeakKWh = 0;
                                           for (const ts of expensiveIntervals) {
                                               const d = new Date(ts); const wd = d.getDay(); const hm = `${('0'+d.getHours()).slice(-2)}:${('0'+(Math.floor(d.getMinutes()/15)*15)).slice(-2)}`;
                                               if (historicalConsumptionProfile[wd]?.[hm]) {
                                                   const sorted = [...historicalConsumptionProfile[wd][hm]].sort((a, b) => a - b);
                                                   totalPeakKWh += sorted[Math.floor(sorted.length / 2)];
                                               }
                                           }
                                           const bruteReserve = totalPeakKWh / CONFIG.DISCHARGE_EFFICIENCY;
                                           const acc = calculatePVAccuracy(getState(IDs.pvTodayKWh).val||0, getState(IDs.pvForecastNow).val||1);
                                           const safePVMorgen = (getState(IDs.pvForecastTomorrow).val || 0) * Math.max(0, acc - 0.2);
                                           
                                           globalReserveKWh = Math.max(bruteReserve * 0.2, bruteReserve - safePVMorgen);
                                           globalReserveKWh = Math.max(0.5, globalReserveKWh);
                                           
                                           if (CONFIG.INFO_LOGS) logInfo(`[KI] PV-Genauigkeit: ${(acc*100).toFixed(0)}% | Brutto-Res: ${bruteReserve.toFixed(2)}kWh | Netto-Res: ${globalReserveKWh.toFixed(2)}kWh`);
                                        
                                           isFirstRun = false; 
                                        }
                                        
                                        
                                        // --- 6. HARDWARE-KONTROLLE ---
                                        
                                        // Diese Funktion sendet die berechneten Werte an den Zendure Hub
                                        // Sie verhindert auch zu schnelles Umschalten (Ping-Pong)
                                        function setBatteryPower(power, states) {
                                           power = Math.round(power); const curMode = states.acMode; 
                                           let targetMode = states.acMode; // Default to CURRENT mode
                                           let targetVal = Math.abs(power);
                                        
                                           if (power > 0) targetMode = AC_MODES.INPUT;
                                           if (power < 0) targetMode = AC_MODES.OUTPUT;
                                           
                                           const lastSetMode = getState(IDs.acModeSet).val; 
                                           const isSwitchingPending = (lastSetMode === targetMode && curMode !== targetMode);
                                        
                                           if (curMode !== targetMode && !isSwitchingPending) {
                                               // Cooldown Check
                                               if (Date.now() - lastAcModeSwitch < CONFIG.AC_MODE_COOLDOWN_MS) {
                                                    const setID = (curMode === AC_MODES.INPUT) ? IDs.inputSet : IDs.outputSet;
                                                    if (getState(setID).val != 0) setState(setID, "0", false); 
                                                    return;
                                               }
                                               
                                               logInfo(`Moduswechsel: ${curMode} -> ${targetMode} (Power: ${targetVal}W). Reset Integral.`);
                                               integral = 0; // WICHTIG: Integral Reset bei Moduswechsel um Überschwingen zu vermeiden
                                               
                                               lastAcModeSwitch = Date.now(); 
                                               setState(IDs.acModeSet, targetMode, false);
                                           } else {
                                               // Werte setzen
                                               const setID = (targetMode === AC_MODES.INPUT) ? IDs.inputSet : IDs.outputSet;
                                               
                                               if (isSwitchingPending) {
                                                   if (Date.now() - lastAcModeSwitch > 120000) { 
                                                       logWarn("Hardware reagiert nicht auf Moduswechsel. Sende erneut...");
                                                       lastAcModeSwitch = Date.now(); 
                                                       setState(IDs.acModeSet, targetMode, false);
                                                   }
                                               }
                                        
                                               // Nur senden wenn Änderung signifikant (>50W) oder Wert noch nicht stimmt
                                               if (getState(setID).val != String(targetVal) || Math.abs((targetMode === AC_MODES.INPUT ? states.currentInput : states.currentOutput) - targetVal) > 50) {
                                                   setState(setID, String(targetVal), false);
                                               }
                                           }
                                        }
                                        
                                        
                                        // --- 7. REGELUNG (MAIN LOOP) ---
                                        
                                        function mainControlLoop() {
                                           try {
                                               // --- 7a. SENSOR-CHECK ---
                                               const rawNetz = getState(IDs.netz).val;
                                               if (rawNetz === null || typeof rawNetz !== 'number') {
                                                   logWarn("Warnung: Netz-Datenpunkt liefert ungültigen Wert (null/NaN). Regelung pausiert.");
                                                   return;
                                               }
                                        
                                               const s = {
                                                   netz: rawNetz, 
                                                   soc: getState(IDs.soc).val || 0, 
                                                   minSoc: getState(IDs.minSoc).val || 0,
                                                   acMode: getState(IDs.acMode).val || AC_MODES.OUTPUT, 
                                                   currentInput: getState(IDs.currentInput).val || 0,
                                                   currentOutput: getState(IDs.currentOutput).val || 0, 
                                                   evccModus: getState(IDs.evccModus).val || 0
                                               };
                                               
                                               let dynamicTargetW = CONFIG.TARGET_W;
                                               let dischargeLimit = CONFIG.MAX_DISCHARGE_W;
                                               let chargeLimit = CONFIG.MAX_CHARGE_W;
                                               let statusOverride = null;
                                               let forceMaxCharge = false; 
                                        
                                               const nowMs = Date.now();
                                               const currentInterval = new Date(Math.floor(nowMs / 900000) * 900000).getTime();
                                        
                                               // --- 7b. ENTSCHEIDUNGS-LOGIK ---
                                        
                                               // 1. Tibber "Force Charge" Condition (Preis sehr niedrig)
                                               const isTibberForceCharge = (CONFIG.ENABLE_TIBBER && cheapestIntervals.includes(currentInterval) && s.soc < 100);
                                               
                                               // 2. EVCC Conditions
                                               // Modus 3: Force Charge (Auto lädt schnell)
                                               // Modus 2: Entladesperre (Auto lädt Min+PV)
                                               const isEvccForceCharge = (CONFIG.ENABLE_EVCC && s.evccModus === 3);
                                               const isEvccDischargeLock = (CONFIG.ENABLE_EVCC && s.evccModus === 2);
                                        
                                               // --- PRIORITY 1: FORCE CHARGE ---
                                               // (Höchste Prio: Wir müssen laden, egal was Haus oder EVCC sagen)
                                               if (isTibberForceCharge || isEvccForceCharge) {
                                                   forceMaxCharge = true;
                                                   dischargeLimit = 0;
                                                   if (isEvccForceCharge) statusOverride = "EVCC: Force Charge (Modus 3)";
                                                   else statusOverride = "KI: Strategische Ladung (MAX-Power)";
                                               }
                                        
                                               // --- PRIORITY 2: DISCHARGE LOCKS ---
                                               // (Greift nur wenn kein Force Charge aktiv ist)
                                               if (!forceMaxCharge) {
                                                   // A. EVCC Lock (Hausakku soll voll bleiben für Nacht)
                                                   if (isEvccDischargeLock) {
                                                       dischargeLimit = 0;
                                                       statusOverride = "EVCC: Entladesperre (Modus 2)";
                                                   }
                                        
                                                   // B. Profit Guard (Preis zu niedrig zum Einspeisen/Verbrauchen)
                                                   if (CONFIG.ENABLE_TIBBER) { 
                                                        const currentPriceObj = tibberPriceData.find(i => nowMs >= i.start.getTime() && nowMs < (i.start.getTime() + i.durationMs));
                                                        if (currentPriceObj) {
                                                            const currentPrice = currentPriceObj.price;
                                                            // Wenn Preis < Floor UND Akku nicht proppenvoll
                                                            if (currentPrice < CONFIG.MIN_PRICE_FLOOR_CT_KWH && s.soc < 90) {
                                                                if (dischargeLimit > 0) {
                                                                   dischargeLimit = 0;
                                                                   statusOverride = `Eco-Stop: Preis ${currentPrice.toFixed(1)}ct < Limit ${CONFIG.MIN_PRICE_FLOOR_CT_KWH}ct`;
                                                                } else if (!isEvccDischargeLock) {
                                                                   statusOverride = `Eco-Stop: Preis ${currentPrice.toFixed(1)}ct < Limit ${CONFIG.MIN_PRICE_FLOOR_CT_KWH}ct`;
                                                                }
                                                            }
                                                        }
                                                   }
                                               }
                                        
                                               let output = 0;
                                               let pShare = 0;
                                               let iShare = 0;
                                               let error = 0;
                                        
                                               // --- 7c. REGELUNG ---
                                        
                                               if (forceMaxCharge) {
                                                   // BYPASS: Volle Ladeleistung erzwingen
                                                   output = chargeLimit; 
                                                   integral = CONFIG.INTEGRAL_MAX; // Integral "vollpumpen", damit nach Ende des ForceCharge nicht sofort entladen wird
                                                   if(CONFIG.DEBUG) logDebug("Bypass aktiv: Force Max Charge. Integral set to Max.");
                                               } else {
                                                   // NORMALER PI-REGLER
                                                   
                                                   // Reserve-Check (KI)
                                                   const isExpNow = expensiveIntervals.some(ts => nowMs >= ts && nowMs < (ts + 900000));
                                                   const usableKWh = (s.soc - s.minSoc) / 100 * CONFIG.BATTERY_CAPACITY_KWH;
                                                   
                                                   if (dischargeLimit > 0) {
                                                       if (s.soc <= s.minSoc) {
                                                           dischargeLimit = 0; // Akku leer
                                                           if(CONFIG.DEBUG) logDebug(`[Limit] Entladung gestoppt: SoC (${s.soc}%) <= MinSoc (${s.minSoc}%)`);
                                                       } else if (!isExpNow && usableKWh <= globalReserveKWh) {
                                                           dischargeLimit = 0; // Reserve erreicht
                                                           if(CONFIG.DEBUG) logDebug(`[Limit] Entladung gestoppt: Reserve unterschritten (Verfügbar: ${usableKWh.toFixed(2)}kWh < Reserve: ${globalReserveKWh.toFixed(2)}kWh)`);
                                                       }
                                                   }
                                        
                                                   error = dynamicTargetW - s.netz;
                                                   if (Math.abs(s.netz) <= CONFIG.DEADBAND_W) error = 0.0; // Totzone
                                                   
                                                   // Verhindern, dass Integral wegläuft, wenn Entladung gesperrt ist
                                                   if (error < 0 && dischargeLimit === 0) {
                                                       if (integral <= 0) error = 0; 
                                                   }
                                        
                                                   // Anti-Deadlock
                                                   if (dischargeLimit === 0 && error > 200 && integral < 0) {
                                                       if(CONFIG.DEBUG) logDebug("Deadlock Break: Reset negative Integral to allow charging.");
                                                       integral = 0;
                                                   }
                                        
                                                   // Saturation Protection
                                                   let stopIntegration = false;
                                                   if (s.soc >= 99 && error > 0) { stopIntegration = true; error = 0; }
                                                   if (s.soc <= s.minSoc && error < 0) { stopIntegration = true; error = 0; }
                                        
                                                   // PI Berechnung
                                                   const dt = CONFIG.REGEL_INTERVALL_MS / 1000.0;
                                                   const curSetMode = getState(IDs.acModeSet).val; 
                                                   const wantChange = (curSetMode === AC_MODES.INPUT && error < -CONFIG.HYSTERESIS_SWITCH_W) || 
                                                                      (curSetMode === AC_MODES.OUTPUT && error > CONFIG.HYSTERESIS_SWITCH_W);
                                                   const inCooldown = (nowMs - lastAcModeSwitch < CONFIG.AC_MODE_COOLDOWN_MS);
                                        
                                                   let potentialIntegral = integral;
                                                   if ((!inCooldown || !wantChange) && !stopIntegration) {
                                                       potentialIntegral = Math.max(CONFIG.INTEGRAL_MIN, Math.min(CONFIG.INTEGRAL_MAX, integral + (error * dt)));
                                                   }
                                                   
                                                   pShare = CONFIG.KP * error;
                                                   iShare = CONFIG.KI * potentialIntegral;
                                                   output = pShare + iShare;
                                                   
                                                   // Output begrenzen (Hardware-Limits & Logik-Limits)
                                                   const rawOutput = output;
                                                   output = Math.max(-dischargeLimit, Math.min(chargeLimit, output));
                                                   
                                                   // Anti-Windup
                                                   const isClamped = (output !== rawOutput);
                                                   if (!isClamped) {
                                                       integral = potentialIntegral;
                                                   } else {
                                                       // Nur integrieren, wenn wir uns aus der Begrenzung herausbewegen
                                                       const isHelping = (output === chargeLimit && error < 0) || (output === -dischargeLimit && error > 0);
                                                       if (isHelping) integral = potentialIntegral;
                                                   }
                                        
                                                   // Hysterese für 0-Punkt (verhindert Flattern bei geringer Last)
                                                   const isInputIntended = (curSetMode === AC_MODES.INPUT);
                                                   if (output > 0 && !isInputIntended && output < CONFIG.HYSTERESIS_SWITCH_W) output = 0;
                                                   else if (output < 0 && isInputIntended && output > -CONFIG.HYSTERESIS_SWITCH_W) output = 0;
                                               }
                                        
                                               if(CONFIG.DEBUG && !forceMaxCharge) {
                                                   logDebug(`PI-Regler: Error=${Math.round(error)}W | P=${Math.round(pShare)} | I=${Math.round(iShare)} (Int=${Math.round(integral)}) | Out=${Math.round(output)}W`);
                                               }
                                        
                                               // Hardware ansteuern
                                               setBatteryPower(output, s);
                                               
                                               // Status setzen
                                               if (statusOverride) {
                                                   setStatus(statusOverride);
                                               } else {
                                                   setStatus(dischargeLimit === 0 && output === 0 && s.netz > 50 ? "PI: Reserve-Stop" : "PI: Regelung");
                                               }
                                        
                                           } catch (e) { log(`Loop Error: ${e}`, 'error'); }
                                        }
                                        
                                        // --- 8. INITIALISIERUNG ---
                                        
                                        async function initialize() {
                                           logInfo("--- Batteriesteuerung v5.5.11 (Logging Info) ---");
                                           
                                           // Versuche, den Regler sanft zu starten, indem der aktuelle Hardware-Zustand übernommen wird
                                           try {
                                               const curMode = getState(IDs.acMode).val;
                                               const curIn = getState(IDs.currentInput).val || 0;
                                               const curOut = getState(IDs.currentOutput).val || 0;
                                               
                                               let startPower = 0;
                                               if (curMode === AC_MODES.INPUT) startPower = curIn;
                                               else if (curMode === AC_MODES.OUTPUT) startPower = -curOut;
                                               
                                               if (CONFIG.KI > 0 && startPower !== 0) {
                                                   integral = startPower / CONFIG.KI;
                                                   integral = Math.max(CONFIG.INTEGRAL_MIN, Math.min(CONFIG.INTEGRAL_MAX, integral));
                                                   logInfo(`[Init] Laufenden Betrieb erkannt: ${curMode} @ ${Math.abs(startPower)}W. Integral auf ${Math.round(integral)} gesetzt.`);
                                               }
                                           } catch (e) {
                                               logWarn(`[Init] Konnte Start-Zustand nicht ermitteln: ${e}`);
                                           }
                                        
                                           lastAcModeSwitch = Date.now() - CONFIG.AC_MODE_COOLDOWN_MS; 
                                           
                                           // Zeitpläne starten
                                           await runPredictiveAnalysis(); 
                                           schedule('5 * * * *', runPredictiveAnalysis); // Jede Stunde xx:05 Strategie neu berechnen
                                           schedule('0 3 * * *', updateConsumptionProfileFromSQL); // Nachts um 03:00 Profil lernen
                                           setInterval(mainControlLoop, CONFIG.REGEL_INTERVALL_MS); // Hauptschleife
                                        }
                                        
                                        initialize();
                                        

                                        1 Antwort Letzte Antwort
                                        0
                                        • L Offline
                                          L Offline
                                          lesiflo
                                          Most Active
                                          schrieb am zuletzt editiert von lesiflo
                                          #54

                                          @schimi: So was ähnliches habe ich mir auch "gebaut". Bei mir werden die Daten in einer Influx-DB gespeichert und ich habe es auf mehrere Scripte aufgeteilt. Eins reicht nicht, da ich mir die Daten noch über echarts grafisch aufbereiten lasse. Das mit dem evcc Optimizer läuft aktuell noch parallel zu meinen Scripten. Er wird ja auch seitens evcc bisher auch nur für Entwicklungszwecke verwendet und soll zeigen was demnächst möglich ist.

                                          S 1 Antwort Letzte Antwort
                                          0
                                          Antworten
                                          • In einem neuen Thema antworten
                                          Anmelden zum Antworten
                                          • Älteste zuerst
                                          • Neuste zuerst
                                          • Meiste Stimmen


                                          Support us

                                          ioBroker
                                          Community Adapters
                                          Donate

                                          854

                                          Online

                                          32.6k

                                          Benutzer

                                          82.0k

                                          Themen

                                          1.3m

                                          Beiträge
                                          Community
                                          Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                                          ioBroker Community 2014-2025
                                          logo
                                          • Anmelden

                                          • Du hast noch kein Konto? Registrieren

                                          • Anmelden oder registrieren, um zu suchen
                                          • Erster Beitrag
                                            Letzter Beitrag
                                          0
                                          • Home
                                          • Aktuell
                                          • Tags
                                          • Ungelesen 0
                                          • Kategorien
                                          • Unreplied
                                          • Beliebt
                                          • GitHub
                                          • Docu
                                          • Hilfe