Weiter zum Inhalt
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Hell
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dunkel
  • 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 SolarFlow (MQTT) - Nulleinspeisung & EVCC-Optimizer

NEWS

  • wichtiges UPDATE für controller 7.2.2 im stable
    HomoranH
    Homoran
    8
    1
    558

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    15
    1
    2.8k

  • Neuer ioBroker-Blog online: Monatsrückblick März/April 2026
    BluefoxB
    Bluefox
    8
    1
    2.9k

Zendure SolarFlow (MQTT) - Nulleinspeisung & EVCC-Optimizer

Geplant Angeheftet Gesperrt Verschoben JavaScript
66 Beiträge 9 Kommentatoren 6.7k Aufrufe 9 Beobachtet
  • Ä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.
  • S Online
    S Online
    Schimi
    schrieb am zuletzt editiert von Schimi
    #1

    [TypeSkript] Zendure SolarFlow: Nulleinspeisung & EVCC-Optimizer Steuerung

    Dieses Projekt besteht aus zwei TypeScript-Skripten, die entwickelt wurden, um eine Zendure SolarFlow Batterieanlage (z.B. mit Zendure 2400 AC) optimal und netzdienlich über ioBroker zu steuern.

    Das Setup ist modular aufgebaut: Das Hauptskript zur Batteriesteuerung (Nullpunktregelung) funktioniert komplett eigenständig. Wer jedoch dynamische Stromtarife und eine intelligente Ladeplanung nutzt, kann das EVCC-Optimizer-Skript als smartes Add-on zuschalten.

    Hier ist die detaillierte Erklärung, was die Skripte tun und wie sie interagieren:

    1. Das Hauptskript: Zendure Controller (Batteriesteuerung)

    Dieses Skript ist das Herzstück. Es liest den aktuellen Stromverbrauch des Hauses (z.B. über einen Shelly 3EM) aus und regelt die Einspeisung oder Ladung des Zendure-Akkus sekundengenau, um den Netzbezug auf 0 Watt (oder einen definierten Zielwert) zu halten.

    Dieses Skript kann völlig autark ohne den EVCC-Optimizer genutzt werden!

    Kernfunktionen:

    • PI-Regler (Nullpunktregelung): Ein mathematischer Regelalgorithmus (Proportional-Integral) berechnet kontinuierlich, wie viel Leistung der Akku abgeben oder aufnehmen muss, um das Haus optimal zu versorgen, ohne Strom ins Netz zu verschenken.

    • Stoßfreie Übernahme (Bumpless Transfer): Startet man das Skript neu, erkennt es den aktuellen Zustand der Hardware und übernimmt diesen nahtlos, ohne dass der Wechselrichter plötzliche Leistungssprünge macht.

    • Umfassender Hardwareschutz:

      • Anti-Windup & Integral-Freeze: Verhindert, dass der Regler "durchdreht" und astronomische Werte aufbaut, wenn der Akku z.B. voll, leer oder manuell gesperrt ist.

      • Hysterese & Cooldowns: Moduswechsel (Laden/Entladen) passieren erst ab einer bestimmten Schwelle (z.B. 100W) und mit zeitlicher Verzögerung (30s), um die Relais des Zendure-Systems zu schonen.

      • MinSoC-Schutz: Stoppt die Entladung hart, sobald der eingestellte Mindest-Akkustand erreicht ist.

    • Manuelle Overrides (Prio-System): Über generierte Datenpunkte (z.B. Laden_erzwingen) kann der Regler von außen übersteuert werden.

    2. Das Add-on: EVCC-Optimizer

    Wer EVCC nutzt (insbesondere mit dynamischen Stromtarifen wie Tibber), hat oft das Problem: Wann soll der Heimspeicher laden, wann das Auto, und wann ist der Strom so günstig, dass der Heimspeicher aus dem Netz geladen werden sollte? Hier setzt der Optimizer an.

    ###Kernfunktionen:

    • JSON-Analyse: Das Skript lauscht auf den evopt MQTT-Datenpunkt von EVCC. Darin teilt EVCC seine geplante Lade- und Netzbezugsstrategie mit.

    • Zustandserkennung pro Zeitfenster: Das Skript analysiert für die aktuelle Uhrzeit drei Faktoren aus dem EVCC-JSON:

      1. Ist überhaupt eine Batterieladung geplant? (charging_power)
      2. Ist für diese Zeit Netzbezug geplant? (grid_import)
      3. Wie hoch ist die erwartete PV-Leistung? (ft)
    • Entscheidungslogik:

      • Modus 0 (PV-Überschuss): EVCC plant eine Ladung, aber ohne Netzbezug. Das ist reguläres PV-Laden.
      • Modus 1 (Grid / Zwangsladung): Wenn EVCC eine Batterieladung plant und gleichzeitig Netzbezug ansteht (oder die PV-Prognose zu gering ist), weiß das Skript: EVCC lädt den Akku gerade absichtlich mit billigem Netzstrom.
      • Modus 2 (Warten / Entladen): EVCC plant aktuell gar keine Ladung. Der Akku darf nur das Haus versorgen.

    3. Wie arbeiten beide Skripte zusammen?

    Die Skripte kommunizieren über einen einzigen ioBroker-Datenpunkt, der vom Hauptskript automatisch angelegt wird: 0_userdata.0.zendure.Laden_erzwingen.

    1. Der EVCC-Optimizer wertet alle paar Minuten die komplexen EVCC-Pläne aus und bricht sie auf eine simple Zahl herunter: 0, 1 oder 2. Diese Zahl schreibt er in den Datenpunkt Laden_erzwingen.

    2. Der Zendure Controller läuft alle 2 Sekunden. Er prüft in jedem Zyklus diesen Datenpunkt.

      • Steht dort eine 0 (Auto), verschwindet der Override und der Controller übernimmt wieder zu 100% autark die Ausregelung von PV-Überschuss und Hausverbrauch.
      • Steht dort eine 1 (MaxCharge), friert der PI-Regler sofort ein und das Skript sendet den Befehl an Zendure, mit maximaler Leistung aus dem Netz zu laden (z.B. wegen billigem Tibber-Strom).
      • Steht dort eine 2 (DischargeOnly), weiß der Controller: Laden ist aktuell verboten (z.B. weil das Auto gerade Priorität bei der PV-Erzeugung hat). Die reguläre Nullpunktregelung zur Hausversorgung (Entladen) läuft aber weiter! Positive Leistungen (Laden) werden einfach auf 0 gekappt.

    Fazit: Der Zendure-Controller ist der "Muskel", der die Hardware schont und die Feinregelung übernimmt. Der EVCC-Optimizer ist das "Gehirn", das bei Bedarf von oben eingreift, um dynamische Stromtarife und E-Auto-Ladungen wirtschaftlich mit dem Heimspeicher zu synchronisieren.

    Letztes Update: 24.06.2026 - 19:22 Uhr

    Batteriesteuerung - Skript:

    /**
     * ZENDURE SOLARFLOW CONTROLLER (TypeScript Edition) - FINAL VERSION
     * * CHANGELOG:
     * - v5.4.0: 2026-06-22 - FEATURE: Erweiterung der 'forceCharge' Logik (0=Auto, 1=MaxCharge, 2=DischargeOnly). 
     * Implementierung von isChargeBlocked inkl. Anti-Windup Schutz.
     * - v5.4.1: 2026-06-22 - FIX: Prioritäts-Leck zwischen EVCC und manuellem Block behoben. 
     * FIX: Negativen Integral-Windup bei Erreichen des MinSoC behoben. FIX: Falsy-Fallback für MinSoC=0.
     * - v5.4.2: 2026-06-23 - FIX: Integral Freeze Deadlock im Anti-Windup behoben. Integrator darf nun auf 0 abbauen.
     * - v5.3.1: 2026-03-21 - FEATURE: Bumpless Transfer (Stoßfreie Übernahme) beim Skriptstart.
     * - v5.3.0: 2026-03-04 - FEATURE: Hysterese für Moduswechsel (SWITCH_THRESHOLD_W) hinzugefügt.
     * * KONTEXT:
     * - Hardware: Zendure SolarFlow 2400 AC via MQTT, Shelly 3EM (Netzbezug).
     * - Schnittstellen: MQTT-Adapter (JSON/Strings), Userdata (Steuerung).
     * * ZIELE:
     * - Nullpunktregelung am Hausanschluss mit PI-Algorithmus.
     * - Hardware-Schutz (Cooldown, MinSoC, Hysterese, Bumpless Transfer).
     * - E-Auto-Priorisierung (EVCC Integration).
     */
    
    // ====================================================================================
    // 1. USER KONFIGURATION (Hier anpassen!)
    // ====================================================================================
    
    // --- ZENDURE MQTT BASISDATEN ---
    const MQTT_BASE_PATH = "mqtt.2.Zendure"; 
    const DEVICE_ID      = "HOxxxxxxxxxxxxxx";
    
    // --- HAUPTKONFIGURATION ---
    /**
     * Zentrale Steuerungsparameter.
     * @KI_HINWEIS: Änderungen hier erfordern einen Neustart des Skripts.
     */
    const CONFIG: ControllerConfig = {
        ENABLE_EVCC: true,
        INFO_LOGS: true,
        DEBUG: false,
    
        MAX_CHARGE_W: 2400,
        MAX_DISCHARGE_W: 2400,
    
        KP: 0.7,
        KI: 0.08,
        DEADBAND_W: 15,
        TARGET_W: 0,
        INTEGRAL_MAX: 30000,
        INTEGRAL_MIN: -30000,
    
        SWITCH_THRESHOLD_W: 100,
    
        REGEL_INTERVALL_MS: 2000,
        AC_MODE_COOLDOWN_MS: 30000,
    };
    
    // --- DATENPUNKTE (IDs) ---
    /**
     * Mapping der ioBroker State-IDs.
     */
    const IDs: DataPoints = {
        netz: "shelly.0.SHEM-3#8CAAB5619A05#1.Total.InstantPower",
    
        acMode:        `${MQTT_BASE_PATH}.select.${DEVICE_ID}.acMode`,
        acModeSet:     `${MQTT_BASE_PATH}.select.${DEVICE_ID}.acMode.set`,
        currentInput:  `${MQTT_BASE_PATH}.number.${DEVICE_ID}.inputLimit`,
        inputSet:      `${MQTT_BASE_PATH}.number.${DEVICE_ID}.inputLimit.set`,
        currentOutput: `${MQTT_BASE_PATH}.number.${DEVICE_ID}.outputLimit`,
        outputSet:     `${MQTT_BASE_PATH}.number.${DEVICE_ID}.outputLimit.set`,
        soc:           `${MQTT_BASE_PATH}.sensor.${DEVICE_ID}.electricLevel`,
        minSoc:        `${MQTT_BASE_PATH}.number.${DEVICE_ID}.minSoc`,
    
        // 1 = Normalbetrieb (Laden/Entladen nach PI-Regler)
        // 2 = Laden bei überschuss erlaubt, entladen gesperrt
        // 3 = Zwangsladen (Maximales Laden erzwungen)
        evccModus:   "0_userdata.0.zendure.EVCC_Modus",
        
        // Manuelles Zwangsladen / Sperren (Override)
        // 0 = Normalbetrieb (Laden/Entladen nach PI-Regler)
        // 1 = Zwangsladen (Maximales Laden erzwungen)
        // 2 = Nur Entladen erlaubt (Laden komplett deaktiviert)
        forceCharge: "0_userdata.0.zendure.Laden_erzwingen"
    };
    
    // ====================================================================================
    // 2. TECHNISCHE DEFINITIONEN
    // ====================================================================================
    
    enum AcMode {
        INPUT = "Input mode",
        OUTPUT = "Output mode"
    }
    
    interface ControllerConfig {
        readonly ENABLE_EVCC: boolean;
        readonly INFO_LOGS: boolean;
        readonly DEBUG: boolean;
        readonly MAX_CHARGE_W: number;
        readonly MAX_DISCHARGE_W: number;
        readonly KP: number;
        readonly KI: number;
        readonly DEADBAND_W: number;
        readonly TARGET_W: number;
        readonly INTEGRAL_MAX: number;
        readonly INTEGRAL_MIN: number;
        readonly SWITCH_THRESHOLD_W: number;
        readonly REGEL_INTERVALL_MS: number;
        readonly AC_MODE_COOLDOWN_MS: number;
    }
    
    interface DataPoints {
        netz: string;
        acMode: string;
        acModeSet: string;
        currentInput: string;
        inputSet: string;
        currentOutput: string;
        outputSet: string;
        soc: string;
        minSoc: string;
        evccModus: string;
        forceCharge: string;
    }
    
    // ====================================================================================
    // 3. LOGIK KLASSE (Controller)
    // ====================================================================================
    
    class ZendureController {
        private integral: number = 0;
        private lastAcModeSwitch: number = 0;
        
        // @KI_HINWEIS: 'any' statt NodeJS.Timeout für maximale Kompatibilität im ioBroker JS-Adapter
        private intervalId: any = null;
        private lastOverrideStatus: string = "";
        
        private isDischargeBlocked: boolean = false; 
        private isChargeBlocked: boolean = false;
    
        /**
         * Konstruktor der ZendureController Klasse.
         */
        constructor() {
            this.logInfo("--- Zendure Controller (TS v5.4.2 Final) initialisiert ---");
            this.init();
        }
    
        /**
         * Initialisierung: Erstellt Datenpunkte und startet den Reglerzyklus.
         */
        private async init(): Promise<void> {
            await this.createScriptStates();
            this.start();
        }
    
        /**
         * Prüft und generiert fehlende Datenpunkte in 0_userdata.0.
         */
        private async createScriptStates(): Promise<void> {
            try {
                if (IDs.evccModus && IDs.evccModus !== "") {
                    await this.ensureState(IDs.evccModus, "EVCC Modus (1=Normal, 2=NoDischarge, 3=ForceCharge)", 1, "");
                }
                if (IDs.forceCharge && IDs.forceCharge !== "") {
                    await this.ensureState(IDs.forceCharge, "Laden erzwingen (0=Auto, 1=MaxCharge, 2=DischargeOnly)", 0, "");
                }
            } catch (e) {
                log(`[Init] Fehler beim Erstellen der Datenpunkte: ${e}`, 'error');
            }
        }
    
        /**
         * Hilfsfunktion zum asynchronen Erstellen eines State-Datenpunkts.
         * @param id Der vollständige Pfad zum Datenpunkt.
         * @param name Bezeichnung des Datenpunkts.
         * @param defVal Standardwert bei Neuerstellung.
         * @param unit Physikalische Einheit (oder Leerstring).
         */
        private async ensureState(id: string, name: string, defVal: number, unit: string): Promise<void> {
            if (!existsState(id)) {
                await createStateAsync(id, {
                    name: name,
                    type: 'number',
                    role: 'value',
                    read: true,
                    write: true,
                    def: defVal,
                    unit: unit || undefined 
                });
                this.logInfo(`Datenpunkt erstellt: ${id}`);
            }
        }
    
        /**
         * Führt den Bumpless Transfer aus und startet das zyklische Intervall.
         */
        private start(): void {
            this.logInfo(`Initialisiere Hardware-Analyse für stoßfreien Start...`);
            
            // --- BUMPLESS TRANSFER (Stoßfreies Umschalten) ---
            const currentModeRaw = this.getStateString(IDs.acMode);
            const currentMode = this.normalizeAcMode(currentModeRaw);
            
            let initialPower = 0;
            if (currentMode === AcMode.OUTPUT) {
                initialPower = -Math.abs(this.getStateSafe(IDs.currentOutput));
            } else if (currentMode === AcMode.INPUT) {
                initialPower = Math.abs(this.getStateSafe(IDs.currentInput));
            }
    
            if (CONFIG.KI > 0) {
                this.integral = initialPower / CONFIG.KI;
                this.integral = Math.max(CONFIG.INTEGRAL_MIN, Math.min(CONFIG.INTEGRAL_MAX, this.integral));
                this.logInfo(`Hardware liefert aktuell ${Math.abs(initialPower)}W (${currentModeRaw || 'Standby'}). Regler vorgeladen.`);
            }
    
            this.lastAcModeSwitch = Date.now();
            
            if (this.intervalId) clearInterval(this.intervalId);
            this.intervalId = setInterval(() => this.controlLoop(), CONFIG.REGEL_INTERVALL_MS);
            this.logInfo(`Regel-Loop gestartet (${CONFIG.REGEL_INTERVALL_MS}ms).`);
    
            this.registerTriggers();
            this.checkOverridesAndAct();
        }
    
        /**
         * Registriert Event-Listener auf Konfigurations-Datenpunkte.
         */
        private registerTriggers(): void {
            const triggerIDs: string[] = [];
            if (CONFIG.ENABLE_EVCC && IDs.evccModus && IDs.evccModus !== "") triggerIDs.push(IDs.evccModus);
            if (IDs.forceCharge && IDs.forceCharge !== "") triggerIDs.push(IDs.forceCharge);
    
            if (triggerIDs.length > 0) {
                on({ id: triggerIDs, change: "any" }, (obj) => {
                    this.logInfo(`Trigger: ${obj.id} geändert auf ${obj.state.val}`);
                    this.lastOverrideStatus = ""; 
                    this.checkOverridesAndAct();
                });
            }
        }
    
        /**
         * Prüft Soft- und Hard-Overrides. Setzt interne Blocker-Flags.
         * @returns true, wenn ein Hard-Override (Zwangsladen) aktiv ist. Der Regler soll dann pausieren.
         */
        private checkOverridesAndAct(): boolean {
            this.isDischargeBlocked = false;
            this.isChargeBlocked = false;
            let manualOverrideActive = false;
    
            // PRIO 1: Manuelle Steuerung (Überschreibt EVCC)
            if (IDs.forceCharge && IDs.forceCharge !== "") {
                const forceVal = this.getStateSafe(IDs.forceCharge);
                if (forceVal === 1) {
                    if (this.lastOverrideStatus !== "MANUAL_FORCE_CHARGE") {
                        this.logInfo("Override: Manuelles Laden erzwungen (Prio 1).");
                        this.lastOverrideStatus = "MANUAL_FORCE_CHARGE";
                    }
                    this.forcePower(AcMode.INPUT, CONFIG.MAX_CHARGE_W);
                    return true; // Hard Override
                } else if (forceVal === 2) {
                    if (this.lastOverrideStatus !== "MANUAL_BLOCK_CHARGE") {
                        this.logInfo("Override: Manuelles Laden deaktiviert. Nur Entladen erlaubt (Modus 2).");
                        this.lastOverrideStatus = "MANUAL_BLOCK_CHARGE";
                    }
                    // @KI_HINWEIS: Soft-Block. Regler läuft weiter, aber positive Leistung (Laden) wird auf 0 gekappt.
                    this.isChargeBlocked = true;
                    manualOverrideActive = true;
                }
            }
    
            // PRIO 2: EVCC Steuerung (Wird ignoriert, wenn manueller Block aktiv ist)
            if (!manualOverrideActive && CONFIG.ENABLE_EVCC && IDs.evccModus && IDs.evccModus !== "") {
                const evccMode = this.getStateSafe(IDs.evccModus);
                
                if (evccMode === 3) { 
                    if (this.lastOverrideStatus !== "EVCC_FORCE_CHARGE") {
                        this.logInfo("Override: EVCC Laden erzwingen (Prio 2).");
                        this.lastOverrideStatus = "EVCC_FORCE_CHARGE";
                    }
                    this.forcePower(AcMode.INPUT, CONFIG.MAX_CHARGE_W);
                    return true; 
                }
                
                if (evccMode === 2) { 
                    if (this.lastOverrideStatus !== "EVCC_BLOCK_DISCHARGE") {
                        this.logInfo("Status: Entladen durch EVCC gesperrt (Laden bei Überschuss aktiv).");
                        this.lastOverrideStatus = "EVCC_BLOCK_DISCHARGE";
                    }
                    this.isDischargeBlocked = true;
                }
            }
    
            return false;
        }
    
        /**
         * Haupt-Regelschleife. Berechnet die Ziel-Leistung basierend auf dem Netzbezug.
         */
        private controlLoop(): void {
            try {
                const isHardOverrideActive = this.checkOverridesAndAct();
                if (isHardOverrideActive) return;
    
                // Status-Übergang zu Normalbetrieb protokollieren
                if (!this.isDischargeBlocked && !this.isChargeBlocked) {
                    if (this.lastOverrideStatus !== "NORMAL") {
                        if (this.lastOverrideStatus !== "") { 
                            this.logInfo("Alle Overrides beendet. Normalbetrieb (PI-Regler) aktiv.");
                        }
                        this.lastOverrideStatus = "NORMAL";
                    }
                }
    
                // 2. Validierung Netz-Sensor (CRITICAL SAFETY)
                const gridState = getState(IDs.netz);
                if (!gridState || gridState.val === null || gridState.val === undefined) {
                    this.logDebug("Netz-Sensor liefert ungültige Daten (null). Regelung pausiert.");
                    return;
                }
                const gridPower = parseFloat(String(gridState.val));
                
                // Hardware-Limits für Freeze-Logik vorab auslesen
                const currentSoC = this.getStateSafe(IDs.soc);
                const hardwareMinSoc = this.getStateSafe(IDs.minSoc, 10);
                const isSoCBlocked = (currentSoC <= hardwareMinSoc);
    
                // 3. PI-Berechnung
                const dt = CONFIG.REGEL_INTERVALL_MS / 1000.0;
                let error = CONFIG.TARGET_W - gridPower;
    
                if (Math.abs(error) <= CONFIG.DEADBAND_W) {
                    error = 0.0;
                    this.logDebug(`Deadband aktiv. Fehler ignoriert.`);
                }
    
                // Integral-Berechnung mit Freeze-Logik (SAFETY / Anti-Windup)
                // @KI_HINWEIS: Verhindert extremes Aufsummieren des Integrals, wenn der Aktor durch Overrides gesperrt ist.
                // FIX v5.4.2: Freeze darf nur greifen, wenn das Integral sich im jeweils blockierten Bereich befindet!
                // Sonst kann ein negatives Integral (Entladung) bei Charge-Block nie wieder auf 0 abbauen.
                if (this.isDischargeBlocked && error < 0 && this.integral <= 0) {
                    this.logDebug("Integral Freeze aktiv (Discharge Block).");
                } else if (this.isChargeBlocked && error > 0 && this.integral >= 0) {
                    this.logDebug("Integral Freeze aktiv (Charge Block).");
                } else if (isSoCBlocked && error < 0 && this.integral <= 0) {
                    this.logDebug("Integral Freeze aktiv (MinSoC erreicht). Verhindere negativen Windup.");
                } else {
                    this.integral += error * dt;
                    this.integral = Math.max(CONFIG.INTEGRAL_MIN, Math.min(CONFIG.INTEGRAL_MAX, this.integral));
                }
    
                // Summe PI
                const pShare = CONFIG.KP * error;
                const iShare = CONFIG.KI * this.integral;
                let outputPower = pShare + iShare;
    
                this.logDebug(`PI-Loop: Netz=${gridPower}W | Err=${error} | P=${pShare.toFixed(1)} | I=${iShare.toFixed(1)} | Out=${outputPower.toFixed(1)}`);
    
                // --- FILTER 1: SoC-Schutz (Hardware Limits) ---
                if (outputPower < 0) { 
                    if (isSoCBlocked) {
                        this.logDebug(`SoC Limit erreicht (${currentSoC}% <= ${hardwareMinSoc}%). Entladung gestoppt.`);
                        outputPower = 0;
                        // @KI_HINWEIS: Harter Reset auf mindestens 0, damit bei einkehrender Sonne sofort geladen wird.
                        this.integral = Math.max(this.integral, 0); 
                    }
                }
    
                // --- FILTER 2: EVCC Modus 2 (Entladen sperren) ---
                if (this.isDischargeBlocked && outputPower < 0) {
                    this.logDebug("Entladen durch aktiven Soft-Block blockiert.");
                    outputPower = 0;
                    // @KI_HINWEIS: Harter Reset auf mindestens 0, um sauberes Wiederanlaufen zu garantieren.
                    this.integral = Math.max(this.integral, 0);
                }
    
                // --- FILTER 3: Manuelles Laden sperren (Laden erzwingen = 2) ---
                if (this.isChargeBlocked && outputPower > 0) {
                    this.logDebug("Laden durch manuellen Override blockiert.");
                    outputPower = 0;
                    // @KI_HINWEIS: Harter Reset auf höchstens 0, um sauberes Wiederanlaufen zu garantieren.
                    this.integral = Math.min(this.integral, 0);
                }
    
                // Clamping (Hardware-Limits einhalten)
                if (outputPower > 0) {
                    outputPower = Math.min(outputPower, CONFIG.MAX_CHARGE_W);
                } else if (outputPower < 0) {
                    outputPower = Math.max(outputPower, -CONFIG.MAX_DISCHARGE_W);
                }
    
                // 4. Aktor ansteuern
                this.setBatteryPower(outputPower);
    
            } catch (e) {
                log(`[ZendureController] Error in controlLoop: ${e}`, 'error');
            }
        }
    
        /**
         * Sendet Befehle an die Hardware via MQTT inkl. Hysterese- und Cooldown-Handling.
         * @param power Ziel-Leistung (Negativ = Entladen, Positiv = Laden)
         */
        private setBatteryPower(power: number): void {
            if (!IDs.acModeSet || !IDs.inputSet || !IDs.outputSet) return;
    
            const cleanPower = Math.round(power);
            const currentModeRaw = this.getStateString(IDs.acMode);
            const currentMode = this.normalizeAcMode(currentModeRaw);
            
            if (currentMode === null) {
                this.logDebug(`AC-Mode unbekannt (${currentModeRaw}). Überspringe Zyklus.`);
                return;
            }
    
            let targetMode = AcMode.OUTPUT;
            let targetVal = Math.abs(cleanPower);
    
            if (cleanPower > 0) {
                targetMode = AcMode.INPUT;
                targetVal = cleanPower;
            }
    
            // Hysterese Logik
            if (currentMode !== targetMode && targetVal < CONFIG.SWITCH_THRESHOLD_W) {
                this.logDebug(`Moduswechsel ignoriert: Anforderung (${targetVal}W) unter Schwelle (${CONFIG.SWITCH_THRESHOLD_W}W).`);
                targetMode = currentMode;
                targetVal = 0;
            }
    
            // Moduswechsel Logik
            if (currentMode !== targetMode) {
                const now = Date.now();
    
                if (now - this.lastAcModeSwitch < CONFIG.AC_MODE_COOLDOWN_MS) {
                    if (currentMode === AcMode.INPUT || currentMode === AcMode.OUTPUT) {
                        const currentSetID = (currentMode === AcMode.INPUT) ? IDs.inputSet : IDs.outputSet;
                        const currentState = getState(currentSetID);
                        if (!currentState || currentState.val != 0) {
                            setState(currentSetID, "0", false); 
                            this.logDebug(`Cooldown aktiv. Zwinge ${currentMode} auf 0W.`);
                        }
                    }
                    return;
                }
    
                this.logInfo(`Moduswechsel: ${currentModeRaw} -> ${targetMode} (${targetVal}W)`);
                this.lastAcModeSwitch = now;
    
                setState(IDs.acModeSet, targetMode, false);
    
                if (targetMode === AcMode.INPUT) {
                    setState(IDs.outputSet, "0", false);
                    setState(IDs.inputSet, String(targetVal), false); 
                } else {
                    setState(IDs.inputSet, "0", false);
                    setState(IDs.outputSet, String(targetVal), false);
                }
    
            } else {
                const activeSetID = (targetMode === AcMode.INPUT) ? IDs.inputSet : IDs.outputSet;
                const targetStr = String(targetVal); 
                
                const currentState = getState(activeSetID);
                if (!currentState || String(currentState.val) !== targetStr) {
                    setState(activeSetID, targetStr, false);
                }
            }
        }
    
        /**
         * Wrapper für setBatteryPower bei Hard-Overrides.
         * @param mode Gewünschter AC Modus (Input oder Output)
         * @param power Die absolute Leistung in Watt
         */
        private forcePower(mode: AcMode, power: number): void {
            let signedPower = Math.abs(power);
            if (mode === AcMode.OUTPUT) {
                signedPower = -signedPower;
            }
            this.setBatteryPower(signedPower);
        }
    
        /**
         * Normalisiert den AC-Mode String der Hardware zu enum.
         * @param val Rohwert vom MQTT (z.B. "Input mode")
         * @returns Das korrekte Enum oder null bei ungültigem Wert.
         */
        private normalizeAcMode(val: string): AcMode | null {
            if (!val) return null;
            const v = val.toLowerCase();
            if (v.includes("input")) return AcMode.INPUT;
            if (v.includes("output")) return AcMode.OUTPUT;
            return null;
        }
    
        /**
         * Liest einen State sicher als Number aus.
         * @param id ioBroker State ID
         * @param fallback Standardwert, falls der State leer oder fehlerhaft ist.
         * @returns Wert als Number oder Fallback bei Fehler.
         */
        private getStateSafe(id: string, fallback: number = 0): number {
            if (!id) return fallback;
            try {
                const state = getState(id);
                if (!state || state.val === null || state.val === undefined) return fallback;
                const num = parseFloat(String(state.val));
                return isNaN(num) ? fallback : num;
            } catch {
                return fallback;
            }
        }
    
        /**
         * Liest einen State sicher als String aus.
         * @param id ioBroker State ID
         * @returns Wert als String oder leer.
         */
        private getStateString(id: string): string {
            if (!id) return "";
            try {
                const state = getState(id);
                return state && state.val ? String(state.val) : "";
            } catch {
                return "";
            }
        }
    
        /**
         * Schreibt eine Information ins ioBroker Log, wenn INFO_LOGS aktiv ist.
         * @param msg Die Nachricht.
         */
        private logInfo(msg: string): void {
            if (CONFIG.INFO_LOGS) log(`[Info] ${msg}`, 'info');
        }
    
        /**
         * Schreibt eine Debug-Information ins ioBroker Log, wenn DEBUG aktiv ist.
         * @param msg Die Nachricht.
         */
        private logDebug(msg: string): void {
            if (CONFIG.DEBUG) log(`[Debug] ${msg}`, 'info');
        }
    }
    
    // ====================================================================================
    // 4. MAIN EXECUTION
    // ====================================================================================
    const controller = new ZendureController();
    

    EVCC-Optimizer - Skript:

    /**
    * @fileoverview Steuerung eines Zendure Akkus basierend auf EVCC evopt JSON-Daten.
    * Das Skript wertet geplante Ladeleistungen, PV-Prognosen und Netzbezug aus,
    * um den Akku zwischen PV-Laden (0), erzwungenem Netz-Laden (1) und Entladen/Warten (2) zu schalten.
    */
    
    const DP_EVOPT = 'mqtt.1.evcc.site.evopt';
    const DP_ZENDURE_MODE = '0_userdata.0.zendure.Laden_erzwingen';
    
    /**
    * Typdefinitionen für das EVCC evopt JSON
    */
    interface EvoptBatteryRes {
       charging_power: number[];
       discharging_power: number[];
       state_of_charge: number[];
    }
    
    interface EvoptTimeSeries {
       dt: number[];
       ft: number[]; // PV-Prognose
       gt: number[];
       p_E: number[];
       p_N: number[];
    }
    
    interface EvoptJson {
       updated: string;
       req: {
           time_series: EvoptTimeSeries;
       };
       res: {
           batteries: EvoptBatteryRes[];
           grid_import: number[];
           grid_export: number[];
       };
       details: {
           timestamp: string[];
       };
    }
    
    /**
    * Initialisiert die Ziel-Datenpunkte und startet den Listener.
    * @returns {Promise<void>}
    */
    async function init(): Promise<void> {
       const stateExists = await existsObjectAsync(DP_ZENDURE_MODE);
       if (!stateExists) {
           await createStateAsync(DP_ZENDURE_MODE, 2, {
               name: 'Zendure Lademodus (0=PV, 1=Grid, 2=Entladen/Warten)',
               type: 'number',
               role: 'value',
               read: true,
               write: true,
               states: {
                   0: 'PV-Geführt (Laden/Entladen)',
                   1: 'Grid (Laden erzwingen)',
                   2: 'Warten (Nur Entladen)'
               }
           });
           log(`Datenpunkt ${DP_ZENDURE_MODE} wurde erstellt.`, 'info');
       }
    
       // Initiale Auswertung bei Skriptstart
       const initialState = await getStateAsync(DP_EVOPT);
       if (initialState && typeof initialState.val === 'string') {
           processEvoptJson(initialState.val);
       }
    
       // Trigger bei jeder Aktualisierung des EVCC JSON
       on({ id: DP_EVOPT, change: 'ne' }, (obj) => {
           if (obj.state && typeof obj.state.val === 'string') {
               processEvoptJson(obj.state.val);
           }
       });
    }
    
    /**
    * Sucht den aktuellen Zeitfenster-Index basierend auf der aktuellen Uhrzeit.
    * @param {string[]} timestamps - Array mit ISO-Zeitstempeln aus dem JSON.
    * @returns {number} Der Index des aktuell gültigen Zeitfensters.
    */
    function getCurrentTimeIndex(timestamps: string[]): number {
       if (!timestamps || timestamps.length === 0) return 0;
       
       const now = new Date().getTime();
       for (let i = 0; i < timestamps.length; i++) {
           const slotTime = new Date(timestamps[i]).getTime();
           // Sobald wir einen Zeitstempel in der Zukunft finden, war der vorherige der aktuelle
           if (slotTime > now) {
               return i > 0 ? i - 1 : 0;
           }
       }
       // Falls alle Zeitstempel in der Vergangenheit liegen, nehmen wir den aktuellsten (letzten)
       return timestamps.length - 1;
    }
    
    /**
    * Analysiert das EVCC JSON und leitet den Zielmodus für den Akku ab.
    * @KI_HINWEIS: [Unterscheidung PV / Grid] Da das JSON keinen direkten Boolean für den Lademodus pro Timeslot bietet, 
    * wird verglichen: Wenn charging_power > 0 ist, prüfen wir den geplanten grid_import. Ist dieser > 0 ODER
    * ist die PV-Prognose (ft) kleiner als 50% der Ladeleistung, muss der Strom logischerweise aus dem Netz kommen (Modus 1).
    * Sonst ist es regulärer PV-Überschuss (Modus 0). Bei charging_power === 0 wird der Warten-Modus (2) aktiv.
    * * @param {string} jsonString - Das rohe JSON aus dem MQTT Datenpunkt.
    * @returns {void}
    */
    function processEvoptJson(jsonString: string): void {
       try {
           const data = JSON.parse(jsonString) as EvoptJson;
           
           // Null/Undefined Checks für die benötigten Hierarchien
           if (!data?.res?.batteries?.[0] || !data?.req?.time_series || !data?.details?.timestamp) {
               log('Ungültiges oder unvollständiges EVOPT JSON erhalten.', 'warn');
               return;
           }
    
           const batteryRes = data.res.batteries[0];
           const currentIdx = getCurrentTimeIndex(data.details.timestamp);
    
           const chargePlan = batteryRes.charging_power[currentIdx] || 0;
           const gridImportPlan = data.res.grid_import[currentIdx] || 0;
           const pvForecast = data.req.time_series.ft[currentIdx] || 0;
    
           let targetMode = 2; // Default: 2 = Nur entladen / Warten
    
           if (chargePlan > 0) {
               // EVCC plant aktive Ladung
               if (gridImportPlan > 0 || pvForecast < (chargePlan * 0.5)) {
                   // Erzwungene Netzladung: Wenn Netzbezug geplant ist oder die PV-Prognose die Ladeleistung nicht annähernd deckt
                   targetMode = 1;
                   log(`[Modus 1] EVCC plant Netzladung: Ladeleistung=${chargePlan}W, GridImport=${gridImportPlan}W, PV-Prognose=${pvForecast}W`, 'debug');
               } else {
                   // PV-geführte Ladung
                   targetMode = 0;
                   log(`[Modus 0] EVCC plant PV-Ladung: Ladeleistung=${chargePlan}W, GridImport=${gridImportPlan}W, PV-Prognose=${pvForecast}W`, 'debug');
               }
           } else {
               // Keine Ladung geplant -> Warten auf Ladefenster / Hausversorgung erlauben
               targetMode = 2;
               log(`[Modus 2] Keine Ladung geplant (Ladeleistung=0W). Akku in Warte-/Entlademodus versetzt.`, 'debug');
           }
    
           // Ziel-Datenpunkt aktualisieren (Ack = true, da Skript den Wert vorgibt)
           setState(DP_ZENDURE_MODE, targetMode, true);
    
       } catch (err) {
           log(`Fehler beim Parsen des EVCC JSON: ${(err as Error).message}`, 'error');
       }
    }
    
    // Skript-Start
    init();
    

    L 1 Antwort Letzte Antwort
    1
    • S Schimi

      [TypeSkript] Zendure SolarFlow: Nulleinspeisung & EVCC-Optimizer Steuerung

      Dieses Projekt besteht aus zwei TypeScript-Skripten, die entwickelt wurden, um eine Zendure SolarFlow Batterieanlage (z.B. mit Zendure 2400 AC) optimal und netzdienlich über ioBroker zu steuern.

      Das Setup ist modular aufgebaut: Das Hauptskript zur Batteriesteuerung (Nullpunktregelung) funktioniert komplett eigenständig. Wer jedoch dynamische Stromtarife und eine intelligente Ladeplanung nutzt, kann das EVCC-Optimizer-Skript als smartes Add-on zuschalten.

      Hier ist die detaillierte Erklärung, was die Skripte tun und wie sie interagieren:

      1. Das Hauptskript: Zendure Controller (Batteriesteuerung)

      Dieses Skript ist das Herzstück. Es liest den aktuellen Stromverbrauch des Hauses (z.B. über einen Shelly 3EM) aus und regelt die Einspeisung oder Ladung des Zendure-Akkus sekundengenau, um den Netzbezug auf 0 Watt (oder einen definierten Zielwert) zu halten.

      Dieses Skript kann völlig autark ohne den EVCC-Optimizer genutzt werden!

      Kernfunktionen:

      • PI-Regler (Nullpunktregelung): Ein mathematischer Regelalgorithmus (Proportional-Integral) berechnet kontinuierlich, wie viel Leistung der Akku abgeben oder aufnehmen muss, um das Haus optimal zu versorgen, ohne Strom ins Netz zu verschenken.

      • Stoßfreie Übernahme (Bumpless Transfer): Startet man das Skript neu, erkennt es den aktuellen Zustand der Hardware und übernimmt diesen nahtlos, ohne dass der Wechselrichter plötzliche Leistungssprünge macht.

      • Umfassender Hardwareschutz:

        • Anti-Windup & Integral-Freeze: Verhindert, dass der Regler "durchdreht" und astronomische Werte aufbaut, wenn der Akku z.B. voll, leer oder manuell gesperrt ist.

        • Hysterese & Cooldowns: Moduswechsel (Laden/Entladen) passieren erst ab einer bestimmten Schwelle (z.B. 100W) und mit zeitlicher Verzögerung (30s), um die Relais des Zendure-Systems zu schonen.

        • MinSoC-Schutz: Stoppt die Entladung hart, sobald der eingestellte Mindest-Akkustand erreicht ist.

      • Manuelle Overrides (Prio-System): Über generierte Datenpunkte (z.B. Laden_erzwingen) kann der Regler von außen übersteuert werden.

      2. Das Add-on: EVCC-Optimizer

      Wer EVCC nutzt (insbesondere mit dynamischen Stromtarifen wie Tibber), hat oft das Problem: Wann soll der Heimspeicher laden, wann das Auto, und wann ist der Strom so günstig, dass der Heimspeicher aus dem Netz geladen werden sollte? Hier setzt der Optimizer an.

      ###Kernfunktionen:

      • JSON-Analyse: Das Skript lauscht auf den evopt MQTT-Datenpunkt von EVCC. Darin teilt EVCC seine geplante Lade- und Netzbezugsstrategie mit.

      • Zustandserkennung pro Zeitfenster: Das Skript analysiert für die aktuelle Uhrzeit drei Faktoren aus dem EVCC-JSON:

        1. Ist überhaupt eine Batterieladung geplant? (charging_power)
        2. Ist für diese Zeit Netzbezug geplant? (grid_import)
        3. Wie hoch ist die erwartete PV-Leistung? (ft)
      • Entscheidungslogik:

        • Modus 0 (PV-Überschuss): EVCC plant eine Ladung, aber ohne Netzbezug. Das ist reguläres PV-Laden.
        • Modus 1 (Grid / Zwangsladung): Wenn EVCC eine Batterieladung plant und gleichzeitig Netzbezug ansteht (oder die PV-Prognose zu gering ist), weiß das Skript: EVCC lädt den Akku gerade absichtlich mit billigem Netzstrom.
        • Modus 2 (Warten / Entladen): EVCC plant aktuell gar keine Ladung. Der Akku darf nur das Haus versorgen.

      3. Wie arbeiten beide Skripte zusammen?

      Die Skripte kommunizieren über einen einzigen ioBroker-Datenpunkt, der vom Hauptskript automatisch angelegt wird: 0_userdata.0.zendure.Laden_erzwingen.

      1. Der EVCC-Optimizer wertet alle paar Minuten die komplexen EVCC-Pläne aus und bricht sie auf eine simple Zahl herunter: 0, 1 oder 2. Diese Zahl schreibt er in den Datenpunkt Laden_erzwingen.

      2. Der Zendure Controller läuft alle 2 Sekunden. Er prüft in jedem Zyklus diesen Datenpunkt.

        • Steht dort eine 0 (Auto), verschwindet der Override und der Controller übernimmt wieder zu 100% autark die Ausregelung von PV-Überschuss und Hausverbrauch.
        • Steht dort eine 1 (MaxCharge), friert der PI-Regler sofort ein und das Skript sendet den Befehl an Zendure, mit maximaler Leistung aus dem Netz zu laden (z.B. wegen billigem Tibber-Strom).
        • Steht dort eine 2 (DischargeOnly), weiß der Controller: Laden ist aktuell verboten (z.B. weil das Auto gerade Priorität bei der PV-Erzeugung hat). Die reguläre Nullpunktregelung zur Hausversorgung (Entladen) läuft aber weiter! Positive Leistungen (Laden) werden einfach auf 0 gekappt.

      Fazit: Der Zendure-Controller ist der "Muskel", der die Hardware schont und die Feinregelung übernimmt. Der EVCC-Optimizer ist das "Gehirn", das bei Bedarf von oben eingreift, um dynamische Stromtarife und E-Auto-Ladungen wirtschaftlich mit dem Heimspeicher zu synchronisieren.

      Letztes Update: 24.06.2026 - 19:22 Uhr

      Batteriesteuerung - Skript:

      /**
       * ZENDURE SOLARFLOW CONTROLLER (TypeScript Edition) - FINAL VERSION
       * * CHANGELOG:
       * - v5.4.0: 2026-06-22 - FEATURE: Erweiterung der 'forceCharge' Logik (0=Auto, 1=MaxCharge, 2=DischargeOnly). 
       * Implementierung von isChargeBlocked inkl. Anti-Windup Schutz.
       * - v5.4.1: 2026-06-22 - FIX: Prioritäts-Leck zwischen EVCC und manuellem Block behoben. 
       * FIX: Negativen Integral-Windup bei Erreichen des MinSoC behoben. FIX: Falsy-Fallback für MinSoC=0.
       * - v5.4.2: 2026-06-23 - FIX: Integral Freeze Deadlock im Anti-Windup behoben. Integrator darf nun auf 0 abbauen.
       * - v5.3.1: 2026-03-21 - FEATURE: Bumpless Transfer (Stoßfreie Übernahme) beim Skriptstart.
       * - v5.3.0: 2026-03-04 - FEATURE: Hysterese für Moduswechsel (SWITCH_THRESHOLD_W) hinzugefügt.
       * * KONTEXT:
       * - Hardware: Zendure SolarFlow 2400 AC via MQTT, Shelly 3EM (Netzbezug).
       * - Schnittstellen: MQTT-Adapter (JSON/Strings), Userdata (Steuerung).
       * * ZIELE:
       * - Nullpunktregelung am Hausanschluss mit PI-Algorithmus.
       * - Hardware-Schutz (Cooldown, MinSoC, Hysterese, Bumpless Transfer).
       * - E-Auto-Priorisierung (EVCC Integration).
       */
      
      // ====================================================================================
      // 1. USER KONFIGURATION (Hier anpassen!)
      // ====================================================================================
      
      // --- ZENDURE MQTT BASISDATEN ---
      const MQTT_BASE_PATH = "mqtt.2.Zendure"; 
      const DEVICE_ID      = "HOxxxxxxxxxxxxxx";
      
      // --- HAUPTKONFIGURATION ---
      /**
       * Zentrale Steuerungsparameter.
       * @KI_HINWEIS: Änderungen hier erfordern einen Neustart des Skripts.
       */
      const CONFIG: ControllerConfig = {
          ENABLE_EVCC: true,
          INFO_LOGS: true,
          DEBUG: false,
      
          MAX_CHARGE_W: 2400,
          MAX_DISCHARGE_W: 2400,
      
          KP: 0.7,
          KI: 0.08,
          DEADBAND_W: 15,
          TARGET_W: 0,
          INTEGRAL_MAX: 30000,
          INTEGRAL_MIN: -30000,
      
          SWITCH_THRESHOLD_W: 100,
      
          REGEL_INTERVALL_MS: 2000,
          AC_MODE_COOLDOWN_MS: 30000,
      };
      
      // --- DATENPUNKTE (IDs) ---
      /**
       * Mapping der ioBroker State-IDs.
       */
      const IDs: DataPoints = {
          netz: "shelly.0.SHEM-3#8CAAB5619A05#1.Total.InstantPower",
      
          acMode:        `${MQTT_BASE_PATH}.select.${DEVICE_ID}.acMode`,
          acModeSet:     `${MQTT_BASE_PATH}.select.${DEVICE_ID}.acMode.set`,
          currentInput:  `${MQTT_BASE_PATH}.number.${DEVICE_ID}.inputLimit`,
          inputSet:      `${MQTT_BASE_PATH}.number.${DEVICE_ID}.inputLimit.set`,
          currentOutput: `${MQTT_BASE_PATH}.number.${DEVICE_ID}.outputLimit`,
          outputSet:     `${MQTT_BASE_PATH}.number.${DEVICE_ID}.outputLimit.set`,
          soc:           `${MQTT_BASE_PATH}.sensor.${DEVICE_ID}.electricLevel`,
          minSoc:        `${MQTT_BASE_PATH}.number.${DEVICE_ID}.minSoc`,
      
          // 1 = Normalbetrieb (Laden/Entladen nach PI-Regler)
          // 2 = Laden bei überschuss erlaubt, entladen gesperrt
          // 3 = Zwangsladen (Maximales Laden erzwungen)
          evccModus:   "0_userdata.0.zendure.EVCC_Modus",
          
          // Manuelles Zwangsladen / Sperren (Override)
          // 0 = Normalbetrieb (Laden/Entladen nach PI-Regler)
          // 1 = Zwangsladen (Maximales Laden erzwungen)
          // 2 = Nur Entladen erlaubt (Laden komplett deaktiviert)
          forceCharge: "0_userdata.0.zendure.Laden_erzwingen"
      };
      
      // ====================================================================================
      // 2. TECHNISCHE DEFINITIONEN
      // ====================================================================================
      
      enum AcMode {
          INPUT = "Input mode",
          OUTPUT = "Output mode"
      }
      
      interface ControllerConfig {
          readonly ENABLE_EVCC: boolean;
          readonly INFO_LOGS: boolean;
          readonly DEBUG: boolean;
          readonly MAX_CHARGE_W: number;
          readonly MAX_DISCHARGE_W: number;
          readonly KP: number;
          readonly KI: number;
          readonly DEADBAND_W: number;
          readonly TARGET_W: number;
          readonly INTEGRAL_MAX: number;
          readonly INTEGRAL_MIN: number;
          readonly SWITCH_THRESHOLD_W: number;
          readonly REGEL_INTERVALL_MS: number;
          readonly AC_MODE_COOLDOWN_MS: number;
      }
      
      interface DataPoints {
          netz: string;
          acMode: string;
          acModeSet: string;
          currentInput: string;
          inputSet: string;
          currentOutput: string;
          outputSet: string;
          soc: string;
          minSoc: string;
          evccModus: string;
          forceCharge: string;
      }
      
      // ====================================================================================
      // 3. LOGIK KLASSE (Controller)
      // ====================================================================================
      
      class ZendureController {
          private integral: number = 0;
          private lastAcModeSwitch: number = 0;
          
          // @KI_HINWEIS: 'any' statt NodeJS.Timeout für maximale Kompatibilität im ioBroker JS-Adapter
          private intervalId: any = null;
          private lastOverrideStatus: string = "";
          
          private isDischargeBlocked: boolean = false; 
          private isChargeBlocked: boolean = false;
      
          /**
           * Konstruktor der ZendureController Klasse.
           */
          constructor() {
              this.logInfo("--- Zendure Controller (TS v5.4.2 Final) initialisiert ---");
              this.init();
          }
      
          /**
           * Initialisierung: Erstellt Datenpunkte und startet den Reglerzyklus.
           */
          private async init(): Promise<void> {
              await this.createScriptStates();
              this.start();
          }
      
          /**
           * Prüft und generiert fehlende Datenpunkte in 0_userdata.0.
           */
          private async createScriptStates(): Promise<void> {
              try {
                  if (IDs.evccModus && IDs.evccModus !== "") {
                      await this.ensureState(IDs.evccModus, "EVCC Modus (1=Normal, 2=NoDischarge, 3=ForceCharge)", 1, "");
                  }
                  if (IDs.forceCharge && IDs.forceCharge !== "") {
                      await this.ensureState(IDs.forceCharge, "Laden erzwingen (0=Auto, 1=MaxCharge, 2=DischargeOnly)", 0, "");
                  }
              } catch (e) {
                  log(`[Init] Fehler beim Erstellen der Datenpunkte: ${e}`, 'error');
              }
          }
      
          /**
           * Hilfsfunktion zum asynchronen Erstellen eines State-Datenpunkts.
           * @param id Der vollständige Pfad zum Datenpunkt.
           * @param name Bezeichnung des Datenpunkts.
           * @param defVal Standardwert bei Neuerstellung.
           * @param unit Physikalische Einheit (oder Leerstring).
           */
          private async ensureState(id: string, name: string, defVal: number, unit: string): Promise<void> {
              if (!existsState(id)) {
                  await createStateAsync(id, {
                      name: name,
                      type: 'number',
                      role: 'value',
                      read: true,
                      write: true,
                      def: defVal,
                      unit: unit || undefined 
                  });
                  this.logInfo(`Datenpunkt erstellt: ${id}`);
              }
          }
      
          /**
           * Führt den Bumpless Transfer aus und startet das zyklische Intervall.
           */
          private start(): void {
              this.logInfo(`Initialisiere Hardware-Analyse für stoßfreien Start...`);
              
              // --- BUMPLESS TRANSFER (Stoßfreies Umschalten) ---
              const currentModeRaw = this.getStateString(IDs.acMode);
              const currentMode = this.normalizeAcMode(currentModeRaw);
              
              let initialPower = 0;
              if (currentMode === AcMode.OUTPUT) {
                  initialPower = -Math.abs(this.getStateSafe(IDs.currentOutput));
              } else if (currentMode === AcMode.INPUT) {
                  initialPower = Math.abs(this.getStateSafe(IDs.currentInput));
              }
      
              if (CONFIG.KI > 0) {
                  this.integral = initialPower / CONFIG.KI;
                  this.integral = Math.max(CONFIG.INTEGRAL_MIN, Math.min(CONFIG.INTEGRAL_MAX, this.integral));
                  this.logInfo(`Hardware liefert aktuell ${Math.abs(initialPower)}W (${currentModeRaw || 'Standby'}). Regler vorgeladen.`);
              }
      
              this.lastAcModeSwitch = Date.now();
              
              if (this.intervalId) clearInterval(this.intervalId);
              this.intervalId = setInterval(() => this.controlLoop(), CONFIG.REGEL_INTERVALL_MS);
              this.logInfo(`Regel-Loop gestartet (${CONFIG.REGEL_INTERVALL_MS}ms).`);
      
              this.registerTriggers();
              this.checkOverridesAndAct();
          }
      
          /**
           * Registriert Event-Listener auf Konfigurations-Datenpunkte.
           */
          private registerTriggers(): void {
              const triggerIDs: string[] = [];
              if (CONFIG.ENABLE_EVCC && IDs.evccModus && IDs.evccModus !== "") triggerIDs.push(IDs.evccModus);
              if (IDs.forceCharge && IDs.forceCharge !== "") triggerIDs.push(IDs.forceCharge);
      
              if (triggerIDs.length > 0) {
                  on({ id: triggerIDs, change: "any" }, (obj) => {
                      this.logInfo(`Trigger: ${obj.id} geändert auf ${obj.state.val}`);
                      this.lastOverrideStatus = ""; 
                      this.checkOverridesAndAct();
                  });
              }
          }
      
          /**
           * Prüft Soft- und Hard-Overrides. Setzt interne Blocker-Flags.
           * @returns true, wenn ein Hard-Override (Zwangsladen) aktiv ist. Der Regler soll dann pausieren.
           */
          private checkOverridesAndAct(): boolean {
              this.isDischargeBlocked = false;
              this.isChargeBlocked = false;
              let manualOverrideActive = false;
      
              // PRIO 1: Manuelle Steuerung (Überschreibt EVCC)
              if (IDs.forceCharge && IDs.forceCharge !== "") {
                  const forceVal = this.getStateSafe(IDs.forceCharge);
                  if (forceVal === 1) {
                      if (this.lastOverrideStatus !== "MANUAL_FORCE_CHARGE") {
                          this.logInfo("Override: Manuelles Laden erzwungen (Prio 1).");
                          this.lastOverrideStatus = "MANUAL_FORCE_CHARGE";
                      }
                      this.forcePower(AcMode.INPUT, CONFIG.MAX_CHARGE_W);
                      return true; // Hard Override
                  } else if (forceVal === 2) {
                      if (this.lastOverrideStatus !== "MANUAL_BLOCK_CHARGE") {
                          this.logInfo("Override: Manuelles Laden deaktiviert. Nur Entladen erlaubt (Modus 2).");
                          this.lastOverrideStatus = "MANUAL_BLOCK_CHARGE";
                      }
                      // @KI_HINWEIS: Soft-Block. Regler läuft weiter, aber positive Leistung (Laden) wird auf 0 gekappt.
                      this.isChargeBlocked = true;
                      manualOverrideActive = true;
                  }
              }
      
              // PRIO 2: EVCC Steuerung (Wird ignoriert, wenn manueller Block aktiv ist)
              if (!manualOverrideActive && CONFIG.ENABLE_EVCC && IDs.evccModus && IDs.evccModus !== "") {
                  const evccMode = this.getStateSafe(IDs.evccModus);
                  
                  if (evccMode === 3) { 
                      if (this.lastOverrideStatus !== "EVCC_FORCE_CHARGE") {
                          this.logInfo("Override: EVCC Laden erzwingen (Prio 2).");
                          this.lastOverrideStatus = "EVCC_FORCE_CHARGE";
                      }
                      this.forcePower(AcMode.INPUT, CONFIG.MAX_CHARGE_W);
                      return true; 
                  }
                  
                  if (evccMode === 2) { 
                      if (this.lastOverrideStatus !== "EVCC_BLOCK_DISCHARGE") {
                          this.logInfo("Status: Entladen durch EVCC gesperrt (Laden bei Überschuss aktiv).");
                          this.lastOverrideStatus = "EVCC_BLOCK_DISCHARGE";
                      }
                      this.isDischargeBlocked = true;
                  }
              }
      
              return false;
          }
      
          /**
           * Haupt-Regelschleife. Berechnet die Ziel-Leistung basierend auf dem Netzbezug.
           */
          private controlLoop(): void {
              try {
                  const isHardOverrideActive = this.checkOverridesAndAct();
                  if (isHardOverrideActive) return;
      
                  // Status-Übergang zu Normalbetrieb protokollieren
                  if (!this.isDischargeBlocked && !this.isChargeBlocked) {
                      if (this.lastOverrideStatus !== "NORMAL") {
                          if (this.lastOverrideStatus !== "") { 
                              this.logInfo("Alle Overrides beendet. Normalbetrieb (PI-Regler) aktiv.");
                          }
                          this.lastOverrideStatus = "NORMAL";
                      }
                  }
      
                  // 2. Validierung Netz-Sensor (CRITICAL SAFETY)
                  const gridState = getState(IDs.netz);
                  if (!gridState || gridState.val === null || gridState.val === undefined) {
                      this.logDebug("Netz-Sensor liefert ungültige Daten (null). Regelung pausiert.");
                      return;
                  }
                  const gridPower = parseFloat(String(gridState.val));
                  
                  // Hardware-Limits für Freeze-Logik vorab auslesen
                  const currentSoC = this.getStateSafe(IDs.soc);
                  const hardwareMinSoc = this.getStateSafe(IDs.minSoc, 10);
                  const isSoCBlocked = (currentSoC <= hardwareMinSoc);
      
                  // 3. PI-Berechnung
                  const dt = CONFIG.REGEL_INTERVALL_MS / 1000.0;
                  let error = CONFIG.TARGET_W - gridPower;
      
                  if (Math.abs(error) <= CONFIG.DEADBAND_W) {
                      error = 0.0;
                      this.logDebug(`Deadband aktiv. Fehler ignoriert.`);
                  }
      
                  // Integral-Berechnung mit Freeze-Logik (SAFETY / Anti-Windup)
                  // @KI_HINWEIS: Verhindert extremes Aufsummieren des Integrals, wenn der Aktor durch Overrides gesperrt ist.
                  // FIX v5.4.2: Freeze darf nur greifen, wenn das Integral sich im jeweils blockierten Bereich befindet!
                  // Sonst kann ein negatives Integral (Entladung) bei Charge-Block nie wieder auf 0 abbauen.
                  if (this.isDischargeBlocked && error < 0 && this.integral <= 0) {
                      this.logDebug("Integral Freeze aktiv (Discharge Block).");
                  } else if (this.isChargeBlocked && error > 0 && this.integral >= 0) {
                      this.logDebug("Integral Freeze aktiv (Charge Block).");
                  } else if (isSoCBlocked && error < 0 && this.integral <= 0) {
                      this.logDebug("Integral Freeze aktiv (MinSoC erreicht). Verhindere negativen Windup.");
                  } else {
                      this.integral += error * dt;
                      this.integral = Math.max(CONFIG.INTEGRAL_MIN, Math.min(CONFIG.INTEGRAL_MAX, this.integral));
                  }
      
                  // Summe PI
                  const pShare = CONFIG.KP * error;
                  const iShare = CONFIG.KI * this.integral;
                  let outputPower = pShare + iShare;
      
                  this.logDebug(`PI-Loop: Netz=${gridPower}W | Err=${error} | P=${pShare.toFixed(1)} | I=${iShare.toFixed(1)} | Out=${outputPower.toFixed(1)}`);
      
                  // --- FILTER 1: SoC-Schutz (Hardware Limits) ---
                  if (outputPower < 0) { 
                      if (isSoCBlocked) {
                          this.logDebug(`SoC Limit erreicht (${currentSoC}% <= ${hardwareMinSoc}%). Entladung gestoppt.`);
                          outputPower = 0;
                          // @KI_HINWEIS: Harter Reset auf mindestens 0, damit bei einkehrender Sonne sofort geladen wird.
                          this.integral = Math.max(this.integral, 0); 
                      }
                  }
      
                  // --- FILTER 2: EVCC Modus 2 (Entladen sperren) ---
                  if (this.isDischargeBlocked && outputPower < 0) {
                      this.logDebug("Entladen durch aktiven Soft-Block blockiert.");
                      outputPower = 0;
                      // @KI_HINWEIS: Harter Reset auf mindestens 0, um sauberes Wiederanlaufen zu garantieren.
                      this.integral = Math.max(this.integral, 0);
                  }
      
                  // --- FILTER 3: Manuelles Laden sperren (Laden erzwingen = 2) ---
                  if (this.isChargeBlocked && outputPower > 0) {
                      this.logDebug("Laden durch manuellen Override blockiert.");
                      outputPower = 0;
                      // @KI_HINWEIS: Harter Reset auf höchstens 0, um sauberes Wiederanlaufen zu garantieren.
                      this.integral = Math.min(this.integral, 0);
                  }
      
                  // Clamping (Hardware-Limits einhalten)
                  if (outputPower > 0) {
                      outputPower = Math.min(outputPower, CONFIG.MAX_CHARGE_W);
                  } else if (outputPower < 0) {
                      outputPower = Math.max(outputPower, -CONFIG.MAX_DISCHARGE_W);
                  }
      
                  // 4. Aktor ansteuern
                  this.setBatteryPower(outputPower);
      
              } catch (e) {
                  log(`[ZendureController] Error in controlLoop: ${e}`, 'error');
              }
          }
      
          /**
           * Sendet Befehle an die Hardware via MQTT inkl. Hysterese- und Cooldown-Handling.
           * @param power Ziel-Leistung (Negativ = Entladen, Positiv = Laden)
           */
          private setBatteryPower(power: number): void {
              if (!IDs.acModeSet || !IDs.inputSet || !IDs.outputSet) return;
      
              const cleanPower = Math.round(power);
              const currentModeRaw = this.getStateString(IDs.acMode);
              const currentMode = this.normalizeAcMode(currentModeRaw);
              
              if (currentMode === null) {
                  this.logDebug(`AC-Mode unbekannt (${currentModeRaw}). Überspringe Zyklus.`);
                  return;
              }
      
              let targetMode = AcMode.OUTPUT;
              let targetVal = Math.abs(cleanPower);
      
              if (cleanPower > 0) {
                  targetMode = AcMode.INPUT;
                  targetVal = cleanPower;
              }
      
              // Hysterese Logik
              if (currentMode !== targetMode && targetVal < CONFIG.SWITCH_THRESHOLD_W) {
                  this.logDebug(`Moduswechsel ignoriert: Anforderung (${targetVal}W) unter Schwelle (${CONFIG.SWITCH_THRESHOLD_W}W).`);
                  targetMode = currentMode;
                  targetVal = 0;
              }
      
              // Moduswechsel Logik
              if (currentMode !== targetMode) {
                  const now = Date.now();
      
                  if (now - this.lastAcModeSwitch < CONFIG.AC_MODE_COOLDOWN_MS) {
                      if (currentMode === AcMode.INPUT || currentMode === AcMode.OUTPUT) {
                          const currentSetID = (currentMode === AcMode.INPUT) ? IDs.inputSet : IDs.outputSet;
                          const currentState = getState(currentSetID);
                          if (!currentState || currentState.val != 0) {
                              setState(currentSetID, "0", false); 
                              this.logDebug(`Cooldown aktiv. Zwinge ${currentMode} auf 0W.`);
                          }
                      }
                      return;
                  }
      
                  this.logInfo(`Moduswechsel: ${currentModeRaw} -> ${targetMode} (${targetVal}W)`);
                  this.lastAcModeSwitch = now;
      
                  setState(IDs.acModeSet, targetMode, false);
      
                  if (targetMode === AcMode.INPUT) {
                      setState(IDs.outputSet, "0", false);
                      setState(IDs.inputSet, String(targetVal), false); 
                  } else {
                      setState(IDs.inputSet, "0", false);
                      setState(IDs.outputSet, String(targetVal), false);
                  }
      
              } else {
                  const activeSetID = (targetMode === AcMode.INPUT) ? IDs.inputSet : IDs.outputSet;
                  const targetStr = String(targetVal); 
                  
                  const currentState = getState(activeSetID);
                  if (!currentState || String(currentState.val) !== targetStr) {
                      setState(activeSetID, targetStr, false);
                  }
              }
          }
      
          /**
           * Wrapper für setBatteryPower bei Hard-Overrides.
           * @param mode Gewünschter AC Modus (Input oder Output)
           * @param power Die absolute Leistung in Watt
           */
          private forcePower(mode: AcMode, power: number): void {
              let signedPower = Math.abs(power);
              if (mode === AcMode.OUTPUT) {
                  signedPower = -signedPower;
              }
              this.setBatteryPower(signedPower);
          }
      
          /**
           * Normalisiert den AC-Mode String der Hardware zu enum.
           * @param val Rohwert vom MQTT (z.B. "Input mode")
           * @returns Das korrekte Enum oder null bei ungültigem Wert.
           */
          private normalizeAcMode(val: string): AcMode | null {
              if (!val) return null;
              const v = val.toLowerCase();
              if (v.includes("input")) return AcMode.INPUT;
              if (v.includes("output")) return AcMode.OUTPUT;
              return null;
          }
      
          /**
           * Liest einen State sicher als Number aus.
           * @param id ioBroker State ID
           * @param fallback Standardwert, falls der State leer oder fehlerhaft ist.
           * @returns Wert als Number oder Fallback bei Fehler.
           */
          private getStateSafe(id: string, fallback: number = 0): number {
              if (!id) return fallback;
              try {
                  const state = getState(id);
                  if (!state || state.val === null || state.val === undefined) return fallback;
                  const num = parseFloat(String(state.val));
                  return isNaN(num) ? fallback : num;
              } catch {
                  return fallback;
              }
          }
      
          /**
           * Liest einen State sicher als String aus.
           * @param id ioBroker State ID
           * @returns Wert als String oder leer.
           */
          private getStateString(id: string): string {
              if (!id) return "";
              try {
                  const state = getState(id);
                  return state && state.val ? String(state.val) : "";
              } catch {
                  return "";
              }
          }
      
          /**
           * Schreibt eine Information ins ioBroker Log, wenn INFO_LOGS aktiv ist.
           * @param msg Die Nachricht.
           */
          private logInfo(msg: string): void {
              if (CONFIG.INFO_LOGS) log(`[Info] ${msg}`, 'info');
          }
      
          /**
           * Schreibt eine Debug-Information ins ioBroker Log, wenn DEBUG aktiv ist.
           * @param msg Die Nachricht.
           */
          private logDebug(msg: string): void {
              if (CONFIG.DEBUG) log(`[Debug] ${msg}`, 'info');
          }
      }
      
      // ====================================================================================
      // 4. MAIN EXECUTION
      // ====================================================================================
      const controller = new ZendureController();
      

      EVCC-Optimizer - Skript:

      /**
      * @fileoverview Steuerung eines Zendure Akkus basierend auf EVCC evopt JSON-Daten.
      * Das Skript wertet geplante Ladeleistungen, PV-Prognosen und Netzbezug aus,
      * um den Akku zwischen PV-Laden (0), erzwungenem Netz-Laden (1) und Entladen/Warten (2) zu schalten.
      */
      
      const DP_EVOPT = 'mqtt.1.evcc.site.evopt';
      const DP_ZENDURE_MODE = '0_userdata.0.zendure.Laden_erzwingen';
      
      /**
      * Typdefinitionen für das EVCC evopt JSON
      */
      interface EvoptBatteryRes {
         charging_power: number[];
         discharging_power: number[];
         state_of_charge: number[];
      }
      
      interface EvoptTimeSeries {
         dt: number[];
         ft: number[]; // PV-Prognose
         gt: number[];
         p_E: number[];
         p_N: number[];
      }
      
      interface EvoptJson {
         updated: string;
         req: {
             time_series: EvoptTimeSeries;
         };
         res: {
             batteries: EvoptBatteryRes[];
             grid_import: number[];
             grid_export: number[];
         };
         details: {
             timestamp: string[];
         };
      }
      
      /**
      * Initialisiert die Ziel-Datenpunkte und startet den Listener.
      * @returns {Promise<void>}
      */
      async function init(): Promise<void> {
         const stateExists = await existsObjectAsync(DP_ZENDURE_MODE);
         if (!stateExists) {
             await createStateAsync(DP_ZENDURE_MODE, 2, {
                 name: 'Zendure Lademodus (0=PV, 1=Grid, 2=Entladen/Warten)',
                 type: 'number',
                 role: 'value',
                 read: true,
                 write: true,
                 states: {
                     0: 'PV-Geführt (Laden/Entladen)',
                     1: 'Grid (Laden erzwingen)',
                     2: 'Warten (Nur Entladen)'
                 }
             });
             log(`Datenpunkt ${DP_ZENDURE_MODE} wurde erstellt.`, 'info');
         }
      
         // Initiale Auswertung bei Skriptstart
         const initialState = await getStateAsync(DP_EVOPT);
         if (initialState && typeof initialState.val === 'string') {
             processEvoptJson(initialState.val);
         }
      
         // Trigger bei jeder Aktualisierung des EVCC JSON
         on({ id: DP_EVOPT, change: 'ne' }, (obj) => {
             if (obj.state && typeof obj.state.val === 'string') {
                 processEvoptJson(obj.state.val);
             }
         });
      }
      
      /**
      * Sucht den aktuellen Zeitfenster-Index basierend auf der aktuellen Uhrzeit.
      * @param {string[]} timestamps - Array mit ISO-Zeitstempeln aus dem JSON.
      * @returns {number} Der Index des aktuell gültigen Zeitfensters.
      */
      function getCurrentTimeIndex(timestamps: string[]): number {
         if (!timestamps || timestamps.length === 0) return 0;
         
         const now = new Date().getTime();
         for (let i = 0; i < timestamps.length; i++) {
             const slotTime = new Date(timestamps[i]).getTime();
             // Sobald wir einen Zeitstempel in der Zukunft finden, war der vorherige der aktuelle
             if (slotTime > now) {
                 return i > 0 ? i - 1 : 0;
             }
         }
         // Falls alle Zeitstempel in der Vergangenheit liegen, nehmen wir den aktuellsten (letzten)
         return timestamps.length - 1;
      }
      
      /**
      * Analysiert das EVCC JSON und leitet den Zielmodus für den Akku ab.
      * @KI_HINWEIS: [Unterscheidung PV / Grid] Da das JSON keinen direkten Boolean für den Lademodus pro Timeslot bietet, 
      * wird verglichen: Wenn charging_power > 0 ist, prüfen wir den geplanten grid_import. Ist dieser > 0 ODER
      * ist die PV-Prognose (ft) kleiner als 50% der Ladeleistung, muss der Strom logischerweise aus dem Netz kommen (Modus 1).
      * Sonst ist es regulärer PV-Überschuss (Modus 0). Bei charging_power === 0 wird der Warten-Modus (2) aktiv.
      * * @param {string} jsonString - Das rohe JSON aus dem MQTT Datenpunkt.
      * @returns {void}
      */
      function processEvoptJson(jsonString: string): void {
         try {
             const data = JSON.parse(jsonString) as EvoptJson;
             
             // Null/Undefined Checks für die benötigten Hierarchien
             if (!data?.res?.batteries?.[0] || !data?.req?.time_series || !data?.details?.timestamp) {
                 log('Ungültiges oder unvollständiges EVOPT JSON erhalten.', 'warn');
                 return;
             }
      
             const batteryRes = data.res.batteries[0];
             const currentIdx = getCurrentTimeIndex(data.details.timestamp);
      
             const chargePlan = batteryRes.charging_power[currentIdx] || 0;
             const gridImportPlan = data.res.grid_import[currentIdx] || 0;
             const pvForecast = data.req.time_series.ft[currentIdx] || 0;
      
             let targetMode = 2; // Default: 2 = Nur entladen / Warten
      
             if (chargePlan > 0) {
                 // EVCC plant aktive Ladung
                 if (gridImportPlan > 0 || pvForecast < (chargePlan * 0.5)) {
                     // Erzwungene Netzladung: Wenn Netzbezug geplant ist oder die PV-Prognose die Ladeleistung nicht annähernd deckt
                     targetMode = 1;
                     log(`[Modus 1] EVCC plant Netzladung: Ladeleistung=${chargePlan}W, GridImport=${gridImportPlan}W, PV-Prognose=${pvForecast}W`, 'debug');
                 } else {
                     // PV-geführte Ladung
                     targetMode = 0;
                     log(`[Modus 0] EVCC plant PV-Ladung: Ladeleistung=${chargePlan}W, GridImport=${gridImportPlan}W, PV-Prognose=${pvForecast}W`, 'debug');
                 }
             } else {
                 // Keine Ladung geplant -> Warten auf Ladefenster / Hausversorgung erlauben
                 targetMode = 2;
                 log(`[Modus 2] Keine Ladung geplant (Ladeleistung=0W). Akku in Warte-/Entlademodus versetzt.`, 'debug');
             }
      
             // Ziel-Datenpunkt aktualisieren (Ack = true, da Skript den Wert vorgibt)
             setState(DP_ZENDURE_MODE, targetMode, true);
      
         } catch (err) {
             log(`Fehler beim Parsen des EVCC JSON: ${(err as Error).message}`, 'error');
         }
      }
      
      // Skript-Start
      init();
      

      L Offline
      L Offline
      lesiflo
      Most Active
      schrieb am zuletzt editiert von
      #2

      @Schimi: Du solltest bei Entladen aus dem Akku noch mit berücksichtigen, ob du viel mit PV oder Netz geladen hast. Wenn viel mit Netz sollte der Entladepreis mindestens 20% über deinem Ladepreis liegen. Soweit ich das sehe hast du das noch nicht mit drin. Oder wie wird das Entladen geregelt?

      S 1 Antwort Letzte Antwort
      1
      • L lesiflo

        @Schimi: Du solltest bei Entladen aus dem Akku noch mit berücksichtigen, ob du viel mit PV oder Netz geladen hast. Wenn viel mit Netz sollte der Entladepreis mindestens 20% über deinem Ladepreis liegen. Soweit ich das sehe hast du das noch nicht mit drin. Oder wie wird das Entladen geregelt?

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

        @lesiflo das stimmt, das wird über Tibber berücksichtigt und man kann es dort direkt konfigurieren... (bei mir z.b. muss sogar 25% Differenz sein)...

        der Hintergedanke war, das die meisten die einen flexiblen Stromtarif haben, irgendwie sowas selber berechnen (kann der tibberlink Adapter direkt).
        Will das Script nicht übertrieben aufblähen...

        Danke für deinen einwand 👍🏼

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

          update

          // v1.9 - 01.11.2025
          // - OPTIMIERUNG: PI-Regelung ist nun Timer-basiert (alle 2000ms), um eine fixe
          // Integrationszeit (dt) und damit eine höhere Stabilität zu gewährleisten.
          // - VERBESSERUNG: Trennung der Log-Funktionen in logInfo (wichtig, immer an)
          // und logDebug (detailliert, ausschaltbar).
          // - NEU: Trigger auf Netzleistung entfernt. Die Regelung erfolgt nur noch über den Timer.*

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

            // v2.0 - 02.11.2025
            // - ANPASSUNG: Die Steuerung von Tibber, EVCC und PV-Forecast erfolgt nun
            // ausschließlich über die internen "CONFIG" Variablen (ENABLE_TIBBER, etc.).
            // - ENTFERNT: Es werden keine externen Datenpunkte mehr zur Aktivierung/Deaktivierung
            // der Module benötigt oder abgefragt.
            // - Check: Timer-basierte Regelung (V1.9) bleibt erhalten.
            //------------------------------------------------------------

            1 Antwort Letzte Antwort
            0
            • M Offline
              M Offline
              Mabbi
              schrieb am zuletzt editiert von Mabbi
              #6

              Hi,

              cooles java script, danke fürs bauen

              Ich habe mal angefangen, Dein Javascript für Nerds wie mich in Blockly zu bauen:
              bb0a3dd6-18cb-4f6e-ba61-fc94a2e9d4cc-grafik.png

              Allerdings werde ich in meiner Version EVCC und Tibber weglassen, dafür wird Sie mit 2x Zendure AC2400 im Wechseltakt arbeiten. Entweder regel ich dann hoffentlich insgesamt schneller oder schone die Hardware etwas...wir werden sehen.

              SMA Wechselrichter Probleme seit letztem Update

              1 Antwort Letzte Antwort
              1
              • M Offline
                M Offline
                Mabbi
                schrieb am zuletzt editiert von Mabbi
                #7

                Habe fertig...alles nun schön in Funktionen gepackt (bis auf die Presets der Konstanten/Globals):

                29a817e2-c3fc-4d46-b636-664fb9e805e6-grafik.png

                Ein bisschen stolz bin ich auf das hier:

                d2452022-d588-4892-9580-745ae3ea0a22-grafik.png

                im Zusammenspiel mit dem hier (Beispiel):

                2e7ebbdc-485c-4d56-8c09-6aebe6ad0882-grafik.png

                Volle Flexibilität bei den Datenpunkten (werden aus Bausteinen je nach Akku_Nr zusammengebaut und um die Object-Subfolder, Zielobjekte und evtl '.SET' (je nachdem ob man liest oder schreibt) erstellt.

                So ist da hinzufügen eines weiteren Akkus einfach nur die Seriennummer und die MQTT.X Nummer in die Variable einzutragen und schon funzt es(hoffentlich).

                Ist noch ein bisschen rough, muss noch 'sonst falls' bei der Akku-Ansteuerung bekommen, die Variaben für den 3. Akku fehlen noch und die Object_ID Funktion werde ich noch mit einem Rückgabewert versehen, aber das Prinzip wird glaube ich schon ersichtlich.

                Heute leider keine Zeit mehr zum Testen... :(

                SMA Wechselrichter Probleme seit letztem Update

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

                  cool!!

                  Ich könnte (wenn bedarf besteht) das JavaScript auch auf "mehrere Geräte" umbauen...
                  Das müsste aber einer testen oder mir min. ein Gerät spenden ;-)

                  edit
                  Ich glaube interessant wäre es auch wenn du das als Code zum importieren anbietest

                  M 1 Antwort Letzte Antwort
                  0
                  • S Schimi

                    cool!!

                    Ich könnte (wenn bedarf besteht) das JavaScript auch auf "mehrere Geräte" umbauen...
                    Das müsste aber einer testen oder mir min. ein Gerät spenden ;-)

                    edit
                    Ich glaube interessant wäre es auch wenn du das als Code zum importieren anbietest

                    M Offline
                    M Offline
                    Mabbi
                    schrieb am zuletzt editiert von Mabbi
                    #9

                    Kleine aber sinnvolle Fortschritte:

                    @Schimi:
                    Ich habe mich von Deinem Integral getrennt :(

                    Was aktuell im Blockly funktioniert:
                    Beide Zendure AC2400 takten und steuern im Wechsel
                    Hat mir viel Kopfzerbrechen gemacht die beiden Regelungen sinnvoll vom Aufschauckeln abzuhalten
                    76d8e8c5-0a3f-4f06-bf3a-175ef9df7e4a-grafik.png
                    bis hin zu einer lädt und der andere speist ein...
                    a37167b5-a751-4bf5-ba43-d89896f13d39-grafik.png

                    Das ist beides gelöst und ich habe nun ein sinnvolles Leistungs-Synchronisation (Loadbalancing) zwischen den beiden:
                    68a3a5d0-d042-47eb-b146-c6dc9be8c6e9-grafik.png

                    Sieht doch schon besser aus :)

                    • Es gibt nun einen Nachtmodus (Sonnenuntergang bis Sonnenaufgang) der beide Zendure strikt im Output-Modus festhält, ich nutze ja kein Tibber o.ä.

                    • SOC Balancing zwischen den beiden Akkus
                      Erstaunlicherweise Laden und Entladen die beiden nicht wirklich parallel (beide Akkus liefen da noch synchron in der Ansteuerung ohne Wechseltaktung):
                      4b1f3b41-15cb-47ac-911c-ca597a6a01c9-grafik.png

                    Vielleicht ist SOC gar nicht so wichtig bei diesem Akkutyp, der blaue und der lila Graph sind die SOC-Werte.
                    Vielleicht laden und entladen die auch unterschiedlich, weil der eine aktuell im Technikraum und der andere bei mir im Büro steht... ergo unterschiedliche Temperaturn in der Nacht (wir reden hier von gerade mal 20° zu 16° Grad Unterschied) ?

                    Aktuell steure ich die Akkus beim einem SOC Unterschied von mehr als 5% gezielter an um das auszugleichen.
                    Bin aber nicht sicher, ob ich den Part behalten werde.

                    • Eine indirekte Unterstützung für meine Wallbox ist integriert.
                      Es gibt ein prozentuales Prio-System, bei gleicher Priorität zwischen Haus-Akku und EV 'sieht' die WB einen Teil der Akku-Ladeleistung als zus. virtuellen PV-Überschuss. Damit wird die WB früher motiviert das EV zu laden.
                      Über einem bestimmten SOC halten die Akkus die Wallbox virtuell (ja. hier kann es zu kurzem geringen Netzbezug kommen weil die Akkus nicht schnell genug schalten auf Entladen) am Leben. Funktioniert noch nicht ganz optimal, aktuell steuert meine go-E nur alle 90 Sekunden per Adapter. Und für genau den Adapter erzeuge ich eine teilweise gefakete PV-Einspeisungs-Leistungswerte.
                      Funktioniert aber schon ganz gut, hier war der Haus-Akku etwas höher in der Prio und hat der WB immer mehr Leistung geklaut, als der Akku fast voll war, bekam die WB wieder mehr Leistung freigeschaltet:
                      41525309-a0e2-4953-838d-2471ad154309-grafik.png
                      Lila oben ist die WB, grün unten die Ladeleistung beider Akkus kombiniert, man kann sogar die kurzen Einspeisungpeaks sehen, als die WB von 1- auf 3-phaisg geschaltet hat und ca. 20 Minuten später wieder zurück wegen insgesamt sinkendem Solarertag.

                    Und auch wenn die roten Netzbezugs-Peaks (ich nutze da MAX-Werte) im oberen Diagramm wild aussehen, das Haus hat heute insgesamt laut Smartmeter gerade mal 560 Wh verbraucht. Und das obwohl ich von 29,3 KWh Produktion nur 3,39KWh eingespeist habe, alles andere ist in Wärme, Haus-Akku für die Nacht und EV geflossen.

                    Langsam bekommt es Hand und Fuß.
                    Ich muss noch etwas an den scripten feilen und testen, dann würde ich die hier auch zu Verfügung stellen, solange @Schimi damit grün ist.

                    Und nun der Wehrmutstropfen am Schluss:
                    Wenn ich die ZendureAC2400 einzeln schneller als 14 Sekunden in der Ansteuerung takte, dann sieht man zwar die mqtt Änderung in den NUMBER/xxx-Limit Werten wie diese vom SET übernommen werden im ca. 1 Sekundentakt, die Werte im Sensor/ gridinput/homeoutput power frieren aber ein und mein Smartmeter bestätigt das auch.

                    Ich komme einfach nicht dahinter, warum beide Zendure AC2400 sich bei mir so verhalten... :(

                    SMA Wechselrichter Probleme seit letztem Update

                    S 1 Antwort Letzte Antwort
                    0
                    • M Mabbi

                      Kleine aber sinnvolle Fortschritte:

                      @Schimi:
                      Ich habe mich von Deinem Integral getrennt :(

                      Was aktuell im Blockly funktioniert:
                      Beide Zendure AC2400 takten und steuern im Wechsel
                      Hat mir viel Kopfzerbrechen gemacht die beiden Regelungen sinnvoll vom Aufschauckeln abzuhalten
                      76d8e8c5-0a3f-4f06-bf3a-175ef9df7e4a-grafik.png
                      bis hin zu einer lädt und der andere speist ein...
                      a37167b5-a751-4bf5-ba43-d89896f13d39-grafik.png

                      Das ist beides gelöst und ich habe nun ein sinnvolles Leistungs-Synchronisation (Loadbalancing) zwischen den beiden:
                      68a3a5d0-d042-47eb-b146-c6dc9be8c6e9-grafik.png

                      Sieht doch schon besser aus :)

                      • Es gibt nun einen Nachtmodus (Sonnenuntergang bis Sonnenaufgang) der beide Zendure strikt im Output-Modus festhält, ich nutze ja kein Tibber o.ä.

                      • SOC Balancing zwischen den beiden Akkus
                        Erstaunlicherweise Laden und Entladen die beiden nicht wirklich parallel (beide Akkus liefen da noch synchron in der Ansteuerung ohne Wechseltaktung):
                        4b1f3b41-15cb-47ac-911c-ca597a6a01c9-grafik.png

                      Vielleicht ist SOC gar nicht so wichtig bei diesem Akkutyp, der blaue und der lila Graph sind die SOC-Werte.
                      Vielleicht laden und entladen die auch unterschiedlich, weil der eine aktuell im Technikraum und der andere bei mir im Büro steht... ergo unterschiedliche Temperaturn in der Nacht (wir reden hier von gerade mal 20° zu 16° Grad Unterschied) ?

                      Aktuell steure ich die Akkus beim einem SOC Unterschied von mehr als 5% gezielter an um das auszugleichen.
                      Bin aber nicht sicher, ob ich den Part behalten werde.

                      • Eine indirekte Unterstützung für meine Wallbox ist integriert.
                        Es gibt ein prozentuales Prio-System, bei gleicher Priorität zwischen Haus-Akku und EV 'sieht' die WB einen Teil der Akku-Ladeleistung als zus. virtuellen PV-Überschuss. Damit wird die WB früher motiviert das EV zu laden.
                        Über einem bestimmten SOC halten die Akkus die Wallbox virtuell (ja. hier kann es zu kurzem geringen Netzbezug kommen weil die Akkus nicht schnell genug schalten auf Entladen) am Leben. Funktioniert noch nicht ganz optimal, aktuell steuert meine go-E nur alle 90 Sekunden per Adapter. Und für genau den Adapter erzeuge ich eine teilweise gefakete PV-Einspeisungs-Leistungswerte.
                        Funktioniert aber schon ganz gut, hier war der Haus-Akku etwas höher in der Prio und hat der WB immer mehr Leistung geklaut, als der Akku fast voll war, bekam die WB wieder mehr Leistung freigeschaltet:
                        41525309-a0e2-4953-838d-2471ad154309-grafik.png
                        Lila oben ist die WB, grün unten die Ladeleistung beider Akkus kombiniert, man kann sogar die kurzen Einspeisungpeaks sehen, als die WB von 1- auf 3-phaisg geschaltet hat und ca. 20 Minuten später wieder zurück wegen insgesamt sinkendem Solarertag.

                      Und auch wenn die roten Netzbezugs-Peaks (ich nutze da MAX-Werte) im oberen Diagramm wild aussehen, das Haus hat heute insgesamt laut Smartmeter gerade mal 560 Wh verbraucht. Und das obwohl ich von 29,3 KWh Produktion nur 3,39KWh eingespeist habe, alles andere ist in Wärme, Haus-Akku für die Nacht und EV geflossen.

                      Langsam bekommt es Hand und Fuß.
                      Ich muss noch etwas an den scripten feilen und testen, dann würde ich die hier auch zu Verfügung stellen, solange @Schimi damit grün ist.

                      Und nun der Wehrmutstropfen am Schluss:
                      Wenn ich die ZendureAC2400 einzeln schneller als 14 Sekunden in der Ansteuerung takte, dann sieht man zwar die mqtt Änderung in den NUMBER/xxx-Limit Werten wie diese vom SET übernommen werden im ca. 1 Sekundentakt, die Werte im Sensor/ gridinput/homeoutput power frieren aber ein und mein Smartmeter bestätigt das auch.

                      Ich komme einfach nicht dahinter, warum beide Zendure AC2400 sich bei mir so verhalten... :(

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

                      @mabbi

                      natürlich bin ich damit "grün" :-) kann man vielleicht dann im ersten Post auf deinen "fertigen" (wenn er dann da ist) verwiesen. Dann können andere direkt hinspringen...

                      Wegen deinem "taktungsproblem" sind wir ja recht ratlos...
                      mir ist noch eingefallen; hast du mal geschaut ob in der App noch zufällig irgend eine Steuerung an ist? Die eventuell über die Cloud reinfunkt?
                      Ich fasse nochmal mein Setup zusammen:

                      • AC2400 im HWR, ca. 3m Luftlinie (hinter eine Tür) vom Unifi Accespoint (U6 Lite) entfernt.

                      • ich habe nicht den DNS "umgebogen" oder sonst wie die Cloud deaktiviert (wobei es damit genau so gut lief)

                      • ich lass parallel noch das Script von @maxclaudi laufen, nutze es aber nicht für irgendeine Steuerung

                      • App:

                      • "Zu HEMS hinzufügen" ist "Aus"

                      • "Off-Grid-Steckdosensteuerung" > "Smarter-Modus" > "Aus"

                      • "Akkueinstellung" min auf 5 und max auf 100%

                      • "MQTT", Aktiviert und entsprechend eingerichtet

                      • Aktuelle Firmware 1.0.8

                      MQTT (iobroker-Adapter):
                      d7ab7a54-6b77-47b5-9164-a59cee36396c-image.png

                      Ping auf den Zendure:
                      9794c002-d17a-473b-9b5f-73261e16e9ea-image.png
                      30741ad1-f0fa-465d-95c2-1525649d385b-image.png
                      f50d0e63-dfc0-4142-b80d-db83bd703738-image.png

                      M 1 Antwort Letzte Antwort
                      0
                      • S Schimi

                        @mabbi

                        natürlich bin ich damit "grün" :-) kann man vielleicht dann im ersten Post auf deinen "fertigen" (wenn er dann da ist) verwiesen. Dann können andere direkt hinspringen...

                        Wegen deinem "taktungsproblem" sind wir ja recht ratlos...
                        mir ist noch eingefallen; hast du mal geschaut ob in der App noch zufällig irgend eine Steuerung an ist? Die eventuell über die Cloud reinfunkt?
                        Ich fasse nochmal mein Setup zusammen:

                        • AC2400 im HWR, ca. 3m Luftlinie (hinter eine Tür) vom Unifi Accespoint (U6 Lite) entfernt.

                        • ich habe nicht den DNS "umgebogen" oder sonst wie die Cloud deaktiviert (wobei es damit genau so gut lief)

                        • ich lass parallel noch das Script von @maxclaudi laufen, nutze es aber nicht für irgendeine Steuerung

                        • App:

                        • "Zu HEMS hinzufügen" ist "Aus"

                        • "Off-Grid-Steckdosensteuerung" > "Smarter-Modus" > "Aus"

                        • "Akkueinstellung" min auf 5 und max auf 100%

                        • "MQTT", Aktiviert und entsprechend eingerichtet

                        • Aktuelle Firmware 1.0.8

                        MQTT (iobroker-Adapter):
                        d7ab7a54-6b77-47b5-9164-a59cee36396c-image.png

                        Ping auf den Zendure:
                        9794c002-d17a-473b-9b5f-73261e16e9ea-image.png
                        30741ad1-f0fa-465d-95c2-1525649d385b-image.png
                        f50d0e63-dfc0-4142-b80d-db83bd703738-image.png

                        M Offline
                        M Offline
                        Mabbi
                        schrieb am zuletzt editiert von Mabbi
                        #11

                        Mein Status:

                        • 2x @maxclaudi script ist an(eins je Akku), Smartmodeinfo: 1 , mqttconnectinfo: 1
                        • Meine 2x mqtt settings sind mit Deinen identisch
                          3d45dd2e-0e76-45bc-8cba-4b2795fa27f3-grafik.png
                        • Beide Akkus sthen keine 2 Meter von einem Accesspoint entfernt
                        • Pings sind ok:
                          34d4de44-9088-4740-b188-c25e24c1cd6e-grafik.png
                        • Die 2 Akkus sind in der App alls offline vermerkt, HEMS bei beiden aus, Mqtt EInstellungen passen, bin aus der App abgemeldet.
                        • Die 2 Akkus sind vom WAN ausgesperrt, der EcoTracker habe ich in der App abgemeldet und ihm dann WAN Zugang gesperrt. Sperre umfasst sowohl IP wie auch MAC-Adresse bei allen 3 Geräten.

                        Hier: 0_userdata.0.zendure.XXXXXXXXXXXXXXX.solarFlow2400AC.version steht bei mir stumpf eine 2 drin
                        Das ist wohl kaum die Firmware.
                        @schimi : Wo sehe ich die Firmmware ?

                        SMA Wechselrichter Probleme seit letztem Update

                        S 1 Antwort Letzte Antwort
                        0
                        • M Mabbi

                          Mein Status:

                          • 2x @maxclaudi script ist an(eins je Akku), Smartmodeinfo: 1 , mqttconnectinfo: 1
                          • Meine 2x mqtt settings sind mit Deinen identisch
                            3d45dd2e-0e76-45bc-8cba-4b2795fa27f3-grafik.png
                          • Beide Akkus sthen keine 2 Meter von einem Accesspoint entfernt
                          • Pings sind ok:
                            34d4de44-9088-4740-b188-c25e24c1cd6e-grafik.png
                          • Die 2 Akkus sind in der App alls offline vermerkt, HEMS bei beiden aus, Mqtt EInstellungen passen, bin aus der App abgemeldet.
                          • Die 2 Akkus sind vom WAN ausgesperrt, der EcoTracker habe ich in der App abgemeldet und ihm dann WAN Zugang gesperrt. Sperre umfasst sowohl IP wie auch MAC-Adresse bei allen 3 Geräten.

                          Hier: 0_userdata.0.zendure.XXXXXXXXXXXXXXX.solarFlow2400AC.version steht bei mir stumpf eine 2 drin
                          Das ist wohl kaum die Firmware.
                          @schimi : Wo sehe ich die Firmmware ?

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

                          @mabbi ich sehe die in der App...

                          ich erlaube meinem den zugangs ins netz und durch das eingebaute Mqtt, kann man gleichzeitig die cloud Verbindung nutzen

                          Screenshot_20251106-210931.png

                          M 1 Antwort Letzte Antwort
                          0
                          • S Schimi

                            @mabbi ich sehe die in der App...

                            ich erlaube meinem den zugangs ins netz und durch das eingebaute Mqtt, kann man gleichzeitig die cloud Verbindung nutzen

                            Screenshot_20251106-210931.png

                            M Offline
                            M Offline
                            Mabbi
                            schrieb am zuletzt editiert von Mabbi
                            #13

                            @schimi

                            Ah ok, da bin ich auf der neusten Version, letztes Update kam vor ca. 2 Wochen.
                            Hat Dein AC2400 irgendwo einen Hinweis auf die Hardwarerevision ?
                            Meine sind vom Juni und von Oktober, nicht das sich da was geändert hat, weil die evtl. neuer sind als Deiner ?

                            Btw...für alle Interessierten, Zendure verkauft den AC2400 plus 1x AB3000X (2.88 KWh) aktuell für 3 stellig.
                            Ich war kurz geneigt den 3. zu kaufen, was ich eigentlich erst in 2026 machen wollte.

                            SMA Wechselrichter Probleme seit letztem Update

                            S 1 Antwort Letzte Antwort
                            0
                            • M Mabbi

                              @schimi

                              Ah ok, da bin ich auf der neusten Version, letztes Update kam vor ca. 2 Wochen.
                              Hat Dein AC2400 irgendwo einen Hinweis auf die Hardwarerevision ?
                              Meine sind vom Juni und von Oktober, nicht das sich da was geändert hat, weil die evtl. neuer sind als Deiner ?

                              Btw...für alle Interessierten, Zendure verkauft den AC2400 plus 1x AB3000X (2.88 KWh) aktuell für 3 stellig.
                              Ich war kurz geneigt den 3. zu kaufen, was ich eigentlich erst in 2026 machen wollte.

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

                              @mabbi nicht das ich wüsste...Falls ich dazu komme, schaue ich mal am we ob ich was am Gerät, direkt finde...

                              hmmmmm

                              M 1 Antwort Letzte Antwort
                              0
                              • S Schimi

                                @mabbi nicht das ich wüsste...Falls ich dazu komme, schaue ich mal am we ob ich was am Gerät, direkt finde...

                                hmmmmm

                                M Offline
                                M Offline
                                Mabbi
                                schrieb am zuletzt editiert von Mabbi
                                #15

                                Die Dämpfung, die ich eingebaut habe (fast ein Integral :) ) führt im Wechselbetrieb der beiden AC2400 zu einem pumpen, und das nur bei der Enspeisung. Wenn ich die AC2400 synchron laufen lasse, dann ist es weg ... ?
                                7c41d697-162e-442b-b58f-28b2742b13ac-grafik.png

                                Das wechselseitig ansteuern der AC2400 ist komplexer als ich dachte, vor allem in dem dynamischen Bereich, wenn es gerade um die PV-Ertragsschwelle wo das Haus versorgt ist zwischen Laden und Entladen schwankt.
                                Dafür war eigentlich die Dämpfung drin.... am WE nochmal an die Tastatur :)

                                Nicht das ich schon 3 Loadbalancing/Dämpfungs-Funktionen in den letzten Wochen durch hätte...
                                cd12e593-4509-4bcb-be34-60614fe1d6e4-grafik.png

                                Nach dem check der Daten von heute habe ich die Nachts-Nicht-Laden Logik erweitert:
                                f45d74ee-3369-45c5-b91b-99c002799376-grafik.png

                                Wenn der durchschnittliche PV-Ertrag der letzten 5 Minuten unter 250 W fällt wird Akku-Laden nun auch verhindert.
                                Diese Abfrage versetzt beide Akkus strikt in den output-Modus(Entladen) solange mindestens eines der beiden Argumente erfüllt ist.

                                SMA Wechselrichter Probleme seit letztem Update

                                S 1 Antwort Letzte Antwort
                                0
                                • M Mabbi

                                  Die Dämpfung, die ich eingebaut habe (fast ein Integral :) ) führt im Wechselbetrieb der beiden AC2400 zu einem pumpen, und das nur bei der Enspeisung. Wenn ich die AC2400 synchron laufen lasse, dann ist es weg ... ?
                                  7c41d697-162e-442b-b58f-28b2742b13ac-grafik.png

                                  Das wechselseitig ansteuern der AC2400 ist komplexer als ich dachte, vor allem in dem dynamischen Bereich, wenn es gerade um die PV-Ertragsschwelle wo das Haus versorgt ist zwischen Laden und Entladen schwankt.
                                  Dafür war eigentlich die Dämpfung drin.... am WE nochmal an die Tastatur :)

                                  Nicht das ich schon 3 Loadbalancing/Dämpfungs-Funktionen in den letzten Wochen durch hätte...
                                  cd12e593-4509-4bcb-be34-60614fe1d6e4-grafik.png

                                  Nach dem check der Daten von heute habe ich die Nachts-Nicht-Laden Logik erweitert:
                                  f45d74ee-3369-45c5-b91b-99c002799376-grafik.png

                                  Wenn der durchschnittliche PV-Ertrag der letzten 5 Minuten unter 250 W fällt wird Akku-Laden nun auch verhindert.
                                  Diese Abfrage versetzt beide Akkus strikt in den output-Modus(Entladen) solange mindestens eines der beiden Argumente erfüllt ist.

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

                                  @mabbi mal ne schnelle Überlegung am Handy...

                                  was ist, wenn du den wert nimmst und durch 2 teilst und jeweils an die beiden 2400AC schickst....

                                  theoretisch sollten die sich gleich ent bzw. beladen

                                  M 1 Antwort Letzte Antwort
                                  0
                                  • S Schimi

                                    @mabbi mal ne schnelle Überlegung am Handy...

                                    was ist, wenn du den wert nimmst und durch 2 teilst und jeweils an die beiden 2400AC schickst....

                                    theoretisch sollten die sich gleich ent bzw. beladen

                                    M Offline
                                    M Offline
                                    Mabbi
                                    schrieb am zuletzt editiert von Mabbi
                                    #17

                                    @schimi
                                    45b59511-e8cb-4742-b1c8-daa897113185-grafik.png

                                    genau das mache ich auch.
                                    Aber wenn die Akkus nicht synchron sondern im Wechsel angesteuert werden, fängt der 'Regelkreis' manchmal an zu pumpen (nur bei Einspeisung), und das obwohl ich sowohl die Gesamtleistung plus/minus der Akkus heranziehe und auch die Einzelleistung pro Akku vergleiche und Loadbalancing zwischen den beiden mache, und das optional mit IST-Werten oder einem Delta über den Regelzeitraum der Akkus.

                                    Die Problematik ist wohl, dass bei niedrigem PV-Ertrag, wo die Akkus theoretisch zwischen Laden und Entladen schalten würden, je nachdem welcher Verbraucher sich gerade zu oder abschaltet, die ganze Regellung in eine Schwingung versetzt wird, aus der Sie nicht mehr rauskommt.
                                    Rein technisch habe ich die Regelung in .net mit simulierten Daten laufen lassen (ist halt viel einfacher zu debuggen), da bekomme ich ein relativ sauberes Verhalten, Schwingungen werden im Load-Balancing wieder eingefangen.
                                    Aber ich habe hier gerade einen klassischen 'Theorie und Realität' Fall.
                                    Im Augenblick habe ich die Trägheit der Steuerung im Verdacht, ich regel ja nur alle 15 Sekunden und genau an der PV-Ertrags Grenze können in dem Zeitraum auch in den Delta/Zeit Werten schon wilde Sachen passieren.
                                    Ich will das aber nicht mit einer übermässig grossen Totzone erschlagen.

                                    Wenn ich mit exakt der gleichen Steuerung auf die abwechselnde Ansterung der Akkus verzichte
                                    6023692f-9852-40f2-be4b-fe2407282368-grafik.png

                                    ist alle scool und ich kann ohne Totzone sauber regeln.

                                    Ich werde mal alle relevanten Regel-Daten in Datenpunkte schreiben, die Historie aktivieren und mir das dann per Diagramm anschauen... im zeilenweisen Textdebugging komme ich hier aktuell nicht mehr wirklich weiter.

                                    SMA Wechselrichter Probleme seit letztem Update

                                    S 1 Antwort Letzte Antwort
                                    0
                                    • M Mabbi

                                      @schimi
                                      45b59511-e8cb-4742-b1c8-daa897113185-grafik.png

                                      genau das mache ich auch.
                                      Aber wenn die Akkus nicht synchron sondern im Wechsel angesteuert werden, fängt der 'Regelkreis' manchmal an zu pumpen (nur bei Einspeisung), und das obwohl ich sowohl die Gesamtleistung plus/minus der Akkus heranziehe und auch die Einzelleistung pro Akku vergleiche und Loadbalancing zwischen den beiden mache, und das optional mit IST-Werten oder einem Delta über den Regelzeitraum der Akkus.

                                      Die Problematik ist wohl, dass bei niedrigem PV-Ertrag, wo die Akkus theoretisch zwischen Laden und Entladen schalten würden, je nachdem welcher Verbraucher sich gerade zu oder abschaltet, die ganze Regellung in eine Schwingung versetzt wird, aus der Sie nicht mehr rauskommt.
                                      Rein technisch habe ich die Regelung in .net mit simulierten Daten laufen lassen (ist halt viel einfacher zu debuggen), da bekomme ich ein relativ sauberes Verhalten, Schwingungen werden im Load-Balancing wieder eingefangen.
                                      Aber ich habe hier gerade einen klassischen 'Theorie und Realität' Fall.
                                      Im Augenblick habe ich die Trägheit der Steuerung im Verdacht, ich regel ja nur alle 15 Sekunden und genau an der PV-Ertrags Grenze können in dem Zeitraum auch in den Delta/Zeit Werten schon wilde Sachen passieren.
                                      Ich will das aber nicht mit einer übermässig grossen Totzone erschlagen.

                                      Wenn ich mit exakt der gleichen Steuerung auf die abwechselnde Ansterung der Akkus verzichte
                                      6023692f-9852-40f2-be4b-fe2407282368-grafik.png

                                      ist alle scool und ich kann ohne Totzone sauber regeln.

                                      Ich werde mal alle relevanten Regel-Daten in Datenpunkte schreiben, die Historie aktivieren und mir das dann per Diagramm anschauen... im zeilenweisen Textdebugging komme ich hier aktuell nicht mehr wirklich weiter.

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

                                      @mabbi was wäre wenn du entweder das regeln erst bei z.B. +-50 watt unterschied zum vorherigen wert erlaubst...

                                      oder im bereich bis -+200 Watt nur kleine sprünge erlaubst (z.B. 20 Watt)... würe die Regelung in dem Bereich langsam machen....

                                      hmm, sonst habe ich keine idee gerade..

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

                                        Hallo @Mabbi
                                        ich habe hier mal mitgelesen und einen ähnlichen Aufbau, allerdings mit 2 Hyper und habe das mit Java-Script gelöst.
                                        Die Akkus werden bei mir über die angeschlossenen PV-Panels und externer PV geladen. Ich steuere beide Hyper zusammen. Hier mal ein Auszug wie ich die Leistung ermittle. Bin aber in der Cloud.

                                        const hyper1 = {
                                            name:           'Hyper1',
                                            inputLimitDP:   'zendure-solarflow.0.xxxxxx.yyyyyyyy.control.setInputLimit',
                                            outputLimitDP:  'zendure-solarflow.0.xxxxxx.yyyyyyyy.control.setOutputLimit',
                                            inputDP:        'zendure-solarflow.0.xxxxxx.yyyyyyyy.gridInputPower',
                                            outputDP:       'zendure-solarflow.0.xxxxxx.yyyyyyyy.outputHomePower',
                                            acModeDP:       'zendure-solarflow.0.xxxxxx.yyyyyyyy.control.acMode',
                                            pvLeistungDP:   'zendure-solarflow.0.xxxxxx.yyyyyyyy.solarInputPower',
                                            chargePowerDP:  'zendure-solarflow.0.xxxxxx.yyyyyyyy.outputPackPower',
                                            socDP:          'zendure-solarflow.0.xxxxxx.yyyyyyyy.electricLevel',
                                            setsocDP:       'zendure-solarflow.0.xxxxxx.yyyyyyyy.control.dischargeLimit',
                                            ladenGesamtDP:  'zendure-solarflow.0.xxxxxx.yyyyyyyy.calculations.outputPackEnergyTodaykWh',
                                        };
                                        
                                        const hyper2 = {
                                            name:           'Hyper2',
                                            inputLimitDP:   'zendure-solarflow.0.xxxxxx.zzzzzzzz.control.setInputLimit',
                                            outputLimitDP:  'zendure-solarflow.0.xxxxxx.zzzzzzzz.control.setOutputLimit',
                                            inputDP:        'zendure-solarflow.0.xxxxxx.zzzzzzzz.gridInputPower',
                                            outputDP:       'zendure-solarflow.0.xxxxxx.zzzzzzzz.outputHomePower',
                                            acModeDP:       'zendure-solarflow.0.xxxxxx.zzzzzzzz.control.acMode',
                                            pvLeistungDP:   'zendure-solarflow.0.xxxxxx.zzzzzzzz.solarInputPower',
                                            chargePowerDP:  'zendure-solarflow.0.xxxxxx.zzzzzzzz.outputPackPower',
                                            socDP:          'zendure-solarflow.0.xxxxxx.zzzzzzzz.electricLevel',
                                            setsocDP:       'zendure-solarflow.0.xxxxxx.zzzzzzzz.control.dischargeLimit',
                                            ladenGesamtDP:  'zendure-solarflow.0.xxxxxx.zzzzzzzz.calculations.outputPackEnergyTodaykWh',
                                        };
                                        
                                        const evuLeistungDP         = 'mqtt.0.openWB.evu.W';
                                        const aktuelleEinspeisung   = Number(getStateVal(evuLeistungDP, 0));
                                        
                                        const hyper1Leistung = Number(getStateVal(hyper1.inputDP, 0)) - Number(getStateVal(hyper1.outputDP, 0));
                                        const hyper2Leistung = Number(getStateVal(hyper2.inputDP, 0)) - Number(getStateVal(hyper2.outputDP, 0));
                                        
                                        const leistungAusAkkus = aktuelleEinspeisung - hyper1Leistung - hyper2Leistung; // wie im Original
                                        
                                        // PV-Überschuss-Erkennung: negative leistungAusAkkus = Überschuss? (behalte dein Vorzeichenmodell)
                                        if (leistungAusAkkus < -ladenStartSchwelle && speicherMinSOC < zielSOC) {
                                             ladenErlaubt = true;                
                                             visLog('☀️ PV-Überschuss → Laden erlaubt', 'ok');
                                        }
                                        

                                        Kann gerne mal das gesamte Script posten, ist aber sehr an meine Bedingungen angepasst.

                                        1 Antwort Letzte Antwort
                                        0
                                        • U Offline
                                          U Offline
                                          Unterums
                                          schrieb am zuletzt editiert von
                                          #20

                                          Hallo,
                                          ich suche jemanden der mir (auch gerne gegen Bezahlung) hilft,

                                          einen Flow einzurichten in IO-Broker. Habe die Zendure Batterie mittels MQTT zum Luafen gebracht und würde nun gerne folgende Werte schreiben können:
                                          1.) Laden über AC Ein / Aus
                                          2.) Entladen über AC Ein / Aus
                                          3.) Die Leistung für 1 oder 2

                                          Ist hier jemand der Lust darauf hat ?

                                          1 Antwort Letzte Antwort
                                          0

                                          Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.

                                          Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.

                                          Mit deinem Input könnte dieser Beitrag noch besser werden 💗

                                          Registrieren Anmelden
                                          Antworten
                                          • In einem neuen Thema antworten
                                          Anmelden zum Antworten
                                          • Älteste zuerst
                                          • Neuste zuerst
                                          • Meiste Stimmen


                                          Support us

                                          ioBroker
                                          Community Adapters
                                          Donate

                                          525

                                          Online

                                          33.0k

                                          Benutzer

                                          83.3k

                                          Themen

                                          1.3m

                                          Beiträge
                                          Community
                                          Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                                          ioBroker Community 2014-2026
                                          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