Hallo Zusammen,
ich hatte mit einem SolarFlow 800 Plus + D0 Zähler angefangen und alles über MQTT ausgelesen.
Soweit,so gut.
Bin dann auf HEMS (Home Energy Management System) umgestiegen, wodurch MQTT für den SF800P deaktiviert wurde.
Habe jetzt auch noch einen SolarFlow 800 mit dabei, deswegen habe ich mir ein Modul bauen lassen.
Ich stelle es hier gerne ohne Gewähr zur Verfügung.
define Gerätename ZendureLocal IP Seriennummer
Hi,
ich versuche seit zwei Monaten genau dieses Problem zu lösen - mit dem Modul funktioniert es einwandfrei, vielen Dank!
Und wer (wie ich) nach der Umrechnung zur Temperatur sucht: https://github.com/Zendure/zenSDK/blob/main/docs/en_properties.md
Grüße!
Gerne.
Anbei eine neue Version inkl. der Umrechnung.
Hi,
runtergeladen, Fhem neu gestartet, definiert ... läuft, d.h. Werte kommen
Echt klasse !
Danke !!!
Ich habe ein Hyper 2000 im HEMS. Wenn ich das Modul ausprobiere, dann erhalte ich "Connection refused (111)".
MQTT geht bei mir aber weiterhin, auch mit HEMS.
Weil der wohl noch keinen lokalen Zugang bietet. Aber wenn MQTT, passt es doch
Wie gesagt, bei mir läufts mit einem 2400AC gut.
Gibts für das HEMS Protokoll irgendwo 'ne Doku von Zendure ?
z.B. der Wert p_remainOutTime funktioniert, solange der Wert unter x h liegt, die Grenze weiss ich noch nicht
Aktuell sagt Fhem 255 aber Home Assistant berichtet aktuell 14h und 32m
Gestern abend hats gepasst und war bei 2:30 h gleich bei beiden Systemen
@Mitch
Von "wem" hast Du das programmieren lassen ? KI ? Und anhand welcher Doku ?
Gruss
Joe
PS: Das Setzen des minSOC funktioniert auch nicht
2026-03-29_11h20_42.png
Wenn ich den auf 9% setze bleibt er trotzdem bei 11%
Allerdings werden die 11% als 110 in Fhem angezeigt (p_minSoc 110)
Nach weiterer Recherche kann man mit der URL : http://ip_zendure_xxx/properties/report die ganzen Werte als JSON anfragen
Diese Werte werden von Mitch's Modul ausgelesen und in FHem angezeigt.
Hier steht auch der SOCmin (z.B. 11% bei mir) mit "110" genauso drin.
Komische Formatierung, deren Beschreibung mich interessieren würde.
Bei besserer Kenntnis der Formatierung könnte ich bzw. man die anpassen.
Ich habe ChatGPT genutzt. Die Doku stammt teilweise von Github (https://github.com/Zendure/zenSDK/blob/main/docs/en_properties.md), leider sind aber nicht alle Werte dokumentiert.
Hier mal ein Update:
package main;
use strict;
use warnings;
use HttpUtils;
use JSON qw(decode_json encode_json);
use Time::HiRes qw(gettimeofday);
sub ZendureLocal_Initialize($);
sub ZendureLocal_Define($$);
sub ZendureLocal_Undef($$);
sub ZendureLocal_Get($$@);
sub ZendureLocal_Set($$@);
sub ZendureLocal_Poll($);
sub ZendureLocal_DoRequest($$$$$);
sub ZendureLocal_ParsePropertiesReport($$);
sub ZendureLocal_FormatMinutes($);
sub ZendureLocal_MapState($);
sub ZendureLocal_Initialize($) {
my ($hash) = @_;
$hash->{DefFn} = "ZendureLocal_Define";
$hash->{UndefFn} = "ZendureLocal_Undef";
$hash->{GetFn} = "ZendureLocal_Get";
$hash->{SetFn} = "ZendureLocal_Set";
$hash->{AttrList} = join(" ", qw(
interval
timeout
disable:0,1
readingPrefix
room
)) . " " . $readingFnAttributes;
return;
}
sub ZendureLocal_Define($$) {
my ($hash, $def) = @_;
my @a = split("[ \t]+", $def);
return "Usage: define <name> ZendureLocal <ip> <sn>" if (@a != 4);
my ($name, undef, $ip, $sn) = @a;
$hash->{NAME} = $name;
$hash->{IP} = $ip;
$hash->{SN} = $sn;
$attr{$name}{interval} //= 10;
$attr{$name}{timeout} //= 5;
$attr{$name}{readingPrefix} //= "";
readingsSingleUpdate($hash, "state", "defined", 1);
ZendureLocal_Poll($hash);
return undef;
}
sub ZendureLocal_Undef($$) {
my ($hash, $arg) = @_;
RemoveInternalTimer($hash);
return undef;
}
sub ZendureLocal_Get($$@) {
my ($hash, $name, @a) = @_;
return "get $name needs an argument" if (@a < 1);
my $cmd = $a[0];
if ($cmd eq "properties") {
ZendureLocal_DoRequest($hash, "GET", "/properties/report", undef, sub {
my ($h, $ok, $msg) = @_;
readingsSingleUpdate($h, "state", $ok ? "ok" : "error", 1);
readingsSingleUpdate($h, "lastError", $msg, 1) if (!$ok && defined $msg && $msg ne "");
});
return "OK (request started)";
}
if ($cmd eq "rpc") {
return "Usage: get $name rpc <method>" if (@a < 2);
my $method = $a[1];
my $body = {
sn => $hash->{SN},
method => $method,
params => {}
};
ZendureLocal_DoRequest($hash, "POST", "/rpc", $body, sub {
my ($h, $ok, $msg) = @_;
readingsSingleUpdate($h, "state", $ok ? "ok" : "error", 1);
readingsSingleUpdate($h, "lastRpc", $msg // "", 1);
});
return "OK (request started)";
}
return "Unknown argument $cmd, choose one of properties rpc";
}
sub ZendureLocal_Set($$@) {
my ($hash, $name, @a) = @_;
my $choices = "outputLimit acMode minSoc smartMode property pollNow";
return "set $name needs an argument, choose one of $choices" if (@a < 1);
my $cmd = $a[0];
if ($cmd eq "?") {
return "choose one of $choices";
}
if ($cmd eq "pollNow") {
ZendureLocal_Poll($hash);
return "OK";
}
my $send_write = sub {
my ($key, $val) = @_;
my $val_json = ($val =~ /^-?\d+(?:\.\d+)?$/) ? 0 + $val : $val;
my $body = {
sn => $hash->{SN},
properties => { $key => $val_json }
};
ZendureLocal_DoRequest($hash, "POST", "/properties/write", $body, sub {
my ($h, $ok, $msg) = @_;
readingsSingleUpdate($h, "state", $ok ? "ok" : "error", 1);
readingsSingleUpdate($h, "lastSet", "$key=$val", 1) if $ok;
readingsSingleUpdate($h, "lastError", $msg, 1) if (!$ok && defined $msg && $msg ne "");
});
return "OK (request started)";
};
if ($cmd eq "property") {
return "Usage: set $name property <key> <value>" if (@a < 3);
my ($key, $val) = ($a[1], $a[2]);
return $send_write->($key, $val);
}
if ($cmd =~ /^(outputLimit|acMode|minSoc|smartMode)$/) {
return "Usage: set $name $cmd <value>" if (@a < 2);
my $val = $a[1];
return $send_write->($cmd, $val);
}
return "Unknown argument $cmd, choose one of $choices";
}
sub ZendureLocal_Poll($) {
my ($hash) = @_;
my $name = $hash->{NAME};
RemoveInternalTimer($hash);
return if AttrVal($name, "disable", 0);
ZendureLocal_DoRequest($hash, "GET", "/properties/report", undef, sub {
my ($h, $ok, $msg) = @_;
readingsSingleUpdate($h, "state", $ok ? "ok" : "error", 1);
readingsSingleUpdate($h, "lastError", $msg, 1) if (!$ok && defined $msg && $msg ne "");
});
my $interval = int(AttrVal($name, "interval", 10));
$interval = 10 if ($interval < 2);
InternalTimer(gettimeofday() + $interval, "ZendureLocal_Poll", $hash, 0);
}
sub ZendureLocal_DoRequest($$$$$) {
my ($hash, $method, $path, $body, $cb) = @_;
my $name = $hash->{NAME};
my $ip = $hash->{IP};
my $timeout = int(AttrVal($name, "timeout", 5));
my $url = "http://$ip$path";
my $data;
my $header = {};
if (defined $body) {
$data = encode_json($body);
$header->{"Content-Type"} = "application/json";
}
my $param = {
url => $url,
method => $method,
timeout => $timeout,
header => $header,
data => $data,
hash => $hash,
callback => sub {
my ($p, $err, $resp) = @_;
my $h = $p->{hash};
if (defined $err && $err ne "") {
$cb->($h, 0, $err);
return;
}
if (!defined $resp || $resp eq "") {
$cb->($h, 0, "$url: empty answer received");
return;
}
my $json;
if (ref($resp)) {
$json = $resp;
} else {
eval { $json = decode_json($resp); 1; } or do {
$cb->($h, 0, "JSON parse failed");
return;
};
}
if ($path eq "/properties/report") {
ZendureLocal_ParsePropertiesReport($h, $json);
} else {
readingsBeginUpdate($h);
readingsBulkUpdate($h, "lastResponse", ref($resp) ? encode_json($resp) : $resp);
readingsEndUpdate($h, 1);
}
$cb->($h, 1, undef);
}
};
HttpUtils_NonblockingGet($param);
}
sub ZendureLocal_FormatMinutes($) {
my ($m) = @_;
return "" if (!defined $m || $m eq "" || $m !~ /^-?\d+$/);
my $h = int($m / 60);
my $min = $m % 60;
return sprintf("%dh %02dm", $h, $min);
}
sub ZendureLocal_MapState($) {
my ($v) = @_;
return "" if (!defined $v || $v eq "");
my %map = (
0 => "standby",
1 => "charging",
2 => "discharging",
);
return exists $map{$v} ? $map{$v} : $v;
}
sub ZendureLocal_ParsePropertiesReport($$) {
my ($hash, $json) = @_;
my $name = $hash->{NAME};
my $prefix = AttrVal($name, "readingPrefix", "");
if (!ref($json)) {
my $tmp;
eval { $tmp = decode_json($json); 1; };
$json = $tmp if ($tmp && ref($tmp));
}
readingsBeginUpdate($hash);
if (ref($json) eq "HASH") {
foreach my $k (sort keys %{$json}) {
next if ($k eq "properties" || $k eq "packData");
my $v = $json->{$k};
next if !defined $v;
readingsBulkUpdate($hash, $prefix.$k, ref($v) ? encode_json($v) : $v);
}
}
if (exists $json->{properties}) {
my $p = $json->{properties};
if (!ref($p)) {
my $tmp;
eval { $tmp = decode_json($p); 1; };
$p = $tmp if ($tmp && ref($tmp));
}
if (ref($p) eq "HASH") {
foreach my $k (sort keys %{$p}) {
my $v = $p->{$k};
next if !defined $v;
my $rname = $prefix . "p_" . $k;
readingsBulkUpdate($hash, $rname, ref($v) ? encode_json($v) : $v);
if ($k eq "hyperTmp" && $v =~ /^\d+$/) {
my $c = ($v - 2731) / 10.0;
readingsBulkUpdate($hash, $prefix . "p_hyperTmp_C", sprintf("%.1f", $c));
}
if (($k eq "minSoc" || $k eq "socSet") && $v =~ /^\d+$/) {
my $pct = $v / 10.0;
readingsBulkUpdate($hash, $prefix . "p_" . $k . "_pct", sprintf("%.1f", $pct));
}
if ($k eq "packState") {
readingsBulkUpdate($hash, $prefix . "p_packState_txt", ZendureLocal_MapState($v));
}
if ($k eq "acMode") {
readingsBulkUpdate($hash, $prefix . "p_acMode_txt", ZendureLocal_MapState($v));
}
if ($k eq "remainOutTime" && $v =~ /^\d+$/) {
readingsBulkUpdate($hash, $prefix . "p_remainOutTime_hm", ZendureLocal_FormatMinutes($v));
}
}
} else {
readingsBulkUpdate($hash, $prefix . "properties", ref($json->{properties}) ? encode_json($json->{properties}) : $json->{properties});
}
}
if (exists $json->{packData}) {
my $arr = $json->{packData};
if (!ref($arr)) {
my $tmp;
eval { $tmp = decode_json($arr); 1; };
$arr = $tmp if ($tmp && ref($tmp) eq "ARRAY");
}
if (ref($arr) eq "ARRAY") {
for (my $i = 0; $i < @$arr; $i++) {
next if ref($arr->[$i]) ne "HASH";
my $bd = $arr->[$i];
foreach my $k (sort keys %{$bd}) {
my $v = $bd->{$k};
next if !defined $v;
my $rname = $prefix . "bat${i}_" . $k;
readingsBulkUpdate($hash, $rname, ref($v) ? encode_json($v) : $v);
if ($k eq "maxTemp" && $v =~ /^\d+$/) {
my $c = ($v - 2731) / 10.0;
readingsBulkUpdate($hash, $prefix . "bat${i}_maxTemp_C", sprintf("%.1f", $c));
}
if ($k eq "batcur" && $v =~ /^\d+$/) {
my $raw = int($v) & 0xFFFF;
my $signed = ($raw & 0x8000) ? ($raw - 0x10000) : $raw;
my $a = $signed / 10.0;
readingsBulkUpdate($hash, $prefix . "bat${i}_batcur_A", sprintf("%.1f", $a));
}
}
}
readingsBulkUpdate($hash, $prefix . "bat_count", scalar(@$arr));
} else {
readingsBulkUpdate($hash, $prefix . "packData", ref($json->{packData}) ? encode_json($json->{packData}) : $json->{packData});
}
}
readingsBulkUpdate($hash, $prefix . "lastUpdate", gettimeofday());
readingsEndUpdate($hash, 1);
}
1;
=pod
=item device
=item summary Zendure SolarFlow Local API (HTTP) reader/writer
=begin html
<a name="ZendureLocal"></a>
<h3>ZendureLocal</h3>
<ul>
<li><b>Define</b><br>
<code>define <name> ZendureLocal <ip> <sn></code>
</li>
<li><b>Attributes</b><br>
<ul>
<li>interval (default 10)</li>
<li>timeout (default 5)</li>
<li>disable (0/1)</li>
<li>readingPrefix (default empty)</li>
<li>room</li>
</ul>
</li>
<li><b>Get</b><br>
<code>get <name> properties</code><br>
<code>get <name> rpc <method></code>
</li>
<li><b>Set</b><br>
<code>set <name> outputLimit <W></code><br>
<code>set <name> acMode <num></code><br>
<code>set <name> minSoc <num></code><br>
<code>set <name> smartMode <0|1></code><br>
<code>set <name> property <key> <value></code><br>
<code>set <name> pollNow</code>
</li>
</ul>
=end html
=cut
Ok.
Du hast u.a. die SOCs in Prozent umgerechnet und als neue Readings hinzugefuegt. Ebenso die Remaining time.
Ich habe mir mal erlaubt eine Versionierung incl. Reading einzufuegen ... siehe Anhang
Das setzen des min/max SOC funktioniert aber immer noch nicht.
Weist Du wie die URL dazu aussehen sollte ? Dann könnte man mal einen Direktaufruf testen ...
Dann könnte man die min/max SOCs mit Fhem automatisch je nach Jahreszeit anpassen ...
Ich habe ein paar Fragen zu dem Modul:
1. Muss mein SF800pro im HEMS sein oder funktioniert das Modul unabhängig davon?
2. MQTT brauche ich dazu nicht, oder?
3. Wenn ich einen SET-Befehl absetze (z.B. set SF800pro acMode 1), dann erhalte ich folgende Anwort:
lastResponse {"timestamp":1776462842,"messageId":159,"error":"SN does not match","code":401} 2026-04-17 23:54:02
lastSet acMode=1 2026-04-17 23:54:02
"SN does not match" bedeutet vermutlich "Seriennummer stimmt nicht überein". Die ist aber definitiv richtig. Readings erscheinen jedenfalls jede Menge. Was läuft da schief?
Wenn du kein HEMS nutzt, brauchst du das Modul ja gar nicht, kannst alles schön über MQTT lokal machen.
Hallo zusammen ich habe Heute meinen Zendur Hyper 2000 bekommen und will diesen natürlich auch local in fhem einbinden.
Das modul habe ich in Fhem hinzugefügt, dann via define den Hyper2000 definiert mit IP und SERIENNUMMER aber es kommen keine Werte.
Muss ich mit dem Hyper noch was machen? Im Wlan ist er und daten kommen in der APP an.
define EnergieSpeicher ZendureLocal 192.168.2.214 XXXXXXXXXXXXXXXX
attr EnergieSpeicher disable 1
attr EnergieSpeicher interval 10
attr EnergieSpeicher readingPrefix
attr EnergieSpeicher room Strom,Solar
attr EnergieSpeicher timeout 5
# CFGFN
# DEF 192.168.2.214 XXXXXXXXXXXXXXXXX
# FUUID 69f4c7e7-f33f-91a4-886f-b2cad5ad1c4ceb03
# IP 192.168.2.214
# NAME EnergieSpeicher
# NR 888
# SN EE1BHNEEN360190
# STATE error
# TYPE ZendureLocal
# eventCount 20
# READINGS:
# 2026-05-08 11:09:31 lastError 192.168.2.214: Verbindungsaufbau abgelehnt (111)
# 2026-05-08 11:09:31 state error
#
setstate EnergieSpeicher error
setstate EnergieSpeicher 2026-05-08 11:09:31 lastError 192.168.2.214: Verbindungsaufbau abgelehnt (111)
setstate EnergieSpeicher 2026-05-08 11:09:31 state error
Freue und bedanke mich für die Hilfe
Der Hyper 2000 kann das leider nicht, der hat keinen lokalen Zugriff
Okay wie bekomme ich den denn via MQTT angebunden? kannst du da helfen? Für homeAssistent scheint es ja Module zu geben.
Lg Holger
Ich bin selber gerade dabei den Hyper 2000 einzubinden. Ist nicht so einfach, aber ich probiere es weiter.
Okay danke gib bitte feedback sobald du einen weg hast, Ich probiere es wie in einigen beiträgen beschreieben,scheitere aber schon daran die APP auf global umzustellen da die auswahl bei mir ausgegraut ist.
lg
Habe jetzt noch ein Cloud Modul, das holt die Daten vom Hyper sauber ab.
okay wird das genau so definiert?? oder wie sieht die definition aus??
lg und danke dir
Ich arbeite an einem All In One Module, bitte noch Gedult
Okay, das klappt soweit, daten kommen rein.
Kann ich darüber auch den ausgang anpassen??
Mir gehts darum quasi eine 0 Einspeisung über Fhem zu machen und den AKKU zu laden wenn der Strompreis Niedrig BZW negativ ist, bin Bei tibber habe aber keinen Puls sondern einen Smartmeter von Discovergy/Inexogy
lg holger
Hier mal ein Update.
# Cloud:
define myZen Zendure cloud mich@mail.de SN1234,SN5678,SN9012
# Lokal:
define myHyper Zendure local 192.168.1.42 SN5678901
Wie bekomme ich den Hyper denn Local angebunden??
Der steht bei mir immer auf error.
Und über Cloud bekomme ich zwar Daten aber kann ihn nicht steuern.
LG Holger
Der Hyper kann nicht so einfach lokal benutzt werden.
Es gibt wohl ein lokales MQTT Projekt für HA: https://github.com/Schwippser/Solarflow-mqtt-HA
Sollte sich einfach auf fhem übertragen lassen.
Das ist sehr nett und das kannte ich auch bereits Aberdings denke ich übersteigt das meine Fähigkeiten. So langsam habe ich aber das Gefühl das ein Umstieg von FHEM auf HA lohnt.
Tibber ist da sauberer integriert, der wirlpool von mir, einige zigbee Geräte und auch Zendure.
Leider integrieren fiele Hersteller HA aber nicht FHEM was ich schade finde.
LG Holger
Hat alles seine Vor- un Nachteile. Ich nutze beides.
HA hat extrem viele Integrationen und eine riesige hilfsbereite Community.
fhem hat mit DOIF für ich die besten Automationsmöglichkeiten und breiteste Unterstützung für verschiedene Standards. Vor allem HM, was ich noch viel habe.
Kann ich es auf einem raspberry pi parallel nutzen ohne docker container oder wie das heißt?
Es gibt fertigen Docker
Okay aber mit docker kenne ich mich gar nicht aus, hatte gehofft ich könnte HA einfach Parallel auf dem pi installieren via apt-install, das scheint aber nicht zu gehen.
LG Holger