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 zenSDK Lokal API, SmartMode, SolarFlow AC 800 Pro 2

NEWS

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

  • Verwendung von KI bitte immer deutlich kennzeichnen
    HomoranH
    Homoran
    10
    1
    573

  • Monatsrückblick Januar/Februar 2026 ist online!
    BluefoxB
    Bluefox
    18
    1
    1.1k

Zendure zenSDK Lokal API, SmartMode, SolarFlow AC 800 Pro 2

Geplant Angeheftet Gesperrt Verschoben JavaScript
274 Beiträge 12 Kommentatoren 21.2k Aufrufe 13 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.
  • maxclaudiM maxclaudi

    Zendure-Geräte Webserver: Steuerung über das zenSDK (HTTP)
    2026.05.02_00.39h; update 05.05.26 14:45h Error-sync.
    für das ioBroker-Forum
    In memory of Daisy 02.05.24 – miss you.

    Dieses Skript ermöglicht die lokale Steuerung eines Zendure-Geräts.
    Es funktioniert sofort und benötigt keinerlei Keys oder sonstige Authentifizierungen.
    Es ist mit APP und Cloud nutzbar.
    Oder auch nur lokal, wenn für Gerät(e) der Internetzugang gesperrt wird.
    Dann ist jedoch die Verwendung der App per WLAN nicht mehr möglich.
    Außerdem sollte eine Möglichkeit geschaffen werden, einen Zeitserver zu erreichen.
    Also nur die Zendure spezifischen URL sperren.
    Näher möchte ich hier nicht darauf eingehen.

    Voraussetzungen:

    • Hardware: Zendure-Geräte (Generation 2025/2026) mit integriertem Webserver.

    • Aktivierung: Vor der ersten Nutzung muss HEMS einmalig aktiviert, kurz abgewartet und anschließend wieder deaktiviert werden.

    • Netzwerk: Die IP-Adresse des Zendure-Geräts muss bekannt sein. Empfehle dringend, dem Gerät im Router oder Access Point eine dauerhafte, feste IP zuzuweisen.

    Wichtiger Hinweis zum Flash-Speicher:
    Ich bin kein Freund von automatischen Befehlsketten, die für den Nutzer nicht nachvollziehbar sind – vor allem, wenn sie zu unnötigen Schreibvorgängen im Flash-Speicher führen.

    Nach aktuellem Stand ist sicher: Wenn smartMode: 1 gesetzt ist, werden zumindest die Werte für outputLimit und inputLimit lediglich in den RAM geschrieben.

    Ein Modewechsel (z. B. acMode, Energiepläne, Automodi, MQTT-Konfiguration) wird hingegen fast immer in den Flash-Speicher geschrieben.
    Dabei wird oft nicht nur der einzelne Wert, sondern die gesamte Konfiguration dauerhaft gespeichert.


    Neue Datenpunkte unter "control"
    Im aktuellen Skript gibt es zusätzliche Datenpunkte sowie den Ordner automaticKonfig mit zwei Schaltern (Boolean):

    1. auto_inputLimitMode (Watt)
      Hier kann ein Wert in Watt für das Ladelimit (inputLimit) gesetzt werden.
      Das Skript prüft daraufhin automatisch:
    • Ist acMode: 1 und outputLimit: 0 W gesetzt?

    • Falls nicht, wird automatisch acMode: 1 und outputLimit: 0 gesetzt, bevor mit dem gewählten Wert vom Netz geladen wird.

    • Ist der Schalter input_output_LimitMode_smartMode_RAM aktiviert, wird vor dem Senden zusätzlich geprüft, ob smartMode: 1 aktiv ist.
      Wenn nicht, wird dieser automatisch mitgesendet.

    • Prinzip: Es wird nur gesendet, was zwingend nötig ist (Vorab-Prüfung).

    1. auto_outputLimitMode (Watt)
      Dies ist das Gegenstück zum auto_inputLimitMode für die Entladesteuerung.

    2. smartModeWatcher (Schalter)
      Wenn dieser Slider aktiviert ist (true), wird bei jedem empfangenen Report automatisch geprüft, ob smartMode: 1 gesetzt ist.
      Sollte der Wert auf 0 stehen, setzt das Skript ihn automatisch wieder auf 1. Dieser Slider kann manuell oder per Skript (de-)aktiviert werden.

    Weitere Hinweise

    • inverseMaxPower: Dieser Wert sollte nicht zur laufenden Regelung verwendet werden. Er definiert die Obergrenze, die der Wechselrichter maximal ausgeben darf, und wird sehr wahrscheinlich in den Flash geschrieben.

    • chargeMaxLimit:
      Mit Vorsicht zu genießen. Diesen Wert am besten nicht für regelmäßige Limit-Anpassungen verwenden.

    • Verbraucher-Geräte:
      Vorsicht bei Consumer-Produkten (auch Zendure); hier wird oft bei jeder Parameteränderung die komplette Konfiguration in den Flash geschrieben.

    • mDNS: Funktioniert zwar, hat sich in Tests jedoch als unzuverlässig erwiesen. Eine feste IP-Adresse ist die bessere Wahl.

    • Struktur-Update: Die Verzeichnisse befinden sich nun direkt im Hauptordner:

    • control (inkl. automaticKonfig)

    • packData (unter Cxxxxx finden sich die jeweiligen Batteriedaten)

    • properties

    maxclaudiM Offline
    maxclaudiM Offline
    maxclaudi
    schrieb am zuletzt editiert von maxclaudi
    #258

    NEU
    Zendure-Geräte Webserver: Steuerung über das zenSDK (HTTP)
    2026.05.02_00.39h für das ioBroker-Forum; update 05.05.26 14:45h Error-sync.
    In memory of Daisy 02.05.24 – miss you.

    Viel Spaß beim Ausprobieren!


    // ioBroker JavaScript: Zendure zenSDK Adapter-Ersatz für ein Zendure-Gerät.
    // Für alle Geräte ab 2025/2026, die zenSDK unterstützen, wie z. B.:
    // SF800, SF800 PLUS, 800Pro (2), 1600AC, SF2400AC(+) usw.
    // (c) maxclaudi 2026.05.02_00.39h für das ioBroker-Forum; update 05.05.26 14:45h Error-sync.
    // In memory of Daisy 02.05.24 – miss you.
    // 
    // EIN WORT IN EIGENER SACHE:
    // Dieses Skript basiert auf vielen Stunden Hardware-Tests und Analysen (besonders zum Flash-Schutz).
    // In der Vergangenheit wurden meine Erkenntnisse oft ohne Erwähnung in andere Projekte übernommen.
    // Ich teile diesen Code gerne. Wer diese Logik in öffentliche Adapter integriert, ist herzlich 
    // eingeladen – ich bitte jedoch um die Fairness, die Quelle zu nennen. 
    // Das ist der "Lohn" für meine Zeit und Forschung.
    //----------------------------------------------------------------------------------------------------
    // Konfiguration
    //
    // Hinweis bei mehreren Zendure-Geräten:
    //   -> Für jedes Gerät ein eigenes Skript mit individueller Konfiguration verwenden!
    //   -> IP-Adresse anpassen.
    //   -> maxInputLimit / maxOutputLimit (abhängig vom Gerätetyp) einstellen.
    //   -> Intervall: getIntervalMs = 5000 ms (Standard). 
    //      Bei Verbindungsproblemen oder WLAN-Lags wird ein Wert von mindestens 8000 ms empfohlen.
    //
    // Empfehlung zur Geräteanzahl:
    //   • Bis zu 3 Geräte: völlig unkritisch.
    //   • 4 Geräte: problemlos möglich.
    //   • Mehr als 4 Geräte: nicht empfohlen.
    //
    // Das offizielle MQTT ist mit einer Aktualisierungsrate von bis zu 90 Sek. zu langsam.
    // Ich empfehle die Nutzung des zenSDK.
    // Hinweis: Das (De-)Aktivieren von MQTT sowie die MQTT-Verbindungsüberwachung werden nicht unterstützt.
    //
    // Damit neue Datenpunkte automatisch aus dem JSON erstellt werden können:
    // -> Instanzen -> JavaScript-Adapter -> Allgemeine Einstellungen -> "Kommando 'setObject' erlauben" aktivieren!
    //
    // Bei kurzem Abfrageintervall:
    // -> Instanzen -> JavaScript-Adapter -> Allgemeine Einstellungen -> 
    //    "Maximale setState-Anfragen pro Minute pro Skript" auf 5000 erhöhen.
    //    (Testweise reicht ein geringerer Wert wie 2000; das Skript wurde optimiert).
    //
    // Datenpunkte im Ordner "control" werden automatisch aktualisiert.
    //----------------------------------------------------------------------------------------------------
    //----------------------------------------------------------------------------------------------------
    // CONFIG
    //----------------------------------------------------------------------------------------------------
     
    // IP Zendure Gerät
    const IP = "192.168.40.20";              // IP des Zendure Geräts
    // Bitte sicherstelen:
    // richtige IP verwendet?
    // Im Router dem Zendure-Gerät eine feste, dauerhafte IP zuweisen!
     
    //maximum inputLimit -> Maximal mögliche (unterstützte) Ladeleistung des Zendure-Geräts 
    //Beispiele: SF800 800W / SF800 PRO: 1000W / 1600AC: 1600 / SF2400AC: 2400W
    const maxInputLimit = 1600;
     
    //maximum outputLimit -> Maximal möglich (unterstützte) Entladeleistung des Zendure-Geräts
    //Beipiele: SF800 800W / SF800 PRO: 800W / 1600AC: 1600 / SF2400AC: 2400W
    const maxOutputLimit = 1600;
     
    // Haupt-Verzeichnis für das Zendure-Gerät
    // Der Name für das Hauptverzeichnis kann frei gewählt werden.
    // Muss aber bei mehreren scripten und Zendure-Geräte unterschiedlich sein.
    // Beispiel für "1600plus_01":
    // const folderZendureApi = '0_userdata.0.zendure.' + "1600ACplus_01";
    const folderZendureApi = '0_userdata.0.zendure.' + "zenSDK_01";
     
    // Timout Handler HTTP GET /POST
    // timeoutHttp MUSS kleiner sein als getIntervalMs!! Besser nicht ändern. Standard: 2000ms
    const postTimeoutMs = 2000; // Empfehlung: 2000 ms, am besten nicht ändern.
     
    // GET intervall
    const getIntervalMs = 5000; // Intervall für GET 5000ms. Bei Problemen erhöhen. NICHT < 5000!
     
    //----------------------------------------------------------------------------------------------------
    // END CONFIG
    //----------------------------------------------------------------------------------------------------
     
    let rxNewRam = "";
    let rxOldRam = "";
    let rxDiffRam = "";
    let txRam = "";
     
    const dpSmartRAM = folderZendureApi + ".control.automaticKonfig.input_output_LimitMode_smartMode_RAM";
    const dpSmartWatcher = folderZendureApi + ".control.automaticKonfig.smartModeWatcher";
     
    // helper
    function getRxNew() { return rxNewRam; }
    function setRxNew(val) {
        rxNewRam = val;
    }
     
    function getRxOld() { return rxOldRam; }
    function setRxOld(val) {
        rxOldRam = val;
    }
     
    function getRxDiff() { return rxDiffRam; }
    function setRxDiff(val) {
        rxDiffRam = val;
    }
     
    function setTx(val) {
        txRam = val;
    }
     
    const postCooldownMs = 2000;
    const batchWindowMs = 300;
     
    let postActive = false;
    let postQueue = {};
    let batchTimer = null;
    let getTimer = null;
     
    let retryCount = 0; 
    const maxRetries = 2; 
    let getErrorCount = 0; 
    let SN = '';
     
    // Struktur sicherstellen
    setTimeout(() => {
        ensureStructure();
    }, 100);
     
    function ensureStructure() {
        const folders = [
            folderZendureApi,
            folderZendureApi + ".properties",
            folderZendureApi + ".packData",
            folderZendureApi + ".control",
            folderZendureApi + ".control.automaticKonfig"
        ];
        folders.forEach(path => {
            if (!existsObject(path)) {
                setObject(path, {
                    type: "folder",
                    common: { name: path.split(".").pop() },
                    native: {}
                });
            }
        });
        if (!existsState(dpSmartRAM)) {
            createState(dpSmartRAM, false, { name: "Input/Output Limit Mode SmartMode RAM", type: "boolean", role: "switch", read: true, write: true });
        }
        if (!existsState(dpSmartWatcher)) {
            createState(dpSmartWatcher, false, { name: "Smart Mode Watcher", type: "boolean", role: "switch", read: true, write: true });
        }
    	if (existsState(dpSmartRAM)) setState(dpSmartRAM, false, true);
    	if (existsState(dpSmartWatcher)) setState(dpSmartWatcher, false, true);
    }
     
    // Control
    const schemaControl = {
    	acMode: {
            name: "set acMode; 1: input charge, 2: output discharge",
            role: "level",
            type: "number",
            write: true,
            min: 1,
            max: 2,
            apiKey: "acMode",
        },
    	
    	chargeMaxLimit: {
            name: "set Charge Max Limit",
            unit: "W",
            role: "level",
            type: "number",
            write: true,
            min: 0,
            max: maxInputLimit,
            apiKey: "chargeMaxLimit",
        },
    	
    	gridOffMode: {
            name: "set gridOffMode; 0: Standard Mode, 1: Economic Mode, 2: Closure",
            role: "level",
            type: "number",
            write: true,
            min: 0,
            max: 2,
            apiKey: "gridOffMode",
        },
    	
    	gridReverse: {
            name: "set gridReverse; 0: Disabled, 1: Allowed reverse flow, 2: Forbidden reverse flow",
            role: "level",
            type: "number",
            write: true,
            min: 0,
            max: 2,
            apiKey: "gridReverse",
        },
    	
    	gridStandard: {
            name: "set gridStandard; 0: Germany 1: France 2: Austria 3: Switzerland 4: Netherlands 5: Spain 6: Belgium 7: Greece 8: Denmark 9: Italy",
            role: "level",
            type: "number",
            write: true,
            min: 0,
            max: 9,
            apiKey: "gridStandard",
        },
    	
    	inputLimit: {
            name: "set inputLimit",
            unit: "W",
            role: "level",
            type: "number",
            write: true,
            min: 0,
            max: maxInputLimit,
            apiKey: "inputLimit",
        },
    	
    	inverseMaxPower: {
            name: "set inverseMaxPower; Max inverter output Power Limit",
            unit: "W",
            role: "level",
            type: "number",
            write: true,
            min: 600,
            max: maxOutputLimit,
            apiKey: "inverseMaxPower",
        },
    	
    	minSoc: {
            name: "set minSoc; minimum SOC 0-50",
            unit: "%",
            role: "level",
            type: "number",
            write: true,
            min: 0,
            max: 50,
            apiKey: "minSoc",
            transformWrite: (val) => val * 10
        },
    	
    	outputLimit: {
            name: "set outputLimit",
            unit: "W",
            role: "level",
            type: "number",
            write: true,
            min: 0,
            max: maxOutputLimit,
            apiKey: "outputLimit",
        },
    	
    	smartMode: {
            name: "set smartMode; parameter are written to: 1: RAM / 0: Flash",
            role: "level",
            type: "number",
            write: true,
            min: 0,
            max: 1,
            apiKey: "smartMode",
        },
    	
    	socSet: {
            name: "set socSet; SOC Target 70%-100%",
            unit: "%",
            role: "level",
            type: "number",
            write: true,
            min: 70,
            max: 100,
            apiKey: "socSet",
            transformWrite: (val) => val * 10
        },
    	
    	lampSwitch: {
            name: "set lamp switch; 0: lamp off / 1: lamp on",
            role: "level",
            type: "number",
            write: true,
            min: 0,
            max: 1,
            apiKey: "lampSwitch",
        }
    };
     
    createControlStates();
     
    function createControlStates() {
        const base = folderZendureApi + ".control";
        Object.keys(schemaControl).forEach(key => {
            const def = schemaControl[key];
            const dp = `${base}.${key}`;
            if (!existsState(dp)) {
                createState(dp, 0, {
                    name: def.name, type: def.type, role: def.role, unit: def.unit,
                    read: true, write: true, min: def.min, max: def.max
                });
            }
        });
    		
    	const extraStates = [
    		{ id: `${base}.auto_inputLimitMode`, name: "Set InputLimit (Automatic)", unit: "W", min: 0, max: maxInputLimit },
    		{ id: `${base}.auto_outputLimitMode`, name: "Set OutputLimit (Automatic)", unit: "W", min: 0, max: maxOutputLimit }
    	];
     
    	extraStates.forEach(def => {
    		if (!existsState(def.id)) {
    			createState(def.id, 0, {
    				name: def.name, type: "number", role: "level", unit: def.unit,
    				read: true, write: true, min: def.min, max: def.max
    			});
    		}
    	});
    }
     
    on({ id: new RegExp(`^${folderZendureApi}\\.control\\.`), change: "ne" }, obj => {
        if (obj.state.ack) return; 
        const key = obj.id.split('.').pop();
    	
        if (key === "auto_inputLimitMode") {
            handleInputLimitMode(obj.state.val);
            setState(obj.id, obj.state.val, true);
            return;
        }
     
        if (key === "auto_outputLimitMode") {
            handleOutputLimitMode(obj.state.val);
            setState(obj.id, obj.state.val, true);
            return;
        }
    	
        const def = schemaControl[key];
        if (!def) return;
     
        let val = obj.state.val;
        if (def.min !== undefined && val < def.min) val = def.min;
        if (def.max !== undefined && val > def.max) val = def.max;
        if (def.transformWrite) val = def.transformWrite(val);
     
        sendToDevice(def.apiKey, val);
        setState(obj.id, obj.state.val, true);
    });
     
    function sendToDevice(apiKey, value) {
    	if (!SN || SN === "null" || SN === "") return;
        const payload = { sn: SN, properties: {} };
        payload.properties[apiKey] = value;
        setTx(JSON.stringify(payload));
        queuePost({ [apiKey]: value });
    }
     
    function queuePost(obj) {
        Object.assign(postQueue, obj);
        if (batchTimer) return;
        batchTimer = setTimeout(() => {
            batchTimer = null;
            executePost();
        }, batchWindowMs);
    }
     
    function executePost() {
        if (postActive) return;
        if (Object.keys(postQueue).length === 0) return;
        postActive = true;
        stopGetLoop();
        const payload = { sn: SN, properties: { ...postQueue } };
        postQueue = {};
        const json = JSON.stringify(payload);
        setTx(json); 
        httpPost(`http://${IP}/properties/write`, json, { timeout: postTimeoutMs, responseType: "text" }, (err, response) => {
            let success = false;
            if (!err && response && response.statusCode === 200) {
                try {
                    const data = JSON.parse(response.data);
                    success = data.success === true && data.code === 200;
                } catch (e) { success = false; }
            }
            if (!success) {
                retryCount++;
                if (retryCount <= maxRetries) {
                    log(`POST Retry ${retryCount}/${maxRetries}`, "warn");
                    setTimeout(() => {
                        postQueue = { ...payload.properties };
                        postActive = false;
                        executePost();
                    }, postCooldownMs);
                    return;
                }
                log("POST endgültig fehlgeschlagen", "error");
                retryCount = 0;
                finishPost(false);
                return;
            }
            retryCount = 0;
            finishPost(true);
        });
    }
    
    function finishPost(success) {
        if (!success) { syncControlFromProperties(); }
    	setTimeout(() => { postActive = false; startGetLoop(); }, postCooldownMs);
    	}
    
    function syncControlFromProperties() {
        const baseCtrl = folderZendureApi + ".control";
        const baseProp = folderZendureApi + ".properties";
    
        Object.keys(schemaControl).forEach(ctrlKey => {
            const def = schemaControl[ctrlKey];
            const apiKey = def.apiKey || ctrlKey;
    
            const propState = getState(`${baseProp}.${apiKey}`);
            if (!propState || propState.val === undefined) return;
    
            const ctrlId = `${baseCtrl}.${ctrlKey}`;
    		const ctrlState = getState(ctrlId);
    		const ctrlVal = ctrlState ? ctrlState.val : undefined;
            const propVal = propState.val;
    
            if (ctrlVal !== propVal) {
                setState(ctrlId, propVal, true);
            }
        });
    
        const inputProp = getState(`${baseProp}.inputLimit`)?.val;
        if (inputProp !== undefined) {
            const id = `${baseCtrl}.auto_inputLimitMode`;
            if (getState(id)?.val !== inputProp) {
                setState(id, inputProp, true);
            }
        }
    
        const outputProp = getState(`${baseProp}.outputLimit`)?.val;
        if (outputProp !== undefined) {
            const id = `${baseCtrl}.auto_outputLimitMode`;
            if (getState(id)?.val !== outputProp) {
                setState(id, outputProp, true);
            }
        }
    }
    
    function startGetLoop() {
        if (getTimer) return;
        getTimer = setInterval(() => {
            if (postActive) return;
            const url = `http://${IP}/properties/report`;
            httpGet(url, { timeout: 2000, responseType: 'text' }, (err, response) => {
                if (err || !response || response.statusCode !== 200) {
                    getErrorCount++;
                    if (getErrorCount <= 3) log(`GET Fehler (${getErrorCount}): ${err || response?.statusCode}`, "warn");
                    if (getErrorCount === 4) log("Keine Verbindung möglich. Zendure-Geräte IP prüfen!", "error");
                    return;
                }
                if (getErrorCount > 0) log("Verbindung wieder OK", "info");
                getErrorCount = 0;
                if (!response.data) return;
                setRxNew(response.data);
                handleRxNewUpdate(response.data);
            });
        }, getIntervalMs);
    }
     
    function stopGetLoop() { if (getTimer) { clearInterval(getTimer); getTimer = null; } }
     
    function handleInputLimitMode(val) {
        const currentAcMode = getState(folderZendureApi + ".properties.acMode")?.val;
        const currentOutput = getState(folderZendureApi + ".properties.outputLimit")?.val;
    	const currentSmartMode = getState(folderZendureApi + ".properties.smartMode")?.val;
        const smartRAMActive = getState(dpSmartRAM)?.val;
     
        const payload = {};
        if (currentAcMode !== 1) payload.acMode = 1;
        if (currentOutput !== 0) payload.outputLimit = 0;
    	if (smartRAMActive === true && currentSmartMode !== 1) payload.smartMode = 1;
        
        payload.inputLimit = val;
        sendBatch(payload);
    }
     
    function handleOutputLimitMode(val) {
        const currentAcMode = getState(folderZendureApi + ".properties.acMode")?.val;
        const currentInput = getState(folderZendureApi + ".properties.inputLimit")?.val;
    	const currentSmartMode = getState(folderZendureApi + ".properties.smartMode")?.val;
        const smartRAMActive = getState(dpSmartRAM)?.val;
     
        const payload = {};
        if (currentAcMode !== 2) payload.acMode = 2;
        if (currentInput !== 0) payload.inputLimit = 0;
    	if (smartRAMActive === true && currentSmartMode !== 1) payload.smartMode = 1;
     
        payload.outputLimit = val;
        sendBatch(payload);
    }
     
    function sendBatch(properties) {
        if (!SN || SN === "null" || SN === "") return;
        const payload = { sn: SN, properties: properties };
        setTx(JSON.stringify(payload)); 
    	queuePost(properties); 
    }
     
    const schema = {
        properties: {
            BatVolt: {
                name: "Battery Voltage",
                unit: "V",
                role: "value.voltage",
                type: "number"
            },
    		Fanmode: {
                name: "Fanmode - 0: Fan off, 1: Fan on. / Unauthorized modification is not recommended.",
                role: "value",
                type: "number"
            },	
    		Fanspeed: {
                name: "Fanspeed - 0: Auto, 1: 1st gear, 2: 2nd gear / Unauthorized modification is not recommended.",
                role: "value",
                type: "number"
            },
    		acMode: {
                name: "acMode - 1: input charge / 2: output discharge",
                role: "value",
                type: "number"
            },
    		acStatus: {
                name: "AC state 0-2",
                role: "value",
                type: "number"
            },
    		batCalTime: {
                name: "batCalTime - Battery Calibration Time. Unauthorized modifications are not recommended",
    			unit: "min",
                role: "value",
                type: "number"
            },
    		chargeMaxLimit: {
                name: "chargeMaxLimit - Max charge power",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		dataReady: {
                name: "Data ready flag - 0: Data not ready, 1: Data ready",
                role: "value",
                type: "number"
            },
    		dcStatus: {
                name: "dcStatus - DC State 0-2",
                role: "value",
                type: "number"
            },
    		electricLevel: {
                name: "electricLevel - Average SOC of all Batterys",
                unit: "%",
                role: "value.battery",
                type: "number"
            },
    		gridInputPower: {
                name: "gridInputPower - Grid Input Power to Battery",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		gridOffMode: {
                name: "gridOffMode - 0: Standard Mode, 1: Economic Mode, 2: Closure",
                role: "value",
                type: "number"
            },
    		gridOffPower: {
                name: "gridOffPower - Off-grid power",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		gridReverse: {
                name: "gridReverse - 0: Disabled, 1: Allowed reverse flow, 2: Forbidden reverse flow",
                role: "value",
                type: "number"
            },
    		gridStandard: {
                name: "gridStandard - 0: Germany 1: France 2: Austria 3: Switzerland 4: Netherlands 5: Spain 6: Belgium 7: Greece 8: Denmark 9: Italy",
                role: "value",
                type: "number"
            },
    		gridState: {
                name: "gridState - Grid connection state, 0: Not connected, 1: Connected",
                role: "value",
                type: "number"
            },
    		heatState: {
                name: "heatState - Battery Heat State, 0: Not heating, 1: heating",
                role: "value",
                type: "number"
            },
    		hyperTmp: {
                name: "Temperature",
                unit: "°C",
                role: "value.temperature",
                type: "number"
            },
    		inputLimit: {
                name: "inputLimit - AC charging power limit to Battery",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		inverseMaxPower: {
                name: "inverseMaxPower - Max inverter output Power Limit",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		lampSwitch: {
                name: "lampSwitch - Lamp state, 0: lamp off / 1: lamp on",
                role: "value",
                type: "number"
            },
    		minSoc: {
                name: "minSoc - Minimum SOC 0%-50%",
                unit: "%",
                role: "value",
                type: "number"
            },
    		outputHomePower: {
                name: "outputHomePower - Output to home",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		outputLimit: {
                name: "outputLimit - Output power limit",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		outputPackPower: {
                name: "outputPackPower - Battery charge power",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		packInputPower: {
                name: "packInputPower - Battery discharge power",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		packState: {
                name: "packState - Battery State, 0: Standby, 1: Charging, 2: Discharging",
                role: "value",
                type: "number"
            },
    		pass: {
                name: "pass - 0: Bypass Automatic, 1: Bypass OFF, 2: Bypass ON",
                role: "value",
                type: "number"
            },
    		pvStatus: {
                name: "pvStatus - PV State producing, 0: Stopped, 1: Running",
                role: "value",
                type: "number"
            },
    		remainOutTime: {
                name: "remainOutTime - Estimated discharge time in minutes, if not predictable: 59940",
    			unit: "min",
                role: "value",
                type: "number"
            },
    		reverseState: {
                name: "reverseState - Reverse flow, 0: No, 1: Reverse flow",
                role: "value",
                type: "number"
            },
    		smartMode: {
                name: "smartMode - 1: parameter are written to RAM / 0: parameter are written to flash",
                role: "value",
                type: "number"
            },
    		socLimit: {
                name: "socLimit - 0: normal, 1: Charge limit reached, 2: Discharge limit reached",
                role: "value",
                type: "number"
            },		
            socSet: {
                name: "socSet - SOC Target 70%-100%",
                unit: "%",
                role: "value",
                type: "number"
            },
    		socStatus: {
                name: "socStatus - SOC calibration Info, 0: No Calibrating / 1: Calibrating",
                role: "value",
                type: "number"
            },
    		solarInputPower: {
                name: "solarInputPower - Total Solar Input Power",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		solarPower1: {
                name: "solarPower1 - Solar line 1 input power",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		solarPower2: {
                name: "solarPower2 - Solar line 2 input power",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		solarPower3: {
                name: "solarPower3 - Solar line 3 input power",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		solarPower4: {
                name: "solarPower4 - Solar line 4 input power",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		solarPower5: {
                name: "solarPower5 - Solar line 5 input power",
    			unit: "W",
                role: "value.power",
                type: "number"
            },
    		solarPower6: {
                name: "solarPower6 - Solar line 6 input power",
    			unit: "W",
                role: "value.power",
                type: "number"
            }
        },
        packData: {
            batcur: {
                name: "Battery Current flow - negativ: discharge / positiv: charge",
                unit: "A",
                role: "value.current",
                type: "number"
            },
    		maxTemp: {
                name: "Battery - max. Stored temperature value",
                unit: "°C",
                role: "value.temperature",
                type: "number"
            },
    		maxVol: {
                name: "maxVol - Max cell voltage",
                unit: "V",
                role: "value.voltage",
                type: "number"
            },
    		minVol: {
                name: "minVol - Min cell voltage",
                unit: "V",
                role: "value.voltage",
                type: "number"
            },
    		packType: {
                name: "Pack Type",
                role: "indicator",
                type: "number"
            },
    		power: {
                name: "Battery pack power",
                unit: "W",
                role: "value.power",
                type: "number"
            },
    		sn: {
                name: "Battery pack serial number",
                role: "indicator",
                type: "string"
            },
    		socLevel: {
                name: "socLevel - State of charge in Percent",
    			unit: "%",
                role: "value.battery",
                type: "number"
            },
    		state: {
    			name: "Battery state, 0: Standby, 1: Charging, 2: Discharging",
                role: "indicator",
                type: "number"
            },
    		totalVol: {
    			name: "Battery Total Voltage",
                unit: "V",
                role: "value.voltage",
                type: "number"
            }
        }
    };
     
    function formatTime(unix) {
        if (unix === undefined || unix === null) return "";
        const d = new Date(unix * 1000);
        const timeOptions = { timeZone: "Europe/Berlin", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false };
        const dateOptions = { timeZone: "Europe/Berlin", day: "2-digit", month: "2-digit", year: "2-digit" };
        return `${d.toLocaleTimeString("de-DE", timeOptions)}, ${d.toLocaleDateString("de-DE", dateOptions)}`;
    }
     
    function getBatteryType(sn, model) {
        let batType = "";
        if (!sn || typeof sn !== "string") return "unknown";
        if (sn.startsWith("A")) {
            batType = "AB1000";
        } 
        else if (sn.startsWith("B")) {
            batType = "AB1000S";
        } 
        else if (sn.startsWith("C")) {
    		const sub = sn.length > 3 ? sn[3] : "";
            if (sub === "F") batType = "AB2000S";
            else if (sub === "E") batType = "AB2000X";
            else if (sub === "A") batType = "AB2000X internal";
            else batType = "AB2000";
        } 
        else if (sn.startsWith("F")) {
            batType = "AB3000X";
        }
        if (model && typeof model === "string" && model.trim()) {
            batType = model.trim();
        }
        return batType || "unknown";
    }
     
    function writeMainKeys(json) {
        const keys = ["timestamp", "messageId", "sn", "version", "product"];
        keys.forEach(key => {
            if (json[key] === undefined) return;
    		if (key === "sn" && SN === '') { SN = json[key]; }
            const dp = `${folderZendureApi}.${key}`;
            if (existsState(dp)) {
                if (getState(dp).val !== json[key]) { setState(dp, json[key], true); }
            } else {
                createState(dp, json[key], { name: key, type: typeof json[key], role: "info", read: true, write: false });
            }
        });
    }
     
    function writeProperties(obj) {
        if (!obj) return;
        const base = folderZendureApi + ".properties";
        Object.keys(obj).forEach(key => {
            const dp = `${base}.${key}`;
            const val = obj[key];
            const meta = schema.properties?.[key] || {};
            if (existsState(dp)) {
                if (getState(dp).val !== val) { setState(dp, val, true); }
            } else {
                createState(dp, val, { name: meta.name || key, type: meta.type || typeof val, role: meta.role || "value", unit: meta.unit, read: true, write: false });
            }
        });
    }
     
    function writePackData(arr) {
        if (!Array.isArray(arr)) return;
        const base = folderZendureApi + ".packData";
        arr.forEach(pack => {
            if (!pack.sn) return;
            const folder = `${base}.${pack.sn}`;
            if (!existsObject(folder)) {
                setObject(folder, { type: "folder", common: { name: pack.sn }, native: {} });
            }
            Object.keys(pack).forEach(key => {
                const dp = `${folder}.${key}`;
                const val = pack[key];
    			const meta = schema.packData?.[key] || {};
                if (existsState(dp)) {
                    if (getState(dp).val !== val) { setState(dp, val, true); }
                } else {
                    createState(dp, val, { name: meta.name || key, type: meta.type || typeof val, role: meta.role || "value", unit: meta.unit, read: true, write: false });
                }
            });
        });
    }
     
    function buildDiff(oldJson, newJson) {
        if (!oldJson || !newJson) return null;
        const diff = {};
        const mainKeys = ["timestamp", "messageId", "sn", "version", "product"];
    	mainKeys.forEach(key => {
            if (newJson[key] !== undefined && newJson[key] !== oldJson[key]) { diff[key] = newJson[key]; }
        });
        if (newJson.properties) {
            const propDiff = {};
            Object.keys(newJson.properties).forEach(key => {
                if (key === "rssi") return;
                const newVal = newJson.properties[key];
                const oldVal = oldJson.properties ? oldJson.properties[key] : undefined;
                if (newVal !== oldVal) { propDiff[key] = newVal; }
            });
            if (Object.keys(propDiff).length > 0) { diff.properties = propDiff; }
        }
        if (Array.isArray(newJson.packData)) {
            const packDiff = [];
            const oldMap = {};
            if (Array.isArray(oldJson.packData)) {
                oldJson.packData.forEach(p => { if (p.sn) oldMap[p.sn] = p; });
            }
            newJson.packData.forEach(newPack => {
                if (!newPack.sn) return;
                const oldPack = oldMap[newPack.sn];
                const singleDiff = { sn: newPack.sn };
                let changed = false;
                Object.keys(newPack).forEach(key => {
                    if (key === "sn") return;
                    const newVal = newPack[key];
                    const oldVal = oldPack ? oldPack[key] : undefined;
                    if (newVal !== oldVal) { singleDiff[key] = newVal; changed = true; }
                });
                if (changed) { packDiff.push(singleDiff); }
            });
            if (packDiff.length > 0) { diff.packData = packDiff; }
        }
        return Object.keys(diff).length === 0 ? null : diff;
    }
     
    function handleRxNewUpdate(val) {
        if (!val) return;
        let newJson;
        try { newJson = JSON.parse(val); } catch (e) { log("rxNew parse error: " + e, "warn"); return; }
        const smartModeWatcher = getState(dpSmartWatcher)?.val;
        if (smartModeWatcher === true && newJson.properties && newJson.properties.smartMode === 0) {
            //log("SmartModeWatcher: smartMode ist 0! Erzwinge RAM-Modus (1)...", "info");
            queuePost({ smartMode: 1 });
        }
        const oldStr = getRxOld(); 
        if (!oldStr || oldStr === "null" || oldStr === "") {
    		let full = transformJson(JSON.parse(JSON.stringify(newJson)));
    		writeMainKeys(full);
    		if (full.properties) {
    			writeProperties(full.properties);
    			const base = folderZendureApi + ".control";
                Object.keys(schemaControl).forEach(ctrlKey => {
                    const def = schemaControl[ctrlKey];
                    const apiKey = def.apiKey || ctrlKey;
                    if (full.properties[apiKey] !== undefined) { setState(`${base}.${ctrlKey}`, full.properties[apiKey], true); }
                });
                if (full.properties.inputLimit !== undefined) { setState(`${base}.auto_inputLimitMode`, full.properties.inputLimit, true); }
                if (full.properties.outputLimit !== undefined) { setState(`${base}.auto_outputLimitMode`, full.properties.outputLimit, true); }	
    		} 
    		if (full.packData) { writePackData(full.packData); }
    		setRxOld(val); setRxDiff(""); return;
    	}
        let oldJson;
        try { oldJson = JSON.parse(oldStr); } catch (e) { setRxOld(val); return; }
    	const isNewer = (newJson.timestamp && oldJson.timestamp && newJson.timestamp > oldJson.timestamp) || (!oldJson.timestamp && newJson.timestamp);
        if (!isNewer) return;
        const diff = buildDiff(oldJson, newJson);
        if (diff && Object.keys(diff).length > 0) {
            const diffStr = JSON.stringify(diff);
            setRxDiff(diffStr); handleRxDiffUpdate(diffStr); 
        } else { setRxDiff(""); }
        setRxOld(val);
    }
     
    function handleRxDiffUpdate(val) {
        if (!val) return;
        let diff;
        try { diff = JSON.parse(val); } catch (e) { log("rxDiff parse error: " + e, "warn"); return; }
        diff = transformJson(diff);
    	if (diff.properties) { syncControlFromDiff(diff); }							   
        writeMainKeys(diff);
        if (diff.properties) { writeProperties(diff.properties); }
        if (diff.packData) { writePackData(diff.packData); }
    }
     
    function syncControlFromDiff(diff) {
        if (!diff || !diff.properties) return;
        const base = folderZendureApi + ".control";
    	if (diff.properties.inputLimit !== undefined) { setState(`${base}.auto_inputLimitMode`, diff.properties.inputLimit, true); }
    	if (diff.properties.outputLimit !== undefined) { setState(`${base}.auto_outputLimitMode`, diff.properties.outputLimit, true); }	
    	Object.keys(schemaControl).forEach(ctrlKey => {
    		const def = schemaControl[ctrlKey];
    		const apiKey = def.apiKey || ctrlKey;
    		if (diff.properties[apiKey] !== undefined) { setState(`${base}.${ctrlKey}`, diff.properties[apiKey], true); }
    	}); 
    }
     
    function createOrSet(id, val, common) {
        if (existsState(id)) {
            if (getState(id).val !== val) { setState(id, val, true); }
        } else { createState(id, val, { read: true, write: false, ...common }); }
    }
     
    function transformJson(obj) {
        if (!obj) return obj;
        if (obj.timestamp !== undefined && obj.timestamp !== null) {
            const formatted = formatTime(obj.timestamp);
            createOrSet(`${folderZendureApi}.timeUpdateTimestamp`, formatted, { type: "string", role: "text", name: "TimeUpdate (timestamp)" });
        }
        if (obj.properties) {
            Object.keys(obj.properties).forEach(key => {
                let val = obj.properties[key];
                switch (key) {
    				case "BatVolt": val = val / 100; break;
    				case "acCouplingState": {
    					let states = [];
    					if (val & (1 << 0)) states.push("AC-coupled input present");
    					if (val & (1 << 1)) states.push("AC input present flag");
    					if (val & (1 << 2)) states.push("AC-coupled overload");
    					if (val & (1 << 3)) states.push("Excess AC input power");
    					const statusText = states.length > 0 ? states.join(", ") : "Normal / No Flags";
    					createOrSet(`${folderZendureApi}.properties.acCouplingState_String`, statusText, { type: "string", role: "text", name: "AC Coupling State (Text)" });
    				} break;
    				case "hyperTmp": val = (val - 2731) / 10.0; break;
                    case "minSoc":
    				case "socSet": val = val / 10; break;
                    case "ts":
    					if (val !== undefined && val !== null) {
    						const formatted = formatTime(val);
    						createOrSet(`${folderZendureApi}.properties.timeUpdateTs`, formatted, { type: "string", role: "text", name: "TimeUpdate (ts)" });
    					}
    					break;
                }
                obj.properties[key] = val;
            });
        }
        if (Array.isArray(obj.packData)) {
    		obj.packData.forEach(pack => {
    			if (pack.sn) {
    				const batType = getBatteryType(pack.sn, pack.model);
    				createOrSet(`${folderZendureApi}.packData.${pack.sn}.model`, batType, { type: "string", role: "text", name: "Battery Model" });
    			}
    		}); 	
            obj.packData.forEach(pack => {
                Object.keys(pack).forEach(key => {
                    let val = pack[key];
                    switch (key) {
    					case "batcur":
    						if (val > 32767) { val = val - 65536; }
    						val = val / 10; break;
                        case "maxTemp": val = (val - 2731) / 10; break;
    					case "maxVol":
    					case "minVol":
    					case "totalVol": val = val / 100; break;
    					case "softVersion":
                            const major = Math.floor(val / 4096);
                            const minor = Math.floor((val % 4096) / 256);
                            const patch = val % 256;
                            val = `V${major}.${minor}.${patch}`; break;  
                    }
                    pack[key] = val;
                });
            });
        }
        return obj;
    }
    
    startGetLoop();
    
    

    Ich schreibe meistens sehr direkt – bitte nicht falsch verstehen, es ist nie böse gemeint. Das ist einfach mein Stil und niemals abwertend gemeint.

    1 Antwort Letzte Antwort
    0
    • Bernd1967B Offline
      Bernd1967B Offline
      Bernd1967
      schrieb am zuletzt editiert von Bernd1967
      #259

      Zum Bypass (properties.pass)
      Ich hab dort alle drei Werte, also 0,1,2
      Ich vermute folgendes:
      pass = 1 Bypass + AC Entladen (wenn in der App überschüssige Energie einspeisen erlaubt)
      pass = 2 Nur Bypass

      Nachtrag: Noch nicht sicher, vermutlich genaus anders herum.

      maxclaudiM 1 Antwort Letzte Antwort
      0
      • D Daniel 8

        Vielen Dank für die Infos.
        Ja das in der Früh der Bypass öfter hin und her schaltet sehe ich ja noch ein. Aber das er am Mittag hin und her schaltet ist komisch. Weil da der Akku definitiv nicht voll war. Er war gestern auf maximal 66%. Ich bin mir auch nicht sicher ob er wirklich umschaltet. Denn PassMode kann ich ja leider nicht umschalten beim Solarflow 800Pro. Zumindest habe ich noch keine Möglichkeit gefunden

        maxclaudiM Offline
        maxclaudiM Offline
        maxclaudi
        schrieb am zuletzt editiert von
        #260

        @Daniel-8 sagte:

        Vielen Dank für die Infos.
        Ja das in der Früh der Bypass öfter hin und her schaltet sehe ich ja noch ein. Aber das er am Mittag hin und her schaltet ist komisch. Weil da der Akku definitiv nicht voll war. Er war gestern auf maximal 66%....

        Das ist durch wechselhaften Lichteinfall, Wolkendecken oder Verschattung der PV-Module bedingt.
        Das kann sogar durch Gegenstände, Menschen oder Tiere passieren, die sich kurz vor den Modulen aufhalten.
        Die Firmware kann nicht zwischen Abenddämmerung und Wolken oder kurzzeitiger Verschattung unterscheiden.
        Es wird je nach Lichteinfall (PV-Leistung) und Zeitdauer geprüft, ob der Bypass aus- und/oder evtl. wieder eingeschaltet werden soll.

        Sobald wieder mehr Sonne verfügbar ist, wird er aktiviert; bei zu geringer PV-Leistung – nach einer gewissen Zeitspanne – wieder deaktiviert.
        Das ist ein normaler Vorgang.
        Wenn der Bypass erst einmal eingeschaltet ist, wird bei der Entscheidung (Bypass aus / evtl. wieder ein) der aktuelle SoC-Level (electricLevel: Ladestand) nur bedingt berücksichtigt – auch abhängig vom gesetzten socSet.

        "Nebenher ist ein neues, verbessertes Skript in Arbeit und zu 80 % fertig."

        Was wird denn verbessert? hat es was mit meinem Smartmode zu tun?

        wie Du siehst ist es nun fertig :-)
        ...und ja, es hat u. a. einen smartModeWatcher.

        Bezüglich zur vorherigen Skript-Version::

        Ich habe Smartmode auf Änderung getrickert und wenn er auf 0 springt hat er gleich wieder 1 gesetzt. Und da habe ich jetzt ein Timeout mit 5 Sekunden rein gemacht. Seit dem funktioniert es.

        Ein Timeout zum Setzen von Werten wird mit dem neuen, aktuellen Skript nicht mehr nötig sein.

        Ich schreibe meistens sehr direkt – bitte nicht falsch verstehen, es ist nie böse gemeint. Das ist einfach mein Stil und niemals abwertend gemeint.

        1 Antwort Letzte Antwort
        1
        • Bernd1967B Bernd1967

          Zum Bypass (properties.pass)
          Ich hab dort alle drei Werte, also 0,1,2
          Ich vermute folgendes:
          pass = 1 Bypass + AC Entladen (wenn in der App überschüssige Energie einspeisen erlaubt)
          pass = 2 Nur Bypass

          Nachtrag: Noch nicht sicher, vermutlich genaus anders herum.

          maxclaudiM Offline
          maxclaudiM Offline
          maxclaudi
          schrieb am zuletzt editiert von maxclaudi
          #261

          @Bernd1967 sagte:

          Zum Bypass (properties.pass)
          Ich hab dort alle drei Werte, also 0,1,2
          Ich vermute folgendes:
          pass = 1 Bypass + AC Entladen (wenn in der App überschüssige Energie einspeisen erlaubt)
          pass = 2 Nur Bypass

          Gut beobachtet und danke für diesen wertvollen Beitrag!

          Solches Feedback hilft allen weiter.
          Bei der Beschreibung der Datenpunkte habe ich mich an das offizielle zenSDK gehalten.
          Dort ist bisher nur Folgendes dokumentiert:

          • Attribute: pass
          • Type: int
          • Access: RO (readonly)
          • Range: 0-1
          • Description: Pass-through state

          Es scheint also, dass die offizielle Doku nicht auf dem aktuellsten Stand der Firmware-Entwicklung ist.
          Bitte weiter beobachten.
          Wenn Du Dir sicher bist, werde ich die Beschreibung im Namen des Datenpunkts gerne erweitern.

          edit 03.05.26:
          Da zenSDK per HTTP für pass Werte liefert (0, 1, 2), die über die Cloud (MQTT) so nicht für pass verwendet werden (dort weiterhin 0/1),
          wird es eher so sein, dass beim lokalen Webserver vermutlich ein direktes Mapping des internen passMode auf den Read-Only Key pass statt findet.
          Die Firmware nutzt die Zustände 1 (fest AUS) und 2 (fest EIN) als Schutzmechanismus gegen zu häufiges Schalten der Relais, während zenSDK dies lediglich als Statuswert spiegelt, ohne Schreibzugriff (über passMode) zu ermöglichen.

          im Script übernommen:
          pass - 0: Bypass Automatic, 1: Bypass OFF, 2: Bypass ON

          Ich schreibe meistens sehr direkt – bitte nicht falsch verstehen, es ist nie böse gemeint. Das ist einfach mein Stil und niemals abwertend gemeint.

          D 1 Antwort Letzte Antwort
          0
          • maxclaudiM maxclaudi

            @Bernd1967 sagte:

            Zum Bypass (properties.pass)
            Ich hab dort alle drei Werte, also 0,1,2
            Ich vermute folgendes:
            pass = 1 Bypass + AC Entladen (wenn in der App überschüssige Energie einspeisen erlaubt)
            pass = 2 Nur Bypass

            Gut beobachtet und danke für diesen wertvollen Beitrag!

            Solches Feedback hilft allen weiter.
            Bei der Beschreibung der Datenpunkte habe ich mich an das offizielle zenSDK gehalten.
            Dort ist bisher nur Folgendes dokumentiert:

            • Attribute: pass
            • Type: int
            • Access: RO (readonly)
            • Range: 0-1
            • Description: Pass-through state

            Es scheint also, dass die offizielle Doku nicht auf dem aktuellsten Stand der Firmware-Entwicklung ist.
            Bitte weiter beobachten.
            Wenn Du Dir sicher bist, werde ich die Beschreibung im Namen des Datenpunkts gerne erweitern.

            edit 03.05.26:
            Da zenSDK per HTTP für pass Werte liefert (0, 1, 2), die über die Cloud (MQTT) so nicht für pass verwendet werden (dort weiterhin 0/1),
            wird es eher so sein, dass beim lokalen Webserver vermutlich ein direktes Mapping des internen passMode auf den Read-Only Key pass statt findet.
            Die Firmware nutzt die Zustände 1 (fest AUS) und 2 (fest EIN) als Schutzmechanismus gegen zu häufiges Schalten der Relais, während zenSDK dies lediglich als Statuswert spiegelt, ohne Schreibzugriff (über passMode) zu ermöglichen.

            im Script übernommen:
            pass - 0: Bypass Automatic, 1: Bypass OFF, 2: Bypass ON

            D Online
            D Online
            Daniel 8
            schrieb am zuletzt editiert von
            #262

            @maxclaudi sagte:

            @Bernd1967 sagte:

            Zum Bypass (properties.pass)
            Ich hab dort alle drei Werte, also 0,1,2
            Ich vermute folgendes:
            pass = 1 Bypass + AC Entladen (wenn in der App überschüssige Energie einspeisen erlaubt)
            pass = 2 Nur Bypass

            Also ich habe in der App auch überschüssige Energie einspeisen erlaubt aktiviert.
            Bei mir kommt aber immer nur pass=2.
            Dann dürfte ja pass=1 kommen wenn der Speicher voll ist und die energie ins Haus geht oder`?

            Solarflow 800 Pro mit 1,3 Kwp / Iobroker / Homematic / Shellys / Mediola / Intertechno

            Bernd1967B 1 Antwort Letzte Antwort
            0
            • maxclaudiM maxclaudi

              Zendure-Geräte Webserver: Steuerung über das zenSDK (HTTP)
              2026.05.02_00.39h; update 05.05.26 14:45h Error-sync.
              für das ioBroker-Forum
              In memory of Daisy 02.05.24 – miss you.

              Dieses Skript ermöglicht die lokale Steuerung eines Zendure-Geräts.
              Es funktioniert sofort und benötigt keinerlei Keys oder sonstige Authentifizierungen.
              Es ist mit APP und Cloud nutzbar.
              Oder auch nur lokal, wenn für Gerät(e) der Internetzugang gesperrt wird.
              Dann ist jedoch die Verwendung der App per WLAN nicht mehr möglich.
              Außerdem sollte eine Möglichkeit geschaffen werden, einen Zeitserver zu erreichen.
              Also nur die Zendure spezifischen URL sperren.
              Näher möchte ich hier nicht darauf eingehen.

              Voraussetzungen:

              • Hardware: Zendure-Geräte (Generation 2025/2026) mit integriertem Webserver.

              • Aktivierung: Vor der ersten Nutzung muss HEMS einmalig aktiviert, kurz abgewartet und anschließend wieder deaktiviert werden.

              • Netzwerk: Die IP-Adresse des Zendure-Geräts muss bekannt sein. Empfehle dringend, dem Gerät im Router oder Access Point eine dauerhafte, feste IP zuzuweisen.

              Wichtiger Hinweis zum Flash-Speicher:
              Ich bin kein Freund von automatischen Befehlsketten, die für den Nutzer nicht nachvollziehbar sind – vor allem, wenn sie zu unnötigen Schreibvorgängen im Flash-Speicher führen.

              Nach aktuellem Stand ist sicher: Wenn smartMode: 1 gesetzt ist, werden zumindest die Werte für outputLimit und inputLimit lediglich in den RAM geschrieben.

              Ein Modewechsel (z. B. acMode, Energiepläne, Automodi, MQTT-Konfiguration) wird hingegen fast immer in den Flash-Speicher geschrieben.
              Dabei wird oft nicht nur der einzelne Wert, sondern die gesamte Konfiguration dauerhaft gespeichert.


              Neue Datenpunkte unter "control"
              Im aktuellen Skript gibt es zusätzliche Datenpunkte sowie den Ordner automaticKonfig mit zwei Schaltern (Boolean):

              1. auto_inputLimitMode (Watt)
                Hier kann ein Wert in Watt für das Ladelimit (inputLimit) gesetzt werden.
                Das Skript prüft daraufhin automatisch:
              • Ist acMode: 1 und outputLimit: 0 W gesetzt?

              • Falls nicht, wird automatisch acMode: 1 und outputLimit: 0 gesetzt, bevor mit dem gewählten Wert vom Netz geladen wird.

              • Ist der Schalter input_output_LimitMode_smartMode_RAM aktiviert, wird vor dem Senden zusätzlich geprüft, ob smartMode: 1 aktiv ist.
                Wenn nicht, wird dieser automatisch mitgesendet.

              • Prinzip: Es wird nur gesendet, was zwingend nötig ist (Vorab-Prüfung).

              1. auto_outputLimitMode (Watt)
                Dies ist das Gegenstück zum auto_inputLimitMode für die Entladesteuerung.

              2. smartModeWatcher (Schalter)
                Wenn dieser Slider aktiviert ist (true), wird bei jedem empfangenen Report automatisch geprüft, ob smartMode: 1 gesetzt ist.
                Sollte der Wert auf 0 stehen, setzt das Skript ihn automatisch wieder auf 1. Dieser Slider kann manuell oder per Skript (de-)aktiviert werden.

              Weitere Hinweise

              • inverseMaxPower: Dieser Wert sollte nicht zur laufenden Regelung verwendet werden. Er definiert die Obergrenze, die der Wechselrichter maximal ausgeben darf, und wird sehr wahrscheinlich in den Flash geschrieben.

              • chargeMaxLimit:
                Mit Vorsicht zu genießen. Diesen Wert am besten nicht für regelmäßige Limit-Anpassungen verwenden.

              • Verbraucher-Geräte:
                Vorsicht bei Consumer-Produkten (auch Zendure); hier wird oft bei jeder Parameteränderung die komplette Konfiguration in den Flash geschrieben.

              • mDNS: Funktioniert zwar, hat sich in Tests jedoch als unzuverlässig erwiesen. Eine feste IP-Adresse ist die bessere Wahl.

              • Struktur-Update: Die Verzeichnisse befinden sich nun direkt im Hauptordner:

              • control (inkl. automaticKonfig)

              • packData (unter Cxxxxx finden sich die jeweiligen Batteriedaten)

              • properties

              D Online
              D Online
              Daniel 8
              schrieb am zuletzt editiert von Daniel 8
              #263

              @maxclaudi
              Vielen Dank für deine große Mühe der Scripte

              • Struktur-Update: Die Verzeichnisse befinden sich nun direkt im Hauptordner:

              Ich hoffe mal das ich dann jetzt nicht meine ganzen Scripte neu Anpassen muss, wenn sich eventuell die Datenpunkte verschieben. Ich werde es mal in Ruhe testen

              Gibt es denn eine schnelle lösung, das ich meine Blocklys so anpassen kann. Habe festgestellt das die Struktur jetzt ja leider anderst ist.
              Ich stellte fest, das ich auch noch meinen ganzen Energieflussadapter dann umprogrammieren muss.

              Das bedeutet für mich wieder einige Abende hier

              Solarflow 800 Pro mit 1,3 Kwp / Iobroker / Homematic / Shellys / Mediola / Intertechno

              maxclaudiM 1 Antwort Letzte Antwort
              0
              • D Daniel 8

                @maxclaudi
                Vielen Dank für deine große Mühe der Scripte

                • Struktur-Update: Die Verzeichnisse befinden sich nun direkt im Hauptordner:

                Ich hoffe mal das ich dann jetzt nicht meine ganzen Scripte neu Anpassen muss, wenn sich eventuell die Datenpunkte verschieben. Ich werde es mal in Ruhe testen

                Gibt es denn eine schnelle lösung, das ich meine Blocklys so anpassen kann. Habe festgestellt das die Struktur jetzt ja leider anderst ist.
                Ich stellte fest, das ich auch noch meinen ganzen Energieflussadapter dann umprogrammieren muss.

                Das bedeutet für mich wieder einige Abende hier

                maxclaudiM Offline
                maxclaudiM Offline
                maxclaudi
                schrieb am zuletzt editiert von maxclaudi
                #264

                @Daniel-8
                Du kannst dir das Ganze deutlich einfacher machen, ohne alles neu bauen zu müssen:

                Nutze Alias-Datenpunkte in ioBroker.

                Du legst dir für deine bisher verwendeten (alten) Datenpunkte einfach Aliase an, die auf die neuen Pfade aus dem aktuellen Script zeigen.
                Deine Blocklys und Scripte können dann unverändert bleiben.

                Prinzip:
                Alter Pfad (den deine Blocklys nutzen): bleibt bestehen als Alias
                Neuer Pfad (aus dem Script): wird als Ziel hinterlegt

                Beispiel (schematisch):
                alias.0.<alterPfad>
                zeigt auf
                0_userdata.0.<neuerPfad>

                Dadurch:
                Keine Anpassung deiner Blocklys nötig
                Energieflussadapter bleibt unverändert
                Einmalig Mapping erstellen, danach läuft alles wie vorher

                Das spart dir die „mehreren Abende“ .

                Künftig wird die Struktur, wie sie jetzt im aktuellen Script (02.05.2026) ist, beibehalten.

                Ich schreibe meistens sehr direkt – bitte nicht falsch verstehen, es ist nie böse gemeint. Das ist einfach mein Stil und niemals abwertend gemeint.

                D 1 Antwort Letzte Antwort
                0
                • D Daniel 8

                  @maxclaudi sagte:

                  @Bernd1967 sagte:

                  Zum Bypass (properties.pass)
                  Ich hab dort alle drei Werte, also 0,1,2
                  Ich vermute folgendes:
                  pass = 1 Bypass + AC Entladen (wenn in der App überschüssige Energie einspeisen erlaubt)
                  pass = 2 Nur Bypass

                  Also ich habe in der App auch überschüssige Energie einspeisen erlaubt aktiviert.
                  Bei mir kommt aber immer nur pass=2.
                  Dann dürfte ja pass=1 kommen wenn der Speicher voll ist und die energie ins Haus geht oder`?

                  Bernd1967B Offline
                  Bernd1967B Offline
                  Bernd1967
                  schrieb am zuletzt editiert von
                  #265

                  @Daniel-8 sagte:

                  Also ich habe in der App auch überschüssige Energie einspeisen erlaubt aktiviert.
                  Bei mir kommt aber immer nur pass=2.
                  Dann dürfte ja pass=1 kommen wenn der Speicher voll ist und die energie ins Haus geht oder`?

                  Ja, war auch erstmal eine Vermutung von mir.
                  Ich hatte dann Gestern noch festgestellt das wenn Speicher voll ist und in das Netz eingespeist wird pass=2 ist, also genau wie bei Dir.
                  Jetzt werde ich mal abwarten wann pass=1 ist.

                  maxclaudiM 1 Antwort Letzte Antwort
                  1
                  • maxclaudiM maxclaudi

                    @Daniel-8
                    Du kannst dir das Ganze deutlich einfacher machen, ohne alles neu bauen zu müssen:

                    Nutze Alias-Datenpunkte in ioBroker.

                    Du legst dir für deine bisher verwendeten (alten) Datenpunkte einfach Aliase an, die auf die neuen Pfade aus dem aktuellen Script zeigen.
                    Deine Blocklys und Scripte können dann unverändert bleiben.

                    Prinzip:
                    Alter Pfad (den deine Blocklys nutzen): bleibt bestehen als Alias
                    Neuer Pfad (aus dem Script): wird als Ziel hinterlegt

                    Beispiel (schematisch):
                    alias.0.<alterPfad>
                    zeigt auf
                    0_userdata.0.<neuerPfad>

                    Dadurch:
                    Keine Anpassung deiner Blocklys nötig
                    Energieflussadapter bleibt unverändert
                    Einmalig Mapping erstellen, danach läuft alles wie vorher

                    Das spart dir die „mehreren Abende“ .

                    Künftig wird die Struktur, wie sie jetzt im aktuellen Script (02.05.2026) ist, beibehalten.

                    D Online
                    D Online
                    Daniel 8
                    schrieb am zuletzt editiert von
                    #266

                    @maxclaudi sagte:

                    @Daniel-8
                    Du kannst dir das Ganze deutlich einfacher machen, ohne alles neu bauen zu müssen:

                    Nutze Alias-Datenpunkte in ioBroker.

                    Du legst dir für deine bisher verwendeten (alten) Datenpunkte einfach Aliase an, die auf die neuen Pfade aus dem aktuellen Script zeigen.
                    Deine Blocklys und Scripte können dann unverändert bleiben.

                    Prinzip:
                    Alter Pfad (den deine Blocklys nutzen): bleibt bestehen als Alias
                    Neuer Pfad (aus dem Script): wird als Ziel hinterlegt

                    Beispiel (schematisch):
                    alias.0.<alterPfad>
                    zeigt auf
                    0_userdata.0.<neuerPfad>

                    Dadurch:
                    Keine Anpassung deiner Blocklys nötig
                    Energieflussadapter bleibt unverändert
                    Einmalig Mapping erstellen, danach läuft alles wie vorher

                    Das spart dir die „mehreren Abende“ .

                    Künftig wird die Struktur, wie sie jetzt im aktuellen Script (02.05.2026) ist, beibehalten.

                    Danke. Da muss ich mich mal einlesen und testen

                    Solarflow 800 Pro mit 1,3 Kwp / Iobroker / Homematic / Shellys / Mediola / Intertechno

                    D 1 Antwort Letzte Antwort
                    0
                    • Bernd1967B Bernd1967

                      @Daniel-8 sagte:

                      Also ich habe in der App auch überschüssige Energie einspeisen erlaubt aktiviert.
                      Bei mir kommt aber immer nur pass=2.
                      Dann dürfte ja pass=1 kommen wenn der Speicher voll ist und die energie ins Haus geht oder`?

                      Ja, war auch erstmal eine Vermutung von mir.
                      Ich hatte dann Gestern noch festgestellt das wenn Speicher voll ist und in das Netz eingespeist wird pass=2 ist, also genau wie bei Dir.
                      Jetzt werde ich mal abwarten wann pass=1 ist.

                      maxclaudiM Offline
                      maxclaudiM Offline
                      maxclaudi
                      schrieb am zuletzt editiert von maxclaudi
                      #267

                      @Bernd1967 @Daniel-8

                      Da zenSDK per HTTP für pass Werte liefert (0, 1, 2), die über die Cloud (MQTT) so nicht für pass verwendet werden (dort weiterhin 0/1),
                      wird es eher so sein, dass beim lokalen Webserver vermutlich ein direktes Mapping des internen passMode auf den Read-Only Key pass statt findet.
                      Die Firmware nutzt die Zustände 1 (fest AUS) und 2 (fest EIN) als Schutzmechanismus gegen zu häufiges Schalten der Relais, während zenSDK dies lediglich als Statuswert spiegelt, ohne Schreibzugriff (über passMode) zu ermöglichen.

                      Ich schreibe meistens sehr direkt – bitte nicht falsch verstehen, es ist nie böse gemeint. Das ist einfach mein Stil und niemals abwertend gemeint.

                      1 Antwort Letzte Antwort
                      1
                      • D Daniel 8

                        @maxclaudi sagte:

                        @Daniel-8
                        Du kannst dir das Ganze deutlich einfacher machen, ohne alles neu bauen zu müssen:

                        Nutze Alias-Datenpunkte in ioBroker.

                        Du legst dir für deine bisher verwendeten (alten) Datenpunkte einfach Aliase an, die auf die neuen Pfade aus dem aktuellen Script zeigen.
                        Deine Blocklys und Scripte können dann unverändert bleiben.

                        Prinzip:
                        Alter Pfad (den deine Blocklys nutzen): bleibt bestehen als Alias
                        Neuer Pfad (aus dem Script): wird als Ziel hinterlegt

                        Beispiel (schematisch):
                        alias.0.<alterPfad>
                        zeigt auf
                        0_userdata.0.<neuerPfad>

                        Dadurch:
                        Keine Anpassung deiner Blocklys nötig
                        Energieflussadapter bleibt unverändert
                        Einmalig Mapping erstellen, danach läuft alles wie vorher

                        Das spart dir die „mehreren Abende“ .

                        Künftig wird die Struktur, wie sie jetzt im aktuellen Script (02.05.2026) ist, beibehalten.

                        Danke. Da muss ich mich mal einlesen und testen

                        D Online
                        D Online
                        Daniel 8
                        schrieb am zuletzt editiert von Daniel 8
                        #268

                        Daniel-8 sagte:

                        @maxclaudi sagte:

                        @Daniel-8
                        Du kannst dir das Ganze deutlich einfacher machen, ohne alles neu bauen zu müssen:

                        Nutze Alias-Datenpunkte in ioBroker.

                        Du legst dir für deine bisher verwendeten (alten) Datenpunkte einfach Aliase an, die auf die neuen Pfade aus dem aktuellen Script zeigen.
                        Deine Blocklys und Scripte können dann unverändert bleiben.

                        Prinzip:
                        Alter Pfad (den deine Blocklys nutzen): bleibt bestehen als Alias
                        Neuer Pfad (aus dem Script): wird als Ziel hinterlegt

                        Beispiel (schematisch):
                        alias.0.<alterPfad>
                        zeigt auf
                        0_userdata.0.<neuerPfad>

                        Irgendwie komme ich damit nicht klar. Ich weiß nicht wie ich den alten und den neuen Pfad in die alias eintragen kann. Vielleicht gibst du mir ja ne kurze Hilfestellung. Da muss ich ja die Blocklys auch auf die alias.0 Datenpunkte umprogrammieren oder was mache oder denke ich falsch

                        Solarflow 800 Pro mit 1,3 Kwp / Iobroker / Homematic / Shellys / Mediola / Intertechno

                        maxclaudiM 1 Antwort Letzte Antwort
                        0
                        • D Daniel 8

                          Daniel-8 sagte:

                          @maxclaudi sagte:

                          @Daniel-8
                          Du kannst dir das Ganze deutlich einfacher machen, ohne alles neu bauen zu müssen:

                          Nutze Alias-Datenpunkte in ioBroker.

                          Du legst dir für deine bisher verwendeten (alten) Datenpunkte einfach Aliase an, die auf die neuen Pfade aus dem aktuellen Script zeigen.
                          Deine Blocklys und Scripte können dann unverändert bleiben.

                          Prinzip:
                          Alter Pfad (den deine Blocklys nutzen): bleibt bestehen als Alias
                          Neuer Pfad (aus dem Script): wird als Ziel hinterlegt

                          Beispiel (schematisch):
                          alias.0.<alterPfad>
                          zeigt auf
                          0_userdata.0.<neuerPfad>

                          Irgendwie komme ich damit nicht klar. Ich weiß nicht wie ich den alten und den neuen Pfad in die alias eintragen kann. Vielleicht gibst du mir ja ne kurze Hilfestellung. Da muss ich ja die Blocklys auch auf die alias.0 Datenpunkte umprogrammieren oder was mache oder denke ich falsch

                          maxclaudiM Offline
                          maxclaudiM Offline
                          maxclaudi
                          schrieb am zuletzt editiert von
                          #269

                          @Daniel-8

                          Ich verstehe, dass die Umstellung nervig ist, aber ich muss hier um dein Verständnis bitten: Die Entwicklung und Pflege solcher Projekte ist extrem zeitintensiv.
                          Ich kann neben der Programmierung keinen individuellen Support für ioBroker wie die Alias-Verwaltung leisten.
                          Das Thema „Alias“ ist ein Standard-Feature von ioBroker, zu dem es hervorragende Anleitungen gibt. Da musst du dich leider selbst einarbeiten, das kann ich dir nicht abnehmen.

                          Suchbegriffe für Google/YouTube: "ioBroker Alias für Anfänger“ oder "Alias Adapter Anleitung“.

                          Evtl. auch hilfreich:

                          organisieren-mit-alias-wie-macht-ihr-das
                          best-practice-alias-raumstruktur
                          alias-best-practices-wie-kann-man-es-besser-machen

                          Ohne ein gewisses Maß an Eigeninitiative bei der Umstellung deiner Struktur wird es leider nicht gehen – egal ob du Aliase nutzt oder die Scripte umschreibst.

                          Viel Erfolg!

                          Ich schreibe meistens sehr direkt – bitte nicht falsch verstehen, es ist nie böse gemeint. Das ist einfach mein Stil und niemals abwertend gemeint.

                          1 Antwort Letzte Antwort
                          0
                          • D Online
                            D Online
                            Daniel 8
                            schrieb am zuletzt editiert von
                            #270

                            @maxclaudi Vielen Dank für deine Nachricht. Trotzdem danke für das tolle Script. Ich habe es jetzt mit suchen und ersetzen umgestellt

                            Solarflow 800 Pro mit 1,3 Kwp / Iobroker / Homematic / Shellys / Mediola / Intertechno

                            1 Antwort Letzte Antwort
                            1
                            • maxclaudiM Offline
                              maxclaudiM Offline
                              maxclaudi
                              schrieb am zuletzt editiert von maxclaudi
                              #271

                              update 05.05.26 14:45h Error-sync.

                              Ich habe einen kleinen Error-Sync-Fix eingespielt.
                              Läuft bei euch alles rund?
                              Mich würde interessieren, ob eure Logs sauber bleiben und wie sich der smartModeWatcher im Alltag schlägt.
                              Schreibt doch kurz, mit welcher Hardware ihr es nutzt – ein kurzes "Läuft stabil" hilft mir auch schon.
                              Kurze Timeouts (ca. 2000ms) kommen beim Zendure-Webserver (nicht nur bei kurzem Intervall) ja leider ab und zu vor, aber mein Skript fängt das sauber ab und loggt die Wiederverbindung.

                              Ich schreibe meistens sehr direkt – bitte nicht falsch verstehen, es ist nie böse gemeint. Das ist einfach mein Stil und niemals abwertend gemeint.

                              1 Antwort Letzte Antwort
                              0
                              • D Online
                                D Online
                                Daniel 8
                                schrieb zuletzt editiert von
                                #272

                                Das mit dem Timeout habe ich auch. Aber meist nur ca. 1 mal am Tag.
                                Was für Error hattest du denn?
                                Den Smartmodewatcher habe ich noch nicht getestet

                                Solarflow 800 Pro mit 1,3 Kwp / Iobroker / Homematic / Shellys / Mediola / Intertechno

                                maxclaudiM 1 Antwort Letzte Antwort
                                0
                                • D Daniel 8

                                  Das mit dem Timeout habe ich auch. Aber meist nur ca. 1 mal am Tag.
                                  Was für Error hattest du denn?
                                  Den Smartmodewatcher habe ich noch nicht getestet

                                  maxclaudiM Offline
                                  maxclaudiM Offline
                                  maxclaudi
                                  schrieb zuletzt editiert von
                                  #273

                                  @Daniel-8
                                  Bei mir läuft es bisher stabil.
                                  Nach 48h Dauerstress (5s Intervall):
                                  Bei über 34.000 Abfragen hatte ich lediglich 9 kleine Aussetzer (Timeouts/ECONNRESET).
                                  Das ist bei HTTP und WLAN-Umgebungen völlig normal und wird vom Skript im Hintergrund lautlos abgefangen.

                                  ECONNRESET: bedeutet, dass der Zendure-Webserver die Verbindung abrupt schließt, weil er intern gerade zu beschäftigt ist
                                  (z.B. Cloud-Sync oder interne Verarbeitung). Das liegt nicht am Skript.

                                  Timeout: Bei einem WLAN-Umfeld mit viel Konkurrenz sind 2000ms zwar großzügig, aber Funkstörungen oder Paketverluste lassen sich nie ganz vermeiden.

                                  wichtiger Hinweis:
                                  Sollte ein Steuerungs-Befehl (POST) mal wirklich scheitern, versucht das Skript es 2x erneut.
                                  Klappt es gar nicht, wird der Datenpunkt automatisch auf den echten Ist-Wert des Geräts zurückgesetzt.
                                  So weiß man genau, was wirklich eingestellt ist und man hat keine "toten" Werte in den control dp.
                                  Vorteil: Wert ist aktuell und lässt sich wieder auf den zuvor versuchten Wert ändern.
                                  Das kam bei mir nach 1 Woche bisher nicht vor.

                                  smartModeWatcher ist aktiviert, hatte aber nichts zu tun.
                                  smartMode blieb bis jetzt stabil auf 1.

                                  Ich schreibe meistens sehr direkt – bitte nicht falsch verstehen, es ist nie böse gemeint. Das ist einfach mein Stil und niemals abwertend gemeint.

                                  1 Antwort Letzte Antwort
                                  0
                                  • D Online
                                    D Online
                                    Daniel 8
                                    schrieb zuletzt editiert von
                                    #274

                                    @maxclaudi

                                    Werde mal bei Gelegenheit das aktuelle Script nochmal einfügen mit deinem Update von gestern und werde berichten.

                                    Solarflow 800 Pro mit 1,3 Kwp / Iobroker / Homematic / Shellys / Mediola / Intertechno

                                    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

                                    606

                                    Online

                                    32.8k

                                    Benutzer

                                    82.9k

                                    Themen

                                    1.3m

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

                                    • Du hast noch kein Konto? Registrieren

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