NEWS
Zendure zenSDK Lokal API, SmartMode, SolarFlow AC 800 Pro 2
-
TLS Verbindung Port: 8883
Das wichtigste Ergebnis vorab:
Der herkömmliche, vorgesehene "Normal-" Betrieb über TLS ist absolut stabil möglich.Kann man den Port über die app Option einfach so einstellen?
Das heißt mit dem läuft alles schneller und über mqtt. Dann bräuchte man Zensdk gar Nicht mehr.
Irgendwie übersteigt das ganze hier jetzt mein können.
TLS Verbindung Port: 8883
Das wichtigste Ergebnis vorab:
Der herkömmliche, vorgesehene "Normal-" Betrieb über TLS ist absolut stabil möglich.Kann man den Port über die app Option einfach so einstellen?
Um es verständlicher zu erklären: Es geht hier um zwei unterschiedliche MQTT-Verbindungen.
Port 1883 ohne TLS:
Man kann den Port 1883 ohne TLS am Gerät und in der App (gewusst wie) freischalten und aktivieren.
Ab dann ist das Gerät über Port 1883 mit einer DNS-Adresse verbunden.
Diese DNS-Adresse muss per AdGuard, Pi-hole oder über einen eigenen DNS-Server auf den lokalen Broker umgebogen werden, wenn man lokal über die Datenpunkte steuern möchte.
Aber bei dieser Verbindung sind zu viele - ich nenne es mal - Möglichkeiten vorhanden und sollte m. M. n. nur kurzzeitig verwendet werden, näher gehe ich darauf nicht ein.
Port 8883 mit TLS:
Das ist der Standard.
So verbindet sich jedes Gerät.
Das Gerät ist über Port 8883 mit einer DNS-Adresse verbunden.
Diese DNS-Adresse kann ebenfalls per AdGuard, Pi-hole oder DNS-Server auf die IP des lokalen Brokers umgebogen werden, wenn man lokal über die Datenpunkte steuern möchte.Alternativ:
Man konfiguriert die Variable für DNS/IP auf dem Gerät mit dem zURLtool um auf den lokalen Broker.
Auf dem lokalen Broker (z.B. Mosquitto) kann zusätzlich eine Bridge eingerichtet werden, damit das Gerät mit der Cloud verbunden bleibt.
Es ist in wenigen Sekunden erledigt, diese Bridge wahlweise abzuschalten oder der Cloud nur Lesezugriffe oder vollen Zugriff zu erlauben.Mit einem lokalen Broker über MQTT ist die Steuerung zuverlässig, stabil und schneller als über die Cloud oder die lokale API (also auch zenSDK).
zenSDK macht nach meinen Analysen und Tests nur einen kleinen Unterschied, solange kein erhöhter HTTP(S)-Traffic im jeweiligen IP-Bereich bzw. Netzwerk vorliegt.
Wenn diese Voraussetzung gegeben ist und ein Abfrageintervall (Get) von etwa 4000 ms stabil möglich ist, kann auch das zenSDK genutzt werden.
Es ist dann nur unwesentlich langsamer als MQTT.Der Grund dafür ist hardwarebedingt.
Auch wenn MQTT durch den schnelleren, automatischen Bericht-Versand (Report Publish) vom Gerät und das schnellere Publizieren neuer Werte klar im Vorteil ist, benötigt das Gerät hardwarebedingt Zeit.
Eine Limit-Umschaltung von inputLimit oder outputLimit braucht beispielsweise Zeit, um den Zielwert vollständig umzusetzen.Einfacher Vergleich:
Du kannst bei einem Auto mit Automatikgetriebe und Turbolader zwar das Gaspedal oder die Bremse voll durchtreten, aber es dauert dennoch eine gewisse Zeit, bis die Höchstgeschwindigkeit oder der Stillstand erreicht sind.Alle Klarheiten beseitigt? ;-)
Das heißt mit dem läuft alles schneller und über mqtt. Dann bräuchte man Zensdk gar Nicht mehr.
Richtig.
zenSDK ist, wie erklärt, unter den passenden Voraussetzungen langsamer als der lokale Broker über MQTT.
Eigentlich ist MQTT vorzuziehen, da hier nicht auf neue Werte gepollt werden muss, sondern diese sofort automatisch publiziert werden.
Zudem kann die lokale API abgeschaltet werden (gewusst wie) und der ESP32-C wird dadurch weiter entlastet.22.10.2026:
ESP32-C, das ist übrigens die Standard-CPU, die verwendet wird und noch nirgendwo offiziell weltweit dokumentiert wurde.Irgendwie übersteigt das ganze hier jetzt mein können.
Jetzt nicht mehr ;-)
Hinweis:
Ich überlege gerade ein MQTT-Steurungs-Script anzupassen, damit es als Ersatz für das zenSDK-Steuerungs-Script verwendet werden kann.
Damit bleiben alle Datenpunkte identisch, wenn man es gleich konfiguriert wie das zenSDK-Steuerungs-Script.
Bei identischer Benennung und Konfiguration, muss man darauf achten dass nur 1 Steuerungs-Script aktiv ist.Bei unterschiedlicher Benennung kann zenSDK und MQTT gleichzeitig, simultan genutzt werden oder auch nur eins von beiden als vorrangiges Steuerungsscripts.
MQTT liefer natürlich einiges mehr an Infos.
-
Vielen Dank für die ausführlichen Antworten.
Dann belasse ich es mal bei der Steuerung über zenSDK.
Ich Kämpfe seit 3 Tagen mit einem Problem wo ich nicht weiß woher das kommt.
Immer um 6.57 Uhr bringt das Script die Meldung Das ich meine IP Adresse überprüfen soll.Und heute hat irgendwie mein Steuerungsscript komplett verrückt gespielt und mal alles ins Netz eingespeist. Das lief bis jetzt ohne Probleme schon eine Weile. Wobei ich heute auch das Firmware Update auf 2
0.5 gemacht habe.War leider arbeiten und konnte nicht schauen warum dies heute so passiert ist.
-
Vielen Dank für die ausführlichen Antworten.
Dann belasse ich es mal bei der Steuerung über zenSDK.
Ich Kämpfe seit 3 Tagen mit einem Problem wo ich nicht weiß woher das kommt.
Immer um 6.57 Uhr bringt das Script die Meldung Das ich meine IP Adresse überprüfen soll.Und heute hat irgendwie mein Steuerungsscript komplett verrückt gespielt und mal alles ins Netz eingespeist. Das lief bis jetzt ohne Probleme schon eine Weile. Wobei ich heute auch das Firmware Update auf 2
0.5 gemacht habe.War leider arbeiten und konnte nicht schauen warum dies heute so passiert ist.
-
@Daniel-8 sagte:
Immer um 6.57 Uhr bringt das Script die Meldung Das ich meine IP Adresse überprüfen soll.Wenn das täglich zur gleichen Uhrzeit vorkommt:
Prüfe mal deinen WLAN-Router oder WLAN-AP, ob dort ein täglicher Neustart konfiguriert ist.Ist nicht immer ganz genau zur gleichen Zeit. So ca. Plus minus 6 Minuten.
Nein habe nichts programmiert. Ich werde mal beobachten. Jetzt ist ja zumindest der Speicher mal neu gestartet mit der neuen Firmware.
Und so werde ich schritt für schritt testen. Habe ja auch nichts verändert. Deswegen verstehe ich es nicht warum es jetzt so ist. -
@Daniel-8 sagte:
Immer um 6.57 Uhr bringt das Script die Meldung Das ich meine IP Adresse überprüfen soll.Wenn das täglich zur gleichen Uhrzeit vorkommt:
Prüfe mal deinen WLAN-Router oder WLAN-AP, ob dort ein täglicher Neustart konfiguriert ist.Ist nicht immer ganz genau zur gleichen Zeit. So ca. Plus minus 6 Minuten.
Nein habe nichts programmiert. Ich werde mal beobachten. Jetzt ist ja zumindest der Speicher mal neu gestartet mit der neuen Firmware.
Und so werde ich schritt für schritt testen. Habe ja auch nichts verändert. Deswegen verstehe ich es nicht warum es jetzt so ist.@Daniel-8 sagte:
Ist nicht immer ganz genau zur gleichen Zeit. So ca. Plus minus 6 Minuten.Nein habe nichts programmiert. Ich werde mal beobachten. Jetzt ist ja zumindest der Speicher mal neu gestartet mit der neuen Firmware.
Und so werde ich schritt für schritt testen. Habe ja auch nichts verändert. Deswegen verstehe ich es nicht warum es jetzt so ist.Ich würde es dennoch nicht ausschließen und überprüfen.
Das gilt vor allem auch für sogenannte Repeater, falls einer im Einsatz ist.Repeater sind für solche Verbindungen nicht optimal.
Besser ist es, einen Access Point (AP) zu verwenden oder den WLAN-Router so nah wie möglich an den Geräten zu platzieren.Ich hatte mal bei einem Bekannten exakt den gleichen Fehler ausfindig gemacht:
Im AP war ab Werk bzw. als Standard ein täglicher Neustart hinterlegt, ohne dass er das selbst aktiviert hatte.
Der AP war nach einem Firmware-Update automatisch auf diesen täglichen Neustart konfiguriert worden. -
Ich habe alles geprüft kein Neustart. Bin noch auf der Suche. Informationen folgen wenn ich näheres weiß.
Der zendure meldet sich auf jedenfall im wlan ab und erst wieder an so sah es zumindest aus, wenn er durch die Sonnenstrahlen geweckt wird.
Als wie er nach einer bestimmten Zeit in einen Tiefschlaf gehen würde. Werde dies beobachten. -
Ich habe alles geprüft kein Neustart. Bin noch auf der Suche. Informationen folgen wenn ich näheres weiß.
Der zendure meldet sich auf jedenfall im wlan ab und erst wieder an so sah es zumindest aus, wenn er durch die Sonnenstrahlen geweckt wird.
Als wie er nach einer bestimmten Zeit in einen Tiefschlaf gehen würde. Werde dies beobachten.@Daniel-8
Könnte Firmwarebedingt sein oder mit dem empfindlichen WLAN des esp zu tun haben.Meine WLAN Verbindung ist eigentlich gut genug:

Dennoch habe ich auch 1x am Tag ein reconnect vom Zendure-Gerät.

LOG:
Der 2400ACpro hat vermutlich in dieser Sekunde kurzzeitig die Verbindung zum Netzwerk verloren.
Beim sofortigen Wiederverbindungsversuch lief die alte Sitzung im Mosquitto-Broker noch, weshalb der Broker die alte Verbindung gekappt hat, um die neue Anmeldung des Geräts zuzulassen.Ich könnte nun auch versuchen herauszufinden, ob zu dieser Uhrzeit (01:35 Uhr) eventuell ein geplanter Neustart des Geräts stattgefunden hat.
Aber weil es so kurz ist und sonst alles tadellos funktioniert, werde ich erst mal nur das LOG im Auge behalten.
-
TLS Verbindung Port: 8883
Das wichtigste Ergebnis vorab:
Der herkömmliche, vorgesehene "Normal-" Betrieb über TLS ist absolut stabil möglich.Kann man den Port über die app Option einfach so einstellen?
Um es verständlicher zu erklären: Es geht hier um zwei unterschiedliche MQTT-Verbindungen.
Port 1883 ohne TLS:
Man kann den Port 1883 ohne TLS am Gerät und in der App (gewusst wie) freischalten und aktivieren.
Ab dann ist das Gerät über Port 1883 mit einer DNS-Adresse verbunden.
Diese DNS-Adresse muss per AdGuard, Pi-hole oder über einen eigenen DNS-Server auf den lokalen Broker umgebogen werden, wenn man lokal über die Datenpunkte steuern möchte.
Aber bei dieser Verbindung sind zu viele - ich nenne es mal - Möglichkeiten vorhanden und sollte m. M. n. nur kurzzeitig verwendet werden, näher gehe ich darauf nicht ein.
Port 8883 mit TLS:
Das ist der Standard.
So verbindet sich jedes Gerät.
Das Gerät ist über Port 8883 mit einer DNS-Adresse verbunden.
Diese DNS-Adresse kann ebenfalls per AdGuard, Pi-hole oder DNS-Server auf die IP des lokalen Brokers umgebogen werden, wenn man lokal über die Datenpunkte steuern möchte.Alternativ:
Man konfiguriert die Variable für DNS/IP auf dem Gerät mit dem zURLtool um auf den lokalen Broker.
Auf dem lokalen Broker (z.B. Mosquitto) kann zusätzlich eine Bridge eingerichtet werden, damit das Gerät mit der Cloud verbunden bleibt.
Es ist in wenigen Sekunden erledigt, diese Bridge wahlweise abzuschalten oder der Cloud nur Lesezugriffe oder vollen Zugriff zu erlauben.Mit einem lokalen Broker über MQTT ist die Steuerung zuverlässig, stabil und schneller als über die Cloud oder die lokale API (also auch zenSDK).
zenSDK macht nach meinen Analysen und Tests nur einen kleinen Unterschied, solange kein erhöhter HTTP(S)-Traffic im jeweiligen IP-Bereich bzw. Netzwerk vorliegt.
Wenn diese Voraussetzung gegeben ist und ein Abfrageintervall (Get) von etwa 4000 ms stabil möglich ist, kann auch das zenSDK genutzt werden.
Es ist dann nur unwesentlich langsamer als MQTT.Der Grund dafür ist hardwarebedingt.
Auch wenn MQTT durch den schnelleren, automatischen Bericht-Versand (Report Publish) vom Gerät und das schnellere Publizieren neuer Werte klar im Vorteil ist, benötigt das Gerät hardwarebedingt Zeit.
Eine Limit-Umschaltung von inputLimit oder outputLimit braucht beispielsweise Zeit, um den Zielwert vollständig umzusetzen.Einfacher Vergleich:
Du kannst bei einem Auto mit Automatikgetriebe und Turbolader zwar das Gaspedal oder die Bremse voll durchtreten, aber es dauert dennoch eine gewisse Zeit, bis die Höchstgeschwindigkeit oder der Stillstand erreicht sind.Alle Klarheiten beseitigt? ;-)
Das heißt mit dem läuft alles schneller und über mqtt. Dann bräuchte man Zensdk gar Nicht mehr.
Richtig.
zenSDK ist, wie erklärt, unter den passenden Voraussetzungen langsamer als der lokale Broker über MQTT.
Eigentlich ist MQTT vorzuziehen, da hier nicht auf neue Werte gepollt werden muss, sondern diese sofort automatisch publiziert werden.
Zudem kann die lokale API abgeschaltet werden (gewusst wie) und der ESP32-C wird dadurch weiter entlastet.22.10.2026:
ESP32-C, das ist übrigens die Standard-CPU, die verwendet wird und noch nirgendwo offiziell weltweit dokumentiert wurde.Irgendwie übersteigt das ganze hier jetzt mein können.
Jetzt nicht mehr ;-)
Hinweis:
Ich überlege gerade ein MQTT-Steurungs-Script anzupassen, damit es als Ersatz für das zenSDK-Steuerungs-Script verwendet werden kann.
Damit bleiben alle Datenpunkte identisch, wenn man es gleich konfiguriert wie das zenSDK-Steuerungs-Script.
Bei identischer Benennung und Konfiguration, muss man darauf achten dass nur 1 Steuerungs-Script aktiv ist.Bei unterschiedlicher Benennung kann zenSDK und MQTT gleichzeitig, simultan genutzt werden oder auch nur eins von beiden als vorrangiges Steuerungsscripts.
MQTT liefer natürlich einiges mehr an Infos.
Ich überlege gerade ein MQTT-Steurungs-Script anzupassen, damit es als Ersatz für das zenSDK-Steuerungs-Script verwendet werden kann.
ja Bitte, und da gleich noch die Berechnung Energie- Tages-, Wochen-, Monats- Jahres-ertrag mit rein.
Ich weis, der Sourceanalytix kann das, ist aber immer noch BETA.
Peter
-
Ich überlege gerade ein MQTT-Steurungs-Script anzupassen, damit es als Ersatz für das zenSDK-Steuerungs-Script verwendet werden kann.
ja Bitte, und da gleich noch die Berechnung Energie- Tages-, Wochen-, Monats- Jahres-ertrag mit rein.
Ich weis, der Sourceanalytix kann das, ist aber immer noch BETA.
Peter
Ich überlege gerade ein MQTT-Steurungs-Script anzupassen, damit es als Ersatz für das zenSDK-Steuerungs-Script verwendet werden kann.
ja Bitte,
Soweit fertig.
Das MQTT-JS-Steuerungsskript (mqttSteuerung_zenSDK_Angleich.js) ist fertig und liefert einige Datenpunkte (Möglichkeiten) mehr. Struktur und Datenpunkte sind ansonsten exakt identisch mit dem zenSDK-JS-Steuerungsskript.und da gleich noch die Berechnung Energie- Tages-, Wochen-, Monats- Jahres-ertrag mit rein.
Dazu habe ich ein separates JS-Skript (zendure_energy.js) geschrieben, damit es wahlweise genutzt werden kann. Die Datenpunkte liefern die Werte direkt in ioBroker. Zusätzlich kann man aktivieren, dass die Werte in CSV-Dateien geschrieben und gesichert werden – aufgeteilt nach Tag, Monat und Jahr.
Das Skript funktioniert, sollte jedoch dauerhaft aktiv sein, da die Werte sonst unvollständig sind. Das CSV-Format bietet sich ideal für den Import in Excel, Grafana oder andere Systeme an.Bitte melde dich mal bei mir.
-
zURLtool V 1.0 dch – Lokaler MQTT-Broker für Zendure 2026
Hallo zusammen,das zURLtool V 1.0 dch ist nun fertig!
Ich habe in den letzten Monaten eine eigene Bibliothek sowie eine komfortable grafische Oberfläche (GUI) für Windows entwickelt.Das Programm ist in seiner Art einzigartig, neu und durch monatelanges Reverse-Engineering, Analysen und intensives Testen entstanden.
Es handelt sich um ein reines Windows-GUI-Programm, das ganz einfach per Doppelklick gestartet wird.
Es verwendet keinen Code von anderen bekannten Projekten (wie dem Solarflow Bluetooth Manager oder dem zendure-cloud-disconnector).
Technische Voraussetzungen & Basis:
Das Programm nutzt zur Kommunikation ausschließlich die offiziellen Windows-BT-Treiber ab Windows 11 und die integrierte Bluetooth-Bibliothek bleak:
LICENSE NOTICE FOR THE UTILIZED BLUETOOTH LIBRARY
This program uses "bleak" for Bluetooth functionality.
https://github.com
Copyright (c) 2020, Henrik Blidh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
Die Bibliothek wurde fest in das Programm eingebunden.
Weitere Bibliotheken oder Frameworks müssen auf eurem PC nicht installiert werden.- Betriebssystem: Windows 11 (64-Bit)
- Hardware: Interner Bluetooth-Chip oder ein standardmäßiger USB-Bluetooth-Stick am Rechner.
- Kompatibilität: Getestet mit Zendure-Geräten bis zur 2400er-Serie (sowohl ältere als auch neueste Generationen / Firmware-Stände).
Inhalt des Programmpakets (zURLtool.zip):
Das ZIP-Archiv enthält genau drei Dateien, die zwingend im selben Ordner liegen bleiben müssen:- zURLtool_GUI.exe (Größe: ca. 11.6 MB / 11.921 KB)
- config.ini (Konfigurationsdatei für eure Zugangsdaten)
- ReadMe_zURLtool.txt (Ausführliche Anleitung in Deutsch & Englisch)
Was genau macht das zURLtool?
Das Tool schreibt im Zendure-Gerät die Konfiguration der Broker-URL um.
-
Es wird NICHT in die Firmware eingegriffen und es wird nichts modifiziert. Die originale Firmware von Zendure bleibt zu 100 % unangetastet.
-
Es werden lediglich die standardmäßigen Bluetooth-Befehle verwendet, die die Firmware von Haus aus für die App bereitstellt.
-
Das Skript verhält sich für das Gerät exakt wie die originale App, greift auf die Konfigurations-Variablen im Speicher zu und überschreibt dort die Wi-Fi-Daten und den Eintrag für die Cloud-URL mit der IP eures eigenen lokalen Brokers.
Es funktioniert quasi wie eine zusätzliche Länderauswahl, bei der man sich einfach mit einem anderen Server verbindet.
Das bietet die Firmware von Haus aus an, da die Geräte sonst permanent fest an ganz bestimmte AWS-Server gebunden wären.Zurück zur Cloud ist auch möglich. Beispiele sind in der config.ini enthalten.
Die Geräte verbinden sich und können beschrieben werden ohne hartes Reset und ohne hartes Trennen von allen Stromquellen.
Warum ist das Programm Closed Source?
Sollte das Programm oder der Code in irgendeiner Form gehackt zur Verfügung gestellt oder verwendet werden, dann war dies mein letzter offizieller Beitrag in Form von Code für die Community.
edit/update 16.08.26 Freigabe vereinfacht.
Der Freischalt-Ablauf:
Vor dem ersten Start muss die config.ini in einem einfachen Text-Editor mit euren echten Daten befüllt werden.
Auswählbare Beispiele und Erklärungen zu den WLAN-Modi sind direkt darin enthalten.

Bilder im Spoiler sagen mehr als Worte, ich denke die Screenshots sind selbsterklärend:
WICHTIGER HINWEIS ZU DEN NEUEN GERÄTEN ODER NEUER FIRMWARE (TLS/Port 8883):
Dieses Tool ist ein reines Konfigurations-Werkzeug und schreibt via Bluetooth ausschließlich die WLAN-Daten und die Broker-URL im Gerät um.
Das Tool generiert KEINE TLS-Zertifikate oder Keys!Um die Bereitstellung der passenden Verschlüsselung auf eurem lokalen MQTT-Broker müsst ihr euch zwingend selbst kümmern.
Wer hierzu Fragen hat: Das Internet und bekannte Foren helfen zu verschlüsselten Mosquitto-Brokern weiter.Von mir wird es dazu keinen Support und keine Zertifikats-Dateien geben.
Viel Spaß mit dem zURLtool!
DOWNLOAD & ENTPACK-ANLEITUNG
zURLtool_V20 (c) maxclaudi 2026 - Version 2.0 cdh
Neu in zURLtool V 2.0 dch
• Sprachauswahl: Deutsch und Englisch sind auswählbar und während der Laufzeit umschaltbar.• Reset-Funktion: Geräte können zurückgesetzt werden (Hinweis: Zum vollständigen Abmelden - z.B. bei Verkauf - Gerät auch in der App entfernen!).
• WLAN-Handling: Das manuelle Setzen des WiFi-Mode entfällt. Ein automatischer Scan zeigt alle vom Zendure-Gerät erreichbaren Netze an. Bei zur config.ini abweichendem, manuell ausgewählten WLAN muss auch das Passwort in der GUI angepasst werden.
• Offline-Modus: Die Option OFFLINE = 1 in der config.ini überspringt den WLAN-Scan, ideal für Vorbereitungen oder Konfigurationen ohne aktiven Router.
• Token & User-ID: Gerätespezifischer Token wird in mqtt_creds.txt gesichert. Für die korrekte userId (bzw. consumerId) empfiehlt sich der Einsatz des Zusatztools zAppEmulationDeviceInfo
• Ein Backup mit den verwendeten Daten in der config.ini wird zusätzlich gespeichert. Beispiel: bak_config_26.10.26_14.51.35.ini
• Hinweis zu TLS / Port 8883: Das Tool ist ein reiner Konfigurator per Bluetooth und generiert keine TLS-Zertifikate oder Keys.


Beschreibung im Spoiler.
Bitte ReadMe_zURLtoolV20.txt beachten, enthält die deutsche und englische Beschreibung.
HINWEIS FÜR BESTEHENDE NUTZER:
Wer bereits für die Version 1.0 eine Seriennummer und einen Freischalt-Code erhalten hat, kann diese Daten direkt weiterverwenden!
Sie sind auch für die Version 2.0 gültig.
Bitte in diesem Fall keine erneute Anfrage.RECHTLICHER HINWEIS ZUR FREIGABE:
Eine Weitergabe von Seriennummer und Freigabe-Code
sowie deren Mehrfachnutzung durch verschiedene Personen ist ausdrücklich UNZULÄSSIG.
Please note ReadMe_zURLtoolV20.txt: contains the German and English description.
NOTE FOR EXISTING USERS:
If you have already received a serial number and activation code for version 1.0, you can reuse them directly!
They are valid for version 2.0 as well. Please do not submit a new request in this case.LEGAL NOTICE ON ACTIVATION:
Sharing the serial number and activation code, as well as their multiple use by different individuals, is strictly PROHIBITED. -
zURLtool_V20 (c) maxclaudi 2026 - Version 2.0 cdh
Neu in zURLtool V 2.0 dch
• Sprachauswahl: Deutsch und Englisch sind auswählbar und während der Laufzeit umschaltbar.• Reset-Funktion: Geräte können zurückgesetzt werden (Hinweis: Zum vollständigen Abmelden - z.B. bei Verkauf - Gerät auch in der App entfernen!).
• WLAN-Handling: Das manuelle Setzen des WiFi-Mode entfällt. Ein automatischer Scan zeigt alle vom Zendure-Gerät erreichbaren Netze an. Bei zur config.ini abweichendem, manuell ausgewählten WLAN muss auch das Passwort in der GUI angepasst werden.
• Offline-Modus: Die Option OFFLINE = 1 in der config.ini überspringt den WLAN-Scan, ideal für Vorbereitungen oder Konfigurationen ohne aktiven Router.
• Token & User-ID: Gerätespezifischer Token wird in mqtt_creds.txt gesichert. Für die korrekte userId (bzw. consumerId) empfiehlt sich der Einsatz des Zusatztools zAppEmulationDeviceInfo
• Ein Backup mit den verwendeten Daten in der config.ini wird zusätzlich gespeichert. Beispiel: bak_config_26.10.26_14.51.35.ini
• Hinweis zu TLS / Port 8883: Das Tool ist ein reiner Konfigurator per Bluetooth und generiert keine TLS-Zertifikate oder Keys.


Beschreibung im Spoiler.
Bitte ReadMe_zURLtoolV20.txt beachten, enthält die deutsche und englische Beschreibung.
HINWEIS FÜR BESTEHENDE NUTZER:
Wer bereits für die Version 1.0 eine Seriennummer und einen Freischalt-Code erhalten hat, kann diese Daten direkt weiterverwenden!
Sie sind auch für die Version 2.0 gültig.
Bitte in diesem Fall keine erneute Anfrage.RECHTLICHER HINWEIS ZUR FREIGABE:
Eine Weitergabe von Seriennummer und Freigabe-Code
sowie deren Mehrfachnutzung durch verschiedene Personen ist ausdrücklich UNZULÄSSIG.
Please note ReadMe_zURLtoolV20.txt: contains the German and English description.
NOTE FOR EXISTING USERS:
If you have already received a serial number and activation code for version 1.0, you can reuse them directly!
They are valid for version 2.0 as well. Please do not submit a new request in this case.LEGAL NOTICE ON ACTIVATION:
Sharing the serial number and activation code, as well as their multiple use by different individuals, is strictly PROHIBITED.Download zURLtoolV20.zip in 4 Teilen.
Teil 1v4:Achtung: Falls die Datei nach dem Download einen anderen Namen als zURLtoolV20.zip hat, benenne sie bitte entsprechend um.
Es kann und soll gerne ein Virenscan erfolgen!HINWEIS ZUM ENTPACKEN (MEHRTEILIGES ARCHIV):
Das Programmpaket ist auf 4 Teile aufgeteilt. Bitte verwende zum Entpacken
ein vollwertiges Packprogramm wie 7-Zip, WinRAR oder Bandizip. Das integrierte Windows-eigene ZIP-Programm kann mehrteilige Archive NICHT korrekt zusammensetzen und führt zu Fehlfunktionen oder Fehlermeldungen beim Entpacken! -
zURLtool_V20 (c) maxclaudi 2026 - Version 2.0 cdh
Neu in zURLtool V 2.0 dch
• Sprachauswahl: Deutsch und Englisch sind auswählbar und während der Laufzeit umschaltbar.• Reset-Funktion: Geräte können zurückgesetzt werden (Hinweis: Zum vollständigen Abmelden - z.B. bei Verkauf - Gerät auch in der App entfernen!).
• WLAN-Handling: Das manuelle Setzen des WiFi-Mode entfällt. Ein automatischer Scan zeigt alle vom Zendure-Gerät erreichbaren Netze an. Bei zur config.ini abweichendem, manuell ausgewählten WLAN muss auch das Passwort in der GUI angepasst werden.
• Offline-Modus: Die Option OFFLINE = 1 in der config.ini überspringt den WLAN-Scan, ideal für Vorbereitungen oder Konfigurationen ohne aktiven Router.
• Token & User-ID: Gerätespezifischer Token wird in mqtt_creds.txt gesichert. Für die korrekte userId (bzw. consumerId) empfiehlt sich der Einsatz des Zusatztools zAppEmulationDeviceInfo
• Ein Backup mit den verwendeten Daten in der config.ini wird zusätzlich gespeichert. Beispiel: bak_config_26.10.26_14.51.35.ini
• Hinweis zu TLS / Port 8883: Das Tool ist ein reiner Konfigurator per Bluetooth und generiert keine TLS-Zertifikate oder Keys.


Beschreibung im Spoiler.
Bitte ReadMe_zURLtoolV20.txt beachten, enthält die deutsche und englische Beschreibung.
HINWEIS FÜR BESTEHENDE NUTZER:
Wer bereits für die Version 1.0 eine Seriennummer und einen Freischalt-Code erhalten hat, kann diese Daten direkt weiterverwenden!
Sie sind auch für die Version 2.0 gültig.
Bitte in diesem Fall keine erneute Anfrage.RECHTLICHER HINWEIS ZUR FREIGABE:
Eine Weitergabe von Seriennummer und Freigabe-Code
sowie deren Mehrfachnutzung durch verschiedene Personen ist ausdrücklich UNZULÄSSIG.
Please note ReadMe_zURLtoolV20.txt: contains the German and English description.
NOTE FOR EXISTING USERS:
If you have already received a serial number and activation code for version 1.0, you can reuse them directly!
They are valid for version 2.0 as well. Please do not submit a new request in this case.LEGAL NOTICE ON ACTIVATION:
Sharing the serial number and activation code, as well as their multiple use by different individuals, is strictly PROHIBITED.Download zURLtoolV20.zip in 4 Teilen.
Teil 2v4:Achtung: Falls die Datei nach dem Download einen anderen Namen als zURLtoolV20.z01 hat, benenne sie bitte entsprechend um.
Es kann und soll gerne ein Virenscan erfolgen!HINWEIS ZUM ENTPACKEN (MEHRTEILIGES ARCHIV):
Das Programmpaket ist auf 4 Teile aufgeteilt. Bitte verwende zum Entpacken
ein vollwertiges Packprogramm wie 7-Zip, WinRAR oder Bandizip. Das integrierte Windows-eigene ZIP-Programm kann mehrteilige Archive NICHT korrekt zusammensetzen und führt zu Fehlfunktionen oder Fehlermeldungen beim Entpacken! -
zURLtool_V20 (c) maxclaudi 2026 - Version 2.0 cdh
Neu in zURLtool V 2.0 dch
• Sprachauswahl: Deutsch und Englisch sind auswählbar und während der Laufzeit umschaltbar.• Reset-Funktion: Geräte können zurückgesetzt werden (Hinweis: Zum vollständigen Abmelden - z.B. bei Verkauf - Gerät auch in der App entfernen!).
• WLAN-Handling: Das manuelle Setzen des WiFi-Mode entfällt. Ein automatischer Scan zeigt alle vom Zendure-Gerät erreichbaren Netze an. Bei zur config.ini abweichendem, manuell ausgewählten WLAN muss auch das Passwort in der GUI angepasst werden.
• Offline-Modus: Die Option OFFLINE = 1 in der config.ini überspringt den WLAN-Scan, ideal für Vorbereitungen oder Konfigurationen ohne aktiven Router.
• Token & User-ID: Gerätespezifischer Token wird in mqtt_creds.txt gesichert. Für die korrekte userId (bzw. consumerId) empfiehlt sich der Einsatz des Zusatztools zAppEmulationDeviceInfo
• Ein Backup mit den verwendeten Daten in der config.ini wird zusätzlich gespeichert. Beispiel: bak_config_26.10.26_14.51.35.ini
• Hinweis zu TLS / Port 8883: Das Tool ist ein reiner Konfigurator per Bluetooth und generiert keine TLS-Zertifikate oder Keys.


Beschreibung im Spoiler.
Bitte ReadMe_zURLtoolV20.txt beachten, enthält die deutsche und englische Beschreibung.
HINWEIS FÜR BESTEHENDE NUTZER:
Wer bereits für die Version 1.0 eine Seriennummer und einen Freischalt-Code erhalten hat, kann diese Daten direkt weiterverwenden!
Sie sind auch für die Version 2.0 gültig.
Bitte in diesem Fall keine erneute Anfrage.RECHTLICHER HINWEIS ZUR FREIGABE:
Eine Weitergabe von Seriennummer und Freigabe-Code
sowie deren Mehrfachnutzung durch verschiedene Personen ist ausdrücklich UNZULÄSSIG.
Please note ReadMe_zURLtoolV20.txt: contains the German and English description.
NOTE FOR EXISTING USERS:
If you have already received a serial number and activation code for version 1.0, you can reuse them directly!
They are valid for version 2.0 as well. Please do not submit a new request in this case.LEGAL NOTICE ON ACTIVATION:
Sharing the serial number and activation code, as well as their multiple use by different individuals, is strictly PROHIBITED.Download zURLtoolV20.zip in 4 Teilen.
Teil 3v4:Achtung: Falls die Datei nach dem Download einen anderen Namen als zURLtoolV20.z02 hat, benenne sie bitte entsprechend um.
Es kann und soll gerne ein Virenscan erfolgen!HINWEIS ZUM ENTPACKEN (MEHRTEILIGES ARCHIV):
Das Programmpaket ist auf 4 Teile aufgeteilt. Bitte verwende zum Entpacken
ein vollwertiges Packprogramm wie 7-Zip, WinRAR oder Bandizip. Das integrierte Windows-eigene ZIP-Programm kann mehrteilige Archive NICHT korrekt zusammensetzen und führt zu Fehlfunktionen oder Fehlermeldungen beim Entpacken! -
zURLtool_V20 (c) maxclaudi 2026 - Version 2.0 cdh
Neu in zURLtool V 2.0 dch
• Sprachauswahl: Deutsch und Englisch sind auswählbar und während der Laufzeit umschaltbar.• Reset-Funktion: Geräte können zurückgesetzt werden (Hinweis: Zum vollständigen Abmelden - z.B. bei Verkauf - Gerät auch in der App entfernen!).
• WLAN-Handling: Das manuelle Setzen des WiFi-Mode entfällt. Ein automatischer Scan zeigt alle vom Zendure-Gerät erreichbaren Netze an. Bei zur config.ini abweichendem, manuell ausgewählten WLAN muss auch das Passwort in der GUI angepasst werden.
• Offline-Modus: Die Option OFFLINE = 1 in der config.ini überspringt den WLAN-Scan, ideal für Vorbereitungen oder Konfigurationen ohne aktiven Router.
• Token & User-ID: Gerätespezifischer Token wird in mqtt_creds.txt gesichert. Für die korrekte userId (bzw. consumerId) empfiehlt sich der Einsatz des Zusatztools zAppEmulationDeviceInfo
• Ein Backup mit den verwendeten Daten in der config.ini wird zusätzlich gespeichert. Beispiel: bak_config_26.10.26_14.51.35.ini
• Hinweis zu TLS / Port 8883: Das Tool ist ein reiner Konfigurator per Bluetooth und generiert keine TLS-Zertifikate oder Keys.


Beschreibung im Spoiler.
Bitte ReadMe_zURLtoolV20.txt beachten, enthält die deutsche und englische Beschreibung.
HINWEIS FÜR BESTEHENDE NUTZER:
Wer bereits für die Version 1.0 eine Seriennummer und einen Freischalt-Code erhalten hat, kann diese Daten direkt weiterverwenden!
Sie sind auch für die Version 2.0 gültig.
Bitte in diesem Fall keine erneute Anfrage.RECHTLICHER HINWEIS ZUR FREIGABE:
Eine Weitergabe von Seriennummer und Freigabe-Code
sowie deren Mehrfachnutzung durch verschiedene Personen ist ausdrücklich UNZULÄSSIG.
Please note ReadMe_zURLtoolV20.txt: contains the German and English description.
NOTE FOR EXISTING USERS:
If you have already received a serial number and activation code for version 1.0, you can reuse them directly!
They are valid for version 2.0 as well. Please do not submit a new request in this case.LEGAL NOTICE ON ACTIVATION:
Sharing the serial number and activation code, as well as their multiple use by different individuals, is strictly PROHIBITED.Download zURLtoolV20.zip in 4 Teilen.
Teil 4v4:Achtung: Falls die Datei nach dem Download einen anderen Namen als zURLtoolV20.z03 hat, benenne sie bitte entsprechend um.
Es kann und soll gerne ein Virenscan erfolgen!HINWEIS ZUM ENTPACKEN (MEHRTEILIGES ARCHIV):
Das Programmpaket ist auf 4 Teile aufgeteilt. Bitte verwende zum Entpacken
ein vollwertiges Packprogramm wie 7-Zip, WinRAR oder Bandizip. Das integrierte Windows-eigene ZIP-Programm kann mehrteilige Archive NICHT korrekt zusammensetzen und führt zu Fehlfunktionen oder Fehlermeldungen beim Entpacken!Für ein Dankeschön reicht ein Voting HIER aus ;-)
-
20.10.2026; 15:18h Fortsetzung von:
15.10.2026; 16:00 Uhr: Weiterer Bericht zum Betrieb über MQTT-Port 1883 und zenSDK POST #628
Da es dazu weltweit quasi null Infos gibt, dachte ich, ich teile meine bisherigen Erkenntnisse mal auszugsweise als kleinen Bericht mit euch.
Die Fortsetzung ist vermutlich gleichzeitig der Abschluss.
Wer möchte, kann den Bericht im Spoiler lesen;
für alle anderen ist der Beitrag dann kürzer und kann ignoriert werden. :-)
edit 21.10.26; 10:35h
Keine Tester mehr gesucht.
Tester und Nuzer denen ich mit mosquitto broker half, bitte per PM oder email melden. Danke.Testers and users whom I have assisted while they were working with the Mosquitto broker: please get in touch with me via pm or email. Thanks.
maxclaudi sagte:
20.10.2026; 15:18h Fortsetzung von:
15.10.2026; 16:00 Uhr: Weiterer Bericht zum Betrieb über MQTT-Port 1883 und zenSDK POST #628
Da es dazu weltweit quasi null Infos gibt, dachte ich, ich teile meine bisherigen Erkenntnisse mal auszugsweise als kleinen Bericht mit euch.
Die Fortsetzung ist vermutlich gleichzeitig der Abschluss.
Wie geht es weiter?
..... Evtl. widme ich mich nun noch etwas dem Thema hemsEp NVS/RAM – hat aber keine Priorität für mich, weil ich es nicht nutzen möchte.Erste Analysen zu HEMS.
Interessant ist z. B. Folgendes, was den acMode betrifft, wenn man selbst über ein Script steuert:Auszug HEMS - Analyse (vermutlich weltweit die erste Veröffentlichung dazu 27.10.2026; 12:44h):
Firmware enthält eine explizite zeitliche Sperre bei hemsEP mit 6 Sekunden Mindestabstand zwischen zwei Mode-Wechseln – das ist eine bewusste Entscheidung der Firmware-Autoren für genau diese eine, sicherheitsrelevante Funktion (Lademodus-Wechsel).Testers and users whom I have assisted while they were working with the Mosquitto broker: please get in touch with me via pm or email. Thanks.
Edit 28.10.2026, 11:00 Uhr
Neu: Soft-Reset – Analyse und Anwendung lokal im ioBroker mittels MQTT und zenSDK möglich – ohne dass in NVS (FLASH) geschrieben wird.detaillierter im Spoiler.
-
maxclaudi sagte:
20.10.2026; 15:18h Fortsetzung von:
15.10.2026; 16:00 Uhr: Weiterer Bericht zum Betrieb über MQTT-Port 1883 und zenSDK POST #628
Da es dazu weltweit quasi null Infos gibt, dachte ich, ich teile meine bisherigen Erkenntnisse mal auszugsweise als kleinen Bericht mit euch.
Die Fortsetzung ist vermutlich gleichzeitig der Abschluss.
Wie geht es weiter?
..... Evtl. widme ich mich nun noch etwas dem Thema hemsEp NVS/RAM – hat aber keine Priorität für mich, weil ich es nicht nutzen möchte.Erste Analysen zu HEMS.
Interessant ist z. B. Folgendes, was den acMode betrifft, wenn man selbst über ein Script steuert:Auszug HEMS - Analyse (vermutlich weltweit die erste Veröffentlichung dazu 27.10.2026; 12:44h):
Firmware enthält eine explizite zeitliche Sperre bei hemsEP mit 6 Sekunden Mindestabstand zwischen zwei Mode-Wechseln – das ist eine bewusste Entscheidung der Firmware-Autoren für genau diese eine, sicherheitsrelevante Funktion (Lademodus-Wechsel).Testers and users whom I have assisted while they were working with the Mosquitto broker: please get in touch with me via pm or email. Thanks.
Edit 28.10.2026, 11:00 Uhr
Neu: Soft-Reset – Analyse und Anwendung lokal im ioBroker mittels MQTT und zenSDK möglich – ohne dass in NVS (FLASH) geschrieben wird.detaillierter im Spoiler.
Wer zusätzlich einen Reset-Button bzw. einen Datenpunkt für einen Soft-Reset anlegen möchte.
Hier ist das bisherige zenSDK-Steuerungs-Script, erweitert um eine Soft-Reset-Funktion für das Gerät.
Der Reset bzw. Neustart wird ausgeführt, ohne in den Flash-Speicher zu schreiben.Dazu wird unter control ein neuer Datenpunkt angelegt:
control.deviceReset// 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; update 25.06.26 10.30h control FAN // update 06.10.26 02:00h neu set batCalTime, fix acMode/auto_in_out_Limit - Übernehmer von Logiken etc. könnten sich auch mal "wenigstens" bedanken! // update 09.10.26 11:30h acMode/auto_in_out_Limit Standardwert Intervall Danke Danitz und petman für die Hinweise. // update 16.10.26 21:00h batCalTime zusätzliche Type-Prüfung; fallback 01:00h / control.socCompSwitch ohne Funktion, entfernt // update 28.10.26 15:30h deviceReset unter control. Löst einen Geräte Soft-Reset aus ohne in den Flash zu schreiben. // // 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. 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 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 = 2400; //maximum outputLimit -> Maximal möglich (unterstützte) Entladeleistung des Zendure-Geräts //Beispiele: SF800 800W / SF800 PRO: 800W / 1600AC: 1600 / SF2400AC: 2400W const maxOutputLimit = 2400; // Haupt-Verzeichnis für das Zendure-Gerät // Der Name für das Hauptverzeichnis kann frei gewählt werden. // Bitte kein "+", keine Sonderzeichen und keine Umlaute etc. verwenden! // Muss aber bei mehreren Skripten und Zendure-Geräte unterschiedlich sein. // Beispiel für "2400ACproZENsdk": //const folderZendureApi = '0_userdata.0.zendure.' + "2400ACproZENsdk"; const folderZendureApi = '0_userdata.0.zendure.' + "2400ACproZENsdk"; // 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 kleiner 4000 m. M. nach sinnfrei und nicht empfohlen (Last / Stabilität). // HINWEIS: Original Firmwarecode verifiziert, dass z.B. bei MQTT Standard alle 5000ms ein report published wird. // wenn noch schneller abgefragt wird, kann es vorkommen, dass alte/identische Werte (hardwarebedingt) empfangen werden. 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"; const dpDeviceReset = folderZendureApi + ".control.deviceReset"; // 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 = ''; // Device-Reset (MQTT-Konfig lesen und setzen) const resetTimeoutMs = 5000; const resetWaitMaxMs = 20000; let getActive = false; let deviceResetBusy = false; // 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); if (existsState(dpDeviceReset)) setState(dpDeviceReset, false, true); } function hhmmToMinutes(str) { const m = /^(\d{1,2}):(\d{2})$/.exec(String(str).trim()); if (!m) return 60; const total = (parseInt(m[1], 10) * 60) + parseInt(m[2], 10); return (total > 1439) ? 60 : total; } function minutesToHhmm(min) { const n = Math.round(Number(min)); if (!Number.isFinite(n) || n < 0 || n > 1439) return String(min); return String(Math.floor(n / 60)).padStart(2, "0") + ":" + String(n % 60).padStart(2, "0"); } // Control const schemaControl = { acMode: { name: "set acMode; 1: input charge, 2: output discharge", role: "level", type: "number", write: true, def: 2, min: 1, max: 2, apiKey: "acMode", }, batCalTime: { name: "set Calibration-Start-Time: 00:00 to 23:59 - on error: 01:00h (HH:MM)", role: "text", unit: "h", type: "string", write: true, def: "01:00", pattern: /^([01][0-9]|2[0-3]):[0-5][0-9]$/, apiKey: "batCalTime", transformWrite: (val) => hhmmToMinutes(val), transformRead: (val) => minutesToHhmm(val), }, chargeMaxLimit: { name: "set Charge Max Limit", unit: "W", role: "level", type: "number", write: true, def: maxInputLimit, min: 0, max: maxInputLimit, apiKey: "chargeMaxLimit", }, Fanmode: { name: "set Fanmode; 0: Fan OFF, 1: Fan ON", role: "level", type: "number", write: true, def: 1, min: 0, max: 1, apiKey: "Fanmode", }, Fanspeed: { name: "set Fanspeed; 0: Automatic, 1: 1st gear, 2: 2nd gear", role: "level", type: "number", write: true, def: 0, min: 0, max: 2, apiKey: "Fanspeed", }, 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", }, 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, def.type === "string" ? def.def : 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 }); } }); } // dp Device-Reset if (!existsState(dpDeviceReset)) { createState(dpDeviceReset, false, { name: "deviceReset; true: MQTT-Konfiguration per HA.Mqtt.GetConfig lesen und per HA.Mqtt.SetConfig erneut setzen", type: "boolean", role: "button", read: true, write: true, def: false }); } on({ id: new RegExp(`^${folderZendureApi}\\.control\\.`), change: "ne" }, obj => { if (obj.state.ack) return; const key = obj.id.split('.').pop(); if (key === "deviceReset") { if (obj.state.val === true) deviceReset(); return; } 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.type === "string" && def.pattern) { val = String(val).trim(); if (!def.pattern.test(val)) { log(`${key}: Fehlerhafte Eingabe "${val}"; Standard ${def.def} wird verwendet.`, "info"); val = def.def; } } 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); } if (key === "batCalTime" && getState(folderZendureApi + ".properties.batCalTime")?.val === sendVal) { setState(obj.id, val, true); // Gerät hat den Wert schon -> nichts senden return; } 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 || deviceResetBusy) return; // während Reset bleibt die Queue erhalten 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, ...postQueue }; postActive = false; executePost(); }, postCooldownMs); return; } log("POST endgültig fehlgeschlagen", "error"); retryCount = 0; finishPost(false); }); } function finishPost(success) { if (!success) { syncControlFromProperties(); } setTimeout(() => { postActive = false; if (deviceResetBusy) return; // Reset übernimmt + startet GET-Zyklus selbst wieder startGetLoop(); }, postCooldownMs); } function syncAutoInOutLimit(diffProps) { const prop = folderZendureApi + ".properties."; const pick = (k) => (diffProps && diffProps[k] !== undefined) ? diffProps[k] : getState(prop + k)?.val; const acMode = pick("acMode"); const inp = pick("inputLimit"); const out = pick("outputLimit"); let val; if (acMode === 2 && out !== undefined) val = out; else if (acMode === 1 && inp !== undefined) val = -inp; else if (acMode === 0) val = 0; else return; val = val || 0; // -0 vermeiden const id = folderZendureApi + ".control.auto_in_out_Limit"; if (getState(id)?.val !== val) setState(id, val, true); } 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 = def.transformRead ? def.transformRead(propState.val) : 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); } } syncAutoInOutLimit(null); } function startGetLoop() { if (getTimer) { clearInterval(getTimer); } getTimer = setInterval(() => { if (postActive || deviceResetBusy || getActive) return; getActive = true; 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"); }) .finally(function () { getActive = false; }); }, getIntervalMs); } function stopGetLoop() { if (getTimer) { clearInterval(getTimer); getTimer = null; } } // Device-Reset: aktuelle Konfiguration lesen und erneut setzen, ohne parallele reguläre GET/POST. function waitFor(conditionFn, maxMs, stepMs = 100) { return new Promise(resolve => { if (conditionFn()) { resolve(true); return; } const start = Date.now(); const t = setInterval(() => { if (conditionFn()) { clearInterval(t); resolve(true); } else if (Date.now() - start >= maxMs) { clearInterval(t); resolve(false); } }, stepMs); }); } async function deviceReset() { if (deviceResetBusy) return; deviceResetBusy = true; stopGetLoop(); try { const idle = await waitFor(() => !getActive && !postActive, resetWaitMaxMs); if (!idle) { log("Device-Reset abgebrochen: laufende HTTP-Anfragen wurden nicht rechtzeitig beendet", "warn"); return; } if (!SN || SN === "null") { log("Device-Reset abgebrochen: Seriennummer (SN) noch nicht bekannt", "warn"); return; } const rpcUrl = `http://${IP}/rpc`; const getResponse = await axios.get(rpcUrl + "?method=HA.Mqtt.GetConfig", { timeout: resetTimeoutMs }); const response = getResponse.data; if (!response || response.result !== true || !response.data) { log("Device-Reset: ungültige GET-Antwort: " + JSON.stringify(response), "warn"); return; } const current = response.data; //log("Device-Reset GET-Konfiguration: " + JSON.stringify({ ...current, password: "***" }), "info"); const match = String(current.server).match(/^(mqtt|mqtts):\/\/([^:/]+)(?::(\d+))?$/i); if (!match) { log("Device-Reset: Serveradresse nicht erkannt: " + current.server, "warn"); return; } const protocol = match[1]; const server = match[2]; const port = match[3] ? Number(match[3]) : (protocol.toLowerCase() === "mqtts" ? 8883 : 1883); const payload = { sn: SN, method: "HA.Mqtt.SetConfig", params: { config: { enable: current.enable, server: server, port: port, protocol: protocol, username: current.username, password: current.password } } }; const postResponse = await axios.post(rpcUrl, payload, { headers: { "Content-Type": "application/json" }, timeout: resetTimeoutMs }); } catch (error) { if (error.response) { log("Device-Reset HTTP-Status: " + error.response.status, "warn"); log("Device-Reset Antwort: " + JSON.stringify(error.response.data), "warn"); } else { log("Device-Reset Fehler: " + error.message, "error"); } } finally { deviceResetBusy = false; setState(dpDeviceReset, false, true); if (Object.keys(postQueue).length > 0) { executePost(); } else { startGetLoop(); } } } function handleInputLimitMode(val) { const currentOutput = Number(getState(folderZendureApi + ".properties.outputLimit")?.val); const currentSmartMode = getState(folderZendureApi + ".properties.smartMode")?.val; const smartRAMActive = getState(dpSmartRAM)?.val; const payload = {}; 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 currentInput = Number(getState(folderZendureApi + ".properties.inputLimit")?.val); const currentSmartMode = getState(folderZendureApi + ".properties.smartMode")?.val; const smartRAMActive = getState(dpSmartRAM)?.val; const payload = {}; 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 currentInput = Number(getState(folderZendureApi + ".properties.inputLimit")?.val); const currentOutput = Number(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; } else if (val > 0) { payload.acMode = 2; if (currentInput !== 0) payload.inputLimit = 0; payload.outputLimit = val; } else if (val < 0) { payload.acMode = 1; if (currentOutput !== 0) payload.outputLimit = 0; payload.inputLimit = Math.abs(val); } if (smartRAMActive === true && currentSmartMode !== 1) payload.smartMode = 1; if (Object.keys(payload).length > 0) 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", role: "value", type: "number" }, Fanspeed: { name: "Fanspeed - 0: Auto, 1: 1st gear, 2: 2nd gear", 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 Start-Time", 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" }, IOTState: { name: "Network 0: disconnected, 1: is being configured, 2: Connection successful", role: "value", 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}`, def.transformRead ? def.transformRead(full.properties[apiKey]) : 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); } syncAutoInOutLimit(full.properties); } 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.acMode !== undefined || diff.properties.inputLimit !== undefined || diff.properties.outputLimit !== undefined) { syncAutoInOutLimit(diff.properties); } 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}`, def.transformRead ? def.transformRead(diff.properties[apiKey]) : 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 "batCalTime": createOrSet(`${folderZendureApi}.properties.batCalTime_h`, minutesToHhmm(val), { type: "string", role: "text", name: "batCalTime_h - Battery Calibration Time (HH:MM)", unit: "h" }); 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(); -
Nachdem ich jetzt ein paar Tage den Fehler Zendure IP überprüfen beobachtet habe, kann ich folgendes feststelln.
Der Fehler tritt immer auf, wenn das Gerät am Abend mit Sonnenuntergang bzw. PV Eingang 0 Watt hat und wegen dem Wetter so wenig ertrag kam, das er auch gleich auf SOC min ist. Somit werden über sehr lange Zeit keine Steuerbefehle mehr gesendet und ich vermute, das das Gerät dann nach ca.13 Stunden in einen Tiefschlaf geht.
Es meldet sich dann vom WLAN ab und erst nachdem wieder ein bisschen PV kommt wieder an.
Die anderen 2 Tage habe ich länger eingespeist und dann erschien der Fehler am nächsten morgen nicht. -
Nachdem ich jetzt ein paar Tage den Fehler Zendure IP überprüfen beobachtet habe, kann ich folgendes feststelln.
Der Fehler tritt immer auf, wenn das Gerät am Abend mit Sonnenuntergang bzw. PV Eingang 0 Watt hat und wegen dem Wetter so wenig ertrag kam, das er auch gleich auf SOC min ist. Somit werden über sehr lange Zeit keine Steuerbefehle mehr gesendet und ich vermute, das das Gerät dann nach ca.13 Stunden in einen Tiefschlaf geht.
Es meldet sich dann vom WLAN ab und erst nachdem wieder ein bisschen PV kommt wieder an.
Die anderen 2 Tage habe ich länger eingespeist und dann erschien der Fehler am nächsten morgen nicht.Nachdem ich jetzt ein paar Tage den Fehler Zendure IP überprüfen beobachtet habe, kann ich folgendes feststelln.
Der Fehler tritt immer auf, wenn das Gerät am Abend mit Sonnenuntergang bzw. PV Eingang 0 Watt hat und wegen dem Wetter so wenig ertrag kam, das er auch gleich auf SOC min ist. Somit werden über sehr lange Zeit keine Steuerbefehle mehr gesendet und ich vermute, das das Gerät dann nach ca.13 Stunden in einen Tiefschlaf geht.
Das ist absolut nachvollziehbar.
Das Abschalten des WLAN-Moduls oder das Versetzen der MCU in einen tiefen Schlafmodus ist eine gängige Schutzfunktion, da die Netzwerkkommunikation einen nicht unerheblichen Teil des Eigenstrombedarfs des Geräts ausmacht.
Wenn über Stunden keine Aktivität (weder PV-Ertrag noch Steuerbefehle) registriert wird, greift irgendwann das Energiemanagement, um eine Tiefentladung der Zellen zu verhindern.Interessant wäre nun zu wissen:
• Auf wie viel % steht dein minSoc aktuell?
• Welchen Wert hat/haben die Batterie(n) dabei bei minVol (niedrigste Zellspannung)?Zusätzlich wäre es gut zu beobachten, wie sich die originale App in diesem Zustand verhält:
• Meldet die App das Gerät zu diesem Zeitpunkt noch als online?
• Lassen sich über die App noch Werte verändern?Falls ja, wäre wichtig festzustellen, ob das Gerät durch die App-Aktivität kurzzeitig aus dem Tiefschlaf erwacht und diese Befehle dann auch tatsächlich ausführt.
Das würde uns zeigen, ob das Gerät im Schlafmodus noch auf bestimmte Wake-up-Signale (z. B. via Bluetooth oder die Cloud-Schnittstelle) reagiert.edit/PS: ist die Akku-Heizung aktiv?
-
Nachdem ich jetzt ein paar Tage den Fehler Zendure IP überprüfen beobachtet habe, kann ich folgendes feststelln.
Der Fehler tritt immer auf, wenn das Gerät am Abend mit Sonnenuntergang bzw. PV Eingang 0 Watt hat und wegen dem Wetter so wenig ertrag kam, das er auch gleich auf SOC min ist. Somit werden über sehr lange Zeit keine Steuerbefehle mehr gesendet und ich vermute, das das Gerät dann nach ca.13 Stunden in einen Tiefschlaf geht.
Das ist absolut nachvollziehbar.
Das Abschalten des WLAN-Moduls oder das Versetzen der MCU in einen tiefen Schlafmodus ist eine gängige Schutzfunktion, da die Netzwerkkommunikation einen nicht unerheblichen Teil des Eigenstrombedarfs des Geräts ausmacht.
Wenn über Stunden keine Aktivität (weder PV-Ertrag noch Steuerbefehle) registriert wird, greift irgendwann das Energiemanagement, um eine Tiefentladung der Zellen zu verhindern.Interessant wäre nun zu wissen:
• Auf wie viel % steht dein minSoc aktuell?
• Welchen Wert hat/haben die Batterie(n) dabei bei minVol (niedrigste Zellspannung)?Zusätzlich wäre es gut zu beobachten, wie sich die originale App in diesem Zustand verhält:
• Meldet die App das Gerät zu diesem Zeitpunkt noch als online?
• Lassen sich über die App noch Werte verändern?Falls ja, wäre wichtig festzustellen, ob das Gerät durch die App-Aktivität kurzzeitig aus dem Tiefschlaf erwacht und diese Befehle dann auch tatsächlich ausführt.
Das würde uns zeigen, ob das Gerät im Schlafmodus noch auf bestimmte Wake-up-Signale (z. B. via Bluetooth oder die Cloud-Schnittstelle) reagiert.edit/PS: ist die Akku-Heizung aktiv?
Nachdem ich jetzt ein paar Tage den Fehler Zendure IP überprüfen beobachtet habe, kann ich folgendes feststelln.
Der Fehler tritt immer auf, wenn das Gerät am Abend mit Sonnenuntergang bzw. PV Eingang 0 Watt hat und wegen dem Wetter so wenig ertrag kam, das er auch gleich auf SOC min ist. Somit werden über sehr lange Zeit keine Steuerbefehle mehr gesendet und ich vermute, das das Gerät dann nach ca.13 Stunden in einen Tiefschlaf geht.
Das ist absolut nachvollziehbar.
Das Abschalten des WLAN-Moduls oder das Versetzen der MCU in einen tiefen Schlafmodus ist eine gängige Schutzfunktion, da die Netzwerkkommunikation einen nicht unerheblichen Teil des Eigenstrombedarfs des Geräts ausmacht.
Wenn über Stunden keine Aktivität (weder PV-Ertrag noch Steuerbefehle) registriert wird, greift irgendwann das Energiemanagement, um eine Tiefentladung der Zellen zu verhindern.Interessant wäre nun zu wissen:
• Auf wie viel % steht dein minSoc aktuell?Der steht auf 50%
• Welchen Wert hat/haben die Batterie(n) dabei bei minVol (niedrigste Zellspannung)?
Das habe ich nicht geschaut. Wenn es dir wichtig ist, kann ich das gerne noch machen.
Zusätzlich wäre es gut zu beobachten, wie sich die originale App in diesem Zustand verhält:
• Meldet die App das Gerät zu diesem Zeitpunkt noch als online?
Nein die App meldet ihn als Offline und versucht per Bluetooth zu verbinden was aber auch nicht funktioniert.
• Lassen sich über die App noch Werte verändern?
Ob man Werte verändern kann habe ich nicht getestet. Kann ich aber gerne nachholen.
Falls ja, wäre wichtig festzustellen, ob das Gerät durch die App-Aktivität kurzzeitig aus dem Tiefschlaf erwacht und diese Befehle dann auch tatsächlich ausführt.
Das würde uns zeigen, ob das Gerät im Schlafmodus noch auf bestimmte Wake-up-Signale (z. B. via Bluetooth oder die Cloud-Schnittstelle) reagiert. -
Nachdem ich jetzt ein paar Tage den Fehler Zendure IP überprüfen beobachtet habe, kann ich folgendes feststelln.
Der Fehler tritt immer auf, wenn das Gerät am Abend mit Sonnenuntergang bzw. PV Eingang 0 Watt hat und wegen dem Wetter so wenig ertrag kam, das er auch gleich auf SOC min ist. Somit werden über sehr lange Zeit keine Steuerbefehle mehr gesendet und ich vermute, das das Gerät dann nach ca.13 Stunden in einen Tiefschlaf geht.
Das ist absolut nachvollziehbar.
Das Abschalten des WLAN-Moduls oder das Versetzen der MCU in einen tiefen Schlafmodus ist eine gängige Schutzfunktion, da die Netzwerkkommunikation einen nicht unerheblichen Teil des Eigenstrombedarfs des Geräts ausmacht.
Wenn über Stunden keine Aktivität (weder PV-Ertrag noch Steuerbefehle) registriert wird, greift irgendwann das Energiemanagement, um eine Tiefentladung der Zellen zu verhindern.Interessant wäre nun zu wissen:
• Auf wie viel % steht dein minSoc aktuell?Der steht auf 50%
• Welchen Wert hat/haben die Batterie(n) dabei bei minVol (niedrigste Zellspannung)?
Das habe ich nicht geschaut. Wenn es dir wichtig ist, kann ich das gerne noch machen.
Zusätzlich wäre es gut zu beobachten, wie sich die originale App in diesem Zustand verhält:
• Meldet die App das Gerät zu diesem Zeitpunkt noch als online?
Nein die App meldet ihn als Offline und versucht per Bluetooth zu verbinden was aber auch nicht funktioniert.
• Lassen sich über die App noch Werte verändern?
Ob man Werte verändern kann habe ich nicht getestet. Kann ich aber gerne nachholen.
Falls ja, wäre wichtig festzustellen, ob das Gerät durch die App-Aktivität kurzzeitig aus dem Tiefschlaf erwacht und diese Befehle dann auch tatsächlich ausführt.
Das würde uns zeigen, ob das Gerät im Schlafmodus noch auf bestimmte Wake-up-Signale (z. B. via Bluetooth oder die Cloud-Schnittstelle) reagiert.@Daniel-8
Deine Rückmeldung bestärkt meine Vermutung.
Dein Akku steht ja draußen im Kalten?Wenn ja, tippe ich stark darauf, dass die integrierte Akkuheizung aktiv war.
Die Heizung zieht extrem viel Strom.
Wenn das Wetter tagsüber so schlecht war, dass der Akku ohnehin kaum geladen wurde, und nachts die Heizung anspringt, ist der Energiebedarf gigantisch.
Um zu verhindern, dass die Heizung den Akku im Winter komplett leersaugt (selbst wenn du den Schutz auf 50 % minSoc gestellt hast), greift vermutlich der Sicherheitsmechanismus der Firmware:
Bei Erreichen des Limits wird dem gesamten System - inklusive WLAN - komplett der Saft abgedreht, um die Zellen zu schützen.
Ein SolarFlow 800 Pro geht normalerweise nicht einfach so grundlos in einen so tiefen Schlaf, dass selbst WLAN abgeschaltet wird – es sei denn, ein interner Großverbraucher wie die Heizung hat das System in den Not-Standby gezwungen.
Da die App das Gerät als offline anzeigt, brauchst du auch keine Wertänderungen testen.
Falls du doch mal die Gelegenheit hast, schau morgens (wenn das Gerät erwacht) mal auf die Werte und ob Heizung aktiv ist.
Die Temperaturen und vor allem die Zellspannungen (minVol) wären dennoch extrem interessant.
So kann man sehen, ob die tatsächlichen Werte mit den 50 % SoC übereinstimmen oder ob die SoC-Berechnung aufgrund einer fehlenden Kalibrierung einfach nicht mehr stimmt.Du solltest deinen Akku am besten einpacken (isolieren) oder anderweitig vor den tieferen Temperaturen schützen, um diesen massiven nächtlichen Energieverlust zu verhindern.
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



