From faf8afa8daa7df77d4756becae862f34d6850788 Mon Sep 17 00:00:00 2001 From: nasseeder1 Date: Sat, 30 Sep 2023 06:06:39 +0000 Subject: [PATCH] 70_PylonLowVoltage: new module for Pylontech low volt batteries git-svn-id: https://svn.fhem.de/fhem/trunk@28012 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/70_PylonLowVoltage.pm | 1814 +++++++++++++++++++++++++++++++ fhem/MAINTAINER.txt | 1 + 3 files changed, 1816 insertions(+) create mode 100644 fhem/FHEM/70_PylonLowVoltage.pm diff --git a/fhem/CHANGED b/fhem/CHANGED index 2201bf267..5c341bbc4 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - new: 70_PylonLowVoltage: new module for Pylontech low volt batteries - change: 88_HMCCU: Fixed bug in GetDutyCycle - change: 88_HMCCU: Fixed substitute and authentication - change: 93_DbRep: Improvement of markouts in special cases, Forum:#134973 diff --git a/fhem/FHEM/70_PylonLowVoltage.pm b/fhem/FHEM/70_PylonLowVoltage.pm new file mode 100644 index 000000000..b5a0cfb3b --- /dev/null +++ b/fhem/FHEM/70_PylonLowVoltage.pm @@ -0,0 +1,1814 @@ +######################################################################################################################### +# $Id$ +######################################################################################################################### +# +# 70_PylonLowVoltage.pm +# +# A FHEM module to read BMS values from Pylontech Low Voltage LiFePo04 batteries. +# +# This module uses the idea and informations from 70_Pylontech.pm written 2019 by Harald Schmitz. +# Further code development and extensions by Heiko Maaz (c) 2023 e-mail: Heiko dot Maaz at t-online dot de +# +# Credits to FHEM user: satprofi, Audi_Coupe_S, abc2006 +# +######################################################################################################################### +# Copyright notice +# +# (c) 2019 Harald Schmitz (70_Pylontech.pm) +# (c) 2023 Heiko Maaz +# +# This script is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# 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 +# +######################################################################################################################### +package FHEM::PylonLowVoltage; ## no critic 'package' + +use strict; +use warnings; +use GPUtils qw(GP_Import GP_Export); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt +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); +use Blocking; +use MIME::Base64; + +eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' +eval "use IO::Socket::Timeout;1" or my $iostAbsent = 'IO::Socket::Timeout'; ## no critic 'eval' +eval "use Storable qw(freeze thaw);1;" or my $storabs = 'Storable'; ## no critic 'eval' + +use FHEM::SynoModules::SMUtils qw(moduleVersion); # Hilfsroutinen Modul +#use Data::Dumper; + +no if $] >= 5.017011, warnings => 'experimental::smartmatch'; + +# Run before module compilation +BEGIN { + # Import from main:: + GP_Import( + qw( + AttrVal + AttrNum + BlockingCall + BlockingKill + data + defs + fhemTimeLocal + fhem + FmtTime + FmtDateTime + init_done + InternalTimer + IsDisabled + Log + Log3 + modules + parseParams + readingsSingleUpdate + readingsBulkUpdate + readingsBulkUpdateIfChanged + readingsBeginUpdate + readingsDelete + readingsEndUpdate + ReadingsAge + ReadingsNum + ReadingsTimestamp + ReadingsVal + RemoveInternalTimer + readingFnAttributes + ) + ); + + # Export to main context with different name + # my $pkg = caller(0); + # my $main = $pkg; + # $main =~ s/^(?:.+::)?([^:]+)$/main::$1\_/g; + # foreach (@_) { + # *{ $main . $_ } = *{ $pkg . '::' . $_ }; + # } + GP_Export( + qw( + Initialize + ) + ); +} + +# Versions History intern (Versions history by Heiko Maaz) +my %vNotesIntern = ( + "0.1.9" => "25.09.2023 fix possible bat adresses ", + "0.1.8" => "23.09.2023 new Attr userBatterytype, change manufacturerInfo, protocolVersion command hash to LENID=0 ", + "0.1.7" => "20.09.2023 extend possible number of bats from 6 to 8 ", + "0.1.6" => "19.09.2023 rework of _callAnalogValue, support of more than 15 cells ", + "0.1.5" => "19.09.2023 internal code change ", + "0.1.4" => "24.08.2023 Serialize and deserialize data for update entry, usage of BlockingCall in case of long timeout ", + "0.1.3" => "22.08.2023 improve responseCheck and others ", + "0.1.2" => "20.08.2023 commandref revised, analogValue -> use 'user defined items', refactoring according PBP ", + "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 ", + "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 ", +); + +## Konstanten +############### +my $invalid = 'unknown'; # default value for invalid readings +my $definterval = 30; # default Abrufintervall der Batteriewerte +my $defto = 0.5; # default connection Timeout zum RS485 Gateway +my @blackl = qw(state nextCycletime); # Ausnahmeliste deleteReadingspec +my $age1def = 60; # default Zyklus Abrufklasse statische Werte (s) + +# Steuerhashes +############### +my %hrtnc = ( # RTN Codes + '00' => { desc => 'normal' }, # normal Code + '01' => { desc => 'VER error' }, + '02' => { desc => 'CHKSUM error' }, + '03' => { desc => 'LCHKSUM error' }, + '04' => { desc => 'CID2 invalidation error' }, + '05' => { desc => 'Command format error' }, + '06' => { desc => 'invalid data error' }, + '90' => { desc => 'ADR error' }, + '91' => { desc => 'Communication error between Master and Slave Pack' }, + '98' => { desc => 'insufficient response length of minimum length received ... discarded' }, + '99' => { desc => 'invalid data received ... discarded' }, +); + +my %fns1 = ( # Abrufklasse statische Werte: + 1 => { fn => \&_callSerialNumber }, # serialNumber + 2 => { fn => \&_callManufacturerInfo }, # manufacturerInfo + 3 => { fn => \&_callProtocolVersion }, # protocolVersion + 4 => { fn => \&_callSoftwareVersion }, # softwareVersion + 5 => { fn => \&_callSystemParameters }, # systemParameters +); + +my %fns2 = ( # Abrufklasse dynamische Werte: + 1 => { fn => \&_callAlarmInfo }, # alarmInfo + 2 => { fn => \&_callChargeManagmentInfo }, # chargeManagmentInfo + 3 => { fn => \&_callAnalogValue }, # analogValue +); + +################################################################################################################################################################## +# The Basic data format SOI (7EH, ASCII '~') and EOI (CR -> 0DH) are explained and transferred in hexadecimal, +# the other items are explained in hexadecimal and transferred by hexadecimal-ASCII, each byte contains two +# ASCII, e.g. CID2 4BH transfer 2byte: +# 34H (the ASCII of ‘4’) and 42H(the ASCII of ‘B’). +# +# HEX-ASCII converter: https://www.rapidtables.com/convert/number/ascii-hex-bin-dec-converter.html +# Modulo Rechner: https://miniwebtool.com/de/modulo-calculator/ +# Pylontech Dokus: https://github.com/Interster/PylonTechBattery +################################################################################################################################################################## +# +# ADR: n=Batterienummer (2-x), m=Group Nr. (0-8), ADR = 0x0n + (0x10 * m) -> f. Batterie 1 = 0x02 + (0x10 * 0) = 0x02 +# CID1: Kommando spezifisch, hier 46H +# CID2: Kommando spezifisch, hier 93H +# LENGTH: LENID + LCHKSUM -> Pylon LFP V2.8 Doku +# INFO: muß hier mit ADR übereinstimmen +# CHKSUM: 32+30+30+32+34+36+39+33+45+30+30+32+30+32 = 02D3H -> modulo 65536 = 02D3H -> bitweise invert = 1111 1101 0010 1100 -> +1 = 1111 1101 0010 1101 -> FD2DH +# +# SOI VER ADR CID1 CID2 LENGTH INFO CHKSUM +# ~ 20 02 46 93 E0 02 02 FD 2D +# 7E 32 30 30 32 34 36 39 33 45 30 30 32 30 32 46 44 32 44 +# +my %hrsnb = ( # Codierung Abruf serialNumber, mlen = Mindestlänge Antwortstring + 1 => { cmd => "~20024693E00202FD2D\x{0d}", mlen => 52 }, + 2 => { cmd => "~20034693E00203FD2B\x{0d}", mlen => 52 }, + 3 => { cmd => "~20044693E00204FD29\x{0d}", mlen => 52 }, + 4 => { cmd => "~20054693E00205FD27\x{0d}", mlen => 52 }, + 5 => { cmd => "~20064693E00206FD25\x{0d}", mlen => 52 }, + 6 => { cmd => "~20074693E00207FD23\x{0d}", mlen => 52 }, + 7 => { cmd => "~20084693E00208FD21\x{0d}", mlen => 52 }, + 8 => { cmd => "~20094693E00209FD1F\x{0d}", mlen => 52 }, +); + +# ADR: n=Batterienummer (2-x), m=Group Nr. (0-8), ADR = 0x0n + (0x10 * m) -> f. Batterie 1 = 0x02 + (0x10 * 0) = 0x02 +# CID1: Kommando spezifisch, hier 46H +# CID2: Kommando spezifisch, hier 51H +# LENGTH: LENID + LCHKSUM -> Pylon LFP V3.3 Doku +# LENID = 0 -> LENID = 0000B + 0000B + 0000B = 0000B -> modulo 16 -> 0000B -> bitweise invert = 1111 -> +1 = 0001 0000 -> LCHKSUM = 0000B -> LENGTH = 0000 0000 0000 0000 -> 0000H +# wenn LENID = 0, dann ist INFO empty (Doku LFP V3.3 S.8) +# CHKSUM: 32+30+30+32+34+36+35+31+30+30+30+30 = 0254H -> modulo 65536 = 0254H -> bitweise invert = 1111 1101 1010 1011 -> +1 = 1111 1101 1010 1100 -> FDACH +# +# SOI VER ADR CID1 CID2 LENGTH INFO CHKSUM +# ~ 20 02 46 51 00 00 empty FD AC +# 7E 32 30 30 32 34 36 35 31 30 30 30 30 - - 46 44 41 43 +# +my %hrmfi = ( # Codierung Abruf manufacturerInfo, mlen = Mindestlänge Antwortstring + 1 => { cmd => "~200246510000FDAC\x{0d}", mlen => 82 }, + 2 => { cmd => "~200346510000FDAB\x{0d}", mlen => 82 }, + 3 => { cmd => "~200446510000FDAA\x{0d}", mlen => 82 }, + 4 => { cmd => "~200546510000FDA9\x{0d}", mlen => 82 }, + 5 => { cmd => "~200646510000FDA8\x{0d}", mlen => 82 }, + 6 => { cmd => "~200746510000FDA7\x{0d}", mlen => 82 }, + 7 => { cmd => "~200846510000FDA6\x{0d}", mlen => 82 }, + 8 => { cmd => "~200946510000FDA5\x{0d}", mlen => 82 }, +); + +# ADR: n=Batterienummer (2-x), m=Group Nr. (0-8), ADR = 0x0n + (0x10 * m) -> f. Batterie 1 = 0x02 + (0x10 * 0) = 0x02 +# CID1: Kommando spezifisch, hier 46H +# CID2: Kommando spezifisch, hier 4FH +# LENGTH: LENID + LCHKSUM -> Pylon LFP V3.3 Doku +# LENID = 0 -> LENID = 0000B + 0000B + 0000B = 0000B -> modulo 16 -> 0000B -> bitweise invert = 1111 -> +1 = 0001 0000 -> LCHKSUM = 0000B -> LENGTH = 0000 0000 0000 0000 -> 0000H +# wenn LENID = 0, dann ist INFO empty (Doku LFP V3.3 S.8) +# CHKSUM: 30+30+30+32+34+36+34+46+30+30+30+30 = 0266H -> modulo 65536 = 0266H -> bitweise invert = 1111 1101 1001 1001 -> +1 = 1111 1101 1001 1010 -> FD9AH +# +# SOI VER ADR CID1 CID2 LENGTH INFO CHKSUM +# ~ 00 02 46 4F 00 00 empty FD 9A +# 7E 30 30 30 32 34 36 34 46 30 30 30 30 - - 46 44 31 46 +# +my %hrprt = ( # Codierung Abruf protocolVersion, mlen = Mindestlänge Antwortstring + 1 => { cmd => "~0002464F0000FD9A\x{0d}", mlen => 18 }, + 2 => { cmd => "~0003464F0000FD99\x{0d}", mlen => 18 }, + 3 => { cmd => "~0004464F0000FD98\x{0d}", mlen => 18 }, + 4 => { cmd => "~0005464F0000FD97\x{0d}", mlen => 18 }, + 5 => { cmd => "~0006464F0000FD96\x{0d}", mlen => 18 }, + 6 => { cmd => "~0007464F0000FD95\x{0d}", mlen => 18 }, + 7 => { cmd => "~0008464F0000FD94\x{0d}", mlen => 18 }, + 8 => { cmd => "~0009464F0000FD93\x{0d}", mlen => 18 }, +); + + +my %hrswv = ( # Codierung Abruf softwareVersion + 1 => { cmd => "~20024696E00202FD2A\x{0d}", mlen => 30 }, + 2 => { cmd => "~20034696E00203FD28\x{0d}", mlen => 30 }, + 3 => { cmd => "~20044696E00204FD26\x{0d}", mlen => 30 }, + 4 => { cmd => "~20054696E00205FD24\x{0d}", mlen => 30 }, + 5 => { cmd => "~20064696E00206FD22\x{0d}", mlen => 30 }, + 6 => { cmd => "~20074696E00207FD20\x{0d}", mlen => 30 }, + 7 => { cmd => "~20084696E00208FD1E\x{0d}", mlen => 30 }, + 8 => { cmd => "~20094696E00209FD1C\x{0d}", mlen => 30 }, +); + +my %hralm = ( # Codierung Abruf alarmInfo + 1 => { cmd => "~20024644E00202FD31\x{0d}", mlen => 82 }, + 2 => { cmd => "~20034644E00203FD2F\x{0d}", mlen => 82 }, + 3 => { cmd => "~20044644E00204FD2D\x{0d}", mlen => 82 }, + 4 => { cmd => "~20054644E00205FD2B\x{0d}", mlen => 82 }, + 5 => { cmd => "~20064644E00206FD29\x{0d}", mlen => 82 }, + 6 => { cmd => "~20074644E00207FD27\x{0d}", mlen => 82 }, + 7 => { cmd => "~20084644E00208FD25\x{0d}", mlen => 82 }, + 8 => { cmd => "~20094644E00209FD23\x{0d}", mlen => 82 }, +); + +my %hrspm = ( # Codierung Abruf Systemparameter + 1 => { cmd => "~20024647E00202FD2E\x{0d}", mlen => 68 }, + 2 => { cmd => "~20034647E00203FD2C\x{0d}", mlen => 68 }, + 3 => { cmd => "~20044647E00204FD2A\x{0d}", mlen => 68 }, + 4 => { cmd => "~20054647E00205FD28\x{0d}", mlen => 68 }, + 5 => { cmd => "~20064647E00206FD26\x{0d}", mlen => 68 }, + 6 => { cmd => "~20074647E00207FD24\x{0d}", mlen => 68 }, + 7 => { cmd => "~20084647E00208FD22\x{0d}", mlen => 68 }, + 8 => { cmd => "~20094647E00209FD20\x{0d}", mlen => 68 }, +); + +my %hrcmi = ( # Codierung Abruf chargeManagmentInfo + 1 => { cmd => "~20024692E00202FD2E\x{0d}", mlen => 38 }, + 2 => { cmd => "~20034692E00203FD2C\x{0d}", mlen => 38 }, + 3 => { cmd => "~20044692E00204FD2A\x{0d}", mlen => 38 }, + 4 => { cmd => "~20054692E00205FD28\x{0d}", mlen => 38 }, + 5 => { cmd => "~20064692E00206FD26\x{0d}", mlen => 38 }, + 6 => { cmd => "~20074692E00207FD24\x{0d}", mlen => 38 }, + 7 => { cmd => "~20084692E00208FD22\x{0d}", mlen => 38 }, + 8 => { cmd => "~20094692E00209FD20\x{0d}", mlen => 38 }, +); + +# ADR: n=Batterienummer (2-x), m=Group Nr. (0-8), ADR = 0x0n + (0x10 * m) -> f. Batterie 1 = 0x02 + (0x10 * 0) = 0x02 +# CID1: Kommando spezifisch, hier 46H +# CID2: Kommando spezifisch, hier 42H LCHK| LENID +# LENGTH: LENID + LCHKSUM -> Pylon LFP V3.3 Doku ---- -------------- +# LENID = 02H -> LENID = 0000B + 0000B + 0010B = 0010B -> modulo 16 -> 0010B -> bitweise invert = 1101 -> +1 = 1110 -> LCHKSUM = 1110B -> LENGTH = 1110 0000 0000 0010 -> E002H +# wenn LENID = 0, dann ist INFO empty (Doku LFP V3.3 S.8) +# CHKSUM: 32+30+30+32+34+36+34+32+45+30+30+32+30+32 = 02CDH -> modulo 65536 = 02CDH -> bitweise invert = 1111 1101 0011 0010 -> +1 = 1111 1101 0011 0011 -> FD33H +# +# SOI VER ADR CID1 CID2 LENGTH INFO CHKSUM +# ~ 20 02 46 42 E0 02 02 FD 33 +# 7E 32 30 30 32 34 36 34 32 45 30 30 32 30 32 46 44 33 33 +# +my %hrcmn = ( # Codierung Abruf analogValue + 1 => { cmd => "~20024642E00202FD33\x{0d}", mlen => 128 }, + 2 => { cmd => "~20034642E00203FD31\x{0d}", mlen => 128 }, + 3 => { cmd => "~20044642E00204FD2F\x{0d}", mlen => 128 }, + 4 => { cmd => "~20054642E00205FD2D\x{0d}", mlen => 128 }, + 5 => { cmd => "~20064642E00206FD2B\x{0d}", mlen => 128 }, + 6 => { cmd => "~20074642E00207FD29\x{0d}", mlen => 128 }, + 7 => { cmd => "~20084642E00208FD27\x{0d}", mlen => 128 }, + 8 => { cmd => "~20094642E00209FD25\x{0d}", mlen => 128 }, +); + + +############################################################### +# PylonLowVoltage Initialize +############################################################### +sub Initialize { + my $hash = shift; + + $hash->{DefFn} = \&Define; + $hash->{UndefFn} = \&Undef; + $hash->{GetFn} = \&Get; + $hash->{AttrFn} = \&Attr; + $hash->{ShutdownFn} = \&Shutdown; + $hash->{AttrList} = "disable:1,0 ". + "interval ". + "timeout ". + "userBatterytype ". + $readingFnAttributes; + + eval { FHEM::Meta::InitMod( __FILE__, $hash ) }; ## no critic 'eval' + +return; +} + +############################################################### +# PylonLowVoltage Define +############################################################### +sub Define { + my ($hash, $def) = @_; + my @args = split m{\s+}x, $def; + + if (int(@args) < 2) { + return "Define: too few arguments. Usage:\n" . + "define PylonLowVoltage : []"; + } + + my $name = $hash->{NAME}; + + if ($iostAbsent) { + my $err = "Perl module >$iostAbsent< is missing. You have to install this perl module."; + Log3 ($name, 1, "$name - ERROR - $err"); + return "Error: $err"; + } + + if ($storabs) { + my $err = "Perl module >$storabs< is missing. You have to install this perl module."; + Log3 ($name, 1, "$name - ERROR - $err"); + return "Error: $err"; + } + + $hash->{HELPER}{MODMETAABSENT} = 1 if($modMetaAbsent); # Modul Meta.pm nicht vorhanden + ($hash->{HOST}, $hash->{PORT}) = split ":", $args[2]; + $hash->{BATADDRESS} = $args[3] // 1; + + if ($hash->{BATADDRESS} !~ /[12345678]/xs) { + return "Define: bataddress must be a value between 1 and 8"; + } + + my $params = { + hash => $hash, + notes => \%vNotesIntern, + useAPI => 0, + useSMUtils => 1, + useErrCodes => 0, + useCTZ => 0, + }; + use version 0.77; our $VERSION = moduleVersion ($params); # Versionsinformationen setzen + + _closeSocket ($hash); + manageUpdate ($hash); + +return; +} + +############################################################### +# PylonLowVoltage Get +############################################################### +sub Get { + my ($hash, @a) = @_; + return qq{"get X" needs at least an argument} if(@a < 2); + my $name = shift @a; + my $opt = shift @a; + my $arg = join " ", map { my $p = $_; $p =~ s/\s//xg; $p; } @a; ## no critic 'Map blocks' + + my $getlist = "Unknown argument $opt, choose one of ". + "data:noArg " + ; + + return if(IsDisabled($name)); + + if ($opt eq 'data') { + manageUpdate ($hash); + return; + } + +return $getlist; +} + +############################################################### +# PylonLowVoltage Attr +############################################################### +sub Attr { + my $cmd = shift; + my $name = shift; + my $aName = shift; + my $aVal = shift; + my $hash = $defs{$name}; + + my ($do,$val); + + # $cmd can be "del" or "set" + # $name is device name + # aName and aVal are Attribute name and value + + if ($aName eq 'disable') { + if($cmd eq 'set') { + $do = $aVal ? 1 : 0; + } + + $do = 0 if($cmd eq 'del'); + $val = ($do == 1 ? 'disabled' : 'initialized'); + + readingsSingleUpdate ($hash, 'state', $val, 1); + + if ($do == 0) { + InternalTimer(gettimeofday() + 2.0, "FHEM::PylonLowVoltage::manageUpdate", $hash, 0); + } + else { + deleteReadingspec ($hash); + readingsDelete ($hash, 'nextCycletime'); + _closeSocket ($hash); + } + } + + if ($aName eq 'interval') { + if (!looks_like_number($aVal)) { + return qq{The value for $aName is invalid, it must be numeric!}; + } + + InternalTimer(gettimeofday()+1.0, "FHEM::PylonLowVoltage::manageUpdate", $hash, 0); + } + + if ($aName eq 'userBatterytype') { + $hash->{HELPER}{AGE1} = 0; + InternalTimer(gettimeofday()+1.0, "FHEM::PylonLowVoltage::manageUpdate", $hash, 0); + } + + if ($aName eq 'timeout') { + if (!looks_like_number($aVal)) { + return qq{The value for $aName is invalid, it must be numeric!}; + } + } + +return; +} + +############################################################### +# Eintritt in den Update-Prozess +############################################################### +sub manageUpdate { + my $hash = shift; + + my $name = $hash->{NAME}; + my $age1 = delete $hash->{HELPER}{AGE1} // $age1def; + + RemoveInternalTimer ($hash); + + if(!$init_done) { + InternalTimer(gettimeofday() + 2, "FHEM::PylonLowVoltage::manageUpdate", $hash, 0); + return; + } + + return if(IsDisabled ($name)); + + my $interval = AttrVal ($name, 'interval', $definterval); # 0 -> manuell gesteuert + my $timeout = AttrVal ($name, 'timeout', $defto); + my $readings; + + if(!$interval) { + $hash->{OPMODE} = 'Manual'; + $readings->{nextCycletime} = 'Manual'; + } + else { + my $new = gettimeofday() + $interval; + InternalTimer ($new, "FHEM::PylonLowVoltage::manageUpdate", $hash, 0); # Wiederholungsintervall + + $hash->{OPMODE} = 'Automatic'; + $readings->{nextCycletime} = FmtTime($new); + } + + Log3 ($name, 4, "$name - start request cycle to battery number >$hash->{BATADDRESS}< at host:port $hash->{HOST}:$hash->{PORT}"); + + if ($timeout < 1.0) { + BlockingKill ($hash->{HELPER}{BKRUNNING}) if(defined $hash->{HELPER}{BKRUNNING}); + Log3 ($name, 4, qq{$name - Cycle started in main process}); + startUpdate ({name => $name, timeout => $timeout, readings => $readings, age1 => $age1}); + } + else { + delete $hash->{HELPER}{BKRUNNING} if(defined $hash->{HELPER}{BKRUNNING} && $hash->{HELPER}{BKRUNNING}{pid} =~ /DEAD/xs); + + if (defined $hash->{HELPER}{BKRUNNING}) { + Log3 ($name, 3, qq{$name - another BlockingCall PID "$hash->{HELPER}{BKRUNNING}{pid}" is already running ... start Update aborted}); + + return; + } + + my $blto = sprintf "%.0f", ($timeout + 10); + + $hash->{HELPER}{BKRUNNING} = BlockingCall ( "FHEM::PylonLowVoltage::startUpdate", + {name => $name, timeout => $timeout, readings => $readings, age1 => $age1, block => 1}, + "FHEM::PylonLowVoltage::finishUpdate", + $blto, # Blocking Timeout höher als INET-Timeout! + "FHEM::PylonLowVoltage::abortUpdate", + $hash + ); + + + if (defined $hash->{HELPER}{BKRUNNING}) { + $hash->{HELPER}{BKRUNNING}{loglevel} = 3; # Forum https://forum.fhem.de/index.php/topic,77057.msg689918.html#msg689918 + + Log3 ($name, 4, qq{$name - Cycle BlockingCall PID "$hash->{HELPER}{BKRUNNING}{pid}" with timeout "$blto" started}); + } + } + +return; +} + +############################################################### +# PylonLowVoltage startUpdate +############################################################### +sub startUpdate { + my $paref = shift; + + my $name = $paref->{name}; + my $timeout = $paref->{timeout}; + my $readings = $paref->{readings}; + my $block = $paref->{block} // 0; + my $age1 = $paref->{age1}; + + my $hash = $defs{$name}; + my $success = 0; + + my ($socket, $serial); + + eval { ## no critic 'eval' + local $SIG{ALRM} = sub { croak 'gatewaytimeout' }; + ualarm ($timeout * 1000000); # ualarm in Mikrosekunden + + $socket = _openSocket ($hash, $timeout, $readings); + + if (!$socket) { + $serial = encode_base64 (Serialize ( {name => $name, readings => $readings} ), ""); + $block ? return ($serial) : return \&finishUpdate ($serial); + } + + if (ReadingsAge ($name, "serialNumber", 6000) >= $age1) { # Abrufklasse statische Werte + for my $idx (sort keys %fns1) { + if (&{$fns1{$idx}{fn}} ($hash, $socket, $readings)) { + $serial = encode_base64 (Serialize ( {name => $name, readings => $readings} ), ""); + $block ? return ($serial) : return \&finishUpdate ($serial); + } + } + } + + for my $idx (sort keys %fns2) { # Abrufklasse dynamische Werte + if (&{$fns2{$idx}{fn}} ($hash, $socket, $readings)) { + $serial = encode_base64 (Serialize ( {name => $name, readings => $readings} ), ""); + $block ? return ($serial) : return \&finishUpdate ($serial); + } + } + + $success = 1; + }; # eval + + if ($@) { + my $errtxt; + if ($@ =~ /gatewaytimeout/xs) { + $errtxt = 'Timeout in communication to RS485 gateway'; + } + else { + $errtxt = $@; + } + + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => $errtxt, + verbose => 3 + } + ); + + $serial = encode_base64 (Serialize ( {name => $name, readings => $readings} ), ""); + $block ? return ($serial) : return \&finishUpdate ($serial); + } + + ualarm(0); + _closeSocket ($hash); + + $serial = encode_base64 (Serialize ({name => $name, success => $success, readings => $readings}), ""); + + if ($block) { + return ($serial); + } + +return \&finishUpdate ($serial); +} + +############################################################### +# Restaufgaben nach Update +############################################################### +sub finishUpdate { + my $serial = decode_base64 (shift); + + my $paref = eval { thaw ($serial) }; # Deserialisierung + my $name = $paref->{name}; + my $success = $paref->{success} // 0; + my $readings = $paref->{readings}; + my $hash = $defs{$name}; + + delete($hash->{HELPER}{BKRUNNING}) if(defined $hash->{HELPER}{BKRUNNING}); + + if ($success) { + Log3 ($name, 4, "$name - got data from battery number >$hash->{BATADDRESS}< successfully"); + + additionalReadings ($readings); # zusätzliche eigene Readings erstellen + $readings->{state} = 'connected'; + } + else { + deleteReadingspec ($hash); + } + + createReadings ($hash, $success, $readings); # Readings erstellen + +return; +} + +#################################################################################################### +# Abbruchroutine BlockingCall Timeout +#################################################################################################### +sub abortUpdate { + my $hash = shift; + my $cause = shift // "Timeout: process terminated"; + my $name = $hash->{NAME}; + + Log3 ($name, 1, "$name -> BlockingCall $hash->{HELPER}{BKRUNNING}{fn} pid:$hash->{HELPER}{BKRUNNING}{pid} aborted: $cause"); + + delete($hash->{HELPER}{BKRUNNING}); + + deleteReadingspec ($hash); + readingsSingleUpdate ($hash, 'state', 'Update (Child) process timed out', 1); + +return; +} + +############################################################### +# Socket erstellen +############################################################### +sub _openSocket { + my $hash = shift; + my $timeout = shift; + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + + my $socket = $hash->{SOCKET}; + + if ($socket && !$socket->connected()) { + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => 'disconnected' + } + ); + + _closeSocket ($hash); + undef $socket; + } + + if (!$socket) { + $socket = IO::Socket::INET->new( Proto => 'tcp', + PeerAddr => $hash->{HOST}, + PeerPort => $hash->{PORT}, + Timeout => $timeout + ) + or do { doOnError ({ hash => $hash, + readings => $readings, + state => 'no connection to RS485 gateway established', + verbose => 3 + } + ); + return; + }; + } + + IO::Socket::Timeout->enable_timeouts_on ($socket); # nur notwendig für read or write timeout + + $socket->read_timeout (0.5); # Read/Writetimeout immer kleiner als Sockettimeout + $socket->write_timeout (0.5); + $socket->autoflush(); + + $hash->{SOCKET} = $socket; + +return $socket; +} + +############################################################### +# Socket schließen und löschen +############################################################### +sub _closeSocket { + my $hash = shift; + + my $name = $hash->{NAME}; + my $socket = $hash->{SOCKET}; + + if ($socket) { + close ($socket); + delete $hash->{SOCKET}; + + Log3 ($name, 4, "$name - Socket/Connection to the RS485 gateway was closed"); + } + +return; +} + +############################################################### +# Abruf serialNumber +############################################################### +sub _callSerialNumber { + my $hash = shift; + my $socket = shift; + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + + my $res = Request ({ hash => $hash, + socket => $socket, + cmd => $hrsnb{$hash->{BATADDRESS}}{cmd}, + cmdtxt => 'serialNumber' + } + ); + + my $rtnerr = responseCheck ($res, $hrsnb{$hash->{BATADDRESS}}{mlen}); + + if ($rtnerr) { + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => $rtnerr + } + ); + return $rtnerr; + } + + __resultLog ($hash, $res); + + my $sernum = substr ($res, 15, 32); + $readings->{serialNumber} = pack ("H*", $sernum); + +return; +} + +############################################################### +# Abruf manufacturerInfo +############################################################### +sub _callManufacturerInfo { + my $hash = shift; + my $socket = shift; + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + + my $res = Request ({ hash => $hash, + socket => $socket, + cmd => $hrmfi{$hash->{BATADDRESS}}{cmd}, + cmdtxt => 'manufacturerInfo' + } + ); + + my $rtnerr = responseCheck ($res, $hrmfi{$hash->{BATADDRESS}}{mlen}); + + if ($rtnerr) { + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => $rtnerr + } + ); + return $rtnerr; + } + + __resultLog ($hash, $res); + + my $name = $hash->{NAME}; + my $ubtt = AttrVal ($name, 'userBatterytype', ''); # evtl. Batterietyp manuell überschreiben + my $BatteryHex = substr ($res, 13, 20); + # my $softwareVersion = 'V'.hex (substr ($res, 33, 2)).'.'.hex (substr ($res, 35, 2)); # unklare Bedeutung + my $ManufacturerHex = substr ($res, 37, 40); + + $readings->{batteryType} = $ubtt ? $ubtt.' (adapted)' : pack ("H*", $BatteryHex); + $readings->{Manufacturer} = pack ("H*", $ManufacturerHex); + +return; +} + +############################################################### +# Abruf protocolVersion +############################################################### +sub _callProtocolVersion { + my $hash = shift; + my $socket = shift; + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + + my $res = Request ({ hash => $hash, + socket => $socket, + cmd => $hrprt{$hash->{BATADDRESS}}{cmd}, + cmdtxt => 'protocolVersion' + } + ); + + my $rtnerr = responseCheck ($res, $hrprt{$hash->{BATADDRESS}}{mlen}); + + if ($rtnerr) { + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => $rtnerr + } + ); + return $rtnerr; + } + + __resultLog ($hash, $res); + + $readings->{protocolVersion} = 'V'.hex (substr ($res, 1, 1)).'.'.hex (substr ($res, 2, 1)); + +return; +} + +############################################################### +# Abruf softwareVersion +############################################################### +sub _callSoftwareVersion { + my $hash = shift; + my $socket = shift; + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + + my $res = Request ({ hash => $hash, + socket => $socket, + cmd => $hrswv{$hash->{BATADDRESS}}{cmd}, + cmdtxt => 'softwareVersion' + } + ); + + my $rtnerr = responseCheck ($res, $hrswv{$hash->{BATADDRESS}}{mlen}); + + if ($rtnerr) { + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => $rtnerr + } + ); + return $rtnerr; + } + + __resultLog ($hash, $res); + + $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)); + +return; +} + +############################################################### +# Abruf systemParameters +############################################################### +sub _callSystemParameters { + my $hash = shift; + my $socket = shift; + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + + my $res = Request ({ hash => $hash, + socket => $socket, + cmd => $hrspm{$hash->{BATADDRESS}}{cmd}, + cmdtxt => 'systemParameters' + } + ); + + my $rtnerr = responseCheck ($res, $hrspm{$hash->{BATADDRESS}}{mlen}); + + if ($rtnerr) { + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => $rtnerr + } + ); + return $rtnerr; + } + + __resultLog ($hash, $res); + + $readings->{paramCellHighVoltLimit} = sprintf "%.3f", (hex substr ($res, 15, 4)) / 1000; + $readings->{paramCellLowVoltLimit} = sprintf "%.3f", (hex substr ($res, 19, 4)) / 1000; # Alarm Limit + $readings->{paramCellUnderVoltLimit} = sprintf "%.3f", (hex substr ($res, 23, 4)) / 1000; # Schutz Limit + $readings->{paramChargeHighTempLimit} = sprintf "%.1f", ((hex substr ($res, 27, 4)) - 2731) / 10; + $readings->{paramChargeLowTempLimit} = sprintf "%.1f", ((hex substr ($res, 31, 4)) - 2731) / 10; + $readings->{paramChargeCurrentLimit} = sprintf "%.3f", (hex substr ($res, 35, 4)) * 100 / 1000; + $readings->{paramModuleHighVoltLimit} = sprintf "%.3f", (hex substr ($res, 39, 4)) / 1000; + $readings->{paramModuleLowVoltLimit} = sprintf "%.3f", (hex substr ($res, 43, 4)) / 1000; # Alarm Limit + $readings->{paramModuleUnderVoltLimit} = sprintf "%.3f", (hex substr ($res, 47, 4)) / 1000; # Schutz Limit + $readings->{paramDischargeHighTempLimit} = sprintf "%.1f", ((hex substr ($res, 51, 4)) - 2731) / 10; + $readings->{paramDischargeLowTempLimit} = sprintf "%.1f", ((hex substr ($res, 55, 4)) - 2731) / 10; + $readings->{paramDischargeCurrentLimit} = sprintf "%.3f", (65535 - (hex substr ($res, 59, 4))) * 100 / 1000; # mit Symbol (-) + +return; +} + +############################################################### +# Abruf alarmInfo +############################################################### +sub _callAlarmInfo { + my $hash = shift; + my $socket = shift; + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + + my $res = Request ({ hash => $hash, + socket => $socket, + cmd => $hralm{$hash->{BATADDRESS}}{cmd}, + cmdtxt => 'alarmInfo' + } + ); + + my $rtnerr = responseCheck ($res, $hralm{$hash->{BATADDRESS}}{mlen}); + + if ($rtnerr) { + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => $rtnerr + } + ); + return $rtnerr; + } + + __resultLog ($hash, $res); + + $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"; + } + +return; +} + +############################################################### +# Abruf chargeManagmentInfo +############################################################### +sub _callChargeManagmentInfo { + my $hash = shift; + my $socket = shift; + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + + my $res = Request ({ hash => $hash, + socket => $socket, + cmd => $hrcmi{$hash->{BATADDRESS}}{cmd}, + cmdtxt => 'chargeManagmentInfo' + } + ); + + my $rtnerr = responseCheck ($res, $hrcmi{$hash->{BATADDRESS}}{mlen}); + + if ($rtnerr) { + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => $rtnerr + } + ); + return $rtnerr; + } + + __resultLog ($hash, $res); + + $readings->{chargeVoltageLimit} = sprintf "%.3f", hex (substr ($res, 15, 4)) / 1000; # Genauigkeit 3 + $readings->{dischargeVoltageLimit} = sprintf "%.3f", hex (substr ($res, 19, 4)) / 1000; # Genauigkeit 3 + $readings->{chargeCurrentLimit} = sprintf "%.1f", hex (substr ($res, 23, 4)) / 10; # Genauigkeit 1 + $readings->{dischargeCurrentLimit} = sprintf "%.1f", (65536 - hex substr ($res, 27, 4)) / 10; # Genauigkeit 1, Fixed point, unsigned integer + + 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->{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 + +return; +} + +################################################################################# +# Abruf analogValue +# Answer from US2000 = 128 Bytes, from US3000 = 140 Bytes +# Remain capacity US2000 hex(substr($res,109,4), US3000 hex(substr($res,123,6) +# Module capacity US2000 hex(substr($res,115,4), US3000 hex(substr($res,129,6) +################################################################################# +sub _callAnalogValue { + my $hash = shift; + my $socket = shift; + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + my $name = $hash->{NAME}; + + my $res = Request ({ hash => $hash, + socket => $socket, + cmd => $hrcmn{$hash->{BATADDRESS}}{cmd}, + cmdtxt => 'analogValue' + } + ); + + my $rtnerr = responseCheck ($res, $hrcmn{$hash->{BATADDRESS}}{mlen}); + + if ($rtnerr) { + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => $rtnerr + } + ); + return $rtnerr; + } + + __resultLog ($hash, $res); + + my $bpos = 17; # Startposition + my $pcc = hex (substr($res, $bpos, 2)); # Anzahl Zellen (15 od. 16) + $bpos += 2; # Pos 19 + + for my $z (1..$pcc) { + my $fz = sprintf "%02d", $z; # formatierter Zähler + $readings->{'cellVoltage_'.$fz} = sprintf "%.3f", hex(substr($res, $bpos, 4)) / 1000; # Pos 19 - 75 bei 15 Zellen + $bpos += 4; # letzter Durchlauf: Pos 79 bei 15 Zellen, Pos 83 bei 16 Zellen + } + + $readings->{numberTempPos} = hex(substr($res, $bpos, 2)); # Anzahl der jetzt folgenden Teperaturpositionen -> 5 + $bpos += 2; + + $readings->{bmsTemperature} = (hex (substr($res, $bpos, 4)) - 2731) / 10; # Pos 81 bei 15 Zellen + $bpos += 4; + + $readings->{cellTemperature_0104} = (hex (substr($res, $bpos, 4)) - 2731) / 10; # Pos 85 + $bpos += 4; + + $readings->{cellTemperature_0508} = (hex (substr($res, $bpos, 4)) - 2731) / 10; # Pos 89 + $bpos += 4; + + $readings->{cellTemperature_0912} = (hex (substr($res, $bpos, 4)) - 2731) / 10; # Pos 93 + $bpos += 4; + + $readings->{'cellTemperature_13'.$pcc} = (hex (substr($res, $bpos, 4)) - 2731) / 10; # Pos 97 + $bpos += 4; + + my $current = hex (substr($res, $bpos, 4)); # Pos 101 + $bpos += 4; + + $readings->{packVolt} = sprintf "%.3f", hex (substr($res, $bpos, 4)) / 1000; # Pos 105 + $bpos += 4; + + my $remcap1 = sprintf "%.3f", hex (substr($res, $bpos, 4)) / 1000; # Pos 109 + $bpos += 4; + + my $udi = hex substr($res, $bpos, 2); # Pos 113, user defined item=Entscheidungskriterium -> 2: Batterien <= 65Ah, 4: Batterien > 65Ah + $bpos += 2; + + my $totcap1 = sprintf "%.3f", hex (substr($res, $bpos, 4)) / 1000; # Pos 115 + $bpos += 4; + + $readings->{packCycles} = hex substr($res, $bpos, 4); # Pos 119 + $bpos += 4; + + my $remcap2 = sprintf "%.3f", hex (substr($res, $bpos, 6)) / 1000; # Pos 123 + $bpos += 6; + + my $totcap2 = sprintf "%.3f", hex (substr($res, $bpos, 6)) / 1000; # Pos 129 + $bpos += 6; + + # kalkulierte Werte generieren + ################################ + if ($udi == 2) { + $readings->{packCapacityRemain} = $remcap1; + $readings->{packCapacity} = $totcap1; + } + elsif ($udi == 4) { + $readings->{packCapacityRemain} = $remcap2; + $readings->{packCapacity} = $totcap2; + } + else { + my $err = 'wrong value retrieve analogValue -> user defined items: '.$udi; + doOnError ({ hash => $hash, + readings => $readings, + sock => $socket, + state => $err + } + ); + return $err; + } + + if ($current & 0x8000) { + $current = $current - 0x10000; + } + + $readings->{packCellcount} = $pcc; + $readings->{packCurrent} = sprintf "%.3f", $current / 10; + +return; +} + +############################################################### +# Logausgabe Result +############################################################### +sub __resultLog { + my $hash = shift; + my $res = shift; + + my $name = $hash->{NAME}; + + Log3 ($name, 5, "$name - data returned raw: ".$res); + Log3 ($name, 5, "$name - data returned:\n" .Hexdump ($res)); + +return; +} + +############################################################### +# Daten Serialisieren +############################################################### +sub Serialize { + my $data = shift; + my $name = $data->{name}; + + my $serial = eval { freeze ($data) + } + or do { Log3 ($name, 2, "$name - Serialization ERROR: $@"); + return; + }; + +return $serial; +} + +############################################################### +# PylonLowVoltage Request +############################################################### +sub Request { + my $paref = shift; + + my $hash = $paref->{hash}; + my $socket = $paref->{socket}; + my $cmd = $paref->{cmd}; + my $cmdtxt = $paref->{cmdtxt} // 'unspecified data'; + + my $name = $hash->{NAME}; + + Log3 ($name, 4, "$name - retrieve battery info: ".$cmdtxt); + Log3 ($name, 4, "$name - request command (ASCII): ".$cmd); + Log3 ($name, 5, "$name - request command (HEX): ".unpack "H*", $cmd); + + printf $socket $cmd; + +return Reread ($hash, $socket); +} + +############################################################### +# RS485 Daten lesen/empfagen +############################################################### +sub Reread { + my $hash = shift; + my $socket = shift; + + my $singlechar; + my $res = q{}; + + do { + $socket->read ($singlechar, 1); + + if (!$singlechar && (0+$! == ETIMEDOUT || 0+$! == EWOULDBLOCK)) { # nur notwendig für read timeout + croak 'Timeout reading data from battery'; + } + + $res = $res . $singlechar if(length $singlechar != 0 && $singlechar =~ /[~A-Z0-9\r]+/xs); + + } while (length $singlechar == 0 || ord($singlechar) != 13); + +return $res; +} + +############################################################### +# PylonLowVoltage Undef +############################################################### +sub Shutdown { + my ($hash, $args) = @_; + + RemoveInternalTimer ($hash); + _closeSocket ($hash); + BlockingKill ($hash->{HELPER}{BKRUNNING}) if(defined $hash->{HELPER}{BKRUNNING}); + +return; +} + +############################################################### +# PylonLowVoltage Hexdump +############################################################### +sub Hexdump { + my $res = shift; + + my $offset = 0; + my $result = ""; + + for my $chunk (unpack "(a16)*", $res) { + my $hex = unpack "H*", $chunk; # hexadecimal magic + $chunk =~ tr/ -~/./c; # replace unprintables + $hex =~ s/(.{1,8})/$1 /gxs; # insert spaces + $result .= sprintf "0x%08x (%05u) %-*s %s\n", $offset, $offset, 36, $hex, $chunk; + $offset += 16; + } + +return $result; +} + +############################################################### +# Response Status ermitteln +############################################################### +sub responseCheck { + my $res = shift; + my $mlen = shift // 0; # Mindestlänge Antwortstring + + my $rtnerr = $hrtnc{99}{desc}; + + if(!$res || $res !~ /^[~A-Fa-f0-9]+\r$/xs || $res =~ tr/~// != 1) { + return $rtnerr; + } + + my $len = length($res); + + if ($len < $mlen) { + $rtnerr = $hrtnc{98}{desc}; + $rtnerr =~ s//$len/xs; + $rtnerr =~ s//$mlen/xs; + return $rtnerr; + } + + my $rtn = q{_}; + $rtn = substr($res,7,2) if($res && $len >= 10); + + if(defined $hrtnc{$rtn}{desc} && substr($res, 0, 1) eq '~') { + $rtnerr = $hrtnc{$rtn}{desc}; + return if($rtnerr eq 'normal'); + } + +return $rtnerr; +} + +############################################################### +# Fehlerausstieg +############################################################### +sub doOnError { + my $paref = shift; + + my $hash = $paref->{hash}; + my $readings = $paref->{readings}; # Referenz auf das Hash der zu erstellenden Readings + my $state = $paref->{state}; + my $socket = $paref->{sock}; + my $verbose = $paref->{verbose} // 4; + + ualarm(0); + + my $name = $hash->{NAME}; + $state = (split "at ", $state)[0]; + $readings->{state} = $state; + $verbose = 3 if($readings->{state} =~ /error/xsi); + + Log3 ($name, $verbose, "$name - ".$readings->{state}); + + _closeSocket ($hash); + +return; +} + +############################################################### +# eigene zusaätzliche Werte erstellen +############################################################### +sub additionalReadings { + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + + my ($vmax, $vmin); + + $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++) { + $i = sprintf "%02d", $i; + $vmax = $readings->{'cellVoltage_'.$i} if(!$vmax || $vmax < $readings->{'cellVoltage_'.$i}); + $vmin = $readings->{'cellVoltage_'.$i} if(!$vmin || $vmin > $readings->{'cellVoltage_'.$i}); + } + + if ($vmax && $vmin) { + my $maxdf = $vmax - $vmin; + $readings->{packImbalance} = sprintf "%.3f", 100 * $maxdf / $readings->{averageCellVolt}; + } + + $readings->{packState} = $readings->{packCurrent} < 0 ? 'discharging' : + $readings->{packCurrent} > 0 ? 'charging' : + 'idle'; + +return; +} + +############################################################### +# Readings erstellen +############################################################### +sub createReadings { + my $hash = shift; + my $success = shift; + my $readings = shift; # Referenz auf das Hash der zu erstellenden Readings + + readingsBeginUpdate ($hash); + + for my $rdg (keys %{$readings}) { + next if(!defined $readings->{$rdg}); + readingsBulkUpdate ($hash, $rdg, $readings->{$rdg}) if($success || $rdg ~~ @blackl); + } + + readingsEndUpdate ($hash, 1); + +return; +} + +################################################################ +# alle Readings eines Devices oder nur Reading-Regex +# löschen +# Readings der Blacklist werden nicht gelöscht +################################################################ +sub deleteReadingspec { + my $hash = shift; + my $spec = shift // ".*"; + + my $readingspec = '^'.$spec.'$'; + + for my $reading ( grep { /$readingspec/x } keys %{$hash->{READINGS}} ) { + next if($reading ~~ @blackl); + readingsDelete ($hash, $reading); + } + +return; +} + +1; + + +=pod +=item device +=item summary Integration of Pylontech low voltage batteries via RS485 ethernet gateway +=item summary_DE Integration von Pylontech Niederspannungsbatterien über RS485-Ethernet-Gateway + +=begin html + + +

PylonLowVoltage

+
+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:
+ +
    +
  • US2000
  • +
  • US2000B Plus
  • +
  • US2000C
  • +
  • US2000 Plus
  • +
  • US3000
  • +
  • US3000C
  • +
+ +The following devices have been successfully used as RS485 Ethernet gateways to date:
+
    +
  • USR-TCP232-304 from the manufacturer USRiot
  • +
  • Waveshare RS485 to Ethernet Converter
  • +
+ +In principle, any other RS485/Ethernet gateway should also be compatible. +

+ +Requirements +

+This module requires the Perl modules: +
    +
  • IO::Socket::INET (apt-get install libio-socket-multicast-perl)
  • +
  • IO::Socket::Timeout (Installation e.g. via the CPAN shell or the FHEM Installer module)
  • +
+ +Limitations +
+The module currently supports a maximum of 8 batteries (master + 7 slaves) in one group. +

+ + +Definition +
    + define <name> PylonLowVoltage <hostname/ip>:<port> [<bataddress>]
    +
    +
  • hostname/ip:
    + Host name or IP address of the RS485/Ethernet gateway +
  • + +
  • port:
    + Port number of the port configured in the RS485/Ethernet gateway +
  • + +
  • bataddress:
    + Device address of the Pylontech battery. Several Pylontech batteries can be connected via a Pylontech-specific + Link connection. The permissible number can be found in the respective Pylontech documentation.
    + The master battery in the network (with open link port 0 or to which the RS485 connection is connected) has the + address 1, the next battery then has address 2 and so on. + If no device address is specified, address 1 is used. +
  • +
    +
+ +Mode of operation +
    +Depending on the setting of the "Interval" attribute, the module cyclically reads values provided by the battery +management system via the RS485 interface. +
+ + +Get +
+
    +
  • data
    + The data query of the battery management system is executed. The timer of the cyclic query is reinitialized according + to the set value of the "interval" attribute. +
    +
  • +
    +
+ + +Attributes + +
    + +
  • disable 0|1
    + Enables/disables the device definition. +
  • +
    + + +
  • interval <seconds>
    + Interval of the data request from the battery in seconds. If "interval" is explicitly set to the value "0", there is + no automatic data request.
    + (default: 30) +
  • +
    + + +
  • timeout <seconds>
    + Timeout for establishing the connection to the RS485 gateway.
    + (default: 0.5) + +

    + Note: If a timeout >= 1 second is set, the module switches internally to the use of a parallel process + (BlockingCall) so that write or read delays on the RS485 interface do not lead to blocking states in FHEM. +
  • +
    + + +
  • userBatterytype
    + The automatically determined battery type (Reading batteryType) is replaced by the specified string. +
  • +
    +
+ + +Readings +
    +
  • averageCellVolt
    Average cell voltage (V)
  • +
  • bmsTemperature
    Temperature (°C) of the battery management system
  • +
  • cellTemperature_0104
    Temperature (°C) of cell packs 1 to 4
  • +
  • cellTemperature_0508
    Temperature (°C) of cell packs 5 to 8
  • +
  • cellTemperature_0912
    Temperature (°C) of the cell packs 9 to 12
  • +
  • cellTemperature_1315
    Temperature (°C) of the cell packs 13 to 15
  • +
  • cellVoltage_XX
    Cell voltage (V) of the cell pack XX. In the battery module "packCellcount" + cell packs are connected in series. Each cell pack consists of single cells + connected in parallel.
  • +
  • chargeCurrentLimit
    current limit value for the charging current (A)
  • +
  • chargeEnable
    current flag loading allowed
  • +
  • chargeFullRequest
    current flag charge battery module fully (from the mains if necessary)
  • +
  • chargeImmediatelySOCXX
    current flag charge battery module immediately + (05: SOC limit 5-9%, 09: SOC limit 9-13%)
  • +
  • chargeVoltageLimit
    current charge voltage limit (V) of the battery module
  • +
  • dischargeCurrentLimit
    current limit value for the discharge current (A)
  • +
  • dischargeEnable
    current flag unloading allowed
  • +
  • dischargeVoltageLimit
    current discharge voltage limit (V) of the battery module
  • + +
  • moduleSoftwareVersion_manufacture
    Firmware version of the battery module
  • + +
  • packAlarmInfo
    Alarm status (ok - battery module is OK, failure - there is a fault in the + battery module)
  • +
  • packCapacity
    nominal capacity (Ah) of the battery module
  • +
  • packCapacityRemain
    current capacity (Ah) of the battery module
  • +
  • packCellcount
    Number of cell packs in the battery module
  • +
  • packCurrent
    current charge current (+) or discharge current (-) of the battery module (A)
  • +
  • packCycles
    Number of full cycles - The number of cycles is, to some extent, a measure of the + wear and tear of the battery. A complete charge and discharge is counted as one + cycle. If the battery is discharged and recharged 50%, it only counts as one + half cycle. Pylontech specifies a lifetime of several 1000 cycles + (see data sheet).
  • +
  • packImbalance
    current imbalance of voltage between the single cells of the + battery module (%)
  • +
  • packPower
    current drawn (+) or delivered (-) power (W) of the battery module
  • +
  • packSOC
    State of charge (%) of the battery module
  • +
  • packState
    current working status of the battery module
  • +
  • packVolt
    current voltage (V) of the battery module
  • + +
  • paramCellHighVoltLimit
    System parameter upper voltage limit (V) of a cell
  • +
  • paramCellLowVoltLimit
    System parameter lower voltage limit (V) of a cell (alarm limit)
  • +
  • paramCellUnderVoltLimit
    System parameter undervoltage limit (V) of a cell (protection limit)
  • +
  • paramChargeCurrentLimit
    System parameter charging current limit (A) of the battery module
  • +
  • paramChargeHighTempLimit
    System parameter upper temperature limit (°C) up to which the battery charges
  • +
  • paramChargeLowTempLimit
    System parameter lower temperature limit (°C) up to which the battery charges
  • +
  • paramDischargeCurrentLimit
    System parameter discharge current limit (A) of the battery module
  • +
  • paramDischargeHighTempLimit
    System parameter upper temperature limit (°C) up to which the battery discharges
  • +
  • paramDischargeLowTempLimit
    System parameter lower temperature limit (°C) up to which the battery discharges
  • +
  • paramModuleHighVoltLimit
    System parameter upper voltage limit (V) of the battery module
  • +
  • paramModuleLowVoltLimit
    System parameter lower voltage limit (V) of the battery module (alarm limit)
  • +
  • paramModuleUnderVoltLimit
    System parameter undervoltage limit (V) of the battery module (protection limit)
  • +
  • protocolVersion
    PYLON low voltage RS485 protocol version
  • +
  • serialNumber
    Serial number
  • +
+

+ +=end html +=begin html_DE + + +

PylonLowVoltage

+
+Modul zur Einbindung von Niedervolt-Batterien mit Batteriemanagmentsystem (BMS) des Herstellers Pylontech über RS485 via +RS485/Ethernet-Gateway. Die Kommunikation zum RS485-Gateway erfolgt ausschließlich über eine Ethernet-Verbindung.
+Das Modul wurde bisher erfolgreich mit Pylontech Batterien folgender Typen eingesetzt:
+ +
    +
  • US2000
  • +
  • US2000B Plus
  • +
  • US2000C
  • +
  • US2000 Plus
  • +
  • US3000
  • +
  • US3000C
  • +
+ +Als RS485-Ethernet-Gateways wurden bisher folgende Geräte erfolgreich eingesetzt:
+
    +
  • USR-TCP232-304 des Herstellers USRiot
  • +
  • Waveshare RS485 to Ethernet Converter
  • +
+ +Prinzipiell sollte auch jedes andere RS485/Ethernet-Gateway kompatibel sein. +

+ +Voraussetzungen +

+Dieses Modul benötigt die Perl-Module: +
    +
  • IO::Socket::INET (apt-get install libio-socket-multicast-perl)
  • +
  • IO::Socket::Timeout (Installation z.B. über die CPAN-Shell oder das FHEM Installer Modul)
  • +
+ +Einschränkungen +
+Das Modul unterstützt zur Zeit maximal 8 Batterien (Master + 7 Slaves) in einer Gruppe. +

+ + +Definition +
    + define <name> PylonLowVoltage <hostname/ip>:<port> [<bataddress>]
    +
    +
  • hostname/ip:
    + Hostname oder IP-Adresse des RS485/Ethernet-Gateways +
  • + +
  • port:
    + Port-Nummer des im RS485/Ethernet-Gateways konfigurierten Ports +
  • + +
  • bataddress:
    + Geräteadresse der Pylontech Batterie. Es können mehrere Pylontech Batterien über eine Pylontech-spezifische + Link-Verbindung verbunden werden. Die zulässige Anzahl ist der jeweiligen Pylontech Dokumentation zu entnehmen.
    + Die Master Batterie im Verbund (mit offenem Link Port 0 bzw. an der die RS485-Verbindung angeschlossen ist) hat die + Adresse 1, die nächste Batterie hat dann die Adresse 2 und so weiter. + Ist keine Geräteadresse angegeben, wird die Adresse 1 verwendet. +
  • +
    +
+ +Arbeitsweise +
    +Das Modul liest entsprechend der Einstellung des Attributes "interval" zyklisch Werte aus, die das +Batteriemanagementsystem über die RS485-Schnittstelle zur Verfügung stellt. +
+ + +Get +
+
    +
  • data
    + Die Datenabfrage des Batteriemanagementsystems wird ausgeführt. Der Zeitgeber der zyklischen Abfrage wird entsprechend + dem gesetzten Wert des Attributes "interval" neu initialisiert. +
    +
  • +
    +
+ + +Attribute + +
    + +
  • disable 0|1
    + Aktiviert/deaktiviert die Gerätedefinition. +
  • +
    + + +
  • interval <Sekunden>
    + Intervall der Datenabfrage von der Batterie in Sekunden. Ist "interval" explizit auf den Wert "0" gesetzt, erfolgt + keine automatische Datenabfrage.
    + (default: 30) +
  • +
    + + +
  • timeout <Sekunden>
    + Timeout für den Verbindungsaufbau zum RS485 Gateway.
    + (default: 0.5) + +

    + Hinweis: Wird ein Timeout >= 1 Sekunde eingestellt, schaltet das Modul intern auf die Verwendung eines + Parallelprozesses (BlockingCall) um damit Schreib- bzw. Leseverzögerungen auf dem RS485 Interface nicht zu + blockierenden Zuständen in FHEM führen. +
  • +
    + + +
  • userBatterytype
    + Der automatisch ermittelte Batterietyp (Reading batteryType) wird durch die angegebene Zeichenfolge ersetzt. +
  • +
    +
+ + +Readings +
    +
  • averageCellVolt
    mittlere Zellenspannung (V)
  • +
  • bmsTemperature
    Temperatur (°C) des Batteriemanagementsystems
  • +
  • cellTemperature_0104
    Temperatur (°C) der Zellenpacks 1 bis 4
  • +
  • cellTemperature_0508
    Temperatur (°C) der Zellenpacks 5 bis 8
  • +
  • cellTemperature_0912
    Temperatur (°C) der Zellenpacks 9 bis 12
  • +
  • cellTemperature_1315
    Temperatur (°C) der Zellenpacks 13 bis 15
  • +
  • cellVoltage_XX
    Zellenspannung (V) des Zellenpacks XX. In dem Batteriemodul sind "packCellcount" + Zellenpacks in Serie geschaltet verbaut. Jedes Zellenpack besteht aus parallel + geschalten Einzelzellen.
  • +
  • chargeCurrentLimit
    aktueller Grenzwert für den Ladestrom (A)
  • +
  • chargeEnable
    aktuelles Flag Laden erlaubt
  • +
  • chargeFullRequest
    aktuelles Flag Batteriemodul voll laden (notfalls aus dem Netz)
  • +
  • chargeImmediatelySOCXX
    aktuelles Flag Batteriemodul sofort laden + (05: SOC Grenze 5-9%, 09: SOC Grenze 9-13%)
  • +
  • chargeVoltageLimit
    aktuelle Ladespannungsgrenze (V) des Batteriemoduls
  • +
  • dischargeCurrentLimit
    aktueller Grenzwert für den Entladestrom (A)
  • +
  • dischargeEnable
    aktuelles Flag Entladen erlaubt
  • +
  • dischargeVoltageLimit
    aktuelle Entladespannungsgrenze (V) des Batteriemoduls
  • + +
  • moduleSoftwareVersion_manufacture
    Firmware Version des Batteriemoduls
  • + +
  • packAlarmInfo
    Alarmstatus (ok - Batterienmodul ist in Ordnung, failure - im Batteriemodul liegt + eine Störung vor)
  • +
  • packCapacity
    nominale Kapazität (Ah) des Batteriemoduls
  • +
  • packCapacityRemain
    aktuelle Kapazität (Ah) des Batteriemoduls
  • +
  • packCellcount
    Anzahl der Zellenpacks im Batteriemodul
  • +
  • packCurrent
    aktueller Ladestrom (+) bzw. Entladstrom (-) des Batteriemoduls (A)
  • +
  • packCycles
    Anzahl der Vollzyklen - Die Anzahl der Zyklen ist in gewisserweise ein Maß für den + Verschleiß der Batterie. Eine komplettes Laden und Entladen wird als ein Zyklus + gewertet. Wird die Batterie 50% entladen und wieder aufgeladen, zählt das nur als ein + halber Zyklus. Pylontech gibt eine Lebensdauer von mehreren 1000 Zyklen an + (siehe Datenblatt).
  • +
  • packImbalance
    aktuelles Ungleichgewicht der Spannung zwischen den Einzelzellen des + Batteriemoduls (%)
  • +
  • packPower
    aktuell bezogene (+) bzw. gelieferte (-) Leistung (W) des Batteriemoduls
  • +
  • packSOC
    Ladezustand (%) des Batteriemoduls
  • +
  • packState
    aktueller Arbeitsstatus des Batteriemoduls
  • +
  • packVolt
    aktuelle Spannung (V) des Batteriemoduls
  • + +
  • paramCellHighVoltLimit
    Systemparameter obere Spannungsgrenze (V) einer Zelle
  • +
  • paramCellLowVoltLimit
    Systemparameter untere Spannungsgrenze (V) einer Zelle (Alarmgrenze)
  • +
  • paramCellUnderVoltLimit
    Systemparameter Unterspannungsgrenze (V) einer Zelle (Schutzgrenze)
  • +
  • paramChargeCurrentLimit
    Systemparameter Ladestromgrenze (A) des Batteriemoduls
  • +
  • paramChargeHighTempLimit
    Systemparameter obere Temperaturgrenze (°C) bis zu der die Batterie lädt
  • +
  • paramChargeLowTempLimit
    Systemparameter untere Temperaturgrenze (°C) bis zu der die Batterie lädt
  • +
  • paramDischargeCurrentLimit
    Systemparameter Entladestromgrenze (A) des Batteriemoduls
  • +
  • paramDischargeHighTempLimit
    Systemparameter obere Temperaturgrenze (°C) bis zu der die Batterie entlädt
  • +
  • paramDischargeLowTempLimit
    Systemparameter untere Temperaturgrenze (°C) bis zu der die Batterie entlädt
  • +
  • paramModuleHighVoltLimit
    Systemparameter obere Spannungsgrenze (V) des Batteriemoduls
  • +
  • paramModuleLowVoltLimit
    Systemparameter untere Spannungsgrenze (V) des Batteriemoduls (Alarmgrenze)
  • +
  • paramModuleUnderVoltLimit
    Systemparameter Unterspannungsgrenze (V) des Batteriemoduls (Schutzgrenze)
  • +
  • protocolVersion
    PYLON low voltage RS485 Prokollversion
  • +
  • serialNumber
    Seriennummer
  • +
+

+ +=end html_DE + +=for :application/json;q=META.json 70_PylonLowVoltage.pm +{ + "abstract": "Integration of pylontech LiFePo4 low voltage batteries (incl. BMS) over RS485 via ethernet gateway (ethernet interface)", + "x_lang": { + "de": { + "abstract": "Integration von Pylontech Niedervolt Batterien (mit BMS) über RS485 via Ethernet-Gateway (Ethernet Interface)" + } + }, + "keywords": [ + "inverter", + "photovoltaik", + "electricity", + "battery", + "Pylontech", + "BMS", + "ESS", + "PV" + ], + "version": "v1.1.1", + "release_status": "stable", + "author": [ + "Heiko Maaz " + ], + "x_fhem_maintainer": [ + "DS_Starter" + ], + "x_fhem_maintainer_github": [ + "nasseeder1" + ], + "prereqs": { + "runtime": { + "requires": { + "FHEM": 5.00918799, + "perl": 5.014, + "GPUtils": 0, + "IO::Socket::INET": 0, + "IO::Socket::Timeout": 0, + "Errno": 0, + "FHEM::SynoModules::SMUtils": 1.0220, + "Time::HiRes": 0, + "Carp": 0, + "Blocking": 0, + "Storable": 0, + "MIME::Base64": 0, + "Scalar::Util": 0 + }, + "recommends": { + "FHEM::Meta": 0 + }, + "suggests": { + } + } + }, + "resources": { + "x_wiki": { + "web": "", + "title": "" + }, + "repository": { + "x_dev": { + "type": "svn", + "url": "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter", + "web": "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter/70_PylonLowVoltage.pm", + "x_branch": "dev", + "x_filepath": "fhem/contrib/", + "x_raw": "https://svn.fhem.de/fhem/trunk/fhem/contrib/DS_Starter/70_PylonLowVoltage.pm" + } + } + } +} +=end :application/json;q=META.json + +=cut \ No newline at end of file diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 22e12ff08..cb34e6d1d 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -358,6 +358,7 @@ FHEM/70_ONKYO_AVR.pm delmar Multimedia FHEM/70_OctoPrint.pm michael.winkler Multimedia FHEM/70_PHTV.pm loredo Multimedia FHEM/70_PIONEERAVR.pm 50watt Multimedia +FHEM/70_PylonLowVoltage.pm DS_Starter Solaranlagen FHEM/70_Pushbullet.pm fhainz Unterstützende Dienste FHEM/70_PushNotifier.pm xusader Unterstützende Dienste FHEM/70_Pushover.pm loredo Unterstützende Dienste