NEWS
Zendure zenSDK Lokal API, SmartMode, SolarFlow AC 800 Pro 2
-
NEU
Zendure-Geräte Webserver: Steuerung über das zenSDK (HTTP)
2026.05.02_00.39h für das ioBroker-Forum; Update 24.05.26 00:15h globale Typsicherung
update axios 21.06.26 00:10h. Danke paul53 für die Idee und das Mitwirken.Update 22.06.26 20:00h zenSDK API 3
API 2 funktioniert weiterhin.
Bitte Beschreibung im Script beachten.In memory of Daisy 02.05.24 – miss you.
Viel Spaß!
// 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 24.05.26 00:15h globale Typsicherung. // update axios 21.06.26 00:10h. Danke paul53 für die Idee und das Mitwirken. // update 22.06.26 20:00h API3 und API2 Unterstützung. // // In memory of Daisy 02.05.24 – miss you. // // // ACHTUNG: "socCompSwitch" ist neu und nur in API 3 verfügbar. // API2 unterstützt NICHT socCompSwitch und auch nicht den control Datenpunkt "set socComp". // Control: set socComp ist rein experimentell und nur zum testen. Tests und Benutzen auf eigenes Risiko und eigene Gefahr! // // // 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. Aber abhängig von Systemleistung und Intervall prüfen. // // 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 //Beispiel: const IP = "192.168.40.20"; const IP = "192.168.40.20"; // IP des Zendure Geräts // Bitte sicherstellen: // 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 //Beispiele: 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 Skripten 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 // Wichtig: // getTimeoutMs < getIntervalMs // postTimeoutMs < getIntervalMs // Bei instabiler Verbindung ggf. erhöhen (z. B. 3000–5000 ms). const getTimeoutMs = 1500; // Empfehlung: 2000 ms. const postTimeoutMs = 1500; // Empfehlung: 2000 ms. // GET intervall // Empfehlung: >= 5000 ms. // Kleinere Werte möglich, aber m. M. nach sinnfrei und nicht empfohlen (Last / Stabilität). 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 axios = require('axios'); 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, def: 1, min: 1, max: 2, apiKey: "acMode", }, chargeMaxLimit: { name: "set Charge Max Limit", unit: "W", role: "level", type: "number", write: true, def: maxInputLimit, min: 0, max: maxInputLimit, apiKey: "chargeMaxLimit", }, gridOffMode: { name: "set gridOffMode; 0: Normal mode, 1: Economic mode, 2: OFF", role: "level", type: "number", write: true, def: 2, 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, def: 0, 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, def: 0, min: 0, max: 9, apiKey: "gridStandard", }, inputLimit: { name: "set inputLimit", unit: "W", role: "level", type: "number", write: true, def: 0, min: 0, max: maxInputLimit, apiKey: "inputLimit", }, inverseMaxPower: { name: "set inverseMaxPower; Max inverter output Power Limit", unit: "W", role: "level", type: "number", write: true, def: maxOutputLimit, min: 200, max: maxOutputLimit, step: 100, apiKey: "inverseMaxPower", }, minSoc: { name: "set minSoc; minimum SOC 0-50", unit: "%", role: "level", type: "number", write: true, def: 10, min: 0, max: 50, apiKey: "minSoc", transformWrite: (val) => val * 10 }, outputLimit: { name: "set outputLimit", unit: "W", role: "level", type: "number", write: true, def: 0, min: 0, max: maxOutputLimit, apiKey: "outputLimit", }, smartMode: { name: "set smartMode; parameter are written to: 1: RAM / 0: Flash", role: "level", type: "number", write: true, def: 0, min: 0, max: 1, apiKey: "smartMode", }, socCompSwitch: { name: "set SoC Comp; Unauthorized modifications are not recommended", role: "level", type: "number", write: true, def: 0, min: 0, max: 2, //0..1 test 2 apiKey: "socCompSwitch" }, socSet: { name: "set socSet; SOC Target 70%-100%", unit: "%", role: "level", type: "number", write: true, def: 100, 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, def: 1, 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, def: def.def, 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 }, { id: `${base}.auto_in_out_Limit`, name: "Set In/Out-Limit-Automatic: Negative: Charging; Positive: Discharging", unit: "W", min: (maxInputLimit * -1), 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") { let val = Math.round(Number(obj.state.val)) || 0; if (val < 0) val = 0; if (val > maxInputLimit) val = maxInputLimit; handleInputLimitMode(val); setState(obj.id, val, true); return; } if (key === "auto_outputLimitMode") { let val = Math.round(Number(obj.state.val)) || 0; if (val < 0) val = 0; if (val > maxOutputLimit) val = maxOutputLimit; handleOutputLimitMode(val); setState(obj.id, val, true); return; } if (key === "auto_in_out_Limit") { let val = Math.round(Number(obj.state.val)) || 0; if (val > maxOutputLimit) val = maxOutputLimit; if ((-val) > maxInputLimit) val = -maxInputLimit; handleAuto_in_out_Limit(val); setState(obj.id, val, true); return; } const def = schemaControl[key]; if (!def) return; let val = obj.state.val; if (def.type === "number") { val = Math.round(Number(val)) || 0; } if (def.min !== undefined && val < def.min) val = def.min; if (def.max !== undefined && val > def.max) val = def.max; let sendVal = val; if (def.transformWrite) { sendVal = def.transformWrite(val); } sendToDevice(def.apiKey, sendVal); setState(obj.id, 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 = {}; setTx(JSON.stringify(payload)); axios .post(`http://${IP}/properties/write`, payload, { timeout: postTimeoutMs, responseType: "json" }) .then((response) => { const data = response.data; if (data && data.success === true && data.code === 200) { retryCount = 0; finishPost(true); } else { throw new Error("Zendure success/code invalid"); } }) .catch((err) => { retryCount++; if (retryCount <= maxRetries) { log(`POST Retry ${retryCount}/${maxRetries} (${err.message || 'Zendure Error'})`, "warn"); setTimeout(() => { postQueue = { ...payload.properties }; postActive = false; executePost(); }, postCooldownMs); return; } log("POST endgültig fehlgeschlagen", "error"); retryCount = 0; finishPost(false); }); } 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); } } const acModeProp = getState(`${baseProp}.acMode`)?.val; if (acModeProp !== undefined && outputProp !== undefined && inputProp !== undefined) { const id = `${baseCtrl}.auto_in_out_Limit`; if (getState(id)?.val !== outputProp && acModeProp === 2) { setState(id, outputProp, true); } if (getState(id)?.val !== inputProp * -1 && acModeProp === 1) { setState(id, inputProp * -1, true); } } } function startGetLoop() { if (getTimer) { clearInterval(getTimer); } getTimer = setInterval(() => { if (postActive) return; const url = `http://${IP}/properties/report`; axios .get(url, { timeout: getTimeoutMs, responseType: 'text' }) .then(function (response) { if (getErrorCount > 0) log("Verbindung wieder OK", "info"); getErrorCount = 0; if (!response.data) return; setRxNew(response.data); handleRxNewUpdate(response.data); }) .catch(function (err) { const errorMsg = err.response ? err.response.status : err.message; getErrorCount++; if (getErrorCount <= 3) log(`GET Fehler (${getErrorCount}): ${errorMsg}`, "info"); if (getErrorCount === 4) log("Keine Verbindung möglich. Zendure-Geräte IP prüfen!", "error"); }); }, 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 handleAuto_in_out_Limit(val) { const currentAcMode = getState(folderZendureApi + ".properties.acMode")?.val; const currentInput = getState(folderZendureApi + ".properties.inputLimit")?.val; const currentOutput = getState(folderZendureApi + ".properties.outputLimit")?.val; const currentSmartMode = getState(folderZendureApi + ".properties.smartMode")?.val; const smartRAMActive = getState(dpSmartRAM)?.val; const payload = {}; if (val === 0) { if (currentInput !== 0) payload.inputLimit = 0; if (currentOutput !== 0) payload.outputLimit = 0; } if (val > 0) { if (currentInput !== 0) payload.inputLimit = 0; if (currentAcMode !== 2) payload.acMode = 2; payload.outputLimit = val; } if (val < 0) { if (currentOutput !== 0) payload.outputLimit = 0; if (currentAcMode !== 1) payload.acMode = 1; payload.inputLimit = val * -1; } if (smartRAMActive === true && currentSmartMode !== 1) payload.smartMode = 1; 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: "BatVolt - Average Battery Voltage of all Batteries", 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: idle, 1: AC Flow out, 2: AC Flow in", 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: "DC State; 0: idle, 1: DC Flow out, 2: DC Flow in", 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: Normal mode, 1: Economic mode, 2: OFF", 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 (Zendure-Device)", 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" }, packNum: { name: "packNum - Number of batteries", role: "value", 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, 3: unknown", 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" }, rssi: { name: "rssi - Received Signal Strength Indicator", unit: "dBm", role: "value", type: "number" }, smartMode: { name: "smartMode - 1: writes to RAM / 0: writes to Flash", role: "value", type: "number" }, socCompSwitch: { //api:3 name: "socCompSwitch; Unauthorized modifications are not recommended", role: "value", type: "number" }, socLimit: { name: "socLimit - 0: normal, 1: Charge limit reached, 2: Discharge limit reached, 3: unknown", 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 = "AB2000L"; else batType = "AB2000"; } else if (sn.startsWith("F")) { batType = "AB3000X"; } else if (sn.startsWith("J")) { const sub = sn.length > 3 ? sn[3] : ""; if (sub === "A") batType = "Internal 2,4kWh"; else batType = "unknown internal"; } else if (sn.startsWith("G")) { const sub = sn.length > 3 ? sn[3] : ""; if (sub === "A") batType = "AB3000L"; else batType = "unknown AB3000?"; } 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}`; const dpName = (key === "version") ? "zenSDK API Version" : key; if (existsState(dp)) { if (getState(dp).val !== json[key]) { setState(dp, json[key], true); } } else { createState(dp, json[key], { name: dpName, 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 => { 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.properties.acMode === 2 && full.properties.outputLimit !== undefined) { setState(`${base}.auto_in_out_Limit`, full.properties.outputLimit, true); } if (full.properties.acMode === 1 && full.properties.inputLimit !== undefined) { setState(`${base}.auto_in_out_Limit`, full.properties.inputLimit * -1, 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"; const acMode = diff.properties.acMode !== undefined ? diff.properties.acMode : getState(folderZendureApi + ".properties.acMode")?.val; if (acMode === 2 && diff.properties.outputLimit !== undefined) { setState(`${base}.auto_in_out_Limit`, diff.properties.outputLimit, true); } if (acMode === 1 && diff.properties.inputLimit !== undefined) { setState(`${base}.auto_in_out_Limit`, diff.properties.inputLimit * -1, true); } 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 "socLimit": { if (val >= 16) { val = val & 0x03; } } 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();Update 22.06.26 20:00h zenSDK API 3
API 2 funktioniert weiterhin.
Bitte Beschreibung im Script beachten.
zum Update / Script -
@maxclaudi Vielen Dank für die Mühe die du dir hier machst

Ich besitze seit dem Wochenende einen SF2400AC+ und versuche gerade das Teil halbwegs sinnvoll zu steuern. Nachdem ich dein super dokumentiertes Skript gefunden habe, ist der solarflow Adapter gleich wieder in Rente gegangen.
Ich habe von Skripten leider nur sehr weinig bis gar keine Ahnung und bin deshalb immer noch auf Vorlagen angewiesen.
Da dein "Hauptskript" leider nicht öffentlich ist, habe ich mich mal bei @lesiflo bedient und versucht sein Script zum Laden/Entladen von Zendure Solarflow für mich an dei Skript anzupassen um erst einmal die grundlegenden Funktionen zu haben.@All Eventuell kann da mal jemand drüber gucken, ob ich da sehr grobe Schnitzer drin habe und eventuell meinen Speicher gleich wieder schrotte.


Hier nochmal der Blockly Export:
Mein Hauszähler wird alle 10 Sekunden per IR-Lesekopf abgefragt, somit werden die Werte dann in diesem Rhythmus erneuert.
Das Laden soll bei 100% beendet werden und frühstens wieder bei 95% starten. Ebenso das Entladen, Stopp bei 10% und frühster Start wieder bei 20%.Das Laden hat schonmal geklappt, ich werde das weiter beobachten.
Falls ich da noch Denkfehler drin habe, würde ich mich über Aufklärung freuen.Nochmals danke für diesen tollen Adapter Ersatz!
Leider gibt es noch nicht viele Vorlagen, die darauf aufbauen.EDIT: Beim Nachladen ab 95% muss es natürlch < 95 heißen.
@Jockel_Bln
Herzlich willkommen :-)
Leider habe ich momentan sehr wenig Zeit.Wenn bitte @paul53 vielleicht über Dein Blockly schauen kann.
"Schrotten" wirst Du den Speicher nicht so schnell.Die Blocklys von @lesiflo sind eigentlich ok und müssten sich leicht umstellen lassen.
-
Bei Netzausfall sinkt SoC bis auf "minSoc" (Test mit 50) und es schaltet dann die Notstromdose aus....
Wert von socLimit:
Welchen Wert meldet socLimit, sobald die Notstromdose wegen Erreichen des Entladelimits (minSoC) abgeschaltet hat?
18?Einstellung gridOffMode:
Welchen Wert hattest du während deines Tests für gridOffMode gesetzt (0 oder 1)?Danke dir für deine Unterstützung.
@maxclaudi [sagte]: Welchen Wert meldet socLimit, sobald die Notstromdose wegen Erreichen des Entladelimits (minSoC) abgeschaltet hat?
Wert 18.
-
@maxclaudi [sagte]: Welchen Wert meldet socLimit, sobald die Notstromdose wegen Erreichen des Entladelimits (minSoC) abgeschaltet hat?
Wert 18.
@maxclaudi [sagte]: Welchen Wert meldet socLimit, sobald die Notstromdose wegen Erreichen des Entladelimits (minSoC) abgeschaltet hat?
Wert 18.
update ist schon raus :-)
Edit/PS: Herzlichen Dank für deine aktive Mitarbeit hier im Forum. Ohne deine schnellen Rückmeldungen würde das Ganze immer viel länger dauern.
@daniel-8 vermisse ich auch – hoffentlich ist bei ihm alles gut.
-
@maxclaudi
Sorry, falls ich nerve.
Vieles was hier diskutiert wird im Zusammenhang mit der Firmware- und HEMS-Aktualisierung trifft für mich nicht zu.
Laden über AC-Anschluss, Entladen über AC-Anschluss - sonst nix.
Bypass-Funktion und Grid-Dose benötige ich nicht.Daher: Kann ich gefahrlos bei Verwendung des HTTP-Scriptes die Updates fahren oder gibt es Probleme mit dem SSL- und Port-Problem?
Vermutlich nicht, da ich ja kein MQTT nutze - richtig?
Danke für Aufklärung ...
-
@maxclaudi
Bei mir ist alles gut. Hab nur grad ein bisschen den Faden hier verloren. Wenn ich das aber richtig gesehen habe, hat Zendure bei Hems 2 und den neuen Firmwares was geändert.
Ich habe noch kein Update gemacht. Bin etwas vorsichtig, da im Zendure Forum sehr viele Probleme haben.Was ist eigentlich der socCompSwitch?
-
@maxclaudi
Sorry, falls ich nerve.
Vieles was hier diskutiert wird im Zusammenhang mit der Firmware- und HEMS-Aktualisierung trifft für mich nicht zu.
Laden über AC-Anschluss, Entladen über AC-Anschluss - sonst nix.
Bypass-Funktion und Grid-Dose benötige ich nicht.Daher: Kann ich gefahrlos bei Verwendung des HTTP-Scriptes die Updates fahren oder gibt es Probleme mit dem SSL- und Port-Problem?
Vermutlich nicht, da ich ja kein MQTT nutze - richtig?
Danke für Aufklärung ...
Allen einen wunderschönen, sonnigen guten Morgen,
@Rico-Sander sagte:
Vieles was hier diskutiert wird im Zusammenhang mit der Firmware- und HEMS-Aktualisierung trifft für mich nicht zu.habe auch kein Update durchgeführt. Weil ich kein HEMS nutze, ist es für mich nicht wichtig.
Von den letzten Updates war ich mehr enttäuscht.
Echte Verbesserungen gab es - für mich - nicht.
Für mich waren es mehr Reglementierungen.@Rico-Sander sagte:
Laden über AC-Anschluss, Entladen über AC-Anschluss - sonst nix.
Bypass-Funktion und Grid-Dose benötige ich nicht.Selbst wenn Du mehr benötigen würdest, wüsste ich nicht was aktuell unter API 2 dagegen spricht.
Die größten Veränderungen/Neuerungen durch das aktuelle Update sind bisher die Einführung von HEMS2 und Umstellung auf TLS.
Zendure schreibt zwar: X bekannte Probleme behoben, jedoch welche das sein sollen, wird nicht erwähnt.Bei meinem Gerät sollen es 2 Probleme sein.
Kann aber kein Problem feststellen und HEMS nutze ich nicht.@Rico-Sander sagte:
Daher: Kann ich gefahrlos bei Verwendung des HTTP-Scriptes die Updates fahren oder gibt es Probleme mit dem SSL- und Port-Problem?
Vermutlich nicht, da ich ja kein MQTT nutze - richtig?SSL mit Port:8883 ist nur für (nicht offizilles , Cloud-) MQTT
Das Script nutzt reines HTTP mit dem Webserver des Zendure-Geräts.
Zendure-Gerät und App nutzen zusätzlich "nicht offizielle" MQTT-Verbindung zur Cloud.Für das Script bedeutet das: Wenn Zendure durch die Firmware zenSDK nicht auch reglementiert oder grundlegend was verändert hat, dann funktioniert es wie zuvor.
Es gab mit dem Update ein paar Änderungen und ein zusätzlicher Key.
Bisher wurden mir keine Einschränkungen gemeldet.Hey, freut mich.
@Daniel-8 sagte:
...hat Zendure bei Hems 2 und den neuen Firmwares was geändert.
Ich habe noch kein Update gemacht. Bin etwas vorsichtig, da im Zendure Forum sehr viele Probleme haben.warte auch ab. Evtl. benötige ich keins.
Das ist nicht bekannt und noch nicht dokumentiert.
Habe eine Vermutung und kann es - ohne Update -nicht testen.socCompSwitch, bedeutet wahrscheinlich: State of Charge Compilation Switch.
Denke es wird sich um einen neuen Schalter handeln, der eine Kalibrierung manuell auslösen wird.
Damit man die automatische (wöchentliche oder monatliche) SoC-Kalibrierung zu nicht gewollten Zeiten
- beeinflussen,
- verhindern
- oder vorziehen kann?
Das ist nur meine Vermutung -reine Spekulation - !
Habe den Schalter mal hinzugefügt, falls jemand auf eigene Gefahr und eigenes Risiko selbst testen möchte, was der Schalter bewirkt.
Unter API 2 habe ich den Schalter schon gesetzt und getestet.
Befehl wird abgeschickt und Empfang bestätigt.
Auswirkungen hatte es bei mir überhaupt keine.
War zu erwarten, weil socCompSwitch in der API2-Firmware nicht implementiert ist.
Konnte den Schalter mehrmals - unter API 2 - gefahrlos bei meinem 1600AC+ setzen - ohne jede Veränderung.
Funktionieren oder was bewirken wird der Schalter (sehr wahrscheinlich nur) erst nach Firmware-Update auf zenSDK API Version: 3.Noch einmal: Alles zu socCompSwitch sind nur Vermutungen aufgrund von Tests und Beobachtungen.
edit:
Falls jemand set socComp nach Update mit zenSDK API Version: 3 testen sollte, wäre ich über Rückmeldungen sehr dankbar. -
Vielen Dank für die Erklärung.
Es gibt ja in der App eine Funktion wo man zumindest die Kalibrierungszeit einstellen kann. Habe es noch nie getestet. Somit ist mir auch nicht klar, ob dann mit dc (Pv Leistung) oder mit netzstrom kalibriert wird.

-
Vielen Dank für die Erklärung.
Es gibt ja in der App eine Funktion wo man zumindest die Kalibrierungszeit einstellen kann. Habe es noch nie getestet. Somit ist mir auch nicht klar, ob dann mit dc (Pv Leistung) oder mit netzstrom kalibriert wird.

@Daniel-8 sagte:
Es gibt ja in der App eine Funktion wo man zumindest die Kalibrierungszeit einstellen kann.Diese Einstellung gibt es in meiner App-Version nicht.

Habe es noch nie getestet. Somit ist mir auch nicht klar, ob dann mit dc (Pv Leistung) oder mit netzstrom kalibriert wird.
Ich vermute, dass die PV-Leistung berücksichtigt wird, weiß es aber nicht sicher.
Zusätzlich stellt sich die Frage wie die Kalibrierung implementiert ist.
Ob die Batterien zuerst komplett entladen und danach voll aufgeladen werden, oder es umgekehrt abläuft?
Einmaliger oder mehrmaliger Prozess?Welche API Version wird bei Dir im Datenpunkt "version" angezeigt?

-
Vielen Dank für die Erklärung.
Es gibt ja in der App eine Funktion wo man zumindest die Kalibrierungszeit einstellen kann. Habe es noch nie getestet. Somit ist mir auch nicht klar, ob dann mit dc (Pv Leistung) oder mit netzstrom kalibriert wird.

Vielen Dank für die Erklärung.
Es gibt ja in der App eine Funktion wo man zumindest die Kalibrierungszeit einstellen kann. Habe es noch nie getestet. Somit ist mir auch nicht klar, ob dann mit dc (Pv Leistung) oder mit netzstrom kalibriert wird.

Moin,
das wird die Funktion sein, die Anfang Juni vom Support in einer Mail angekündigt wurde.
Zitat Support:Gute Neuigkeiten: Unsere automatische Kalibrierungsfunktion steht kurz vor der Veröffentlichung. Sie können dann für Ihre Batterie Start- und Endzeit sowie die Lade-/Entladeleistung festlegen, und das System führt die Kalibrierung innerhalb des gewählten Zeitraums automatisch durch. Das wird den bisherigen Aufwand erheblich reduzieren.Danke für Rückmeldung, dann werde auch ich vorerst auf das Update verzichten.
Einen schönen Tag Euch da draußen... -
Vielen Dank für die Erklärung.
Es gibt ja in der App eine Funktion wo man zumindest die Kalibrierungszeit einstellen kann. Habe es noch nie getestet. Somit ist mir auch nicht klar, ob dann mit dc (Pv Leistung) oder mit netzstrom kalibriert wird.

Moin,
das wird die Funktion sein, die Anfang Juni vom Support in einer Mail angekündigt wurde.
Zitat Support:Gute Neuigkeiten: Unsere automatische Kalibrierungsfunktion steht kurz vor der Veröffentlichung. Sie können dann für Ihre Batterie Start- und Endzeit sowie die Lade-/Entladeleistung festlegen, und das System führt die Kalibrierung innerhalb des gewählten Zeitraums automatisch durch. Das wird den bisherigen Aufwand erheblich reduzieren.Danke für Rückmeldung, dann werde auch ich vorerst auf das Update verzichten.
Einen schönen Tag Euch da draußen...@Daniel-8 sagte:
Es gibt ja in der App eine Funktion wo man zumindest die Kalibrierungszeit einstellen kann.Bei Dir ist Api Version:2; dann verwendest Du vermutlich eine neuere App-Version ohne Firmware-Update.
@Rico-Sander sagte:
das wird die Funktion sein, die Anfang Juni vom Support in einer Mail angekündigt wurde.An alle:
Wenn Firmware aktualisiert wurde auf API 3 und man die App für die Kalibrierung nutzt, dann wäre es interessant was für Werte in der App eingestellt wurden und was die Datenpunkte für Werte liefern:- properties.batCalTime
- properties.socCompSwitch
- properties.socStatus
evtl. noch
properties.pass beobachten. -
@Daniel-8 sagte:
Es gibt ja in der App eine Funktion wo man zumindest die Kalibrierungszeit einstellen kann.Bei Dir ist Api Version:2; dann verwendest Du vermutlich eine neuere App-Version ohne Firmware-Update.
@Rico-Sander sagte:
das wird die Funktion sein, die Anfang Juni vom Support in einer Mail angekündigt wurde.An alle:
Wenn Firmware aktualisiert wurde auf API 3 und man die App für die Kalibrierung nutzt, dann wäre es interessant was für Werte in der App eingestellt wurden und was die Datenpunkte für Werte liefern:- properties.batCalTime
- properties.socCompSwitch
- properties.socStatus
evtl. noch
properties.pass beobachten.@Daniel-8 sagte:
Es gibt ja in der App eine Funktion wo man zumindest die Kalibrierungszeit einstellen kann.Bei Dir ist Api Version:2; dann verwendest Du vermutlich eine neuere App-Version ohne Firmware-Update.
Ja die App ist aktuell ohne Firmwareupdate. Aber ich meine das es in der vorherigen Version der App auch schon drin war. Kann mich aber auch täuschen
@Rico-Sander sagte:
das wird die Funktion sein, die Anfang Juni vom Support in einer Mail angekündigt wurde.An alle:
Wenn Firmware aktualisiert wurde auf API 3 und man die App für die Kalibrierung nutzt, dann wäre es interessant was für Werte in der App eingestellt wurden und was die Datenpunkte für Werte liefern:- properties.batCalTime
- properties.socCompSwitch
- properties.socStatus
evtl. noch
properties.pass beobachten.Ich bin ja noch auf Api 2 und nutze eine Manuelle Kalibrierung über Iobroker. Da wird spätestens nach 27 Tage über PV Strom auf 100% geladen.
-
@maxclaudi Vielen Dank für die Mühe die du dir hier machst

Ich besitze seit dem Wochenende einen SF2400AC+ und versuche gerade das Teil halbwegs sinnvoll zu steuern. Nachdem ich dein super dokumentiertes Skript gefunden habe, ist der solarflow Adapter gleich wieder in Rente gegangen.
Ich habe von Skripten leider nur sehr weinig bis gar keine Ahnung und bin deshalb immer noch auf Vorlagen angewiesen.
Da dein "Hauptskript" leider nicht öffentlich ist, habe ich mich mal bei @lesiflo bedient und versucht sein Script zum Laden/Entladen von Zendure Solarflow für mich an dei Skript anzupassen um erst einmal die grundlegenden Funktionen zu haben.@All Eventuell kann da mal jemand drüber gucken, ob ich da sehr grobe Schnitzer drin habe und eventuell meinen Speicher gleich wieder schrotte.


Hier nochmal der Blockly Export:
Mein Hauszähler wird alle 10 Sekunden per IR-Lesekopf abgefragt, somit werden die Werte dann in diesem Rhythmus erneuert.
Das Laden soll bei 100% beendet werden und frühstens wieder bei 95% starten. Ebenso das Entladen, Stopp bei 10% und frühster Start wieder bei 20%.Das Laden hat schonmal geklappt, ich werde das weiter beobachten.
Falls ich da noch Denkfehler drin habe, würde ich mich über Aufklärung freuen.Nochmals danke für diesen tollen Adapter Ersatz!
Leider gibt es noch nicht viele Vorlagen, die darauf aufbauen.EDIT: Beim Nachladen ab 95% muss es natürlch < 95 heißen.
@Jockel_Bln sagte:
@All Eventuell kann da mal jemand drüber gucken, ob ich da sehr grobe Schnitzer drin habe...
EDIT: Beim Nachladen ab 95% muss es natürlch < 95 heißen.Hi @Jockel_Bln,
das mit „< 96“ hast Du ja selbst schon korrigiert.
Deine einfache Steuerung sollte so als Grundgerüst für den Anfang funktionieren.Ein paar Vorschläge:
Abfrage vor dem Senden:
Prüfe vor jedem erneuten Setzen des output oder input Limits, ob der neue Wert überhaupt vom aktuellen Datenpunktwert abweicht.Variablen statt Festwerte:
feste Zahlen (10, 20, 100, 95) können durch dynamische Datenpunkte wie setSoc (z. B. 90% oder 100%), minSoc oder Vergleiche wie minSoc + 5 ersetzt werden.Messwerte nutzen: Du kannst statt der vom BMS geschätzten SoC-Werte auch echte, verlässliche Messwerte wie MinVol und MaxVol für Vergleiche heranziehen.
Mehrere Batterien einplanen:
Bei Freigabe_Laden: falsch, wenn socLevel... kannst Du direkt alle Batterien berücksichtigen oder das Skript zukunftssicher umbauen.
Dafür kann man den übergreifenden Datenpunkt properties.electricLevel nutzen.Ladegrenzen beachten:
Sobald das eingestellte socSet erreicht ist, stoppt das Laden ohnehin automatisch.
Man kann zusätzlich den Datenpunkt: properties.socLimit (0: normal, 1: Charge limit reached, 2: Discharge limit reached) nutzen, um den Status direkt abzufragen.Mindestdauer & Hysterese:
Man könnte eine Mindestdauer für geänderte Limits einsetzen, um schnelles Hin- und Herschalten zu vermeiden.
Dein Trigger ist zwar alle 10 sec, aber ein setzen von einem neuen Limit bis es ordentlich und ruhig anfängt zu wirken, benötigt allein schon diese Zeit.
Eine kleine Hysterese hilft ebenfalls:
outputLimit erst ändern, wenn der neue Wert um mindestens X Watt (z. B. deine 10W oder 20W) vom aktuellen Wert abweicht.Ist-Werte in Skripten vergleichen:
bitte nicht nur die reinen Sollwerte (z. B. Falls inputLimit === 100) vergleichen.
Es ist idealer wenn stattdessen die tatsächlich wichtigeren Ist-Werte mit eingebunden werden
(z. B. Falls inputLimit === 100 && properties.gridInputPower === 100).
Noch schöner ist es, wenn Du prüfst, ob das inputLimit bei 100 liegt und die gridInputPower innerhalb eines Toleranzbereichs von +/- X Watt oder X % des Limits liegt.Da sind zwar noch ein paar kleine Ungereimtheiten drin, habe es nur grob überflogen, aber fürs erste hoffe ich, hilft dir das weiter.
Viel Erfolg!
-
@Daniel-8 sagte:
Es gibt ja in der App eine Funktion wo man zumindest die Kalibrierungszeit einstellen kann.Bei Dir ist Api Version:2; dann verwendest Du vermutlich eine neuere App-Version ohne Firmware-Update.
Ja die App ist aktuell ohne Firmwareupdate. Aber ich meine das es in der vorherigen Version der App auch schon drin war. Kann mich aber auch täuschen
@Rico-Sander sagte:
das wird die Funktion sein, die Anfang Juni vom Support in einer Mail angekündigt wurde.An alle:
Wenn Firmware aktualisiert wurde auf API 3 und man die App für die Kalibrierung nutzt, dann wäre es interessant was für Werte in der App eingestellt wurden und was die Datenpunkte für Werte liefern:- properties.batCalTime
- properties.socCompSwitch
- properties.socStatus
evtl. noch
properties.pass beobachten.Ich bin ja noch auf Api 2 und nutze eine Manuelle Kalibrierung über Iobroker. Da wird spätestens nach 27 Tage über PV Strom auf 100% geladen.
-
@maxclaudi Vielen Dank für die Mühe die du dir hier machst

Ich besitze seit dem Wochenende einen SF2400AC+ und versuche gerade das Teil halbwegs sinnvoll zu steuern. Nachdem ich dein super dokumentiertes Skript gefunden habe, ist der solarflow Adapter gleich wieder in Rente gegangen.
Ich habe von Skripten leider nur sehr weinig bis gar keine Ahnung und bin deshalb immer noch auf Vorlagen angewiesen.
Da dein "Hauptskript" leider nicht öffentlich ist, habe ich mich mal bei @lesiflo bedient und versucht sein Script zum Laden/Entladen von Zendure Solarflow für mich an dei Skript anzupassen um erst einmal die grundlegenden Funktionen zu haben.@All Eventuell kann da mal jemand drüber gucken, ob ich da sehr grobe Schnitzer drin habe und eventuell meinen Speicher gleich wieder schrotte.


Hier nochmal der Blockly Export:
Mein Hauszähler wird alle 10 Sekunden per IR-Lesekopf abgefragt, somit werden die Werte dann in diesem Rhythmus erneuert.
Das Laden soll bei 100% beendet werden und frühstens wieder bei 95% starten. Ebenso das Entladen, Stopp bei 10% und frühster Start wieder bei 20%.Das Laden hat schonmal geklappt, ich werde das weiter beobachten.
Falls ich da noch Denkfehler drin habe, würde ich mich über Aufklärung freuen.Nochmals danke für diesen tollen Adapter Ersatz!
Leider gibt es noch nicht viele Vorlagen, die darauf aufbauen.EDIT: Beim Nachladen ab 95% muss es natürlch < 95 heißen.
@Jockel_Bln
hätte 2 Fragen, weil Du den 2400AC+ verwendest.- Welche Version verwendest Du?
siehe Bild, Beispiel für version: 2

- in früheren Firmware-Versionen unterscheiden sich die Geräte 24XX zu den anderen Solarflow 8xx
in den Datenpunkten für den Lüfter:
AC24XX hatten (haben noch?):
properties.fanSwitch: 1
properties.fanSpeed": 0
Bei den anderen Geräten SF8XX und 1600AC werden die Keys anders benannt:
properties.Fanmode: 1
properties.Fanspeed: 0siehe Bild:

@Jockel_Bln und Alle die ein Zendure 24XX oder höher verwenden:
Sind die Unterschiede noch vorhanden, oder wurden die keys bei den Geräten 24XX AC oder >2XXX nun angepasst und liefern auch:
properties.Fanmode: 1
properties.Fanspeed: 0
?Danke für jede Rückmeldung.
- Welche Version verwendest Du?
-
@Jockel_Bln
hätte 2 Fragen, weil Du den 2400AC+ verwendest.- Welche Version verwendest Du?
siehe Bild, Beispiel für version: 2

- in früheren Firmware-Versionen unterscheiden sich die Geräte 24XX zu den anderen Solarflow 8xx
in den Datenpunkten für den Lüfter:
AC24XX hatten (haben noch?):
properties.fanSwitch: 1
properties.fanSpeed": 0
Bei den anderen Geräten SF8XX und 1600AC werden die Keys anders benannt:
properties.Fanmode: 1
properties.Fanspeed: 0siehe Bild:

@Jockel_Bln und Alle die ein Zendure 24XX oder höher verwenden:
Sind die Unterschiede noch vorhanden, oder wurden die keys bei den Geräten 24XX AC oder >2XXX nun angepasst und liefern auch:
properties.Fanmode: 1
properties.Fanspeed: 0
?Danke für jede Rückmeldung.
Hallo @maxclaudi
@Jockel_Bln
hätte 2 Fragen, weil Du den 2400AC+ verwendest.- Welche Version verwendest Du?
Da mich die App gleich nach der Installation zum Update genötigt hat, habe ich die Version 3 drauf.
Sind die Unterschiede noch vorhanden, oder wurden die keys bei den Geräten 24XX AC oder >2XXX nun angepasst und liefern auch:
properties.Fanmode: 1
properties.Fanspeed: 0Ich bekomme auch
properties.Fanmode: 1
properties.Fanspeed: 0
- Welche Version verwendest Du?
-
Hallo @maxclaudi
@Jockel_Bln
hätte 2 Fragen, weil Du den 2400AC+ verwendest.- Welche Version verwendest Du?
Da mich die App gleich nach der Installation zum Update genötigt hat, habe ich die Version 3 drauf.
Sind die Unterschiede noch vorhanden, oder wurden die keys bei den Geräten 24XX AC oder >2XXX nun angepasst und liefern auch:
properties.Fanmode: 1
properties.Fanspeed: 0Ich bekomme auch
properties.Fanmode: 1
properties.Fanspeed: 0
@Jockel_Bln
Danke für die schnelle Rückmeldung. -
@maxclaudi
Kannst du etwas zu dieser Beobachtung sagen, da du schon länger Erfahrung mit den Zendure Solarflow gemacht hast?Bis zum "socSet" (85 %) wird mit 800 W aus dem Netz geladen. Danach erhöht sich "electricLevel" alle 2 Stunden um 1 %, was zu der Angabe von "Battery pack power" = 9 W passt.
Werte unter "properties" sind dann allerdings gelogen:- "gridInputPower" = "gridOffPower" um 17 W (Mini-PC: 6 W + Router)
- "outputPackPower": 0 W
Gibt es eine Minimalleistungsaufnahme am Notstrom-Ausgang, damit nicht über den "socSet" hinaus geladen wird?
Als ich zu Beginn mal eine 75 W Glühlampe angeschlossen hatte, blieb der SOC auf 85 % konstant.gut beobachtet.
Das lässt sich elektrotechnisch und physikalisch gut erklären.
Die Werte sind nicht direkt „gelogen“, sondern unterliegen den typischen Grenzen der Sensoren bei Kleinstleistungen.Hier spielen mehrere Effekte zusammen, z. B.:
Sensor-Toleranz im Mikrobereich:
Die internen Stromsensoren (Shunts) des Geräts sind für hohe Ströme (z. B. beim Laden mit 800 W) optimiert.
Wenn das Gerät bei Erreichen des socSet (85 %) das Hauptladen beendet, schaltet es in den Standby-/Erhaltungsmodus.
Die dabei fließenden Ströme sind so minimal, dass die Software auf 0 W abgerundet wird, obwohl im Hintergrund minimale Erhaltungsimpulse fließen.Eigenverbrauch vs. Last am Notstrom-Ausgang:
Dein Mini-PC und der Router ziehen zusammen ca. 17 W.
Das ist extrem wenig.
Wenn das Gerät im Standby läuft, reicht diese geringe Last am Notstrom Ausgang (gridOffPower) bestimmt nicht aus, um die minimale Erhaltungsenergie, die das System intern regelt, vollständig zu verbrauchen,
Ein Teil sickert weiterhin in die Zellen, weshalb der SOC alle 2 Stunden um 1 % nach oben kriecht.Bei 75 W Last war der Verbrauch am Ausgang hoch genug, um die gesamte bereitgestellte Erhaltungsenergie sofort abzunehmen.
Der Akku musste nichts mehr aufnehmen und blieb stabil auf den konfigurierten 85 % stehen.Das ist m. M. n. kein Zendure-eigenes Problem sondern allgemein elektrotechnisch kaum anders möglich.
Abhängig von der Güte der Sensoren, BMS, den jeweiligen Messbereichen usw.
Vermutlich gibt es keine feste „Mindestleistungsaufnahme“, sondern es ist ein reines Balance-Spiel zwischen der minimalen Erhaltungsleistung des BMS und der angeschlossenen Grundlast im Standby.
Wie das genau von Zendure gelöst ist kann ich nicht beurteilen.
Die beobachteten Werte und Wertfindung sind dabei elektrotechnisch jedoch völlig plausibel.EDIT: Übrigens habe ich noch folgende Werte unter "properties", die nicht der Beschreibung entsprechen:
- "pass": 3
- "socLimit": 17
Das ist für mich neu.
Bei mir stimmen die Werte und die Beschreibung (noch).Hast Du die Firmware aktualisiert?
Wenn ja, hat Zendure im Hintergrund vermutlich etwas an den API-Objekten geändert oder es ist ein neuer Bug.
Das ist für mich im Moment auch ein Novum, über das ich aktuell noch keine näheren Informationen habe.@maxclaudi [sagte]: Die Werte sind nicht direkt „gelogen“
Stimmt. Ich messe jetzt mittels einer FRITZ!DECT 200 die Netzleistung des SF. Sie stimmt nahezu mit der "gridOffPower" überein (ca. 17 W), wenn "acMode" = 1 (Netzladen) und der SoC >= "socSet" ist.
Trotzdem steigt oberhalb von "socSet" der SoC um 1 % alle 2 Stunden, was einer Ladeleistung von 9 W entspricht, die auch im DP "Battery pack power" angezeigt wird. Woher kommt diese zusätzliche Leistung? Gibt es eine Zusatzbatterie? -
@maxclaudi [sagte]: Die Werte sind nicht direkt „gelogen“
Stimmt. Ich messe jetzt mittels einer FRITZ!DECT 200 die Netzleistung des SF. Sie stimmt nahezu mit der "gridOffPower" überein (ca. 17 W), wenn "acMode" = 1 (Netzladen) und der SoC >= "socSet" ist.
Trotzdem steigt oberhalb von "socSet" der SoC um 1 % alle 2 Stunden, was einer Ladeleistung von 9 W entspricht, die auch im DP "Battery pack power" angezeigt wird. Woher kommt diese zusätzliche Leistung? Gibt es eine Zusatzbatterie?
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
