diff --git a/fhem/contrib/DS_Starter/70_PylonLowVoltage.pm b/fhem/contrib/DS_Starter/70_PylonLowVoltage.pm index 712950831..ac93e6191 100644 --- a/fhem/contrib/DS_Starter/70_PylonLowVoltage.pm +++ b/fhem/contrib/DS_Starter/70_PylonLowVoltage.pm @@ -4,20 +4,12 @@ # # 70_PylonLowVoltage.pm # -# A FHEM module to read BMS values from a -# Pylontech US2000plus/US3000 LiFePo04 battery +# A FHEM module to read BMS values from Pylontech Low Voltage LiFePo04 batteries. # -# This module is based on 70_Pylontech.pm written 2019 by Harald Schmitz -# Code modifications and extensions: (c) 2023 by Heiko Maaz e-mail: Heiko dot Maaz at t-online dot de +# This module is based on 70_Pylontech.pm written 2019 by Harald Schmitz. +# Code further development and extensions (c) 2023 by Heiko Maaz e-mail: Heiko dot Maaz at t-online dot de # -# Forumlinks: -# https://forum.fhem.de/index.php?topic=117466.0 (Source of original module) -# https://forum.fhem.de/index.php?topic=126361.0 -# https://forum.fhem.de/index.php?topic=112947.0 -# https://forum.fhem.de/index.php?topic=32037.0 -# -# Photovoltaik Forum: -# https://www.photovoltaikforum.com/thread/130061-pylontech-us2000b-daten-protokolle-programme +# Credits to FHEM user: satprofi, Audi_Coupe_S, abc2006 # ######################################################################################################################### # Copyright notice @@ -41,6 +33,16 @@ # This copyright notice MUST APPEAR in all copies of the script! # ######################################################################################################################### +# Forumlinks: +# https://forum.fhem.de/index.php?topic=117466.0 (Source of module 70_Pylontech.pm) +# https://forum.fhem.de/index.php?topic=126361.0 +# https://forum.fhem.de/index.php?topic=112947.0 +# https://forum.fhem.de/index.php?topic=32037.0 +# +# Photovoltaik Forum: +# https://www.photovoltaikforum.com/thread/130061-pylontech-us2000b-daten-protokolle-programme +# +######################################################################################################################### # # Leerzeichen entfernen: sed -i 's/[[:space:]]*$//' 70_PylonLowVoltage.pm # @@ -54,6 +56,7 @@ use Time::HiRes qw(gettimeofday ualarm); use IO::Socket::INET; use Errno qw(ETIMEDOUT EWOULDBLOCK); use Scalar::Util qw(looks_like_number); +use Carp qw(croak carp); eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' eval "use IO::Socket::Timeout;1" or my $iostAbsent = 1; ## no critic 'eval' @@ -113,9 +116,10 @@ BEGIN { # Versions History intern (Versions history by Heiko Maaz) my %vNotesIntern = ( + "0.1.2" => "20.08.2023 commandref revised, analogValue -> use 'user defined items' ", "0.1.1" => "16.08.2023 integrate US3000C, add print request command in HEX to Logfile, attr timeout ". - "change validation of received data, change DEF format, extend evaluation of chargeManagmentInfo ". - "add evaluate systemParameters, additional own values packImbalance, packState ", + "change validation of received data, change DEF format, extend evaluation of chargeManagmentInfo ". + "add evaluate systemParameters, additional own values packImbalance, packState ", "0.1.0" => "12.08.2023 initial version, switch to perl package, attributes: disable, interval, add command hashes ". "get ... data command, add meta support and version management, more code changes ", ); @@ -174,7 +178,6 @@ my $definterval = 30; # default A my $defto = 0.5; # default connection Timeout zum RS485 Gateway my @blackl = qw(state nextCycletime); # Ausnahmeliste deleteReadingspec - # Steuerhashes ############### my %hrtnc = ( # RTN Codes @@ -470,7 +473,8 @@ sub Update { my %readings = (); my $protocol = 'tcp'; my $rtnerr = q{}; - my $socket; + + my ($socket, $success); if(!$interval) { $hash->{OPMODE} = 'Manual'; @@ -486,8 +490,8 @@ sub Update { Log3 ($name, 4, "$name - start request cycle to battery number >$hash->{BATADDRESS}< at host:port $hash->{HOST}:$hash->{PORT}"); - eval { - local $SIG{ALRM} = sub { die 'gatewaytimeout' }; + eval { ## no critic 'eval' + local $SIG{ALRM} = sub { croak 'gatewaytimeout' }; ualarm ($timeout * 1000000); # ualarm in Mikrosekunden $socket = IO::Socket::INET->new( Proto => $protocol, @@ -516,14 +520,18 @@ sub Update { } IO::Socket::Timeout->enable_timeouts_on ($socket); # nur notwendig für read or write timeout - $socket->read_timeout ($timeout - 0.2); # Lesetimeout immer kleiner als Sockettimeout + my $rwto = $timeout - 0.05; + $rwto = $rwto <= 0 ? 0.005 : $rwto; + + $socket->read_timeout ($rwto); # Read/Writetimeout immer kleiner als Sockettimeout + $socket->write_timeout ($rwto); $socket->autoflush(1); my $res; # relativ statische Werte abrufen ################################### - if (ReadingsAge ($name, "serialNumber", 601) >= 0) { + if (ReadingsAge ($name, "serialNumber", 601) >= 60) { # Abruf serialNumber ##################### $res = Request($hash, $socket, $hrsnb{$hash->{BATADDRESS}}{cmd}, 'serialNumber'); @@ -598,33 +606,6 @@ sub Update { $readings{moduleSoftwareVersion_manufacture} = 'V'.hex (substr ($res, 15, 2)).'.'.hex (substr ($res, 17, 2)); $readings{moduleSoftwareVersion_mainline} = 'V'.hex (substr ($res, 19, 2)).'.'.hex (substr ($res, 21, 2)).'.'.hex (substr ($res, 23, 2)); - # Abruf alarmInfo - ################## - $res = Request($hash, $socket, $hralm{$hash->{BATADDRESS}}{cmd}, 'alarmInfo'); - - $rtnerr = respStat ($res); - if ($rtnerr) { - doOnError ({ hash => $hash, - readings => \%readings, - sock => $socket, - state => $rtnerr - } - ); - return; - } - - $readings{packCellcount} = hex (substr($res, 17, 2)); - - if (substr($res, 19, 30)=="000000000000000000000000000000" && - substr($res, 51, 10)=="0000000000" && - substr($res, 67, 2) =="00" && - substr($res, 73, 4) =="0000") { - $readings{packAlarmInfo} = "ok"; - } - else { - $readings{packAlarmInfo} = "failure"; - } - # Abruf Systemparameter ######################## $res = Request($hash, $socket, $hrspm{$hash->{BATADDRESS}}{cmd}, 'systemParameters'); @@ -654,6 +635,33 @@ sub Update { $readings{paramDischargeCurrentLimit} = sprintf "%.3f", (65535 - (hex substr ($res, 59, 4))) * 100 / 1000; # mit Symbol (-) } + # Abruf alarmInfo + ################## + $res = Request($hash, $socket, $hralm{$hash->{BATADDRESS}}{cmd}, 'alarmInfo'); + + $rtnerr = respStat ($res); + if ($rtnerr) { + doOnError ({ hash => $hash, + readings => \%readings, + sock => $socket, + state => $rtnerr + } + ); + return; + } + + $readings{packCellcount} = hex (substr($res, 17, 2)); + + if (substr($res, 19, 30) eq "000000000000000000000000000000" && + substr($res, 51, 10) eq "0000000000" && + substr($res, 67, 2) eq "00" && + substr($res, 73, 4) eq "0000") { + $readings{packAlarmInfo} = "ok"; + } + else { + $readings{packAlarmInfo} = "failure"; + } + # Abruf chargeManagmentInfo ############################ $res = Request($hash, $socket, $hrcmi{$hash->{BATADDRESS}}{cmd}, 'chargeManagmentInfo'); @@ -677,8 +685,8 @@ sub Update { my $cdstat = sprintf "%08b", hex substr ($res, 31, 2); # Rohstatus $readings{chargeEnable} = substr ($cdstat, 0, 1) == 1 ? 'yes' : 'no'; # Bit 7 $readings{dischargeEnable} = substr ($cdstat, 1, 1) == 1 ? 'yes' : 'no'; # Bit 6 - $readings{chargeImmediatelySOC5} = substr ($cdstat, 2, 1) == 1 ? 'yes' : 'no'; # Bit 5 - SOC 5~9% -> für Wechselrichter, die aktives Batteriemanagement bei gegebener DC-Spannungsfunktion haben oder Wechselrichter, der von sich aus einen niedrigen SOC/Spannungsgrenzwert hat - $readings{chargeImmediatelySOC10} = substr ($cdstat, 3, 1) == 1 ? 'yes' : 'no'; # Bit 4 - SOC 9~13% -> für Wechselrichter hat keine aktive Batterieabschaltung haben + $readings{chargeImmediatelySOC05} = substr ($cdstat, 2, 1) == 1 ? 'yes' : 'no'; # Bit 5 - SOC 5~9% -> für Wechselrichter, die aktives Batteriemanagement bei gegebener DC-Spannungsfunktion haben oder Wechselrichter, der von sich aus einen niedrigen SOC/Spannungsgrenzwert hat + $readings{chargeImmediatelySOC09} = substr ($cdstat, 3, 1) == 1 ? 'yes' : 'no'; # Bit 4 - SOC 9~13% -> für Wechselrichter hat keine aktive Batterieabschaltung haben $readings{chargeFullRequest} = substr ($cdstat, 4, 1) == 1 ? 'yes' : 'no'; # Bit 3 - wenn SOC in 30 Tagen nie höher als 97% -> Flag = 1, wenn SOC-Wert ≥ 97% -> Flag = 0 # Abruf analogValue @@ -700,25 +708,7 @@ sub Update { return; } - $readings{packCellcount} = hex (substr($res, 17, 2)); - $readings{packVolt} = hex (substr($res, 105, 4)) / 1000; - my $current = hex (substr($res, 101, 4)); - - if ($current & 0x8000) { - $current = $current - 0x10000; - } - - $readings{packCurrent} = sprintf "%.2f", $current / 10; - - if (length($res) == 128) { - $readings{packCapacity} = hex (substr($res, 115, 4)) / 1000; - $readings{packCapacityRemain} = hex (substr($res, 109, 4)) / 1000; - } - else { - $readings{packCapacity} = hex (substr($res, 129, 6)) / 1000; - $readings{packCapacityRemain} = hex (substr($res, 123, 6)) / 1000; - } - + $readings{packCellcount} = hex (substr($res, 17, 2)); $readings{cellVoltage_01} = sprintf "%.3f", hex(substr($res,19,4)) / 1000; $readings{cellVoltage_02} = sprintf "%.3f", hex(substr($res,23,4)) / 1000; $readings{cellVoltage_03} = sprintf "%.3f", hex(substr($res,27,4)) / 1000; @@ -733,13 +723,43 @@ sub Update { $readings{cellVoltage_12} = sprintf "%.3f", hex(substr($res,63,4)) / 1000; $readings{cellVoltage_13} = sprintf "%.3f", hex(substr($res,67,4)) / 1000; $readings{cellVoltage_14} = sprintf "%.3f", hex(substr($res,71,4)) / 1000; - $readings{cellVoltage_15} = sprintf "%.3f", hex(substr($res,75,4)) / 1000; - $readings{packCycles} = hex (substr($res, 119, 4)); - $readings{bmsTemperature} = (hex (substr($res, 81, 4)) - 2731) / 10; - $readings{cellTemperature_0104} = (hex (substr($res, 85, 4)) - 2731) / 10; - $readings{cellTemperature_0508} = (hex (substr($res, 89, 4)) - 2731) / 10; - $readings{cellTemperature_0912} = (hex (substr($res, 93, 4)) - 2731) / 10; - $readings{cellTemperature_1315} = (hex (substr($res, 97, 4)) - 2731) / 10; + $readings{cellVoltage_15} = sprintf "%.3f", hex(substr($res,75,4)) / 1000; + # $readings{numberOfTempPos} = hex(substr($res,79,2)); # Anzahl der jetzt folgenden Teperaturpositionen -> 5 + $readings{bmsTemperature} = (hex (substr($res, 81, 4)) - 2731) / 10; # 1 + $readings{cellTemperature_0104} = (hex (substr($res, 85, 4)) - 2731) / 10; # 2 + $readings{cellTemperature_0508} = (hex (substr($res, 89, 4)) - 2731) / 10; # 3 + $readings{cellTemperature_0912} = (hex (substr($res, 93, 4)) - 2731) / 10; # 4 + $readings{cellTemperature_1315} = (hex (substr($res, 97, 4)) - 2731) / 10; # 5 + my $current = hex (substr($res, 101, 4)); + $readings{packVolt} = hex (substr($res, 105, 4)) / 1000; + + if ($current & 0x8000) { + $current = $current - 0x10000; + } + + $readings{packCurrent} = sprintf "%.3f", $current / 10; + my $udi = hex substr($res, 113, 2); # user defined item: 2: Batterien <= 65Ah, 4: Batterien > 65Ah + $readings{packCycles} = hex substr($res, 119, 4); +$udi = 0; + if ($udi == 2) { + $readings{packCapacityRemain} = hex (substr($res, 109, 4)) / 1000; + $readings{packCapacity} = hex (substr($res, 115, 4)) / 1000; + } + elsif ($udi == 4) { + $readings{packCapacityRemain} = hex (substr($res, 123, 6)) / 1000; + $readings{packCapacity} = hex (substr($res, 129, 6)) / 1000; + } + else { + doOnError ({ hash => $hash, + readings => \%readings, + sock => $socket, + state => 'wrong value retrieve analogValue -> user defined items: '.$udi + } + ); + return; + } + + $success = 1; }; # eval if ($@) { @@ -763,13 +783,14 @@ sub Update { ualarm(0); close ($socket) if($socket); - Log3 ($name, 4, "$name - got fresh values from battery number >$hash->{BATADDRESS}<"); + if ($success) { + Log3 ($name, 4, "$name - got fresh values from battery number >$hash->{BATADDRESS}<"); + + additionalReadings (\%readings); # zusätzliche eigene Readings erstellen + $readings{state} = 'connected'; + } - additionalReadings (\%readings); # zusätzliche eigene Readings erstellen - - $readings{state} = 'connected' if(!defined $readings{state}); - - createReadings ($hash, \%readings); # Readings erstellen + createReadings ($hash, \%readings); # Readings erstellen return; } @@ -809,7 +830,7 @@ sub Reread { $socket->read ($singlechar, 1); if (!$singlechar && (0+$! == ETIMEDOUT || 0+$! == EWOULDBLOCK)) { # nur notwendig für read timeout - die 'Timeout reading data from battery'; + croak 'Timeout reading data from battery'; } $res = $res . $singlechar if (!(length($res) == 0 && ord($singlechar) == 13)) # ord 13 -> ASCII dezimal für CR (Hex 0d) @@ -817,7 +838,7 @@ sub Reread { } while (length($res) == 0 || ord($singlechar) != 13); Log3 ($name, 5, "$name - data returned raw: ".$res); - Log3 ($name, 5, "$name - data returned:\n" .Hexdump($res)); + Log3 ($name, 5, "$name - data returned:\n" .Hexdump ($res)); return $res; } @@ -836,13 +857,15 @@ return; # PylonLowVoltage Hexdump ############################################################### sub Hexdump { + my $res = shift; + my $offset = 0; my $result = ""; - for my $chunk (unpack "(a16)*", $_[0]) { + for my $chunk (unpack "(a16)*", $res) { my $hex = unpack "H*", $chunk; # hexadecimal magic $chunk =~ tr/ -~/./c; # replace unprintables - $hex =~ s/(.{1,8})/$1 /gs; # insert spaces + $hex =~ s/(.{1,8})/$1 /gxs; # insert spaces $result .= sprintf "0x%08x (%05u) %-*s %s\n", $offset, $offset, 36, $hex, $chunk; $offset += 16; } @@ -901,8 +924,8 @@ sub additionalReadings { my ($vmax, $vmin); - $readings->{averageCellVolt} = sprintf "%.3f", $readings->{packVolt} / $readings->{packCellcount}; - $readings->{packSOC} = sprintf "%.2f", ($readings->{packCapacityRemain} / $readings->{packCapacity} * 100); + $readings->{averageCellVolt} = sprintf "%.3f", $readings->{packVolt} / $readings->{packCellcount} if(defined $readings->{packCellcount}); + $readings->{packSOC} = sprintf "%.2f", ($readings->{packCapacityRemain} / $readings->{packCapacity} * 100) if(defined $readings->{packCapacity}); $readings->{packPower} = sprintf "%.2f", $readings->{packCurrent} * $readings->{packVolt}; for (my $i=1; $i <= $readings->{packCellcount}; $i++) { @@ -966,7 +989,7 @@ return; =pod =item device -=item summary Integration of pylontech LiFePo4 low voltage batteries (incl. BMS) over RS485 via ethernet gateway (ethernet interface) +=item summary Integration of Pylontech LiFePo4 low voltage batteries (incl. BMS) over RS485 via ethernet gateway (ethernet interface) =item summary_DE Integration von Pylontech Niedervolt Batterien (mit BMS) über RS485 via Ethernet-Gateway (Ethernet Interface) =begin html @@ -974,43 +997,61 @@ return;

PylonLowVoltage


-Module for the integration of batteries with battery management system (BMS) from manufacturer Pylontech via RS485 via RS485 / Ethernet gateway.
-The test was carried out with a US2000plus Pylontech battery, which was connected via a USRiot "USR-TCP" low-cost Ethernet gateway.
-In principle, any other RS485 / Ethernet gateway should also be possible here.
-The module thus only communicates via an Ethernet connection.
+Module for integration of low voltage batteries with battery management system (BMS) of the manufacturer Pylontech via +RS485/Ethernet gateway. Communication to the RS485 gateway takes place exclusively via an Ethernet connection.
+The module has been successfully used so far with Pylontech batteries of the following types:
+ + + +The following devices have been successfully used as RS485 Ethernet gateways to date:
+ + +In principle, any other RS485/Ethernet gateway should also be compatible.

Requirements

This module requires the Perl modules: -
-
Definition -Working method +Mode of operation @@ -1018,7 +1059,7 @@ All data is read out at the interval specified in the definition.
-
-Als RS485-Ethernet-Gateways wurden bisher folgende Geräte eingesetzt:
+Als RS485-Ethernet-Gateways wurden bisher folgende Geräte erfolgreich eingesetzt:
-
-Prinzipiell sollte hier auch jedes andere RS485/Ethernet-Gateway möglich sein. +Prinzipiell sollte auch jedes andere RS485/Ethernet-Gateway kompatibel sein.

Voraussetzungen

Dieses Modul benötigt die Perl-Module: -
Definition @@ -1136,8 +1208,8 @@ Dieses Modul benötigt die Perl-Module: Arbeitsweise @@ -1158,7 +1230,7 @@ Alle Daten werden mit dem bei der Definition angegebene Intervall ausgelesen.