diff --git a/fhem/contrib/DS_Starter/76_SMAInverter.pm b/fhem/contrib/DS_Starter/76_SMAInverter.pm new file mode 100644 index 000000000..dabe3466e --- /dev/null +++ b/fhem/contrib/DS_Starter/76_SMAInverter.pm @@ -0,0 +1,1835 @@ +################################################################################################################# +# $Id: 76_SMAInverter.pm 16934 2018-07-02 20:11:29Z DS_Starter $ +################################################################################################################# +# +# +# Copyright notice +# +# Published according Creative Commons : Attribution-NonCommercial-ShareAlike 3.0 Unported (CC BY-NC-SA 3.0) +# Details: https://creativecommons.org/licenses/by-nc-sa/3.0/ +# +# Credits: +# - based on 77_SMASTP.pm by Volker Kettenbach with following credits: +# - based on an Idea by SpenZerX and HDO +# - Waldmensch for various improvements +# - sbfspot (https://sbfspot.codeplex.com/) +# - rewritten by Thomas Schoedl (sct14675) with inputs from Volker, waldmensch and DS_Starter +# +# Description: +# This is an FHEM-Module for SMA Inverters. +# Tested on Sunny Tripower 6000TL-20 and Sunny Island 4.4 +# +# Requirements: +# This module requires: +# - Perl Module: IO::Socket::INET +# - Perl Module: DateTime +# +# +################################################################################################################# +# Versions History by DS_Starter +# +# 2.10.1 28.04.2019 fix perl warnings, Forum:#56080.msg933276.html#msg933276 +# 2.10.0 29.06.2018 Internal MODEL added +# 2.9.2 08.10.2017 adapted to use extended abortArg (Forum:77472) +# 2.9.1 24.04.2017 fix for issue #24 (Wrong INV_TYPE for STP10000TL-20) and fix for issue #25 (unpack out of range for SB1.5-1VL-40) +# 2.9.0 23.04.2017 fixed issue #22: wrong logon command for SunnyBoy systems +# 2.8.3 19.04.2017 enhanced inverter Type-Hash +# 2.8.2 23.03.2017 changed SMA_logon sub +# 2.8.1 06.12.2016 SMAInverter version as internal +# 2.8 05.12.2016 changed commandsections to make sure getting only data from inverters with preset +# $inv_susyid and $inv_serial +# 2.7.4 04.12.2016 change loading of IO::Socket::INET, DateTime +# 2.7.3 04.12.2016 commandref adapted +# 2.7.2 03.12.2016 use Time::HiRes qw(gettimeofday tv_interval) +# 2.7.1 02.12.2016 showproctime improved +# 2.7 02.12.2016 showproctime added +# 2.6.1 29.11.2016 getstatus_DoParse changed due to inititialized issues +# 2.6 28.11.2016 bugfix warnings ParseDone redefine at startup, uninitialized value $avg if FHEM was +# restarted in sleeptime, switched avg_energy to avg_power, commandref updated +# 2.5.2 27.11.2016 bugfix average calc, bugfix warnings at startup +# 2.5.1 26.11.2016 calc of averagebuf changed to 5, 10, 15 minutes +# 2.5 26.11.2016 averagebuf changed, Attr timeout added +# 2.4 26.11.2016 create ringbuffer for calculating average energy last 5, 10, 15 cycles +# 2.3 25.11.2016 bugfixing +# 2.2 24.11.2016 further optimize of non-blocking operation +# 2.1 24.11.2016 avg_energy_lastcycles added +# 2.0 24.11.2016 switched module to non-blocking operation +# 1.8.4 23.11.2016 prepare non-blocking operation +# 1.8.3 23.11.2016 readings opertime_start, opertime_stop +# 1.8.2 22.11.2016 eliminate global vars, prepare non-blocking operation +# 1.8.1 22.11.2016 eliminate global vars, create command array +# 1.8 21.11.2016 eliminate $r_OK, $r_FAIL, create command-array +# 1.7 21.11.2016 devtypes completed, minor bugfixes, commandref completed +# 1.6.1 19.11.2016 bugfix perl warning during fhem start +# 1.6 09.11.2016 added operation control by sunrise,sunset, Attr offset, suppressSleep added +# 1.5 08.11.2016 added device classes hash +# 1.4 07.11.2016 compatibility to SBFSpot improved, bilingual dependend on attr "language" of global-device, +# added hash of SMA device types +# 1.3 07.11.2016 Attr SBFSpotComp added to get compatibility mode with SBFSpot +# 1.2 06.11.2016 function get data added, log output level changed to 4 in sub SMAInverter_Attr, +# some code changes +# 1.1 06.11.2016 Attr mode manual, automatic added +# 1.0 06.11.2016 Attr disable added, +# $globalName replaced by $name in all expressions (due to module redesign to non-blocking later) + + +package main; + +use strict; +use warnings; +eval "use IO::Socket::INET;1" or my $MissModulSocket = "IO::Socket::INET"; +eval "use DateTime;1" or my $MissModulDateTime = "DateTime"; +use Time::HiRes qw(gettimeofday tv_interval); +use Blocking; +use Time::Local; + +my $SMAInverterVersion = "2.10.0"; + +# Inverter Data fields and supported commands flags. +# $inv_SPOT_ETODAY # Today yield +# $inv_SPOT_ETOTAL # Total yield +# $inv_SPOT_PDC1 # DC power input 1 +# $inv_SPOT_PDC2 # DC power input 2 +# $inv_SPOT_PAC1 # Power L1 +# $inv_SPOT_PAC2 # Power L2 +# $inv_SPOT_PAC3 # Power L3 +# $inv_PACMAX1 # Nominal power in Ok Mode +# $inv_PACMAX2 # Nominal power in Warning Mode +# $inv_PACMAX3 # Nominal power in Fault Mode +# $inv_PACMAX1_2 # Maximum active power device (Some inverters like SB3300/SB1200) +# $inv_SPOT_PACTOT # Total Power +# $inv_ChargeStatus # Battery Charge status +# $inv_SPOT_UDC1 # DC voltage input +# $inv_SPOT_UDC2 # DC voltage input +# $inv_SPOT_IDC1 # DC current input +# $inv_SPOT_IDC2 # DC current input +# $inv_SPOT_UAC1 # Grid voltage phase L1 +# $inv_SPOT_UAC2 # Grid voltage phase L2 +# $inv_SPOT_UAC3 # Grid voltage phase L3 +# $inv_SPOT_IAC1 # Grid current phase L1 +# $inv_SPOT_IAC2 # Grid current phase L2 +# $inv_SPOT_IAC3 # Grid current phase L3 +# $inv_BAT_UDC # Battery Voltage +# $inv_BAT_IDC # Battery Current +# $inv_BAT_CYCLES # Battery recharge cycles +# $inv_BAT_TEMP # Battery temperature +# $inv_SPOT_FREQ # Grid Frequency +# $inv_CLASS # Inverter Class +# $inv_TYPE # Inverter Type +# $inv_SPOT_OPERTM # Operation Time +# $inv_SPOT_FEEDTM # Feed-in time +# $inv_TEMP # Inverter temperature +# $inv_GRIDRELAY # Grid Relay/Contactor Status +# $inv_STATUS # Inverter Status + +# Aufbau Wechselrichter Type-Hash +my %SMAInverter_devtypes = ( +0000 => "Unknown Inverter Type", +9015 => "SB 700", +9016 => "SB 700U", +9017 => "SB 1100", +9018 => "SB 1100U", +9019 => "SB 1100LV", +9020 => "SB 1700", +9021 => "SB 1900TLJ", +9022 => "SB 2100TL", +9023 => "SB 2500", +9024 => "SB 2800", +9025 => "SB 2800i", +9026 => "SB 3000", +9027 => "SB 3000US", +9028 => "SB 3300", +9029 => "SB 3300U", +9030 => "SB 3300TL", +9031 => "SB 3300TL HC", +9032 => "SB 3800", +9033 => "SB 3800U", +9034 => "SB 4000US", +9035 => "SB 4200TL", +9036 => "SB 4200TL HC", +9037 => "SB 5000TL", +9038 => "SB 5000TLW", +9039 => "SB 5000TL HC", +9066 => "SB 1200", +9067 => "STP 10000TL-10", +9068 => "STP 12000TL-10", +9069 => "STP 15000TL-10", +9070 => "STP 17000TL-10", +9084 => "WB 3600TL-20", +9085 => "WB 5000TL-20", +9086 => "SB 3800US-10", +9098 => "STP 5000TL-20", +9099 => "STP 6000TL-20", +9100 => "STP 7000TL-20", +9101 => "STP 8000TL-10", +9102 => "STP 9000TL-20", +9103 => "STP 8000TL-20", +9104 => "SB 3000TL-JP-21", +9105 => "SB 3500TL-JP-21", +9106 => "SB 4000TL-JP-21", +9107 => "SB 4500TL-JP-21", +9108 => "SCSMC", +9109 => "SB 1600TL-10", +9131 => "STP 20000TL-10", +9139 => "STP 20000TLHE-10", +9140 => "STP 15000TLHE-10", +9157 => "Sunny Island 2012", +9158 => "Sunny Island 2224", +9159 => "Sunny Island 5048", +9160 => "SB 3600TL-20", +9168 => "SC630HE-11", +9169 => "SC500HE-11", +9170 => "SC400HE-11", +9171 => "WB 3000TL-21", +9172 => "WB 3600TL-21", +9173 => "WB 4000TL-21", +9174 => "WB 5000TL-21", +9175 => "SC 250", +9176 => "SMA Meteo Station", +9177 => "SB 240-10", +9171 => "WB 3000TL-21", +9172 => "WB 3600TL-21", +9173 => "WB 4000TL-21", +9174 => "WB 5000TL-21", +9179 => "Multigate-10", +9180 => "Multigate-US-10", +9181 => "STP 20000TLEE-10", +9182 => "STP 15000TLEE-10", +9183 => "SB 2000TLST-21", +9184 => "SB 2500TLST-21", +9185 => "SB 3000TLST-21", +9186 => "WB 2000TLST-21", +9187 => "WB 2500TLST-21", +9188 => "WB 3000TLST-21", +9189 => "WTP 5000TL-20", +9190 => "WTP 6000TL-20", +9191 => "WTP 7000TL-20", +9192 => "WTP 8000TL-20", +9193 => "WTP 9000TL-20", +9254 => "Sunny Island 3324", +9255 => "Sunny Island 4.0M", +9256 => "Sunny Island 4248", +9257 => "Sunny Island 4248U", +9258 => "Sunny Island 4500", +9259 => "Sunny Island 4548U", +9260 => "Sunny Island 5.4M", +9261 => "Sunny Island 5048U", +9262 => "Sunny Island 6048U", +9278 => "Sunny Island 3.0M", +9279 => "Sunny Island 4.4M", +9281 => "STP 10000TL-20", +9282 => "STP 11000TL-20", +9283 => "STP 12000TL-20", +9284 => "STP 20000TL-30", +9285 => "STP 25000TL-30", +9301 => "SB1.5-1VL-40", +9302 => "SB2.5-1VL-40", +9303 => "SB2.0-1VL-40", +9304 => "SB5.0-1SP-US-40", +9305 => "SB6.0-1SP-US-40", +9306 => "SB8.0-1SP-US-40", +9307 => "Energy Meter", +); + +# Wechselrichter Class-Hash DE +my %SMAInverter_classesDE = ( +8000 => "Alle Geräte", +8001 => "Solar-Wechselrichter", +8002 => "Wind-Wechselrichter", +8007 => "Batterie-Wechselrichter", +8033 => "Verbraucher", +8064 => "Sensorik allgemein", +8065 => "Stromzähler", +8128 => "Kommunikationsprodukte", +); + +# Wechselrichter Class-Hash EN +my %SMAInverter_classesEN = ( +8000 => "All Devices", +8001 => "Solar Inverters", +8002 => "Wind Turbine Inverter", +8007 => "Batterie Inverters", +8033 => "Consumer", +8064 => "Sensor System in General", +8065 => "Electricity meter", +8128 => "Communication products", +); + +############################################################### +# SMAInverter Initialize +############################################################### +sub SMAInverter_Initialize($) { + my ($hash) = @_; + + $hash->{DefFn} = "SMAInverter_Define"; + $hash->{UndefFn} = "SMAInverter_Undef"; + $hash->{GetFn} = "SMAInverter_Get"; + $hash->{AttrList} = "interval " . + "detail-level:0,1,2 " . + "disable:1,0 " . + "mode:manual,automatic ". + "offset ". + "suppressSleep:1,0 ". + "SBFSpotComp:1,0 " . + "showproctime:1,0 ". + "timeout " . + "target-susyid " . + "target-serial " . + $readingFnAttributes; + $hash->{AttrFn} = "SMAInverter_Attr"; + +} + +############################################################### +# SMAInverter Define +############################################################### +sub SMAInverter_Define($$) { + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + + return "Error: Perl module ".$MissModulSocket." is missing. + Install it on Debian with: sudo apt-get install libio-socket-multicast-perl" if($MissModulSocket); + return "Error: Perl module ".$MissModulDateTime." is missing. + Install it on Debian with: sudo apt-get install libdatetime-perl" if($MissModulDateTime); + + return "Wrong syntax: use define SMAInverter " if ((int(@a) < 4) and (int(@a) > 5)); + + my $name = $hash->{NAME}; + $hash->{LASTUPDATE} = 0; + $hash->{INTERVAL} = $hash->{HELPER}{INTERVAL} = AttrVal($name, "interval", 60); + $hash->{VERSION} = $SMAInverterVersion; + $hash->{HELPER}{FAULTEDCYCLES} = 0; + delete($hash->{HELPER}{AVERAGEBUF}) if($hash->{HELPER}{AVERAGEBUF}); + + # protocol related defaults + $hash->{HELPER}{MYSUSYID} = 233; # random number, has to be different from any device in local network + $hash->{HELPER}{MYSERIALNUMBER} = 123321123; # random number, has to be different from any device in local network + $hash->{HELPER}{DEFAULT_TARGET_SUSYID} = 0xFFFF; # 0xFFFF is any susyid + $hash->{HELPER}{DEFAULT_TARGET_SERIAL} = 0xFFFFFFFF; # 0xFFFFFFFF is any serialnumber + $hash->{HELPER}{PKT_ID} = 0x8001; # Packet ID + $hash->{HELPER}{MAXBYTES} = 300; # constant MAXBYTES scalar 300 + + my ($IP,$Host,$Caps); + + my $Pass = $a[2]; # to do: check 1-12 Chars + + # extract IP or Hostname from $a[3] + if (!defined $Host) { + if ( $a[3] =~ /^([A-Za-z0-9_.])/ ) { + $Host = $a[3]; + } + } + + if (!defined $Host) { + return "Argument:{$a[3]} not accepted as Host or IP. Read device specific help file."; + } + + $hash->{PASS} = $Pass; + $hash->{HOST} = $Host; + + InternalTimer(gettimeofday()+5, "SMAInverter_GetData", $hash, 0); # Start Hauptroutine + +return undef; +} + +############################################################### +# SMAInverter Undefine +############################################################### +sub SMAInverter_Undef($$) { + my ($hash, $name) = @_; + RemoveInternalTimer($hash); + BlockingKill($hash->{HELPER}{RUNNING_PID}); +return undef; +} + + +############################################################### +# SMAInverter Get +############################################################### +sub SMAInverter_Get($$) { + my ($hash, @a) = @_; + return "\"get X\" needs at least an argument" if ( @a < 2 ); + my $name = shift @a; + my $opt = shift @a; + my $timeout = AttrVal($name, "timeout", 60); + + my $getlist = "Unknown argument $opt, choose one of ". + "data:noArg "; + + return "module is disabled" if(IsDisabled($name)); + + if ($opt eq "data") { + SMAInverter_GetData($hash); + } else { + return "$getlist"; + } +return undef; +} + +############################################################### +# SMAInverter Attr +############################################################### +sub SMAInverter_Attr(@) { + my ($cmd,$name,$aName,$aVal) = @_; + # $cmd can be "del" or "set" + # $name is device name + # aName and aVal are Attribute name and value + my $hash = $defs{$name}; + my $do; + + if ($aName eq "mode") { + if ($cmd eq "set" && $aVal eq "manual") { + $hash->{INTERVAL} = $aVal; + } else { + $hash->{INTERVAL} = $hash->{HELPER}{INTERVAL}; + } + InternalTimer(time+5, 'SMAInverter_GetData', $hash, 0); + } + + if ($aName eq "disable") { + if($cmd eq "set") { + $do = ($aVal) ? 1 : 0; + } + $do = 0 if($cmd eq "del"); + my $val = ($do == 1 ? "disabled" : "initialized"); + + readingsSingleUpdate($hash, "state", $val, 1); + + if ($do == 0) { + my $mode = AttrVal($name, "mode", "automatic"); + RemoveInternalTimer($hash); + InternalTimer(time+5, 'SMAInverter_GetData', $hash, 0); + } else { + RemoveInternalTimer($hash); + } + } + + if ($aName eq "detail-level") { + delete $defs{$name}{READINGS}; + } + + if ($aName eq "SBFSpotComp") { + delete $defs{$name}{READINGS}; + } + + if ($aName eq "interval") { + if ($cmd eq "set") { + $hash->{HELPER}{INTERVAL} = $aVal; + $hash->{INTERVAL} = $aVal if(AttrVal($name, "mode", "") ne "manual"); + delete($hash->{HELPER}{AVERAGEBUF}) if($hash->{HELPER}{AVERAGEBUF}); + Log3 $name, 3, "$name - Set $aName to $aVal"; + } else { + $hash->{INTERVAL} = $hash->{HELPER}{INTERVAL} = 60; + } + } + + if ($cmd eq "set" && $aName eq "offset") { + if($aVal !~ /^\d+$/ || $aVal < 0 || $aVal > 7200) { return "The Value of $aName is not valid. Use value between 0 ... 7200 !";} + } + if ($cmd eq "set" && $aName eq "timeout") { + unless ($aVal =~ /^[0-9]+$/) { return " The Value for $aName is not valid. Use only figures 1-9 !";} + } +return; +} + +############################################################### +# Hauptschleife Datenabruf +############################################################### +sub SMAInverter_GetData($) { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $interval = AttrVal($name, "interval", 60); + my $timeout = AttrVal($name, "timeout", 60); + + RemoveInternalTimer($hash, "SMAInverter_GetData"); + + if ($init_done != 1) { + InternalTimer(gettimeofday()+5, "SMAInverter_GetData", $hash, 0); + return; + } + + return if(IsDisabled($name)); + + if (exists($hash->{HELPER}{RUNNING_PID})) { + Log3 ($name, 3, "SMAInverter $name - WARNING - old process $hash->{HELPER}{RUNNING_PID}{pid} will be killed now to start a new BlockingCall"); + BlockingKill($hash->{HELPER}{RUNNING_PID}); + } + + Log3 ($name, 4, "$name - ###############################################################"); + Log3 ($name, 4, "$name - ########## Begin of new SMAInverter get data cycle ##########"); + Log3 ($name, 4, "$name - ###############################################################"); + Log3 ($name, 4, "$name - timeout cycles since module start: $hash->{HELPER}{FAULTEDCYCLES}"); + + # decide of operation + if(AttrVal($name,"mode","automatic") eq "automatic") { + # automatic operation mode + InternalTimer(gettimeofday()+$interval, "SMAInverter_GetData", $hash, 0); + } + +$hash->{HELPER}{RUNNING_PID} = BlockingCall("getstatus_DoParse", "$name", "getstatus_ParseDone", $timeout, "getstatus_ParseAborted", $hash); +$hash->{HELPER}{RUNNING_PID}{loglevel} = 4; + +return; +} + +############################################################### +# non-blocking Inverter Datenabruf +############################################################### +sub getstatus_DoParse($) { + my ($name) = @_; + my $hash = $defs{$name}; + my $interval = AttrVal($name, "interval", 60); + my $sc = AttrVal($name, "SBFSpotComp", 0); + my ($sup_EnergyProduction, + $sup_SpotDCPower, + $sup_SpotACPower, + $sup_MaxACPower, + $sup_MaxACPower2, + $sup_SpotACTotalPower, + $sup_ChargeStatus, + $sup_SpotDCVoltage, + $sup_SpotACVoltage, + $sup_BatteryInfo, + $sup_SpotGridFrequency, + $sup_TypeLabel, + $sup_OperationTime, + $sup_InverterTemperature, + $sup_GridRelayStatus, + $sup_DeviceStatus); + my ($inv_TYPE, $inv_CLASS, + $inv_SPOT_ETODAY, $inv_SPOT_ETOTAL, + $inv_susyid, + $inv_serial, + $inv_SPOT_PDC1, $inv_SPOT_PDC2, + $inv_SPOT_PAC1, $inv_SPOT_PAC2, $inv_SPOT_PAC3, $inv_SPOT_PACTOT, + $inv_PACMAX1, $inv_PACMAX2, $inv_PACMAX3, $inv_PACMAX1_2, + $inv_ChargeStatus, + $inv_SPOT_UDC1, $inv_SPOT_UDC2, + $inv_SPOT_IDC1, $inv_SPOT_IDC2, + $inv_SPOT_UAC1, $inv_SPOT_UAC2, $inv_SPOT_UAC3, + $inv_SPOT_IAC1, $inv_SPOT_IAC2, $inv_SPOT_IAC3, + $inv_BAT_UDC, $inv_BAT_IDC, + $inv_BAT_CYCLES, + $inv_BAT_TEMP, + $inv_SPOT_FREQ, $inv_SPOT_OPERTM, $inv_SPOT_FEEDTM, $inv_TEMP, $inv_GRIDRELAY, $inv_STATUS,); + my @row_array; + my @array; + my $avg = 0; + my ($ist,$bst,$irt,$brt,$rt); + + # Background-Startzeit + $bst = [gettimeofday]; + + Log3 ($name, 4, "$name -> Start BlockingCall getstatus_DoParse"); + + # set dependency from surise/sunset used for inverter operation time + my $offset = AttrVal($name,"offset",0); + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); + my ($sunrise_h,$sunrise_m,$sunrise_s) = split(":",sunrise_abs('-'.$offset)); + my ($sunset_h,$sunset_m,$sunset_s) = split(":",sunset_abs('+'.$offset)); + my $oper_start = DateTime->new(year=>$year+1900,month=>$mon+1,day=>$mday,hour=>$sunrise_h,minute=>$sunrise_m,second=>$sunrise_s,time_zone=>'local'); + my $oper_stop = DateTime->new(year=>$year+1900,month=>$mon+1,day=>$mday,hour=>$sunset_h,minute=>$sunset_m,second=>$sunset_s,time_zone=>'local'); + my $dt_now = DateTime->now(time_zone=>'local'); + Log3 $name, 4, "$name - current time: ".$dt_now->dmy('.')." ".$dt_now->hms; + Log3 $name, 4, "$name - operation time begin: ".$oper_start->dmy('.')." ".$oper_start->hms; + Log3 $name, 4, "$name - operation time end: ".$oper_stop->dmy('.')." ".$oper_stop->hms; + my $opertime_start = $oper_start->dmy('.')." ".$oper_start->hms; + my $opertime_stop = $oper_stop->dmy('.')." ".$oper_stop->hms; + + if (($oper_start <= $dt_now && $dt_now <= $oper_stop) || AttrVal($name,"suppressSleep",0)) { + # normal operation or suppressed sleepmode + + # Abfrage Inverter Startzeit + $ist = [gettimeofday]; + + # Get the current attributes + my $detail_level = AttrVal($name, "detail-level", 0); + + # Aufbau Command-Array + my @commands = ("sup_TypeLabel", # Check TypeLabel + "sup_EnergyProduction", # Check EnergyProduction + "sup_SpotDCPower", # Check SpotDCPower + "sup_SpotACPower", # Check SpotACPower + "sup_SpotACTotalPower", # Check SpotACTotalPower + "sup_ChargeStatus" # Check BatteryChargeStatus + ); + + if($detail_level > 0) { + # Detail Level 1 or 2 >> get voltage and current levels + push(@commands, "sup_SpotDCVoltage"); # Check SpotDCVoltage + push(@commands, "sup_SpotACVoltage"); # Check SpotACVoltage + push(@commands, "sup_BatteryInfo"); # Check BatteryInfo + } + + if($detail_level > 1) { + # Detail Level 2 >> get all data + push(@commands, "sup_SpotGridFrequency"); # Check SpotGridFrequency + push(@commands, "sup_OperationTime"); # Check OperationTime + push(@commands, "sup_InverterTemperature"); # Check InverterTemperature + push(@commands, "sup_MaxACPower"); # Check MaxACPower + push(@commands, "sup_MaxACPower2"); # Check MaxACPower2 + push(@commands, "sup_GridRelayStatus"); # Check GridRelayStatus + push(@commands, "sup_DeviceStatus"); # Check DeviceStatus + } + + if(SMA_logon($hash->{HOST}, $hash->{PASS}, $hash)) { + Log3 $name, 5, "$name - Logged in now"; + + foreach my $i(@commands) { + if ($i eq "sup_TypeLabel") { + ($sup_TypeLabel,$inv_TYPE,$inv_CLASS,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x58000200, 0x00821E00, 0x008220FF); + } + elsif ($i eq "sup_EnergyProduction") { + ($sup_EnergyProduction,$inv_SPOT_ETODAY,$inv_SPOT_ETOTAL,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x54000200, 0x00260100, 0x002622FF); + } + elsif ($i eq "sup_SpotDCPower") { + ($sup_SpotDCPower,$inv_SPOT_PDC1,$inv_SPOT_PDC2,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x53800200, 0x00251E00, 0x00251EFF); + } + elsif ($i eq "sup_SpotACPower") { + ($sup_SpotACPower,$inv_SPOT_PAC1,$inv_SPOT_PAC2,$inv_SPOT_PAC3,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x51000200, 0x00464000, 0x004642FF); + } + elsif ($i eq "sup_SpotACTotalPower") { + ($sup_SpotACTotalPower,$inv_SPOT_PACTOT,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x51000200, 0x00263F00, 0x00263FFF); + } + elsif ($i eq "sup_ChargeStatus") { + ($sup_ChargeStatus,$inv_ChargeStatus,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x51000200, 0x00295A00, 0x00295AFF); + } + elsif ($i eq "sup_SpotDCVoltage") { + ($sup_SpotDCVoltage,$inv_SPOT_UDC1,$inv_SPOT_UDC2,$inv_SPOT_IDC1,$inv_SPOT_IDC2,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x53800200, 0x00451F00, 0x004521FF); + } + elsif ($i eq "sup_SpotACVoltage") { + ($sup_SpotACVoltage,$inv_SPOT_UAC1,$inv_SPOT_UAC2,$inv_SPOT_UAC3,$inv_SPOT_IAC1,$inv_SPOT_IAC2,$inv_SPOT_IAC3,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x51000200, 0x00464800, 0x004655FF); + } + elsif ($i eq "sup_BatteryInfo") { + ($sup_BatteryInfo,$inv_BAT_CYCLES,$inv_BAT_TEMP,$inv_BAT_UDC,$inv_BAT_IDC,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x51000200, 0x00491E00, 0x00495DFF); + } + elsif ($i eq "sup_SpotGridFrequency") { + ($sup_SpotGridFrequency,$inv_SPOT_FREQ,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x51000200, 0x00465700, 0x004657FF); + } + elsif ($i eq "sup_OperationTime") { + ($sup_OperationTime,$inv_SPOT_OPERTM,$inv_SPOT_FEEDTM,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x54000200, 0x00462E00, 0x00462FFF); + } + elsif ($i eq "sup_InverterTemperature") { + ($sup_InverterTemperature,$inv_TEMP,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x52000200, 0x00237700, 0x002377FF); + } + elsif ($i eq "sup_MaxACPower") { + ($sup_MaxACPower,$inv_PACMAX1,$inv_PACMAX2,$inv_PACMAX3,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x51000200, 0x00411E00, 0x004120FF); + } + elsif ($i eq "sup_MaxACPower2") { + ($sup_MaxACPower2,$inv_PACMAX1_2,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x51000200, 0x00832A00, 0x00832AFF); + } + elsif ($i eq "sup_GridRelayStatus") { + ($sup_GridRelayStatus,$inv_GRIDRELAY,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x51800200, 0x00416400, 0x004164FF); + } + elsif ($i eq "sup_DeviceStatus") { + ($sup_DeviceStatus,$inv_STATUS,$inv_susyid,$inv_serial) = SMA_command($hash, $hash->{HOST}, 0x51800200, 0x00214800, 0x002148FF); + } + } + + # nothing more to do, just log out + SMA_logout($hash,$hash->{HOST}); + + # Inverter Laufzeit ermitteln + $irt = tv_interval($ist); + + # Aufbau Ergebnis-Array + push(@row_array, "modulstate normal"."\n"); + push(@row_array, "opertime_start ".$opertime_start."\n"); + push(@row_array, "opertime_stop ".$opertime_stop."\n"); + + # Durchschnittswerteberechnung Energieerzeugung der letzten 5, 10, 15 Messungen + + my ($sum05, $sum10, $sum15); + my $cnt05 = int(300/$interval); # Anzahl der Zyklen innerhalb 5 Minuten + my $cnt10 = int(600/$interval); # Anzahl der Zyklen innerhalb 10 Minuten + my $cnt15 = int(900/$interval); # Anzahl der Zyklen innerhalb 15 Minuten = Summe aller Messzyklen + my $cntsum = $cnt15+1; # Sicherheitszuschlag Summe Anzahl aller Zyklen + my @averagebuf; + if ($sup_TypeLabel && $sup_EnergyProduction && $inv_CLASS eq 8001) { + # only for this block because of warnings if values not set at restart + no warnings 'uninitialized'; + if (!$hash->{HELPER}{AVERAGEBUF}) { + for my $count (0..$cntsum) { + # fill with new values + $inv_SPOT_PACTOT = $inv_SPOT_PACTOT?$inv_SPOT_PACTOT:0; + push(@averagebuf, $inv_SPOT_PACTOT); + } + } else { + @averagebuf = split(/,/, $hash->{HELPER}{AVERAGEBUF}) + } + + # rechtes Element aus average buffer löschen + pop(@averagebuf); + # und links mit neuem Wert füllen + unshift(@averagebuf, $inv_SPOT_PACTOT); + $avg = join(',', @averagebuf); + + # calculate average energy and write to array for generate readings + my $k = 1; + my $avgsum = $averagebuf[0]; + while ($k < $cntsum) { + $avgsum = $avgsum + $averagebuf[$k] if($averagebuf[$k]); + if ($k == $cnt05) { + $sum05 = $avgsum; + Log3 $name, 5, "$name - CNT05: $cnt05 SUM05: $sum05"; + } + if ($k == $cnt10) { + $sum10 = $avgsum; + Log3 $name, 5, "$name - CNT10: $cnt10 SUM10: $sum10"; + } + if ($k == $cnt15) { + $sum15 = $avgsum; + Log3 $name, 5, "$name - CNT15: $cnt15 SUM15: $sum15"; + } + $k++; + } + + my $AvP05 = int( $sum05 / ($cnt05+1) ); + my $AvP10 = int( $sum10 / ($cnt10+1) ); + my $AvP15 = int( $sum15 / ($cnt15+1) ); + Log3 $name, 5, "$name - Content of Averagebuffer:"; + Log3 $name, 5, "$name - $avg"; + Log3 $name, 5, "$name - avg_power_lastminutes_05 = $AvP05, avg_power_lastminutes_10 = $AvP10, avg_power_lastminutes_15 = $AvP15"; + + push(@row_array, "avg_power_lastminutes_05 ".$AvP05."\n"); # Average Energy (last) 5 minutes + push(@row_array, "avg_power_lastminutes_10 ".$AvP10."\n"); # Average Energy (last) 10 minutes + push(@row_array, "avg_power_lastminutes_15 ".$AvP15."\n"); # Average Energy (last) 15 minutes + + use warnings; + } + + if ($sc) { # SBFSpot Kompatibilitätsmodus + if($sup_EnergyProduction) { + push(@row_array, "etotal ".($inv_SPOT_ETOTAL/1000)."\n"); + push(@row_array, "etoday ".($inv_SPOT_ETODAY/1000)."\n"); + } + if($sup_SpotDCPower) { + push(@row_array, "string_1_pdc ".sprintf("%.3f",$inv_SPOT_PDC1/1000)."\n"); + push(@row_array, "string_2_pdc ".sprintf("%.3f",$inv_SPOT_PDC2/1000)."\n"); + } + if($sup_SpotACPower) { + push(@row_array, "phase_1_pac ".sprintf("%.3f",$inv_SPOT_PAC1/1000)."\n"); + push(@row_array, "phase_2_pac ".sprintf("%.3f",$inv_SPOT_PAC2/1000)."\n"); + push(@row_array, "phase_3_pac ".sprintf("%.3f",$inv_SPOT_PAC3/1000)."\n"); + } + if($sup_SpotACTotalPower) { + push(@row_array, "total_pac ".sprintf("%.3f",$inv_SPOT_PACTOT/1000)."\n"); + push(@row_array, "state ".sprintf("%.3f",$inv_SPOT_PACTOT/1000)."\n"); + } + if($sup_ChargeStatus) { + push(@row_array, "chargestatus ".$inv_ChargeStatus."\n"); + } + if($inv_CLASS && $inv_CLASS eq 8007 && defined($inv_SPOT_PACTOT)) { # V2.10.1 28.04.2019 + if($inv_SPOT_PACTOT < 0) { + push(@row_array, "power_out "."0"."\n"); + push(@row_array, "power_in ".(-1 * $inv_SPOT_PACTOT)."\n"); + } else { + push(@row_array, "power_out ".$inv_SPOT_PACTOT."\n"); + push(@row_array, "power_in "."0"."\n"); + } + } + + if($detail_level > 0) { + # For Detail Level 1 + if($sup_SpotDCVoltage) { + push(@row_array, "string_1_udc ".sprintf("%.2f",$inv_SPOT_UDC1)."\n"); + push(@row_array, "string_2_udc ".sprintf("%.2f",$inv_SPOT_UDC2)."\n"); + push(@row_array, "string_1_idc ".sprintf("%.3f",$inv_SPOT_IDC1)."\n"); + push(@row_array, "string_2_idc ".sprintf("%.3f",$inv_SPOT_IDC2)."\n"); + } + if($sup_SpotACVoltage) { + push(@row_array, "phase_1_uac ".sprintf("%.2f",$inv_SPOT_UAC1)."\n"); + push(@row_array, "phase_2_uac ".sprintf("%.2f",$inv_SPOT_UAC2)."\n"); + push(@row_array, "phase_3_uac ".sprintf("%.2f",$inv_SPOT_UAC3)."\n"); + push(@row_array, "phase_1_iac ".sprintf("%.3f",$inv_SPOT_IAC1)."\n"); + push(@row_array, "phase_2_iac ".sprintf("%.3f",$inv_SPOT_IAC2)."\n"); + push(@row_array, "phase_3_iac ".sprintf("%.3f",$inv_SPOT_IAC3)."\n"); + } + if($sup_BatteryInfo) { + push(@row_array, "bat_udc ".$inv_BAT_UDC."\n"); + push(@row_array, "bat_idc ".$inv_BAT_IDC."\n"); + } + } + + if($detail_level > 1) { + # For Detail Level 2 + if($sup_BatteryInfo) { + push(@row_array,"bat_cycles ".$inv_BAT_CYCLES."\n"); + push(@row_array, "bat_temp ".$inv_BAT_TEMP."\n"); + } + if($sup_SpotGridFrequency) { + push(@row_array, "grid_freq. ".sprintf("%.2f",$inv_SPOT_FREQ)."\n"); + } + if($sup_TypeLabel) { + push(@row_array, "device_type ".devtype($inv_TYPE)."\n"); + push(@row_array, "device_class ".classtype($inv_CLASS)."\n"); + push(@row_array, "susyid ".$inv_susyid." - SN: ".$inv_serial."\n") if($inv_susyid && $inv_serial); + push(@row_array, "device_name "."SN: ".$inv_serial."\n") if($inv_serial); + push(@row_array, "serial_number ".$inv_serial."\n") if($inv_serial); + } + if($sup_MaxACPower) { + push(@row_array, "pac_max_phase_1 ".$inv_PACMAX1."\n"); + push(@row_array, "pac_max_phase_2 ".$inv_PACMAX2."\n"); + push(@row_array, "pac_max_phase_3 ".$inv_PACMAX3."\n"); + } + if($sup_MaxACPower2) { + push(@row_array, "pac_max_phase_1_2 ".$inv_PACMAX1_2."\n"); + } + if($sup_InverterTemperature) { + push(@row_array, "device_temperature ".sprintf("%.1f",$inv_TEMP)."\n"); + } + if($sup_OperationTime) { + push(@row_array, "feed-in_time ".$inv_SPOT_FEEDTM."\n"); + push(@row_array, "operation_time ".$inv_SPOT_OPERTM."\n"); + } + if($sup_GridRelayStatus) { + push(@row_array, "gridrelay_status ".StatusText($inv_GRIDRELAY)."\n"); + } + if($sup_DeviceStatus) { + push(@row_array, "device_status ".StatusText($inv_STATUS)."\n"); + } + } + } else { # kein SBFSpot Compatibility Mode + if($sup_EnergyProduction) { + push(@row_array, "SPOT_ETOTAL ".$inv_SPOT_ETOTAL."\n"); + push(@row_array, "SPOT_ETODAY ".$inv_SPOT_ETODAY."\n"); + } + if($sup_SpotDCPower) { + push(@row_array, "SPOT_PDC1 ".$inv_SPOT_PDC1."\n"); + push(@row_array, "SPOT_PDC2 ".$inv_SPOT_PDC2."\n"); + } + if($sup_SpotACPower) { + push(@row_array, "SPOT_PAC1 ".$inv_SPOT_PAC1."\n"); + push(@row_array, "SPOT_PAC2 ".$inv_SPOT_PAC2."\n"); + push(@row_array, "SPOT_PAC3 ".$inv_SPOT_PAC3."\n"); + } + if($sup_SpotACTotalPower) { + push(@row_array, "SPOT_PACTOT ".$inv_SPOT_PACTOT."\n"); + push(@row_array, "state ".$inv_SPOT_PACTOT."\n"); + } + if($sup_ChargeStatus) { + push(@row_array, "ChargeStatus ".$inv_ChargeStatus."\n"); + } + if($inv_CLASS && $inv_CLASS eq 8007 && defined($inv_SPOT_PACTOT)) { # V2.10.1 28.04.2019 + if($inv_SPOT_PACTOT < 0) { + push(@row_array, "POWER_OUT "."0"."\n"); + push(@row_array, "POWER_IN ".(-1 * $inv_SPOT_PACTOT)."\n"); + } else { + push(@row_array, "POWER_OUT ".$inv_SPOT_PACTOT."\n"); + push(@row_array, "POWER_IN "."0"."\n"); + } + } + if($detail_level > 0) { + # For Detail Level 1 + if($sup_SpotDCVoltage) { + push(@row_array, "SPOT_UDC1 ".$inv_SPOT_UDC1."\n"); + push(@row_array, "SPOT_UDC2 ".$inv_SPOT_UDC2."\n"); + push(@row_array, "SPOT_IDC1 ".$inv_SPOT_IDC1."\n"); + push(@row_array, "SPOT_IDC2 ".$inv_SPOT_IDC2."\n"); + } + if($sup_SpotACVoltage) { + push(@row_array, "SPOT_UAC1 ".$inv_SPOT_UAC1."\n"); + push(@row_array, "SPOT_UAC2 ".$inv_SPOT_UAC2."\n"); + push(@row_array, "SPOT_UAC3 ".$inv_SPOT_UAC3."\n"); + push(@row_array, "SPOT_IAC1 ".$inv_SPOT_IAC1."\n"); + push(@row_array, "SPOT_IAC2 ".$inv_SPOT_IAC2."\n"); + push(@row_array, "SPOT_IAC3 ".$inv_SPOT_IAC3."\n"); + } + if($sup_BatteryInfo) { + push(@row_array, "BAT_UDC ".$inv_BAT_UDC."\n"); + push(@row_array, "BAT_IDC ".$inv_BAT_IDC."\n"); + } + } + + if($detail_level > 1) { + # For Detail Level 2 + if($sup_BatteryInfo) { + push(@row_array, "BAT_CYCLES ".$inv_BAT_CYCLES."\n"); + push(@row_array, "BAT_TEMP ".$inv_BAT_TEMP."\n"); + } + if($sup_SpotGridFrequency) { + push(@row_array, "SPOT_FREQ ".$inv_SPOT_FREQ."\n"); + } + if($sup_TypeLabel) { + push(@row_array, "INV_TYPE ".devtype($inv_TYPE)."\n"); + push(@row_array, "INV_CLASS ".classtype($inv_CLASS)."\n"); + push(@row_array, "SUSyID ".$inv_susyid."\n") if($inv_susyid); + push(@row_array, "Serialnumber ".$inv_serial."\n") if($inv_serial); + } + if($sup_MaxACPower) { + push(@row_array, "INV_PACMAX1 ".$inv_PACMAX1."\n"); + push(@row_array, "INV_PACMAX2 ".$inv_PACMAX2."\n"); + push(@row_array, "INV_PACMAX3 ".$inv_PACMAX3."\n"); + } + if($sup_MaxACPower2) { + push(@row_array, "INV_PACMAX1_2 ".$inv_PACMAX1_2."\n"); + } + if($sup_InverterTemperature) { + push(@row_array, "INV_TEMP ".$inv_TEMP."\n"); + } + if($sup_OperationTime) { + push(@row_array, "SPOT_FEEDTM ".$inv_SPOT_FEEDTM."\n"); + push(@row_array, "SPOT_OPERTM ".$inv_SPOT_OPERTM."\n"); + } + if($sup_GridRelayStatus) { + push(@row_array, "INV_GRIDRELAY ".StatusText($inv_GRIDRELAY)."\n"); + } + if($sup_DeviceStatus) { + push(@row_array, "INV_STATUS ".StatusText($inv_STATUS)."\n"); + } + } + } + } else { + # Login failed/not possible + push(@row_array, "state Login failed"."\n"); + push(@row_array, "modulstate login failed"."\n"); + } + } else { + # sleepmode at current time and not suppressed + push(@row_array, "modulstate sleep"."\n"); + push(@row_array, "opertime_start ".$opertime_start."\n"); + push(@row_array, "opertime_stop ".$opertime_stop."\n"); + push(@row_array, "state done"."\n"); + } + + Log3 ($name, 5, "$name -> row_array before encoding:"); + foreach my $row (@row_array) { + chomp $row; + Log3 ($name, 5, "$name -> $row"); + } + + # encoding result + my $rowlist = join('|', @row_array); + $rowlist = encode_base64($rowlist,""); + + # Background-Laufzeit ermitteln + $brt = tv_interval($bst); + + $rt = ($irt?$irt:'').",".$brt; + + Log3 ($name, 4, "$name -> BlockingCall getstatus_DoParse finished"); + +return "$name|$rowlist|$avg|$rt"; +} + +############################################################### +# Auswertung non-blocking Inverter Datenabruf +############################################################### +sub getstatus_ParseDone ($) { + my ($string) = @_; + my @a = split("\\|",$string); + my $name = $a[0]; + my $hash = $defs{$name}; + my $rowlist = decode_base64($a[1]); + $hash->{HELPER}{AVERAGEBUF} = $a[2] if($a[2]); + my $rt = $a[3]; + my ($irt,$brt) = split(",", $rt); + + Log3 ($name, 4, "$name -> Start BlockingCall getstatus_ParseDone"); + + # proctime Readings löschen + if(!AttrVal($name, "showproctime", undef)) { + delete($defs{$name}{READINGS}{inverter_processing_time}); + delete($defs{$name}{READINGS}{background_processing_time}); + } else { + delete($defs{$name}{READINGS}{inverter_processing_time}) if(!$irt); + } + + # Get current time + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); + $hash->{LASTUPDATE} = sprintf "%02d.%02d.%04d / %02d:%02d:%02d" , $mday , $mon+=1 ,$year+=1900 , $hour , $min , $sec ; + + my @row_array = split("\\|", $rowlist); + + Log3 ($name, 5, "$name -> row_array after decoding:"); + foreach my $row (@row_array) { + chomp $row; + Log3 ($name, 5, "$name -> $row"); + } + + readingsBeginUpdate($hash); + foreach my $row (@row_array) { + chomp $row; + my @a = split(" ", $row, 2); + $hash->{MODEL} = $a[1] if($a[0] eq "device_type"); + readingsBulkUpdate($hash, $a[0], $a[1]); + } + readingsBulkUpdate($hash, "background_processing_time", sprintf("%.4f",$brt)) if(AttrVal($name, "showproctime", undef)); + readingsBulkUpdate($hash, "inverter_processing_time", sprintf("%.4f",$irt)) if(AttrVal($name, "showproctime", undef) && $irt); + readingsEndUpdate($hash, 1); + + delete($hash->{HELPER}{RUNNING_PID}); + Log3 ($name, 4, "$name -> BlockingCall getstatus_ParseDone finished"); + +return; +} + +############################################################### +# Abbruchroutine Timeout Inverter Abfrage +############################################################### +sub getstatus_ParseAborted(@) { + my ($hash,$cause) = @_; + my $name = $hash->{NAME}; + my $discycles = $hash->{HELPER}{FAULTEDCYCLES}; + $cause = $cause?$cause:"Timeout: process terminated"; + + # count of timeouts since module start + $discycles++; + $hash->{HELPER}{FAULTEDCYCLES} = $discycles; + + Log3 ($name, 1, "SMAInverter $name -> BlockingCall $hash->{HELPER}{RUNNING_PID}{fn} $cause"); + readingsSingleUpdate($hash,"state",$cause, 1); + + delete($hash->{HELPER}{RUNNING_PID}); + +return; +} + +########################################################################## +# SMA Command Execution +########################################################################## +sub SMA_command($$$$$) { + # Parameters: $hash - host - command - first - last + my ($hash,$host,$command,$first,$last) = @_; + my $name = $hash->{NAME}; + my $cmdheader = "534D4100000402A00000000100"; + my $pktlength = "26"; # length = 38 for data commands + my $esignature = "0010606509A0"; + my ($inv_TYPE, $inv_CLASS, + $inv_SPOT_ETODAY, $inv_SPOT_ETOTAL, + $inv_susyid, + $inv_serial, + $inv_SPOT_PDC1, $inv_SPOT_PDC2, + $inv_SPOT_PAC1, $inv_SPOT_PAC2, $inv_SPOT_PAC3, $inv_SPOT_PACTOT, + $inv_PACMAX1, $inv_PACMAX2, $inv_PACMAX3, $inv_PACMAX1_2, + $inv_ChargeStatus, + $inv_SPOT_UDC1, $inv_SPOT_UDC2, + $inv_SPOT_IDC1, $inv_SPOT_IDC2, + $inv_SPOT_UAC1, $inv_SPOT_UAC2, $inv_SPOT_UAC3, + $inv_SPOT_IAC1, $inv_SPOT_IAC2, $inv_SPOT_IAC3, + $inv_BAT_UDC, $inv_BAT_IDC, + $inv_BAT_CYCLES, + $inv_BAT_TEMP, + $inv_SPOT_FREQ, $inv_SPOT_OPERTM, $inv_SPOT_FEEDTM, $inv_TEMP, $inv_GRIDRELAY, $inv_STATUS); + my $mysusyid = $hash->{HELPER}{MYSUSYID}; + my $myserialnumber = $hash->{HELPER}{MYSERIALNUMBER}; + my ($cmd, $myID, $target_ID, $spkt_ID, $cmd_ID); + my ($socket,$data,$size,$data_ID); + my ($i, $temp); # Variables for loops and calculation + + # Seriennummer und SuSyID des Ziel-WR setzen + my $default_target_susyid = $hash->{HELPER}{DEFAULT_TARGET_SUSYID}; + my $default_target_serial = $hash->{HELPER}{DEFAULT_TARGET_SERIAL}; + my $target_susyid = AttrVal($name, "target-susyid", $default_target_susyid); + my $target_serial = AttrVal($name, "target-serial", $default_target_serial); + + # Define own ID and target ID and packet ID + $myID = ByteOrderShort(substr(sprintf("%04X",$mysusyid),0,4)) . ByteOrderLong(sprintf("%08X",$myserialnumber)); + $target_ID = ByteOrderShort(substr(sprintf("%04X",$target_susyid),0,4)) . ByteOrderLong(sprintf("%08X",$target_serial)); + + # Increasing Packet ID + $hash->{HELPER}{PKT_ID} = $hash->{HELPER}{PKT_ID} + 1; + $spkt_ID = ByteOrderShort(sprintf("%04X",$hash->{HELPER}{PKT_ID})); + + $cmd_ID = ByteOrderLong(sprintf("%08X",$command)) . ByteOrderLong(sprintf("%08X",$first)) . ByteOrderLong(sprintf("%08X",$last)); + + #build final command to send + $cmd = $cmdheader . $pktlength . $esignature . $target_ID . "0000" . $myID . "0000" . "00000000" . $spkt_ID . $cmd_ID . "00000000"; + + # flush after every write + $| = 1; + + # Create Socket and check if successful + $socket = new IO::Socket::INET (PeerHost => $host, PeerPort => 9522, Proto => 'udp',); # open Socket + + if (!$socket) { + # in case of error + Log3 $name, 1, "$name - ERROR. Can't open socket to inverter: $!"; + return 0; + }; + + # Send Data + $data = pack("H*",$cmd); + $socket->send($data); + Log3 $name, 3, "$name - Send request $cmd_ID to $host on port 9522"; + Log3 $name, 5, "$name - send: $cmd"; + + # Receive Data and do a first check regarding length + # receive data + $socket->recv($data, $hash->{HELPER}{MAXBYTES}); + $size = length($data); + + # check if something was received + if (defined $size) { + my $received = unpack("H*", $data); + Log3 $name, 5, "$name - Received: $received"; + } + + # Nothing received -> exit + if (not defined $size) { + Log3 $name, 1, "$name - Nothing received..."; + return 0; + } else { + # We have received something! + if ($size > 58) { + # Check all parameters of answer + my $r_susyid = unpack("v*", substr $data, 20, 2); + my $r_serial = unpack("V*", substr $data, 22, 4); + my $r_pkt_ID = unpack("v*", substr $data, 40, 2); + my $r_error = unpack("V*", substr $data, 36, 4); + if (($r_susyid ne $mysusyid) || ($r_serial ne $myserialnumber) || ($r_pkt_ID ne $hash->{HELPER}{PKT_ID}) || ($r_error ne 0)) { + # Response does not match the parameters we have sent, maybe different target + Log3 $name, 3, "$name - Inverter answer does not match our parameters."; + Log3 $name, 5, "$name - Request/Response: SusyID $mysusyid/$r_susyid, Serial $myserialnumber/$r_serial, Packet ID $hash->{HELPER}{PKT_ID}/$r_pkt_ID, Error $r_error"; + $socket->close(); + return 0; + } + } else { + Log3 $name, 3, "$name - Format of inverter response does not fit."; + $socket->close(); + return 0; + } + } + + # All seems ok, data received + $inv_susyid = unpack("v*", substr $data, 28, 2); + $inv_serial = unpack("V*", substr $data, 30, 4); + $socket->close(); + + if (AttrVal($name, "target-serial", undef)) { + return 0 unless($target_serial eq $inv_serial); + } + if (AttrVal($name, "target-susyid", undef)) { + return 0 unless($target_susyid eq $inv_susyid); + } + + # Check the data identifier + $data_ID = unpack("v*", substr $data, 55, 2); + Log3 $name, 5, "$name - Data identifier $data_ID"; + + if($data_ID eq 0x2601) { + $inv_SPOT_ETOTAL = unpack("V*", substr($data, 62, 4)); + $inv_SPOT_ETODAY = unpack("V*", substr $data, 78, 4); + Log3 $name, 5, "$name - Found Data SPOT_ETOTAL=$inv_SPOT_ETOTAL and SPOT_ETODAY=$inv_SPOT_ETODAY"; + return (1,$inv_SPOT_ETODAY,$inv_SPOT_ETOTAL,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x251E) { + $inv_SPOT_PDC1 = unpack("V*", substr $data, 62, 4); + if($size < 90) {$inv_SPOT_PDC2 = 0; } else {$inv_SPOT_PDC2 = unpack("V*", substr $data, 90, 4); } # catch short response, in case PDC2 not supported + $inv_SPOT_PDC1 = ($inv_SPOT_PDC1 == 2147483648) ? 0 : $inv_SPOT_PDC1; + $inv_SPOT_PDC2 = ($inv_SPOT_PDC2 == 2147483648) ? 0 : $inv_SPOT_PDC2; + Log3 $name, 5, "$name - Found Data SPOT_PDC1=$inv_SPOT_PDC1 and SPOT_PDC2=$inv_SPOT_PDC2"; + return (1,$inv_SPOT_PDC1,$inv_SPOT_PDC2,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x4640) { + $inv_SPOT_PAC1 = unpack("l*", substr $data, 62, 4); + if($inv_SPOT_PAC1 eq -2147483648) {$inv_SPOT_PAC1 = 0; } # Catch 0x80000000 as 0 value + $inv_SPOT_PAC2 = unpack("l*", substr $data, 90, 4); + if($inv_SPOT_PAC2 eq -2147483648) {$inv_SPOT_PAC2 = 0; } # Catch 0x80000000 as 0 value + $inv_SPOT_PAC3 = unpack("l*", substr $data, 118, 4); + if($inv_SPOT_PAC3 eq -2147483648) {$inv_SPOT_PAC3 = 0; } # Catch 0x80000000 as 0 value + Log3 $name, 5, "$name - Found Data SPOT_PAC1=$inv_SPOT_PAC1 and SPOT_PAC2=$inv_SPOT_PAC2 and SPOT_PAC3=$inv_SPOT_PAC3"; + return (1,$inv_SPOT_PAC1,$inv_SPOT_PAC2,$inv_SPOT_PAC3,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x411E) { + $inv_PACMAX1 = unpack("V*", substr $data, 62, 4); + $inv_PACMAX2 = unpack("V*", substr $data, 90, 4); + $inv_PACMAX3 = unpack("V*", substr $data, 118, 4); + Log3 $name, 5, "$name - Found Data INV_PACMAX1=$inv_PACMAX1 and INV_PACMAX2=$inv_PACMAX2 and INV_PACMAX3=$inv_PACMAX3"; + return (1,$inv_PACMAX1,$inv_PACMAX2,$inv_PACMAX3,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x832A) { + $inv_PACMAX1_2 = unpack("V*", substr $data, 62, 4); + Log3 $name, 5, "$name - Found Data INV_PACMAX1_2=$inv_PACMAX1_2"; + return (1,$inv_PACMAX1_2,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x263F) { + $inv_SPOT_PACTOT = unpack("l*", substr $data, 62, 4); + if($inv_SPOT_PACTOT eq -2147483648) {$inv_SPOT_PACTOT = 0; } # Catch 0x80000000 as 0 value + Log3 $name, 5, "$name - Found Data SPOT_PACTOT=$inv_SPOT_PACTOT"; + return (1,$inv_SPOT_PACTOT,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x295A) { + $inv_ChargeStatus = unpack("V*", substr $data, 62, 4); + Log3 $name, 5, "$name - Found Data Battery Charge Status=$inv_ChargeStatus"; + return (1,$inv_ChargeStatus,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x451F) { + $inv_SPOT_UDC1 = unpack("l*", substr $data, 62, 4); + # catch shorter responses in case not second string supported + if($size < 146) { + $inv_SPOT_UDC2 = 0; + $inv_SPOT_IDC1 = unpack("l*", substr $data, 90, 4); + $inv_SPOT_IDC2 = 0; + } else { + $inv_SPOT_UDC2 = unpack("l*", substr $data, 90, 4); + $inv_SPOT_IDC1 = unpack("l*", substr $data, 118, 4); + $inv_SPOT_IDC2 = unpack("l*", substr $data, 146, 4); + } + if(($inv_SPOT_UDC1 eq -2147483648) || ($inv_SPOT_UDC1 eq 0xFFFFFFFF)) {$inv_SPOT_UDC1 = 0; } else {$inv_SPOT_UDC1 = $inv_SPOT_UDC1 / 100; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + if(($inv_SPOT_UDC2 eq -2147483648) || ($inv_SPOT_UDC2 eq 0xFFFFFFFF)) {$inv_SPOT_UDC2 = 0; } else {$inv_SPOT_UDC2 = $inv_SPOT_UDC2 / 100; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + if(($inv_SPOT_IDC1 eq -2147483648) || ($inv_SPOT_IDC1 eq 0xFFFFFFFF)) {$inv_SPOT_IDC1 = 0; } else {$inv_SPOT_IDC1 = $inv_SPOT_IDC1 / 1000; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + if(($inv_SPOT_IDC2 eq -2147483648) || ($inv_SPOT_IDC2 eq 0xFFFFFFFF)) {$inv_SPOT_IDC2 = 0; } else {$inv_SPOT_IDC2 = $inv_SPOT_IDC2 / 1000; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + Log3 $name, 5, "$name - Found Data SPOT_UDC1=$inv_SPOT_UDC1 and SPOT_UDC2=$inv_SPOT_UDC2 and SPOT_IDC1=$inv_SPOT_IDC1 and SPOT_IDC2=$inv_SPOT_IDC2"; + return (1,$inv_SPOT_UDC1,$inv_SPOT_UDC2,$inv_SPOT_IDC1,$inv_SPOT_IDC2,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x4648) { + $inv_SPOT_UAC1 = unpack("l*", substr $data, 62, 4); + $inv_SPOT_UAC2 = unpack("l*", substr $data, 90, 4); + $inv_SPOT_UAC3 = unpack("l*", substr $data, 118, 4); + $inv_SPOT_IAC1 = unpack("l*", substr $data, 146, 4); + $inv_SPOT_IAC2 = unpack("l*", substr $data, 174, 4); + $inv_SPOT_IAC3 = unpack("l*", substr $data, 202, 4); + if(($inv_SPOT_UAC1 eq -2147483648) || ($inv_SPOT_UAC1 eq 0xFFFFFFFF) || $inv_SPOT_UAC1 < 0) {$inv_SPOT_UAC1 = 0; } else {$inv_SPOT_UAC1 = $inv_SPOT_UAC1 / 100; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + if(($inv_SPOT_UAC2 eq -2147483648) || ($inv_SPOT_UAC2 eq 0xFFFFFFFF) || $inv_SPOT_UAC2 < 0) {$inv_SPOT_UAC2 = 0; } else {$inv_SPOT_UAC2 = $inv_SPOT_UAC2 / 100; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + if(($inv_SPOT_UAC3 eq -2147483648) || ($inv_SPOT_UAC3 eq 0xFFFFFFFF) || $inv_SPOT_UAC3 < 0) {$inv_SPOT_UAC3 = 0; } else {$inv_SPOT_UAC3 = $inv_SPOT_UAC3 / 100; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + if(($inv_SPOT_IAC1 eq -2147483648) || ($inv_SPOT_IAC1 eq 0xFFFFFFFF)) {$inv_SPOT_IAC1 = 0; } else {$inv_SPOT_IAC1 = $inv_SPOT_IAC1 / 1000; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + if(($inv_SPOT_IAC2 eq -2147483648) || ($inv_SPOT_IAC2 eq 0xFFFFFFFF)) {$inv_SPOT_IAC2 = 0; } else {$inv_SPOT_IAC2 = $inv_SPOT_IAC2 / 1000; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + if(($inv_SPOT_IAC3 eq -2147483648) || ($inv_SPOT_IAC3 eq 0xFFFFFFFF)) {$inv_SPOT_IAC3 = 0; } else {$inv_SPOT_IAC3 = $inv_SPOT_IAC3 / 1000; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + Log3 $name, 5, "$name - Found Data SPOT_UAC1=$inv_SPOT_UAC1 and SPOT_UAC2=$inv_SPOT_UAC2 and SPOT_UAC3=$inv_SPOT_UAC3 and SPOT_IAC1=$inv_SPOT_IAC1 and SPOT_IAC2=$inv_SPOT_IAC2 and SPOT_IAC3=$inv_SPOT_IAC3"; + return (1,$inv_SPOT_UAC1,$inv_SPOT_UAC2,$inv_SPOT_UAC3,$inv_SPOT_IAC1,$inv_SPOT_IAC2,$inv_SPOT_IAC3,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x491E) { + $inv_BAT_CYCLES = unpack("V*", substr $data, 62, 4); + $inv_BAT_TEMP = unpack("V*", substr $data, 90, 4) / 10; + $inv_BAT_UDC = unpack("V*", substr $data, 118, 4) / 100; + $inv_BAT_IDC = unpack("l*", substr $data, 146, 4); + if($inv_BAT_IDC eq -2147483648) {$inv_BAT_IDC = 0; } else { $inv_BAT_IDC = $inv_BAT_IDC / 1000;} # Catch 0x80000000 as 0 value + Log3 $name, 5, "$name - Found Data BAT_CYCLES=$inv_BAT_CYCLES and BAT_TEMP=$inv_BAT_TEMP and BAT_UDC=$inv_BAT_UDC and BAT_IDC=$inv_BAT_IDC"; + return (1,$inv_BAT_CYCLES,$inv_BAT_TEMP,$inv_BAT_UDC,$inv_BAT_IDC,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x2377) { + $inv_TEMP = unpack("l*", substr $data, 62, 4); + if($inv_TEMP eq -2147483648) {$inv_TEMP = 0; } else { $inv_TEMP = $inv_TEMP / 100;} # Catch 0x80000000 as 0 value + Log3 $name, 5, "$name - Found Data Inverter Temp=$inv_TEMP"; + return (1,$inv_TEMP,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x462E) { + $inv_SPOT_OPERTM = int(unpack("V*", substr $data, 62, 4) / 36) / 100; + $inv_SPOT_FEEDTM = int(unpack("V*", substr $data, 78, 4) / 36) / 100; + Log3 $name, 5, "$name - Found Data SPOT_OPERTM=$inv_SPOT_OPERTM and SPOT_FEEDTM=$inv_SPOT_FEEDTM"; + return (1,$inv_SPOT_OPERTM,$inv_SPOT_FEEDTM,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x4657) { + $inv_SPOT_FREQ = unpack("V*", substr $data, 62, 4); + if(($inv_SPOT_FREQ eq -2147483648) || ($inv_SPOT_FREQ eq 0xFFFFFFFF)) {$inv_SPOT_FREQ = 0; } else {$inv_SPOT_FREQ = $inv_SPOT_FREQ / 100; } # Catch 0x80000000 and 0xFFFFFFFF as 0 value + Log3 $name, 5, "$name - Found Data SPOT_FREQ=$inv_SPOT_FREQ"; + return (1,$inv_SPOT_FREQ,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x821E) { + $inv_CLASS = unpack("V*", substr $data, 102, 4) & 0x00FFFFFF; + $i = 142; # start address of INV_TYPE + $inv_TYPE = 0; # initialize to unknown inverter type + do { + $temp = unpack("V*", substr $data, $i, 4); + if(($temp & 0xFF000000) eq 0x01000000) { $inv_TYPE = $temp & 0x00FFFFFF; } # in some models a catalogue is transmitted, right model marked with: 0x01000000 OR INV_Type + $i = $i+4; + } while ((unpack("V*", substr $data, $i, 4) ne 0x00FFFFFE) && ($i<$size)); # 0x00FFFFFE is the end marker for attributes + + Log3 $name, 5, "$name - Found Data CLASS=$inv_CLASS and TYPE=$inv_TYPE"; + return (1,$inv_TYPE,$inv_CLASS,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x4164) { + $i = 0; + $temp = 0; + $inv_GRIDRELAY = 0x00FFFFFD; # Code for No Information; + do { + $temp = unpack("V*", substr $data, 62 + $i*4, 4); + if(($temp & 0xFF000000) ne 0) { $inv_GRIDRELAY = $temp & 0x00FFFFFF; } + $i = $i + 1; + } while ((unpack("V*", substr $data, 62 + $i*4, 4) ne 0x00FFFFFE) && ($i < 5)); # 0x00FFFFFE is the end marker for attributes + Log3 $name, 5, "$name - Found Data INV_GRIDRELAY=$inv_GRIDRELAY"; + return (1,$inv_GRIDRELAY,$inv_susyid,$inv_serial); + } + + if($data_ID eq 0x2148) { + $i = 0; + $temp = 0; + $inv_STATUS = 0x00FFFFFD; # Code for No Information; + do { + $temp = unpack("V*", substr $data, 62 + $i*4, 4); + if(($temp & 0xFF000000) ne 0) { $inv_STATUS = $temp & 0x00FFFFFF; } + $i = $i + 1; + } while ((unpack("V*", substr $data, 62 + $i*4, 4) ne 0x00FFFFFE) && ($i < 5)); # 0x00FFFFFE is the end marker for attributes + Log3 $name, 5, "$name - Found Data inv_STATUS=$inv_STATUS"; + return (1,$inv_STATUS,$inv_susyid,$inv_serial); + } + +return 0; +} + +########################################################################## +# Login +########################################################################## +sub SMA_logon($$$) { + # Parameters: host - passcode + my ($host,$pass,$hash) = @_; + my $cmdheader = "534D4100000402A00000000100"; + my $pktlength = "3A"; # length = 58 for logon command + my $esignature = "001060650EA0"; + my $name = $hash->{NAME}; + my $mysusyid = $hash->{HELPER}{MYSUSYID}; + my $myserialnumber = $hash->{HELPER}{MYSERIALNUMBER}; + my $pkt_ID = $hash->{HELPER}{PKT_ID}; + my ($cmd, $timestmp, $myID, $target_ID, $spkt_ID, $cmd_ID); + my ($socket,$data,$size); + + # Seriennummer und SuSyID des Ziel-WR setzen + my $default_target_susyid = $hash->{HELPER}{DEFAULT_TARGET_SUSYID}; + my $default_target_serial = $hash->{HELPER}{DEFAULT_TARGET_SERIAL}; + my $target_susyid = AttrVal($name, "target-susyid", $default_target_susyid); + my $target_serial = AttrVal($name, "target-serial", $default_target_serial); + + #Encode the password + my $encpasswd = "888888888888888888888888"; # template for password + for my $index (0..length $pass ) # encode password + { + if ( (hex(substr($encpasswd,($index*2),2)) + ord(substr($pass,$index,1))) < 256 ) { + substr($encpasswd,($index*2),2) = substr(sprintf ("%lX", (hex(substr($encpasswd,($index*2),2)) + ord(substr($pass,$index,1)))),0,2); + } else { + substr($encpasswd,($index*2),2) = substr(sprintf ("%lX", (hex(substr($encpasswd,($index*2),2)) + ord(substr($pass,$index,1)))),1,2); + } + } + + # Get current timestamp in epoch format (unix format) + $timestmp = ByteOrderLong(sprintf("%08X",int(time()))); + + # Define own ID and target ID and packet ID + $myID = ByteOrderShort(substr(sprintf("%04X",$mysusyid),0,4)) . ByteOrderLong(sprintf("%08X",$myserialnumber)); + $target_ID = ByteOrderShort(substr(sprintf("%04X",$target_susyid),0,4)) . ByteOrderLong(sprintf("%08X",$target_serial)); + $pkt_ID = 0x8001; # Reset to 0x8001 + $spkt_ID = ByteOrderShort(sprintf("%04X",$pkt_ID)); + + #Logon command + $cmd_ID = "0C04FDFF" . "07000000" . "84030000"; # Logon command + User group "User" + (maybe) Timeout + + #build final command to send + $cmd = $cmdheader . $pktlength . $esignature . $target_ID . "0001" . $myID . "0001" . "00000000" . $spkt_ID . $cmd_ID . $timestmp . "00000000" . $encpasswd . "00000000"; + + # flush after every write + $| = 1; + + # Create Socket and check if successful + $socket = new IO::Socket::INET (PeerHost => $host, PeerPort => 9522, Proto => 'udp',); # open Socket + + if (!$socket) { + # in case of error + Log3 $name, 1, "$name - ERROR - Can't open socket to inverter: $!"; + return 0; + }; + + # Send Data + $data = pack("H*",$cmd); + $socket->send($data); + Log3 $name, 4, "$name - Send login to $host on Port 9522 with password $pass "; + Log3 $name, 5, "$name - Send: $cmd "; + + # Receive Data and do a first check regarding length + eval { + $socket->recv($data, $hash->{HELPER}{MAXBYTES}); + $size = length($data); + }; + + # check if something was received + if (defined $size) { + my $received = unpack("H*", $data); + Log3 $name, 5, "$name - Received: $received"; + } + + # Nothing received -> exit + if (not defined $size) { + Log3 $name, 1, "$name - Nothing received..."; + # send: cmd_logout + $socket->close(); + SMA_logout($hash,$host); + return 0; + } else { + # We have received something! + if ($size > 62) { + # Check all parameters of answer + my $r_susyid = unpack("v*", substr $data, 20, 2); + my $r_serial = unpack("V*", substr $data, 22, 4); + my $r_pkt_ID = unpack("v*", substr $data, 40, 2); + my $r_cmd_ID = unpack("V*", substr $data, 42, 4); + my $r_error = unpack("V*", substr $data, 36, 4); + + if (($r_pkt_ID ne $pkt_ID) || ($r_cmd_ID ne 0xFFFD040D) || ($r_error ne 0)) { + # Response does not match the parameters we have sent, maybe different target + Log3 $name, 1, "$name - Inverter answer does not match our parameters."; + Log3 $name, 5, "$name - Request/Response: SusyID $mysusyid/$r_susyid, Serial $myserialnumber/$r_serial, Packet ID $hash->{HELPER}{PKT_ID}/$r_pkt_ID, Command 0xFFFD040D/$r_cmd_ID, Error $r_error"; + # send: cmd_logout + $socket->close(); + SMA_logout($hash,$host); + return 0; + } + } else { + Log3 $name, 1, "$name - Format of inverter response does not fit."; + # send: cmd_logout + $socket->close(); + SMA_logout($hash,$host); + return 0; + } + } + + # All seems ok, logged in! + my $inv_susyid = unpack("v*", substr $data, 28, 2); + my $inv_serial = unpack("V*", substr $data, 30, 4); + $socket->close(); + + if (AttrVal($name, "target-serial", undef)) { + return 0 unless($inv_serial eq $target_serial); + } + if (AttrVal($name, "target-susyid", undef)) { + return 0 unless($inv_susyid eq $target_susyid); + } + + Log3 $name, 4, "$name - logged in to inverter serial: $inv_serial, susyid: $inv_susyid"; + return 1; +} + +########################################################################## +# Logout +########################################################################## +sub SMA_logout($$) { + # Parameters: host + my ($hash,$host) = @_; + my $name = $hash->{NAME}; + my $cmdheader = "534D4100000402A00000000100"; + my $pktlength = "22"; # length = 34 for logout command + my $esignature = "0010606508A0"; + my $mysusyid = $hash->{HELPER}{MYSUSYID}; + my $myserialnumber = $hash->{HELPER}{MYSERIALNUMBER}; + my $pkt_ID = $hash->{HELPER}{PKT_ID}; + my ($cmd, $myID, $target_ID, $spkt_ID, $cmd_ID); + my ($socket,$data,$size); + + # Seriennummer und SuSyID des Ziel-WR setzen + my $default_target_susyid = $hash->{HELPER}{DEFAULT_TARGET_SUSYID}; + my $default_target_serial = $hash->{HELPER}{DEFAULT_TARGET_SERIAL}; + my $target_susyid = AttrVal($name, "target-susyid", $default_target_susyid); + my $target_serial = AttrVal($name, "target-serial", $default_target_serial); + + # Define own ID and target ID and packet ID + $myID = ByteOrderShort(substr(sprintf("%04X",$mysusyid),0,4)) . ByteOrderLong(sprintf("%08X",$myserialnumber)); + $target_ID = ByteOrderShort(substr(sprintf("%04X",$target_susyid),0,4)) . ByteOrderLong(sprintf("%08X",$target_serial)); + # Increasing Packet ID + $hash->{HELPER}{PKT_ID} = $hash->{HELPER}{PKT_ID} + 1; + $spkt_ID = ByteOrderShort(sprintf("%04X",$hash->{HELPER}{PKT_ID})); + + #Logout command + $cmd_ID = "0E01FDFF" . "FFFFFFFF"; # Logout command + + #build final command to send + $cmd = $cmdheader . $pktlength . $esignature . $target_ID . "0003" . $myID . "0003" . "00000000" . $spkt_ID . $cmd_ID . "00000000"; + + # flush after every write + $| = 1; + + # Create Socket and check if successful + $socket = new IO::Socket::INET (PeerHost => $host, PeerPort => 9522, Proto => 'udp',); # open Socket + + if (!$socket) { + # in case of error + Log3 $name, 1, "$name - ERROR - Can't open socket to inverter: $!"; + return 0; + }; + + # Send Data + $data = pack("H*",$cmd); + $socket->send($data); + Log3 $name, 4, "$name - Send logout to $host on Port 9522"; + Log3 $name, 5, "$name - Send: $cmd "; + + $target_serial = ($target_serial eq $default_target_serial)?"any inverter":$target_serial; + $target_susyid = ($target_susyid eq $default_target_susyid)?"any susyid":$target_susyid; + Log3 $name, 4, "$name - logged out now from inverter serial: $target_serial, susyid: $target_susyid"; + + $socket->close(); + return 1; +} + +########################################################################## +# Hilfsroutinen +########################################################################## + +########################## +sub ByteOrderShort($) { + my $input = $_[0]; + my $output = ""; + $output = substr($input, 2, 2) . substr($input, 0, 2); + return $output; +} + +########################## +sub ByteOrderLong($) { + my $input = $_[0]; + my $output = ""; + $output = substr($input, 6, 2) . substr($input, 4, 2) . substr($input, 2, 2) . substr($input, 0, 2); + return $output; +} + +########################## +sub StatusText($) +{ + # Parameter is the code, return value is the Text or if not known then the code as string + my $code = $_[0]; + + if($code eq 51) { return (AttrVal("global", "language", "EN") eq "DE") ? "geschlossen" : "Closed"; } + if($code eq 311) { return (AttrVal("global", "language", "EN") eq "DE") ? "offen" : "Open"; } + if($code eq 16777213) { return (AttrVal("global", "language", "EN") eq "DE") ? "Information liegt nicht vor" : "No Information"; } + + if($code eq 35) { return (AttrVal("global", "language", "EN") eq "DE") ? "Fehler" : "Fault"; } + if($code eq 303) { return "Off"; } + if($code eq 307) { return "Ok"; } + if($code eq 455) { return (AttrVal("global", "language", "EN") eq "DE") ? "Warnung" : "Warning"; } + + return sprintf("%d", $code); +} + +########################## +# identify device type + +sub devtype ($) { + my ($code) = @_; + + unless (exists($SMAInverter_devtypes{$code})) { return $code;} + my $dev = $SMAInverter_devtypes{$code}; + return ($dev); +} + +########################## +# identify device class + +sub classtype ($) { + my ($code) = @_; + my $class; + + if(AttrVal("global", "language", "EN") eq "DE") { + unless (exists($SMAInverter_classesDE{$code})) { return $code;} + $class = $SMAInverter_classesDE{$code}; + } else { + unless (exists($SMAInverter_classesEN{$code})) { return $code;} + $class = $SMAInverter_classesEN{$code}; + } + +return ($class); +} + +1; + +=pod +=item summary Integration of SMA Inverters over it's Speedwire (=Ethernet) Interface +=item summary_DE Integration von SMA Wechselrichtern über Speedwire (=Ethernet) Interface + +=begin html + + +

SMAInverter

+ +Module for the integration of a SMA Inverter over it's Speedwire (=Ethernet) Interface.
+Tested on Sunny Tripower 6000TL-20 and Sunny Island 4.4 with Speedwire/Webconnect Piggyback. +

+ +Questions and discussions about this module you can find in the FHEM-Forum link:
+76_SMAInverter.pm - Abfrage von SMA Wechselrichter. +

+ +Requirements +

+This module requires: +
    +
  • Perl Module: IO::Socket::INET (apt-get install libio-socket-multicast-perl)
  • +
  • Perl Module: Date::Time (apt-get install libdatetime-perl)
  • +
  • Perl Module: Time::HiRes
  • +
  • FHEM Module: 99_SUNRISE_EL.pm
  • +
  • FHEM Module: Blocking.pm
  • +
+
+
+ + +Define +
    +define <name> SMAInverter <pin> <hostname/ip>
    +
    +
  • pin: User-Password of the SMA Inverter. Default is 0000. Can be changed by "Sunny Explorer" Windows Software
  • +
  • hostname/ip: Hostname or IP-Adress of the inverter (or it's speedwire piggyback module).
  • +
  • Port of the inverter is 9522 by default. Firewall has to allow connection on this port !
  • +
+ + +Operation method +
    +The module sends commands to the inverter and checks if they are supported by the inverter.
    +In case of a positive answer the data is collected and displayed in the readings according to the detail-level.
    +If more than one inverter is installed, set attributes "target-susyid" and "target-serial" with an appropriate value.

    + +The normal operation time of the inverter is supposed from sunrise to sunset. In that time period the inverter will be polled. +The time of sunrise and sunset will be calculated by functions of FHEM module 99_SUNRISE_EL.pm which is loaded automatically by default. +Therefore the global attribute "longitude" and "latitude" should be set to determine the position of the solar system +(see Commandref SUNRISE_EL).

    + +By the attribute "suppressSleep" the sleep mode between sunset and sunrise can be suppressed. Using attribute "offset" you may prefer the sunrise and +defer the sunset virtually. So the working period of the inverter will be extended.

    + +In operating mode "automatic" the inverter will be requested periodically corresponding the preset attribute "interval". The operating mode can be +switched to "manual" to realize the retrieval manually (e.g. to synchronize the requst with a SMA energy meter by notify).

    + +During inverter operating time the average energy production of the last 5, 10 and 15 minutes will be calculated and displayed in the readings +"avg_power_lastminutes_05", "avg_power_lastminutes_10" and "avg_power_lastminutes_15". Note: To permit a precise calculation, you should +also set the real request interval into the attribute "interval" although you would use the "manual" operation mode !

    + +The retrieval of the inverter will be executed non-blocking. You can adjust the timeout value for this background process by attribute "timeout".
    +
+ +Get +
+
    +get <name> data +

    + +The request of the inverter will be executed. Those possibility is especifically created for the "manual" operation mode (see attribute "mode"). + +
+ +Attributes +
    +
  • interval : Queryintreval in seconds
  • +
  • detail-level : "0" - Only Power and Energy / "1" - Including Voltage and Current / "2" - All values
  • +
  • disable : 1 = the module is disabled
  • +
  • mode : automatic = the inverter will be polled by preset interval, manual = query only by command "get <name> data"
  • +
  • offset : time in seconds to prefer the sunrise respectively defer the sunset virtualy (0 ... 7200). You will be able to extend the working + period of the module.
  • +
  • SBFSpotComp : 1 = the readings are created like SBFSpot-style
  • +
  • suppressSleep : the sleep mode (after sunset, before sunrise) is deactivated and the inverter will be polled continuously.
  • +
  • showproctime : shows processing time in background and wasted time to retrieve inverter data
  • +
  • target-susyid : In case of a Multigate the target SUSyID can be defined. If more than one inverter is installed you have to + set the inverter-SUSyID to assign the inverter to the device definition. + Default is 0xFFFF, means any SUSyID
  • +
  • target-serial : In case of a Multigate the target Serialnumber can be defined. If more than one inverter is installed you have to + set the inverter-Serialnumber to assign the inverter to the device definition. + Default is 0xFFFFFFFF, means any Serialnumber
  • +
  • timeout : setup timeout of inverter data request (default 60s)
  • +
+ +Readings +
    +
  • BAT_CYCLES / bat_cycles : Battery recharge cycles
  • +
  • BAT_IDC / bat_idc : Battery Current
  • +
  • BAT_TEMP / bat_temp : Battery temperature
  • +
  • BAT_UDC / bat_udc : Battery Voltage
  • +
  • ChargeStatus / chargestatus : Battery Charge status
  • +
  • CLASS / device_class : Inverter Class
  • +
  • PACMAX1 / pac_max_phase_1 : Nominal power in Ok Mode
  • +
  • PACMAX1_2 / pac_max_phase_1_2 : Maximum active power device (Some inverters like SB3300/SB1200)
  • +
  • PACMAX2 / pac_max_phase_2 : Nominal power in Warning Mode
  • +
  • PACMAX3 / pac_max_phase_3 : Nominal power in Fault Mode
  • +
  • Serialnumber / serial_number : Inverter Serialnumber
  • +
  • SPOT_ETODAY / etoday : Today yield
  • +
  • SPOT_ETOTAL / etotal : Total yield
  • +
  • SPOT_FEEDTM / feed-in_time : Feed-in time
  • +
  • SPOT_FREQ / grid_freq. : Grid Frequency
  • +
  • SPOT_IAC1 / phase_1_iac : Grid current phase L1
  • +
  • SPOT_IAC2 / phase_2_iac : Grid current phase L2
  • +
  • SPOT_IAC3 / phase_3_iac : Grid current phase L3
  • +
  • SPOT_IDC1 / string_1_idc : DC current input
  • +
  • SPOT_IDC2 / string_2_idc : DC current input
  • +
  • SPOT_OPERTM / operation_time : Operation Time
  • +
  • SPOT_PAC1 / phase_1_pac : Power L1
  • +
  • SPOT_PAC2 / phase_2_pac : Power L2
  • +
  • SPOT_PAC3 / phase_3_pac : Power L3
  • +
  • SPOT_PACTOT / total_pac : Total Power
  • +
  • SPOT_PDC1 / string_1_pdc : DC power input 1
  • +
  • SPOT_PDC2 / string_2_pdc : DC power input 2
  • +
  • SPOT_UAC1 / phase_1_uac : Grid voltage phase L1
  • +
  • SPOT_UAC2 / phase_2_uac : Grid voltage phase L2
  • +
  • SPOT_UAC3 / phase_3_uac : Grid voltage phase L3
  • +
  • SPOT_UDC1 / string_1_udc : DC voltage input
  • +
  • SPOT_UDC2 / string_2_udc : DC voltage input
  • +
  • SUSyID / susyid : Inverter SUSyID
  • +
  • INV_TEMP / device_temperature : Inverter temperature
  • +
  • INV_TYPE / device_type : Inverter Type
  • +
  • POWER_IN / power_in : Battery Charging power
  • +
  • POWER_OUT / power_out : Battery Discharging power
  • +
  • INV_GRIDRELAY / gridrelay_status : Grid Relay/Contactor Status
  • +
  • INV_STATUS / device_status : Inverter Status
  • +
  • opertime_start : Begin of iverter operating time corresponding the calculated time of sunrise with consideration of the + attribute "offset" (if set)
  • +
  • opertime_stop : End of iverter operating time corresponding the calculated time of sunrise with consideration of the + attribute "offset" (if set)
  • +
  • modulstate : shows the current module state "normal" or "sleep" if the inverter won't be requested at the time.
  • +
  • avg_power_lastminutes_05 : average power of the last 5 minutes.
  • +
  • avg_power_lastminutes_10 : average power of the last 10 minutes.
  • +
  • avg_power_lastminutes_15 : average power of the last 15 minutes.
  • +
  • inverter_processing_time : wasted time to retrieve the inverter data
  • +
  • background_processing_time : total wasted time by background process (BlockingCall)
  • +
+

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

SMAInverter

+ +Modul zur Einbindung eines SMA Wechselrichters über Speedwire (Ethernet).
+Getestet mit Sunny Tripower 6000TL-20 und Sunny Island 4.4 mit Speedwire/Webconnect Piggyback. +

+ +Fragen und Diskussionen rund um dieses Modul finden sie im FHEM-Forum unter:
+76_SMAInverter.pm - Abfrage von SMA Wechselrichter. +

+ +Voraussetzungen +

+Dieses Modul benötigt: +
    +
  • Perl Modul: IO::Socket::INET (apt-get install libio-socket-multicast-perl)
  • +
  • Perl Modul: Datetime (apt-get install libdatetime-perl)
  • +
  • Perl Modul: Time::HiRes
  • +
  • FHEM Modul: 99_SUNRISE_EL.pm
  • +
  • FHEM Modul: Blocking.pm
  • +
+
+
+ + +Define +
    +define <name> SMAInverter <pin> <hostname/ip>
    +
    +
  • pin: Benutzer-Passwort des SMA STP Wechselrichters. Default ist 0000. Kann über die Windows-Software "Sunny Explorer" geändert werden
  • +
  • hostname/ip: Hostname oder IP-Adresse des Wechselrichters (bzw. dessen Speedwire Moduls mit Ethernetanschluss)
  • +
  • Der Port des Wechselrichters ist 9522. Dieser Port muss in der Firewall freigeschaltet sein !
  • +
+ + +Arbeitsweise +
    +Das Modul schickt Befehle an den Wechselrichter und überprüft, ob diese unterstützt werden.
    +Bei einer positiven Antwort werden die Daten gesammelt und je nach Detail-Level in den Readings dargestellt.
    +Sind mehr als ein Wechselrichter installiert, sind die Attribute "target-susyid" und "target-serial" entsprechend zu setzen um die korrekte +Funktion zu gewährleisten.

    + +Die normale Betriebszeit des Wechselrichters wird in der Zeit vom Sonnenaufgang bis Sonnenuntergang angenommen. In dieser Periode werden die Wechselrichterdaten +abgefragt. Die Ermittlung von Sonnenaufgang / Sonnenuntergang wird über die Funktionen des FHEM-Moduls 99_SUNRISE_EL.pm vorgenommen. Zu diesem Zweck sollten die globalen +Attribute longitude und latitude gesetzt sein um den Standort der Anlage genau zu ermitteln. (siehe Commandref SUNRISE_EL)

    + +Mit dem Attribut "suppressSleep" kann der Schlafmodus unterdrückt werden. Das Attribut "offset" dient dazu den effektiven Zeitpunkt des Sonnenaufgangs / Sonnenuntergangs +um den Betrag "offset" vorzuziehen (Sonnenaufgang) bzw. zu verzögern (Sonnenuntergang) und somit die Abfrageperiode des Wechselrichters zu verlängern.

    + +Im Betriebsmodus "automatic" wird der Wechselrichter entsprechend des eingestellten Attributs "interval" abgefragt. Der Betriebsmodus kann in "manual" +umgestellt werden um eine manuelle Abfrage zu realisieren (z.B. Synchronisierung mit einem SMA Energymeter über ein Notify).

    + +Während der Betriebszeit des Wechselrichters wird die durchschnittliche Energieerzeugung der letzten 5, 10, 15 Minuten berechnet und in den Readings +"avg_power_lastminutes_05", "avg_power_lastminutes_10" und "avg_power_lastminutes_15" ausgegeben. Hinweis: Um eine korrekte Berechnung zu +ermöglichen, sollte auch im Betriebsmodus "manual" das tatsächliche Abfrageinterval im Attribute "interval" hinterlegt werden !

    + +Die Abfrage des Wechselrichters wird non-blocking ausgeführt. Der Timeoutwert für diesen Hintergrundprozess kann mit dem Attribut "timeout" eingestellt werden.
    + +
+ +Get +
+
    +get <name> data +

    + +Die Datenabfrage des Wechselrichters wird ausgeführt. Diese Möglichkeit ist speziell für den Betriebsmodus "manual" vorgesehen (siehe Attribut "mode"). + +
+ +Attribute +
    +
  • interval : Abfrageinterval in Sekunden
  • +
  • detail-level : "0" - Nur Leistung und Energie / "1" - zusätzlich Strom und Spannung / "2" - Alle Werte
  • +
  • disable : 1 = das Modul ist disabled
  • +
  • mode : automatic = die Wechselrichterwerte werden im eingestellten Interval abgefragt, manual = Abfrage nur mit "get <name> data"
  • +
  • offset : Zeit in Sekunden um die der Sonnenaufgang vorgezogen bzw. Sonnenuntergang verzögert wird (0 ... 7200). Dadurch wird die + effektive Aktivzeit des Moduls erweitert.
  • +
  • suppressSleep : der Schlafmodus (nach Sonnenuntergang, vor Sonnenaufgang) wird ausgeschaltet und der WR abgefragt.
  • +
  • showproctime : zeigt die für den Hintergrundprozess und die Abfrage des Wechselrichter verbrauchte Zeit.
  • +
  • SBFSpotComp : 1 = die Readings werden kompatibel zu SBFSpot-Ausgaben erzeugt
  • +
  • target-susyid : Im Falle eines Multigate kann die Ziel-SUSyID definiert werden. Ist mehr als ein Wechselrichter installiert, + muß die Wechselreichter-SUSyID gesetzt werden um den Wechselrichter der Device-Definition eindeutig zuzuweisen. + Default ist 0xFFFF (=keine Einschränkung)
  • +
  • target-serial : Im Falle eines Multigate kann die Ziel-Seriennummer definiert werden. Ist mehr als ein Wechselrichter installiert, + muß die Wechselreichter-Seriennummer gesetzt werden um den Wechselrichter der Device-Definition eindeutig zuzuweisen. + Default ist 0xFFFFFFFF (=keine Einschränkung)
  • +
  • timeout : Einstellung des timeout für die Wechselrichterabfrage (default 60s)
  • +
+ +Readings +
    +
  • BAT_CYCLES / bat_cycles : Akku Ladezyklen
  • +
  • BAT_IDC / bat_idc : Akku Strom
  • +
  • BAT_TEMP / bat_temp : Akku Temperatur
  • +
  • BAT_UDC / bat_udc : Akku Spannung
  • +
  • ChargeStatus / chargestatus : Akku Ladestand
  • +
  • CLASS / device_class : Wechselrichter Klasse
  • +
  • PACMAX1 / pac_max_phase_1 : Nominelle Leistung in Ok Mode
  • +
  • PACMAX1_2 / pac_max_phase_1_2 : Maximale Leistung (für einige Wechselrichtertypen)
  • +
  • PACMAX2 / pac_max_phase_2 : Nominelle Leistung in Warning Mode
  • +
  • PACMAX3 / pac_max_phase_3 : Nominelle Leistung in Fault Mode
  • +
  • Serialnumber / serial_number : Wechselrichter Seriennummer
  • +
  • SPOT_ETODAY / etoday : Energie heute
  • +
  • SPOT_ETOTAL / etotal : Energie Insgesamt
  • +
  • SPOT_FEEDTM / feed-in_time : Einspeise-Stunden
  • +
  • SPOT_FREQ / grid_freq. : Netz Frequenz
  • +
  • SPOT_IAC1 / phase_1_iac : Netz Strom phase L1
  • +
  • SPOT_IAC2 / phase_2_iac : Netz Strom phase L2
  • +
  • SPOT_IAC3 / phase_3_iac : Netz Strom phase L3
  • +
  • SPOT_IDC1 / string_1_idc : DC Strom Eingang 1
  • +
  • SPOT_IDC2 / string_2_idc : DC Strom Eingang 2
  • +
  • SPOT_OPERTM / operation_time : Betriebsstunden
  • +
  • SPOT_PAC1 / phase_1_pac : Leistung L1
  • +
  • SPOT_PAC2 / phase_2_pac : Leistung L2
  • +
  • SPOT_PAC3 / phase_3_pac : Leistung L3
  • +
  • SPOT_PACTOT / total_pac : Gesamtleistung
  • +
  • SPOT_PDC1 / string_1_pdc : DC Leistung Eingang 1
  • +
  • SPOT_PDC2 / string_2_pdc : DC Leistung Eingang 2
  • +
  • SPOT_UAC1 / phase_1_uac : Netz Spannung phase L1
  • +
  • SPOT_UAC2 / phase_2_uac : Netz Spannung phase L2
  • +
  • SPOT_UAC3 / phase_3_uac : Netz Spannung phase L3
  • +
  • SPOT_UDC1 / string_1_udc : DC Spannung Eingang 1
  • +
  • SPOT_UDC2 / string_2_udc : DC Spannung Eingang 2
  • +
  • SUSyID / susyid : Wechselrichter SUSyID
  • +
  • INV_TEMP / device_temperature : Wechselrichter Temperatur
  • +
  • INV_TYPE / device_type : Wechselrichter Typ
  • +
  • POWER_IN / power_in : Akku Ladeleistung
  • +
  • POWER_OUT / power_out : Akku Entladeleistung
  • +
  • INV_GRIDRELAY / gridrelay_status : Netz Relais Status
  • +
  • INV_STATUS / device_status : Wechselrichter Status
  • +
  • opertime_start : Beginn Aktivzeit des Wechselrichters entsprechend des ermittelten Sonnenaufgangs mit Berücksichtigung des + Attributs "offset" (wenn gesetzt)
  • +
  • opertime_stop : Ende Aktivzeit des Wechselrichters entsprechend des ermittelten Sonnenuntergangs mit Berücksichtigung des + Attributs "offset" (wenn gesetzt)
  • +
  • modulstate : zeigt den aktuellen Modulstatus "normal" oder "sleep" falls der Wechselrichter nicht abgefragt wird.
  • +
  • avg_power_lastminutes_05 : durchschnittlich erzeugte Leistung der letzten 5 Minuten.
  • +
  • avg_power_lastminutes_10 : durchschnittlich erzeugte Leistung der letzten 10 Minuten.
  • +
  • avg_power_lastminutes_15 : durchschnittlich erzeugte Leistung der letzten 15 Minuten.
  • +
  • inverter_processing_time : verbrauchte Zeit um den Wechelrichter abzufragen.
  • +
  • background_processing_time : gesamte durch den Hintergrundprozess (BlockingCall) verbrauchte Zeit.
  • + +
+

+ +=end html_DE + +=cut