######################################################################################################################### # $Id: 76_SMAPortal.pm 00000 2019-03-14 20:21:11Z DS_Starter $ ######################################################################################################################### # 76_SMAPortal.pm # # (c) 2019 by Heiko Maaz # e-mail: Heiko dot Maaz at t-online dot de # # This module can be used to get data from SMA Portal https://www.sunnyportal.com/Templates/Start.aspx . # # This script is part of fhem. # # Fhem 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. # # Fhem 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. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # # Credits (Thanks to all!): # Brun von der Gönne : author of 98_SHM.pm # BerndArnold : author of 98_SHMForecastRelative.pm # Wzut/XGuide : creation of SMAPortal graphics # # FHEM Forum: http://forum.fhem.de/index.php/topic,27667.0.html # ######################################################################################################################### # # Definition: define SMAPortal # ######################################################################################################################### package main; use strict; use warnings; eval "use FHEM::Meta;1"; ############################################################### # SMAPortal Initialize # Da ich mit package arbeite müssen für die jeweiligen hashFn # Funktionen der Funktionsname und davor mit :: getrennt der # eigentliche package Name des Modules ############################################################### sub SMAPortal_Initialize($) { my ($hash) = @_; $hash->{DefFn} = "FHEM::SMAPortal::Define"; $hash->{UndefFn} = "FHEM::SMAPortal::Undefine"; $hash->{DeleteFn} = "FHEM::SMAPortal::Delete"; $hash->{AttrFn} = "FHEM::SMAPortal::Attr"; $hash->{SetFn} = "FHEM::SMAPortal::Set"; $hash->{GetFn} = "FHEM::SMAPortal::Get"; $hash->{DbLog_splitFn} = "FHEM::SMAPortal::DbLog_split"; $hash->{AttrList} = "cookieLocation ". "cookielifetime ". "detailLevel:1,2,3,4 ". "disable:0,1 ". "getDataRetries:1,2,3,4,5,6,7,8,9,10 ". "interval ". "showPassInLog:1,0 ". "timeout ". "userAgent ". $readingFnAttributes; eval { FHEM::Meta::InitMod( __FILE__, $hash ) }; # für Meta.pm (https://forum.fhem.de/index.php/topic,97589.0.html) return; } ############################################################### # Begin Package ############################################################### package FHEM::SMAPortal; use strict; use warnings; use GPUtils qw(:all); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt use POSIX; eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; use Data::Dumper; use Blocking; use Time::HiRes qw(gettimeofday); use Time::Local; use LWP::UserAgent; use HTTP::Cookies; use JSON qw(decode_json); use MIME::Base64; use Encode; # Run before module compilation BEGIN { # Import from main:: GP_Import( qw( attr AttrVal AttrNum addToDevAttrList addToAttrList BlockingCall BlockingKill CommandAttr CommandDefine CommandDeleteAttr CommandDeleteReading CommandSet defs delFromDevAttrList delFromAttrList devspec2array deviceEvents Debug FmtDateTime FmtTime FW_makeImage fhemTimeGm getKeyValue gettimeofday genUUID init_done InternalTimer IsDisabled Log Log3 modules readingsSingleUpdate readingsBulkUpdate readingsBulkUpdateIfChanged readingsBeginUpdate readingsDelete readingsEndUpdate ReadingsNum ReadingsTimestamp ReadingsVal RemoveInternalTimer setKeyValue sortTopicNum TimeNow Value json2nameValue FW_directNotify FW_ME FW_subdir FW_room FW_detail FW_wname ) ); } # Standardvariablen und Forward-Deklaration use vars qw($FW_ME); # webname (default is fhem), used by 97_GROUP/weblink # Versions History intern our %vNotesIntern = ( "2.1.2" => "08.06.2019 correct planned time of consumer in PortalAsHtml if planned time is at next day ", "2.1.1" => "08.06.2019 add units to values, some bugs fixed ", "2.1.0" => "07.06.2019 add informations about consumer switch and power state ", "2.0.0" => "03.06.2019 designed for SMAPortalSPG graphics device ", "1.8.0" => "27.05.2019 redesign of SMAPortal graphics by Wzut/XGuide ", "1.7.1" => "01.05.2019 PortalAsHtml: use of colored svg-icons possible ", "1.7.0" => "01.05.2019 code change of PortalAsHtml, new attributes \"portalGraphicColor\" and \"portalGraphicStyle\" ", "1.6.0" => "29.04.2019 function PortalAsHtml ", "1.5.5" => "22.04.2019 fix readings for BattryOut and BatteryIn ", "1.5.4" => "26.03.2019 delete L1_InfoMessages if no info occur ", "1.5.3" => "26.03.2019 delete L1_ErrorMessages, L1_WarningMessages if no errors or warnings occur ", "1.5.2" => "25.03.2019 prevent module from deactivation in case of unavailable Meta.pm ", "1.5.1" => "24.03.2019 fix \$VAR1 problem Forum: #27667.msg922983.html#msg922983 ", "1.5.0" => "23.03.2019 add consumer data ", "1.4.0" => "22.03.2019 add function extractPlantData, DbLog_split, change L2 Readings ", "1.3.0" => "18.03.2019 change module to use package FHEM::SMAPortal and Meta.pm, new sub setVersionInfo ", "1.2.3" => "12.03.2019 make ready for 98_Installer.pm ", "1.2.2" => "11.03.2019 new Errormessage analyze added, make ready for Meta.pm ", "1.2.1" => "10.03.2019 behavior of state changed, commandref revised ", "1.2.0" => "09.03.2019 integrate weather data, minor fixes ", "1.1.0" => "09.03.2019 make get data more stable, new attribute \"getDataRetries\" ", "1.0.0" => "03.03.2019 initial " ); # Web instance ############################################################### # SMAPortal Define ############################################################### sub Define($$) { my ($hash, $def) = @_; my @a = split(/\s+/, $def); return "Wrong syntax: use \"define SMAPortal\" " if(int(@a) < 1); $hash->{HELPER}{MODMETAABSENT} = 1 if($modMetaAbsent); # Modul Meta.pm nicht vorhanden setVersionInfo($hash); # Versionsinformationen setzen getcredentials($hash,1); # Credentials lesen und in RAM laden ($boot=1) CallInfo($hash); # Start Daten Abrufschleife delcookiefile($hash); # Start Schleife regelmäßiges Löschen Cookiefile return undef; } ############################################################### # SMAPortal Undefine ############################################################### sub Undefine($$) { my ($hash, $arg) = @_; RemoveInternalTimer($hash); BlockingKill($hash->{HELPER}{RUNNING_PID}) if($hash->{HELPER}{RUNNING_PID}); return undef; } ############################################################### # SMAPortal Delete ############################################################### sub Delete($$) { my ($hash, $arg) = @_; my $index = $hash->{TYPE}."_".$hash->{NAME}."_credentials"; my $name = $hash->{NAME}; # gespeicherte Credentials löschen setKeyValue($index, undef); return undef; } ############################################################### # SMAPortal Set ############################################################### sub Set($@) { my ($hash, @a) = @_; return "\"set X\" needs at least an argument" if ( @a < 2 ); my $name = $a[0]; my $opt = $a[1]; my $prop = $a[2]; my $prop1 = $a[3]; my ($setlist,$success); return if(IsDisabled($name)); if(!$hash->{CREDENTIALS}) { # initiale setlist für neue Devices $setlist = "Unknown argument $opt, choose one of ". "credentials " ; } else { # erweiterte Setlist wenn Credentials gesetzt $setlist = "Unknown argument $opt, choose one of ". "credentials ". "createPortalGraphic:Generation,Consumption,Generation_Consumption,Differential " ; } if ($opt eq "credentials") { return "Credentials are incomplete, use username password" if (!$prop || !$prop1); ($success) = setcredentials($hash,$prop,$prop1); if($success) { CallInfo($hash); return "Username and Password saved successfully"; } else { return "Error while saving Username / Password - see logfile for details"; } } elsif ($opt eq "createPortalGraphic") { if (!$hash->{CREDENTIALS}) {return "Credentials of $name are not set - make sure you've set it with \"set $name credentials username password\"";} my ($htmldev,$ret,$c,$type,$color2); if ($prop eq "Generation") { $htmldev = "SPG1.$name"; # Grafiktyp Generation (Erzeugung) $type = 'pv'; $c = "SMA Sunny Portal Graphics - Forecast Generation"; $color2 = "000000"; # zweite Farbe als schwarz setzen } elsif ($prop eq "Consumption") { $htmldev = "SPG2.$name"; # Grafiktyp Consumption (Verbrauch) $type = 'co'; $c = "SMA Sunny Portal Graphics - Forecast Consumption"; $color2 = "000000"; # zweite Farbe als schwarz setzen } elsif ($prop eq "Generation_Consumption") { $htmldev = "SPG3.$name"; # Grafiktyp Generation_Consumption (Erzeugung und Verbrauch) $type = 'pvco'; $c = "SMA Sunny Portal Graphics - Forecast Generation & Consumption"; $color2 = "FF5C82"; # zweite Farbe als rot setzen } elsif ($prop eq "Differential") { $htmldev = "SPG4.$name"; # Grafiktyp Differential (Differenzanzeige) $type = 'diff'; $c = "SMA Sunny Portal Graphics - Forecast Differential"; $color2 = "FF5C82"; # zweite Farbe als rot setzen } else { return "Invalid portal graphic devicetype ! Use one of \"Generation\", \"Consumption\", \"Generation_Consumption\", \"Differential\". " } $ret = CommandDefine($hash->{CL},"$htmldev SMAPortalSPG {FHEM::SMAPortal::PortalAsHtml ('$name','$htmldev')}"); return $ret if($ret); CommandAttr($hash->{CL},"$htmldev alias $c"); # Alias setzen $c = "This device provides a praphical output of SMA Sunny Portal values.\n". "The device needs to set attribute \"detailLevel\" in device \"$name\" to level \"4\""; CommandAttr($hash->{CL},"$htmldev comment $c"); # es muß nicht unbedingt jedes der möglichen userattr unbedingt vorbesetzt werden # bzw muß überhaupt hier etwas vorbesetzt werden ? # alle Werte enstprechen eh den AttrVal/ AttrNum default Werten CommandAttr($hash->{CL},"$htmldev hourCount 24"); CommandAttr($hash->{CL},"$htmldev suggestIcon on"); CommandAttr($hash->{CL},"$htmldev showHeader 1"); CommandAttr($hash->{CL},"$htmldev showLink 1"); CommandAttr($hash->{CL},"$htmldev spaceSize 24"); CommandAttr($hash->{CL},"$htmldev showWeather 1"); CommandAttr($hash->{CL},"$htmldev layoutType $type"); # Anzeigetyp setzen # eine mögliche Startfarbe steht beim installiertem f18 Style direkt zur Verfügung # ohne vorhanden f18 Style bestimmt später tr.odd aus der Style css die Anfangsfarbe my $color = %{json2nameValue(AttrVal('WEB','styleData',undef))}{'f18_cols.header'}; if (defined($color)) { CommandAttr($hash->{CL},"$htmldev beamColor $color"); } # zweite Farbe setzen CommandAttr($hash->{CL},"$htmldev beamColor2 $color2"); my $room = AttrVal($name,"room","SMAPortal"); CommandAttr($hash->{CL},"$htmldev room $room"); CommandAttr($hash->{CL},"$name detailLevel 4"); return "SMA Portal Graphics device \"$htmldev\" created and assigned to room \"$room\"."; } else { return "$setlist"; } return; } ############################################################### # SMAPortal DbLog_splitFn ############################################################### sub DbLog_split($$) { my ($event, $device) = @_; my $devhash = $defs{$device}; my ($reading, $value, $unit); if($event =~ m/[_\-fd]Consumption|Quote/) { $event =~ /^L(.*):\s(.*)\s(.*)/; $reading = "L".$1; $value = $2; $unit = $3; } if($event =~ m/Power|PV|FeedIn|SelfSupply|Temperature|Total|Hour:|Hour(\d\d):/) { $event =~ /^L(.*):\s(.*)\s(.*)/; $reading = "L".$1; $value = $2; $unit = $3; } if($event =~ m/Next04Hours-IsConsumption|RestOfDay-IsConsumption|Tomorrow-IsConsumption/) { $event =~ /^L(.*):\s(.*)\s(.*)/; $reading = "L".$1; $value = $2; $unit = $3; } if($event =~ m/summary|next04hours_state/) { $event =~ /(.*):\s(.*)\s(.*)/; $reading = $1; $value = $2; $unit = $3; } return ($reading, $value, $unit); } ###################################################################################### # Username / Paßwort speichern ###################################################################################### sub setcredentials ($@) { my ($hash, @credentials) = @_; my $name = $hash->{NAME}; my ($success, $credstr, $index, $retcode); my (@key,$len,$i); $credstr = encode_base64(join(':', @credentials)); # Beginn Scramble-Routine @key = qw(1 3 4 5 6 3 2 1 9); $len = scalar @key; $i = 0; $credstr = join "", map { $i = ($i + 1) % $len; chr((ord($_) + $key[$i]) % 256) } split //, $credstr; # End Scramble-Routine $index = $hash->{TYPE}."_".$hash->{NAME}."_credentials"; $retcode = setKeyValue($index, $credstr); if ($retcode) { Log3($name, 1, "$name - Error while saving the Credentials - $retcode"); $success = 0; } else { getcredentials($hash,1); # Credentials nach Speicherung lesen und in RAM laden ($boot=1) $success = 1; } return ($success); } ###################################################################################### # Username / Paßwort abrufen ###################################################################################### sub getcredentials ($$) { my ($hash,$boot) = @_; my $name = $hash->{NAME}; my ($success, $username, $passwd, $index, $retcode, $credstr); my (@key,$len,$i); if ($boot) { # mit $boot=1 Credentials von Platte lesen und als scrambled-String in RAM legen $index = $hash->{TYPE}."_".$hash->{NAME}."_credentials"; ($retcode, $credstr) = getKeyValue($index); if ($retcode) { Log3($name, 2, "$name - Unable to read password from file: $retcode"); $success = 0; } if ($credstr) { # beim Boot scrambled Credentials in den RAM laden $hash->{HELPER}{CREDENTIALS} = $credstr; # "Credentials" wird als Statusbit ausgewertet. Wenn nicht gesetzt -> Warnmeldung und keine weitere Verarbeitung $hash->{CREDENTIALS} = "Set"; $success = 1; } } else { # boot = 0 -> Credentials aus RAM lesen, decoden und zurückgeben $credstr = $hash->{HELPER}{CREDENTIALS}; if($credstr) { # Beginn Descramble-Routine @key = qw(1 3 4 5 6 3 2 1 9); $len = scalar @key; $i = 0; $credstr = join "", map { $i = ($i + 1) % $len; chr((ord($_) - $key[$i] + 256) % 256) } split //, $credstr; # Ende Descramble-Routine ($username, $passwd) = split(":",decode_base64($credstr)); my $logpw = AttrVal($name, "showPassInLog", "0") == 1 ? $passwd : "********"; Log3($name, 3, "$name - Credentials read from RAM: $username $logpw"); } else { Log3($name, 1, "$name - Credentials not set in RAM !"); } $success = (defined($passwd)) ? 1 : 0; } return ($success, $username, $passwd); } ############################################################### # SMAPortal Get ############################################################### sub Get($$) { my ($hash, @a) = @_; return "\"get X\" needs at least an argument" if ( @a < 2 ); my $name = shift @a; my $opt = shift @a; my $getlist = "Unknown argument $opt, choose one of ". "storedCredentials:noArg ". "data:noArg "; return "module is disabled" if(IsDisabled($name)); if ($opt eq "data") { CallInfo($hash); } elsif ($opt eq "storedCredentials") { if(!$hash->{CREDENTIALS}) {return "Credentials of $name are not set - make sure you've set it with \"set $name credentials <username> <password>\"";} # Credentials abrufen my ($success, $username, $password) = getcredentials($hash,0); unless ($success) {return "Credentials couldn't be retrieved successfully - see logfile"}; return "Stored Credentials to access SMA Portal:\n". "========================================\n". "Username: $username, Password: $password\n". "\n"; } else { return "$getlist"; } return undef; } ############################################################### # SMAPortal Attr ############################################################### sub Attr($$$$) { my ($cmd,$name,$aName,$aVal) = @_; 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"); if($do) { delread($hash); delete $hash->{MODE}; RemoveInternalTimer($hash); delcookiefile($hash,1); } else { InternalTimer(gettimeofday()+1.0, "FHEM::SMAPortal::CallInfo", $hash, 0); InternalTimer(gettimeofday()+5.0, "FHEM::SMAPortal::delcookiefile", $hash, 0); } readingsBeginUpdate($hash); readingsBulkUpdate($hash, "state", $val); readingsEndUpdate($hash, 1); InternalTimer(gettimeofday()+2.0, "FHEM::SMAPortal::SPGRefresh", "$name,0,1", 0); } if ($cmd eq "set") { if ($aName =~ m/timeout|interval/) { unless ($aVal =~ /^\d+$/) {return " The Value for $aName is not valid. Use only figures 0-9 !";} } if($aName =~ m/interval/) { $_[3] = 120 if($aVal > 0 && $aVal < 120); InternalTimer(gettimeofday()+1.0, "FHEM::SMAPortal::CallInfo", $hash, 0); } } return undef; } ################################################################ ## Hauptschleife BlockingCall ################################################################ sub CallInfo($) { my ($hash) = @_; my $name = $hash->{NAME}; my $timeout = AttrVal($name, "timeout", 30); my $interval = AttrVal($name, "interval", 300); my $new; RemoveInternalTimer($hash,"FHEM::SMAPortal::CallInfo"); if($init_done == 1) { if(!$hash->{CREDENTIALS}) { Log3($name, 1, "$name - Credentials not set. Set it with \"set $name credentials \""); readingsSingleUpdate($hash, "state", "Credentials not set", 1); return; } if(!$interval) { $hash->{MODE} = "Manual"; } else { $new = gettimeofday()+$interval; InternalTimer($new, "FHEM::SMAPortal::CallInfo", $hash, 0); $hash->{MODE} = "Automatic - next polltime: ".FmtTime($new); } return if(IsDisabled($name)); if ($hash->{HELPER}{RUNNING_PID}) { BlockingKill($hash->{HELPER}{RUNNING_PID}); delete($hash->{HELPER}{RUNNING_PID}); } $hash->{HELPER}{RETRIES} = AttrVal($name, "getDataRetries", 3); $hash->{HELPER}{RUNNING_PID} = BlockingCall("FHEM::SMAPortal::GetData", $name, "FHEM::SMAPortal::ParseData", $timeout, "FHEM::SMAPortal::ParseAborted", $hash); $hash->{HELPER}{RUNNING_PID}{loglevel} = 5 if($hash->{HELPER}{RUNNING_PID}); # Forum #77057 } else { InternalTimer(gettimeofday()+5, "FHEM::SMAPortal::CallInfo", $hash, 0); } return; } ################################################################ ## Datenabruf SMA-Portal ################################################################ sub GetData($) { my ($name) = @_; my $hash = $defs{$name}; my ($livedata_content); my $login_state = 0; my ($forecast_content,$weatherdata_content,$consumerlivedata_content) = ("","",""); my $useragent = AttrVal($name, "userAgent", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)"); my $cookieLocation = AttrVal($name, "cookieLocation", "./log/mycookies.txt"); Log3 $name, 5, "$name - Start BlockingCall GetData with CookieLocation: $cookieLocation and UserAgent: $useragent"; my $ua = LWP::UserAgent->new; # Define user agent type $ua->agent("$useragent"); # Cookies $ua->cookie_jar(HTTP::Cookies->new( file => "$cookieLocation", ignore_discard => 1, autosave => 1 ) ); # Sunny Home Manager Seite abfragen my $livedata = $ua->get('https://www.sunnyportal.com/homemanager'); if(($livedata->content =~ m/FeedIn/i) && ($livedata->content !~ m/expired/i)) { Log3 $name, 4, "$name - Login to SMA-Portal succesful"; # JSON Live Daten $livedata_content = $livedata->content; $login_state = 1; Log3 $name, 4, "$name - Getting live data now"; Log3 $name, 5, "$name - Data received:\n".Dumper decode_json($livedata_content); # JSON Wetterdaten Log3 $name, 4, "$name - Getting weather data now"; my $weatherdata = $ua->get('https://www.sunnyportal.com/Dashboard/Weather'); $weatherdata_content = $weatherdata->content; Log3 $name, 5, "$name - Data received:\n".Dumper decode_json($weatherdata_content); # JSON Forecast Daten my $dl = AttrVal($name, "detailLevel", 1); if($dl > 1) { Log3 $name, 4, "$name - Getting forecast data now"; my $forecast_page = $ua->get('https://www.sunnyportal.com/HoMan/Forecast/LoadRecommendationData'); Log3 $name, 5, "$name - Return Code: ".$forecast_page->code; if ($forecast_page->content =~ m/ForecastChartDataPoint/i) { $forecast_content = $forecast_page->content; Log3 $name, 5, "$name - Forecast data received:\n".Dumper decode_json($forecast_content); } } # JSON Consumer Livedaten if($dl > 2) { Log3 $name, 4, "$name - Getting consumer live data now"; my $consumerlivedata = $ua->get('https://www.sunnyportal.com/Homan/ConsumerBalance/GetLiveProxyValues'); Log3 $name, 5, "$name - Return Code: ".$consumerlivedata->code; if ($consumerlivedata->content =~ m/HoManConsumerLiveData/i) { $consumerlivedata_content = $consumerlivedata->content; Log3 $name, 5, "$name - Consumer live data received:\n".Dumper decode_json($consumerlivedata_content); } } } else { my $usernameField = "ctl00\$ContentPlaceHolder1\$Logincontrol1\$txtUserName"; my $passwordField = "ctl00\$ContentPlaceHolder1\$Logincontrol1\$txtPassword"; my $loginField = "__EVENTTARGET"; my $loginButton = "ctl00\$ContentPlaceHolder1\$Logincontrol1\$LoginBtn"; Log3 $name, 3, "$name - not logged in. Try again ..."; # Credentials abrufen my ($success, $username, $password) = getcredentials($hash,0); unless ($success) { Log3($name, 1, "$name - Credentials couldn't be retrieved successfully - make sure you've set it with \"set $name credentials \""); $login_state = 0; } else { my $loginp = $ua->post('https://www.sunnyportal.com/Templates/Start.aspx',[$usernameField => $username, $passwordField => $password, "__EVENTTARGET" => $loginButton]); Log3 $name, 4, "$name -> ".$loginp->code; Log3 $name, 5, "$name -> Login-Page return: ".$loginp->content; if( $loginp->content =~ /Logincontrol1_ErrorLabel/i ) { Log3 $name, 1, "$name - Error: login to SMA-Portal failed"; $livedata_content = "{\"Login-Status\":\"failed\"}"; } else { Log3 $name, 3, "$name - login to SMA-Portal successful ... "; $livedata_content = '{"Login-Status":"successful", "InfoMessages":["login to SMA-Portal successful but get data with next data cycle."]}'; $login_state = 1; } my $shmp = $ua->get('https://www.sunnyportal.com/FixedPages/HoManLive.aspx'); Log3 $name, 5, "$name -> ".$shmp->code; } } my ($reread,$retry) = analivedat($hash,$livedata_content); # Daten müssen als Einzeiler zurückgegeben werden $livedata_content = encode_base64($livedata_content,""); $forecast_content = encode_base64($forecast_content,"") if($forecast_content); $weatherdata_content = encode_base64($weatherdata_content,"") if($weatherdata_content); $consumerlivedata_content = encode_base64($consumerlivedata_content,"") if($consumerlivedata_content); return "$name|$livedata_content|$forecast_content|$weatherdata_content|$consumerlivedata_content|$login_state|$reread|$retry"; } ################################################################ ## Verarbeitung empfangene Daten, setzen Readings ################################################################ sub ParseData($) { my ($string) = @_; my @a = split("\\|",$string); my $hash = $defs{$a[0]}; my $name = $hash->{NAME}; my $ld_response = decode_base64($a[1]); my $fd_response = decode_base64($a[2]) if($a[2]); my $wd_response = decode_base64($a[3]) if($a[3]); my $cd_response = decode_base64($a[4]) if($a[4]); my $login_state = $a[5]; my $reread = $a[6]; my $retry = $a[7]; my $livedata_content = decode_json($ld_response); my $forecast_content = decode_json($fd_response) if($fd_response); my $weatherdata_content = decode_json($wd_response) if($wd_response); my $consumerlivedata_content = decode_json($cd_response) if($cd_response); my $state = "ok"; my $timeout = AttrVal($name, "timeout", 30); if($reread) { # login war erfolgreich, aber Daten müssen jetzt noch gelesen werden delete($hash->{HELPER}{RUNNING_PID}); readingsSingleUpdate($hash, "L1_Login-Status", "successful", 1); $hash->{HELPER}{oldlogintime} = gettimeofday(); $hash->{HELPER}{RUNNING_PID} = BlockingCall("FHEM::SMAPortal::GetData", $name, "FHEM::SMAPortal::ParseData", $timeout, "FHEM::SMAPortal::ParseAborted", $hash); $hash->{HELPER}{RUNNING_PID}{loglevel} = 5 if($hash->{HELPER}{RUNNING_PID}); # Forum #77057 return; } if($retry && $hash->{HELPER}{RETRIES}) { # Livedaten konnte nicht gelesen werden, neuer Versuch zeitverzögert delete($hash->{HELPER}{RUNNING_PID}); $hash->{HELPER}{RETRIES} -= 1; InternalTimer(gettimeofday()+5, "FHEM::SMAPortal::retrygetdata", $hash, 0); return; } my $dl = AttrVal($name, "detailLevel", 1); delread($hash, $dl+1); readingsBeginUpdate($hash); my ($FeedIn_done,$GridConsumption_done,$PV_done,$AutarkyQuote_done,$SelfConsumption_done) = (0,0,0,0,0); my ($SelfConsumptionQuote_done,$SelfSupply_done,$errMsg,$warnMsg,$infoMsg) = (0,0,0,0,0); my ($batteryin,$batteryout); for my $k (keys %$livedata_content) { my $new_val = ""; if (defined $livedata_content->{$k}) { if (($livedata_content->{$k} =~ m/ARRAY/i) || ($livedata_content->{$k} =~ m/HASH/i)) { Log3 $name, 4, "$name - Livedata content \"$k\": ".($livedata_content->{$k}); if($livedata_content->{$k} =~ m/ARRAY/i) { my $hd0 = $livedata_content->{$k}[0]; if(!defined $hd0) { next; } chomp $hd0; $hd0 =~ s/[;']//g; $hd0 = encode("utf8", $hd0); Log3 $name, 4, "$name - Livedata \"$k\": $hd0"; $new_val = $hd0; } } else { $new_val = $livedata_content->{$k}; } if ($new_val && $k !~ /__type/i) { if($k =~ /^FeedIn$/) { $new_val = $new_val." W"; $FeedIn_done = 1 } if($k =~ /^GridConsumption$/) { $new_val = $new_val." W"; $GridConsumption_done = 1; } if($k =~ /^PV$/) { $new_val = $new_val." W"; $PV_done = 1; } if($k =~ /^AutarkyQuote$/) { $new_val = $new_val." %"; $AutarkyQuote_done = 1; } if($k =~ /^SelfConsumption$/) { $new_val = $new_val." W"; $SelfConsumption_done = 1; } if($k =~ /^SelfConsumptionQuote$/) { $new_val = $new_val." %"; $SelfConsumptionQuote_done = 1; } if($k =~ /^SelfSupply$/) { $new_val = $new_val." W"; $SelfSupply_done = 1; } if($k =~ /^TotalConsumption$/) { $new_val = $new_val." W"; } if($k =~ /^BatteryIn$/) { $new_val = $new_val." W"; $batteryin = 1; } if($k =~ /^BatteryOut$/) { $new_val = $new_val." W"; $batteryout = 1; } $errMsg = 1 if($k =~ /^ErrorMessages$/); $warnMsg = 1 if($k =~ /^WarningMessages$/); $infoMsg = 1 if($k =~ /^InfoMessages$/); Log3 $name, 4, "$name -> $k - $new_val"; readingsBulkUpdate($hash, "L1_$k", $new_val); } } } readingsBulkUpdate($hash, "L1_FeedIn", "0 W") if(!$FeedIn_done); readingsBulkUpdate($hash, "L1_GridConsumption", "0 W") if(!$GridConsumption_done); readingsBulkUpdate($hash, "L1_PV", "0 W") if(!$PV_done); readingsBulkUpdate($hash, "L1_AutarkyQuote", "0 %") if(!$AutarkyQuote_done); readingsBulkUpdate($hash, "L1_SelfConsumption", "0 W") if(!$SelfConsumption_done); readingsBulkUpdate($hash, "L1_SelfConsumptionQuote", "0 %") if(!$SelfConsumptionQuote_done); readingsBulkUpdate($hash, "L1_SelfSupply", "0 W") if(!$SelfSupply_done); if(defined $batteryin || defined $batteryout) { readingsBulkUpdate($hash, "L1_BatteryIn", "0 W") if(!$batteryin); readingsBulkUpdate($hash, "L1_BatteryOut", "0 W") if(!$batteryout); } readingsEndUpdate($hash, 1); readingsDelete($hash,"L1_ErrorMessages") if(!$errMsg); readingsDelete($hash,"L1_WarningMessages") if(!$warnMsg); readingsDelete($hash,"L1_InfoMessages") if(!$infoMsg); if ($forecast_content && $forecast_content !~ m/undefined/i) { # Auswertung der Forecast Daten extractForecastData($hash,$forecast_content); extractPlantData($hash,$forecast_content); extractConsumerData($hash,$forecast_content); } if ($consumerlivedata_content && $consumerlivedata_content !~ m/undefined/i) { # Auswertung Consumer Live Daten extractConsumerLiveData($hash,$consumerlivedata_content); } if ($weatherdata_content && $weatherdata_content !~ m/undefined/i) { # Auswertung Wetterdaten extractWeatherData($hash,$weatherdata_content); } my $pv = ReadingsNum($name, "L1_PV", 0); my $fi = ReadingsNum($name, "L1_FeedIn", 0); my $gc = ReadingsNum($name, "L1_GridConsumption", 0); my $sum = $fi-$gc; if(!$hash->{HELPER}{RETRIES} && !$pv && !$fi && !$gc) { # keine Anlagendaten vorhanden $state = "Data can't be retrieved from SMA-Portal. Reread at next scheduled cycle."; Log3 ($name, 2, "$name - $state"); } readingsBeginUpdate($hash); if($login_state) { readingsBulkUpdate($hash, "state", $state); readingsBulkUpdate($hash, "summary", "$sum W"); } readingsEndUpdate($hash, 1); delete($hash->{HELPER}{RUNNING_PID}); SPGRefresh($hash,0,1); return; } ################################################################ ## Timeout BlockingCall ################################################################ sub ParseAborted($) { my ($hash,$cause) = @_; my $name = $hash->{NAME}; $cause = $cause?$cause:"Timeout: process terminated"; Log3 ($name, 1, "$name -> BlockingCall $hash->{HELPER}{RUNNING_PID}{fn} pid:$hash->{HELPER}{RUNNING_PID}{pid} $cause"); delete($hash->{HELPER}{RUNNING_PID}); return; } ################################################################ ## regelmäßig Cookie-Datei löschen ################################################################ sub delcookiefile ($;$) { my ($hash,$must) = @_; my $name = $hash->{NAME}; my ($validperiod, $cookieLocation, $oldlogintime, $delfile); RemoveInternalTimer($hash,"FHEM::SMAPortal::delcookiefile"); # Gültigkeitsdauer Cookie in Sekunden $validperiod = AttrVal($name, "cookielifetime", 3000); $cookieLocation = AttrVal($name, "cookieLocation", "./log/mycookies.txt"); if($must) { # Cookie Zwangslöschung $delfile = unlink($cookieLocation); } $oldlogintime = $hash->{HELPER}{oldlogintime}?$hash->{HELPER}{oldlogintime}:0; if($init_done == 1) { # Abfrage ob gettimeofday() größer ist als gettimeofday()+$validperiod if (gettimeofday() > $oldlogintime+$validperiod) { $delfile = unlink($cookieLocation); } } if($delfile) { Log3 $name, 3, "$name - cookie file deleted: $cookieLocation"; } return if(IsDisabled($name)); InternalTimer(gettimeofday()+30, "FHEM::SMAPortal::delcookiefile", $hash, 0); return; } ################################################################ ## Auswertung Forecast Daten ################################################################ sub extractForecastData($$) { my ($hash,$forecast) = @_; my $name = $hash->{NAME}; my $dl = AttrVal($name, "detailLevel", 1); if($dl <= 1) { return; } my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); $year += 1900; $mon += 1; my $today = "$year-".sprintf("%02d", $mon)."-".sprintf("%02d", $mday)."T"; my $PV_sum = 0; my $consum_sum = 0; my $sum = 0; readingsBeginUpdate($hash); my $plantOid = $forecast->{'ForecastTimeframes'}->{'PlantOid'}; $hash->{HELPER}{PLANTOID} = $plantOid; # wichtig für erweiterte Selektionen # Counter for forecast objects my $obj_nr = 0; # The next few hours... my %nextFewHoursSum = ("PV" => 0, "Consumption" => 0, "Total" => 0, "ConsumpRcmd" => 0); # Rest of the day... my %restOfDaySum = ("PV" => 0, "Consumption" => 0, "Total" => 0, "ConsumpRcmd" => 0); # Tomorrow... my %tomorrowSum = ("PV" => 0, "Consumption" => 0, "Total" => 0, "ConsumpRcmd" => 0); # Get the current day (for 2016-02-26, this is 26) my $current_day = (localtime)[3]; # Loop through all forecast objects # Energie wird als "J" geliefert, Wh = J / 3600 foreach my $fc_obj (@{$forecast->{'ForecastSeries'}}) { my $fc_datetime = $fc_obj->{'TimeStamp'}->{'DateTime'}; # Example for DateTime: 2016-02-15T23:00:00 my $tkind = $fc_obj->{'TimeStamp'}->{'Kind'}; # Zeitart: Unspecified, Utc # Calculate Unix timestamp (month begins at 0, year at 1900) my ($fc_year, $fc_month, $fc_day, $fc_hour) = $fc_datetime =~ /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):00:00$/; my $fc_uts = POSIX::mktime( 0, 0, $fc_hour, $fc_day, $fc_month - 1, $fc_year - 1900 ); my $fc_diff_seconds = $fc_uts - time + 3600; # So we go above 0 for the current hour my $fc_diff_hours = int( $fc_diff_seconds / 3600 ); # Use also old data to integrate daily PV and Consumption if ($current_day == $fc_day) { $PV_sum += int($fc_obj->{'PvMeanPower'}->{'Amount'}); # integrator of daily PV in Wh $consum_sum += int($fc_obj->{'ConsumptionForecast'}->{'Amount'}/3600); # integrator of daily Consumption forecast in Wh } # Don't use old data next if $fc_diff_seconds < 0; # Sum up for the next few hours (4 hours total, this is current hour plus the next 3 hours) if ($obj_nr < 4) { $nextFewHoursSum{'PV'} += $fc_obj->{'PvMeanPower'}->{'Amount'}; # Wh $nextFewHoursSum{'Consumption'} += $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600; # Wh $nextFewHoursSum{'Total'} += $fc_obj->{'PvMeanPower'}->{'Amount'} - $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600; # Wh $nextFewHoursSum{'ConsumpRcmd'} += $fc_obj->{'IsConsumptionRecommended'} ? 1 : 0; } # If data is for the rest of the current day if ( $current_day == $fc_day ) { $restOfDaySum{'PV'} += $fc_obj->{'PvMeanPower'}->{'Amount'}; # Wh $restOfDaySum{'Consumption'} += $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600; # Wh $restOfDaySum{'Total'} += $fc_obj->{'PvMeanPower'}->{'Amount'} - $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600; # Wh $restOfDaySum{'ConsumpRcmd'} += $fc_obj->{'IsConsumptionRecommended'} ? 1 : 0; } # If data is for the next day (quick and dirty: current day different from this object's day) # Assuming only the current day and the next day are returned from Sunny Portal if ( $current_day != $fc_day ) { $tomorrowSum{'PV'} += $fc_obj->{'PvMeanPower'}->{'Amount'} if(exists($fc_obj->{'PvMeanPower'}->{'Amount'})); # Wh $tomorrowSum{'Consumption'} += $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600; # Wh $tomorrowSum{'Total'} += $fc_obj->{'PvMeanPower'}->{'Amount'} - $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 if ($fc_obj->{'PvMeanPower'}->{'Amount'}); # Wh $tomorrowSum{'ConsumpRcmd'} += $fc_obj->{'IsConsumptionRecommended'} ? 1 : 0; } # Update values in Fhem if less than 24 hours in the future # TimeStamp Kind: "Unspecified" if($dl >= 2) { if ($obj_nr < 24) { my $time_str = "ThisHour"; $time_str = "NextHour".sprintf("%02d", $obj_nr) if($fc_diff_hours>0); if($time_str =~ /NextHour/ && $dl >= 4) { readingsBulkUpdate( $hash, "L4_${time_str}_Time", TimeAdjust($hash,$fc_obj->{'TimeStamp'}->{'DateTime'},$tkind) ); readingsBulkUpdate( $hash, "L4_${time_str}_PvMeanPower", int( $fc_obj->{'PvMeanPower'}->{'Amount'} )." Wh" ); # in W als Durchschnitt geliefet, d.h. eine Stunde -> Wh readingsBulkUpdate( $hash, "L4_${time_str}_Consumption", int( $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 )." Wh" ); # {'ConsumptionForecast'}->{'Amount'} wird als J = Ws geliefert readingsBulkUpdate( $hash, "L4_${time_str}_IsConsumptionRecommended", ($fc_obj->{'IsConsumptionRecommended'} ? "yes" : "no") ); readingsBulkUpdate( $hash, "L4_${time_str}", (int($fc_obj->{'PvMeanPower'}->{'Amount'}) - int($fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600))." Wh" ); # add WeatherId Helper to show weather icon $hash->{HELPER}{"L4_".${time_str}."_WeatherId"} = int($fc_obj->{'WeatherId'}); } if($time_str =~ /ThisHour/ && $dl >= 2) { readingsBulkUpdate( $hash, "L2_${time_str}_Time", TimeAdjust($hash,$fc_obj->{'TimeStamp'}->{'DateTime'},$tkind) ); readingsBulkUpdate( $hash, "L2_${time_str}_PvMeanPower", int( $fc_obj->{'PvMeanPower'}->{'Amount'} )." Wh" ); # in W als Durchschnitt geliefet, d.h. eine Stunde -> Wh readingsBulkUpdate( $hash, "L2_${time_str}_Consumption", int( $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 )." Wh" ); # {'ConsumptionForecast'}->{'Amount'} wird als J = Ws geliefert readingsBulkUpdate( $hash, "L2_${time_str}_IsConsumptionRecommended", ($fc_obj->{'IsConsumptionRecommended'} ? "yes" : "no") ); readingsBulkUpdate( $hash, "L2_${time_str}", (int($fc_obj->{'PvMeanPower'}->{'Amount'}) - int($fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600))." Wh" ); # add WeatherId Helper to show weather icon $hash->{HELPER}{"L2_".${time_str}."_WeatherId"} = int($fc_obj->{'WeatherId'}); } } } # Increment object counter $obj_nr++; } if($dl >= 2) { readingsBulkUpdate($hash, "L2_Next04Hours-Consumption", int( $nextFewHoursSum{'Consumption'} )." Wh" ); readingsBulkUpdate($hash, "L2_Next04Hours-PV", int( $nextFewHoursSum{'PV'} )." Wh" ); readingsBulkUpdate($hash, "L2_Next04Hours-Total", int( $nextFewHoursSum{'Total'} )." Wh" ); readingsBulkUpdate($hash, "L2_Next04Hours-IsConsumptionRecommended", int( $nextFewHoursSum{'ConsumpRcmd'} )." h" ); readingsBulkUpdate($hash, "next04hours_state", int( $nextFewHoursSum{'PV'} )." Wh" ); readingsBulkUpdate($hash, "L2_Forecast-Today-Consumption", $consum_sum." Wh" ); # publish consumption forecast values readingsBulkUpdate($hash, "L2_Forecast-Today-PV", $PV_sum." Wh"); # publish integrated PV } if($dl >= 3) { readingsBulkUpdate($hash, "L3_RestOfDay-Consumption", int( $restOfDaySum{'Consumption'} )." Wh" ); readingsBulkUpdate($hash, "L3_RestOfDay-PV", int( $restOfDaySum{'PV'} )." Wh" ); readingsBulkUpdate($hash, "L3_RestOfDay-Total", int( $restOfDaySum{'Total'} )." Wh" ); readingsBulkUpdate($hash, "L3_RestOfDay-IsConsumptionRecommended", int( $restOfDaySum{'ConsumpRcmd'} )." h" ); readingsBulkUpdate($hash, "L3_Tomorrow-Consumption", int( $tomorrowSum{'Consumption'} )." Wh" ); readingsBulkUpdate($hash, "L3_Tomorrow-PV", int( $tomorrowSum{'PV'} )." Wh" ); readingsBulkUpdate($hash, "L3_Tomorrow-Total", int( $tomorrowSum{'Total'} )." Wh" ); readingsBulkUpdate($hash, "L3_Tomorrow-IsConsumptionRecommended", int( $tomorrowSum{'ConsumpRcmd'} )." h" ); } if($dl >= 4) { readingsBulkUpdate($hash,"L4_plantOid",$plantOid); } readingsEndUpdate($hash, 1); return; } ################################################################ ## Auswertung Wetterdaten ################################################################ sub extractWeatherData($$) { my ($hash,$weather) = @_; my $name = $hash->{NAME}; my ($tsymbol,$ttoday,$ttomorrow); my $dl = AttrVal($name, "detailLevel", 1); readingsBeginUpdate($hash); for my $k (keys %$weather) { my $new_val = ""; if (defined $weather->{$k}) { Log3 $name, 4, "$name - Weatherdata content \"$k\": ".($weather->{$k}); if ($weather->{$k} =~ m/HASH/i) { my $ih = $weather->{$k}; for my $i (keys %$ih) { my $hd0 = $weather->{$k}{$i}; if(!$hd0) { next; } chomp $hd0; $hd0 =~ s/[;']//g; $hd0 = ($hd0 =~ /^undef$/)?"none":$hd0; $hd0 = encode("utf8", $hd0); Log3 $name, 4, "$name - Weatherdata \"$k $i\": $hd0"; next if($i =~ /^WeatherIcon$/); $new_val = $hd0; if ($new_val) { if($i =~ /^TemperatureSymbol$/) { $tsymbol = $new_val; next; } if($i =~ /^Temperature$/) { if($k =~ /^today$/) { $ttoday = sprintf("%.1f",$new_val); } if($k =~ /^tomorrow$/) { $ttomorrow = sprintf("%.1f",$new_val); } next; } Log3 $name, 4, "$name -> ${k}_${i} - $new_val"; readingsBulkUpdate($hash, "L1_${k}_${i}", $new_val); } } } } } readingsBulkUpdate($hash, "L1_today_Temperature", "$ttoday $tsymbol") if($ttoday && $tsymbol); readingsBulkUpdate($hash, "L1_tomorrow_Temperature", "$ttomorrow $tsymbol") if($ttomorrow && $tsymbol); readingsEndUpdate($hash, 1); return; } ################################################################ ## Auswertung Anlagendaten ################################################################ sub extractPlantData($$) { my ($hash,$forecast) = @_; my $name = $hash->{NAME}; my ($amount,$unit); my $dl = AttrVal($name, "detailLevel", 1); if($dl <= 1) { return; } readingsBeginUpdate($hash); my $ppp = $forecast->{'PlantPeakPower'}; if($ppp && $dl >= 2) { $amount = $forecast->{'PlantPeakPower'}{'Amount'}; $unit = $forecast->{'PlantPeakPower'}{'StandardUnit'}{'Symbol'}; Log3 $name, 4, "$name - Plantdata \"PlantPeakPower Amount\": $amount"; Log3 $name, 4, "$name - Plantdata \"PlantPeakPower Symbol\": $unit"; } readingsBulkUpdate($hash, "L2_PlantPeakPower", "$amount $unit"); readingsEndUpdate($hash, 1); return; } ################################################################ ## Auswertung Consumer Data ################################################################ sub extractConsumerData($$) { my ($hash,$forecast) = @_; my $name = $hash->{NAME}; my %consumers; my ($key,$val); my $dl = AttrVal($name, "detailLevel", 1); if($dl <= 1) { return; } readingsBeginUpdate($hash); # Schleife über alle Consumer Objekte my $i = 0; foreach my $c (@{$forecast->{'Consumers'}}) { $consumers{"${i}_ConsumerName"} = encode("utf8", $c->{'ConsumerName'} ); $consumers{"${i}_ConsumerOid"} = $c->{'ConsumerOid'}; $i++; } if(%consumers && $forecast->{'ForecastTimeframes'}) { # es sind Vorhersagen zu geplanten Verbraucherschaltzeiten vorhanden # TimeFrameStart/End Kind: "Utc" foreach my $c (@{$forecast->{'ForecastTimeframes'}{'PlannedTimeFrames'}}) { my $tkind = $c->{'TimeFrameStart'}->{'Kind'}; # Zeitart: Unspecified, Utc my $deviceOid = $c->{'DeviceOid'}; my $timeFrameStart = TimeAdjust($hash,$c->{'TimeFrameStart'}{'DateTime'},$tkind); # wandele UTC my $timeFrameEnd = TimeAdjust($hash,$c->{'TimeFrameEnd'}{'DateTime'},$tkind); # wandele UTC my $tz = $c->{'TimeFrameStart'}{'Kind'}; foreach my $k (keys(%consumers)) { $val = $consumers{$k}; if($val eq $deviceOid) { $k =~ /^(\d+)_.*$/; my $lfn = $1; # $consumer = $consumers{"${lfn}_ConsumerName"}; $consumers{"${lfn}_PlannedOpTimeStart"} = $timeFrameStart; $consumers{"${lfn}_PlannedOpTimeEnd"} = $timeFrameEnd; } } } } if(%consumers) { foreach my $key (keys(%consumers)) { Log3 $name, 4, "$name - Consumer data \"$key\": ".$consumers{$key}; if($key =~ /ConsumerName/ && $dl >= 3) { $key =~ /^(\d+)_.*$/; my $lfn = $1; my $cn = $consumers{"${lfn}_ConsumerName"}; # Verbrauchername $cn = substUmlauts($cn); # evtl. Umlaute im Verbrauchernamen ersetzen my $pos = $consumers{"${lfn}_PlannedOpTimeStart"}; # geplanter Start my $poe = $consumers{"${lfn}_PlannedOpTimeEnd"}; # geplantes Ende my $rb = "L3_${cn}_PlannedOpTimeBegin"; my $re = "L3_${cn}_PlannedOpTimeEnd"; my $rp = "L3_${cn}_Planned"; if($pos) { readingsBulkUpdate($hash, $rb, $pos); readingsBulkUpdate($hash, $rp, "yes"); } else { readingsBulkUpdate($hash, $rb, "undefined"); readingsBulkUpdate($hash, $rp, "no"); } if($poe) { readingsBulkUpdate($hash, $re, $poe); } else { readingsBulkUpdate($hash, $re, "undefined"); } } } } readingsEndUpdate($hash, 1); return; } ################################################################ ## Auswertung Consumer Livedata ################################################################ sub extractConsumerLiveData($$) { my ($hash,$clivedata) = @_; my $name = $hash->{NAME}; my %consumers; my ($key,$val,$i,$res); my $dl = AttrVal($name, "detailLevel", 1); if($dl <= 2) { return; } readingsBeginUpdate($hash); # allen Consumer Objekte die ID zuordnen $i = 0; foreach my $c (@{$clivedata->{'MeasurementData'}}) { $consumers{"${i}_ConsumerName"} = encode("utf8", $c->{'DeviceName'} ); $consumers{"${i}_ConsumerOid"} = $c->{'Consume'}{'ConsumerOid'}; $consumers{"${i}_ConsumerLfd"} = $i; my $cpower = $c->{'Consume'}{'Measurement'}; # aktueller Energieverbrauch in W my $cn = $consumers{"${i}_ConsumerName"}; # Verbrauchername $cn = substUmlauts($cn); $hash->{HELPER}{CONSUMER}{$i}{DeviceName} = $cn; $hash->{HELPER}{CONSUMER}{$i}{ConsumerOid} = $consumers{"${i}_ConsumerOid"}; $hash->{HELPER}{CONSUMER}{$i}{SerialNumber} = $c->{'SerialNumber'}; $hash->{HELPER}{CONSUMER}{$i}{SUSyID} = $c->{'SUSyID'}; readingsBulkUpdate($hash, "L3_${cn}_Power", $cpower." W") if(defined($cpower)); $i++; } if(%consumers && $clivedata->{'ParameterData'}) { # es sind Daten zu den Verbrauchern vorhanden # Kind: "Utc" ? $i = 0; foreach my $c (@{$clivedata->{'ParameterData'}}) { my $tkind = $c->{'Parameters'}[0]{'Timestamp'}{'Kind'}; # Zeitart: Unspecified, Utc # Log3 ($name, 1, "$name - $tkind"); my $GriSwStt = $c->{'Parameters'}[0]{'Value'}; # on: 1, off: 0 my $GriSwAuto = $c->{'Parameters'}[1]{'Value'}; # automatic = 1 my $OperationAutoEna = $c->{'Parameters'}[2]{'Value'}; # Automatic Betrieb erlaubt ? my $ltchange = TimeAdjust($hash,$c->{'Parameters'}[0]{'Timestamp'}{'DateTime'},$tkind); # letzter Schaltzeitpunkt der Bluetooth-Steckdose (Verbraucher) my $cn = $consumers{"${i}_ConsumerName"}; # Verbrauchername $cn = substUmlauts($cn); # evtl. Umlaute im Verbrauchernamen ersetzen if(!$GriSwStt && $GriSwAuto) { $res = "off (automatic)"; } elsif (!$GriSwStt && !$GriSwAuto) { $res = "off"; } elsif ($GriSwStt) { $res = "on"; } else { $res = "undefined"; } readingsBulkUpdate($hash, "L3_${cn}_Switch", $res); readingsBulkUpdate($hash, "L3_${cn}_SwitchLastTime", $ltchange); $i++; } } readingsEndUpdate($hash, 1); return; } ################################################################ # sortiert eine Liste von Versionsnummern x.x.x # Schwartzian Transform and the GRT transform # Übergabe: "asc | desc", ################################################################ sub sortVersionNum (@) { my ($sseq,@versions) = @_; my @sorted = map {$_->[0]} sort {$a->[1] cmp $b->[1]} map {[$_, pack "C*", split /\./]} @versions; @sorted = map {join ".", unpack "C*", $_} sort map {pack "C*", split /\./} @versions; if($sseq eq "desc") { @sorted = reverse @sorted; } return @sorted; } ################################################################ # Versionierungen des Moduls setzen # Die Verwendung von Meta.pm und Packages wird berücksichtigt ################################################################ sub setVersionInfo($) { my ($hash) = @_; my $name = $hash->{NAME}; my $v = (sortTopicNum("desc",keys %vNotesIntern))[0]; my $type = $hash->{TYPE}; $hash->{HELPER}{PACKAGE} = __PACKAGE__; $hash->{HELPER}{VERSION} = $v; if($modules{$type}{META}{x_prereqs_src} && !$hash->{HELPER}{MODMETAABSENT}) { # META-Daten sind vorhanden $modules{$type}{META}{version} = "v".$v; # Version aus META.json überschreiben, Anzeige mit {Dumper $modules{SMAPortal}{META}} if($modules{$type}{META}{x_version}) { # {x_version} ( nur gesetzt wenn $Id: ... $ im Kopf komplett! vorhanden ) $modules{$type}{META}{x_version} =~ s/1.1.1/$v/g; } else { $modules{$type}{META}{x_version} = $v; } return $@ unless (FHEM::Meta::SetInternals($hash)); # FVERSION wird gesetzt ( nur gesetzt wenn $Id: ... $ im Kopf komplett! vorhanden ) if(__PACKAGE__ eq "FHEM::$type" || __PACKAGE__ eq $type) { # es wird mit Packages gearbeitet -> Perl übliche Modulversion setzen # mit {->VERSION()} im FHEMWEB kann Modulversion abgefragt werden use version 0.77; our $VERSION = FHEM::Meta::Get( $hash, 'version' ); } } else { # herkömmliche Modulstruktur $hash->{VERSION} = $v; } return; } ################################################################ # delete Readings # $dl = detailLevel ab dem das Reading gelöscht werden soll ################################################################ sub delread($;$) { my ($hash,$dl) = @_; my $name = $hash->{NAME}; my @allrds = keys%{$defs{$name}{READINGS}}; if($dl) { # Readings ab dem angegebenen Detail-Level löschen foreach my $key(@allrds) { $key =~ m/^L(\d)_.*$/; if($1 && $1 >= $dl) { delete($defs{$name}{READINGS}{$key}); } } return; } foreach my $key(@allrds) { delete($defs{$name}{READINGS}{$key}) if($key ne "state"); } return; } ################################################################ # analysiere Livedaten ################################################################ sub analivedat($$) { my ($hash,$lc) = @_; my $name = $hash->{NAME}; my ($reread,$retry) = (0,0); my $livedata_content = decode_json($lc); for my $k (keys %$livedata_content) { my $new_val = ""; if (defined $livedata_content->{$k}) { if (($livedata_content->{$k} =~ m/ARRAY/i) || ($livedata_content->{$k} =~ m/HASH/i)) { if($livedata_content->{$k} =~ m/ARRAY/i) { my $hd0 = Dumper($livedata_content->{$k}[0]); if(!$hd0) { next; } chomp $hd0; $hd0 =~ s/[;']//g; $hd0 = ($hd0 =~ /^undef$/)?"none":$hd0; # Log3 $name, 4, "$name - livedata ARRAY content \"$k\": $hd0"; $new_val = $hd0; } } else { $new_val = $livedata_content->{$k}; } if ($new_val && $k !~ /__type/i) { if($k =~ /InfoMessages/ && $new_val =~ /.*login to SMA-Portal successful.*/) { # Login war erfolgreich, Daten neu lesen Log3 $name, 3, "$name - get data again"; $reread = 1; } if($k =~ /ErrorMessages/ && $new_val =~ /.*The current data cannot be retrieved from the PV system. Check the cabling and configuration of the following energy meters.*/) { # Energiedaten konnten nicht ermittelt werden, Daten neu lesen mit Zeitverzögerung Log3 $name, 3, "$name - The current data cannot be retrieved from PV system, get data again."; $retry = 1; } if($k =~ /ErrorMessages/ && $new_val =~ /.*Communication with the Sunny Home Manager is currently not possible.*/) { # Energiedaten konnten nicht ermittelt werden, Daten neu lesen mit Zeitverzögerung Log3 $name, 3, "$name - Communication with the Sunny Home Manager currently impossible, get data again."; $retry = 1; } } } } return ($reread,$retry); } ################################################################ # Restart get Data ################################################################ sub retrygetdata($) { my ($hash) = @_; my $name = $hash->{NAME}; my $timeout = AttrVal($name, "timeout", 30); $hash->{HELPER}{RUNNING_PID} = BlockingCall("FHEM::SMAPortal::GetData", $name, "FHEM::SMAPortal::ParseData", $timeout, "FHEM::SMAPortal::ParseAborted", $hash); $hash->{HELPER}{RUNNING_PID}{loglevel} = 5 if($hash->{HELPER}{RUNNING_PID}); # Forum #77057 return; } ################################################################ # Timestamp korrigieren ################################################################ sub TimeAdjust($$$) { my ($hash,$t,$tkind) = @_; $t =~ s/T/ /; my ($datehour, $rest) = split(/:/,$t,2); my ($year, $month, $day, $hour) = $datehour =~ /(\d+)-(\d\d)-(\d\d)\s+(\d\d)/; # Time::timegm - a UTC version of mktime() # proto: $time = timegm($sec,$min,$hour,$mday,$mon,$year); my $epoch = timegm(0,0,$hour,$day,$month-1,$year); # proto: ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); my (undef,undef,undef,undef,undef,undef,undef,undef,$isdst) = localtime(time); if(lc($tkind) =~ /unspecified/) { if($isdst) { $epoch = $epoch - 7200; } else { $epoch = $epoch - 3600; } } # proto: ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); my ($lyear,$lmonth,$lday,$lhour) = (localtime($epoch))[5,4,3,2]; $lyear += 1900; # year is 1900 based $lmonth++; # month number is zero based if(AttrVal("global","language","EN") eq "DE") { return (sprintf("%02d.%02d.%04d %02d:%s", $lday,$lmonth,$lyear,$lhour,$rest)); } else { return (sprintf("%04d-%02d-%02d %02d:%s", $lyear,$lmonth,$lday,$lhour,$rest)); } } ############################################################################### # Umlaute für Readingerstellung ersetzen ############################################################################### sub substUmlauts ($) { my ($txt) = @_; $txt =~ s/ß/ss/g; $txt =~ s/ä/ae/g; $txt =~ s/ö/oe/g; $txt =~ s/ü/ue/g; $txt =~ s/Ä/Ae/g; $txt =~ s/Ö/Oe/g; $txt =~ s/Ü/Ue/g; return($txt); } ############################################################################### # Subroutine für Portalgrafik ############################################################################### sub PortalAsHtml ($$) { my ($name,$wlname) = @_; my $hash = $defs{$name}; my $ret = ""; my ($i,$icon,$colorv,$colorc,$maxhours,$hourstyle,$header,$legend,$legend_txt,$legend_style); my ($val,$height,$fsize,$html_start,$html_end,$wlalias,$weather,$colorw,$maxVal,$show_night,$type,$kw); my ($maxDif,$minDif,$maxCon,$v,$z2,$z3,$z4,$show_diff,$width,$w); my $he; # Balkenhöhe my (%pv,%is,%t,%we,%di,%co); my @pgCDev; # Kontext des aufrufenden SMAPortalSPG-Devices speichern für Refresh $hash->{HELPER}{SPGDEV} = $wlname; # Name des aufrufenden SMAPortalSPG-Devices $hash->{HELPER}{SPGROOM} = $FW_room?$FW_room:""; # Raum aus dem das SMAPortalSPG-Device die Funktion aufrief $hash->{HELPER}{SPGDETAIL} = $FW_detail?$FW_detail:""; # Name des SMAPortalSPG-Devices (wenn Detailansicht) my $dl = AttrVal($name, "detailLevel", 1); my $pv0 = ReadingsNum($name,"L2_ThisHour_PvMeanPower", undef); my $pv1 = ReadingsNum($name,"L4_NextHour01_PvMeanPower", undef); if(!$hash || !defined($defs{$wlname}) || $dl != 4 || !defined $pv0 || !defined $pv1) { $height = AttrNum($wlname, 'beamHeight', 200); $ret .= ""; $ret .= ""; $ret .= ""; $ret .= ""; $ret .= "
"; if(!$hash) { $ret .= "Device \"$name\" doesn't exist !"; } elsif (!defined($defs{$wlname})) { $ret .= "Graphic device \"$wlname\" doesn't exist !"; } elsif ($dl != 4) { $ret .= "The attribute \"detailLevel\" of device \"$name\" has to be set to level \"4\" !"; } elsif (!defined $pv0) { $ret .= "Awaiting level 2 data ..."; } elsif (!defined $pv1) { $ret .= "Awaiting level 4 data ..."; } $ret .= "
"; return $ret; } @pgCDev = split(',',AttrVal($wlname,"consumerList","")); # definierte Verbraucher ermitteln ($legend_style, $legend) = split('_',AttrVal($wlname,'consumerLegend','icon_top')); $legend = '' if(($legend_style eq 'none') || (!int(@pgCDev))); if ($legend) { foreach (@pgCDev) { my($txt,$im) = split(':',$_); # $txt ist der Verbrauchername my $swstate = ReadingsVal($name,"L3_".$txt."_Switch", "undef"); my $swicon = ""; if($swstate eq "off") { $swicon = ""; } elsif ($swstate eq "on") { $swicon = ""; } elsif ($swstate =~ /off.*automatic.*/i) { $swicon = ""; } if ($legend_style eq 'icon') { # mögliche Umbruchstellen mit normalen Blanks vorsehen ! $legend_txt .= $txt.' '.FW_makeImage($im).' '.$swicon.'  '; } else { my (undef,$co) = split('\@',$im); $co = '#cccccc' if (!$co); # Farbe per default $legend_txt .= ''.$txt.' '.$swicon.'  '; # hier auch Umbruch erlauben } } } # Parameter f. Anzeige extrahieren $maxhours = AttrNum($wlname, 'hourCount', 24); $hourstyle = AttrVal($wlname, 'hourStyle', undef); $colorv = AttrVal($wlname, 'beamColor', undef); $colorc = AttrVal($wlname, 'beamColor2', '000000'); # schwarz wenn keine Userauswahl; $icon = AttrVal($wlname, 'suggestIcon', undef); $html_start = AttrVal($wlname, 'htmlStart', undef); # beliebige HTML Strings die vor der Grafik ausgegeben werden $html_end = AttrVal($wlname, 'htmlEnd', undef); # beliebige HTML Strings die nach der Grafik ausgegeben werden $type = AttrVal($wlname, 'layoutType', 'pv'); $kw = AttrVal($wlname, 'W/kW', 'W'); $height = AttrNum($wlname, 'beamHeight', 200); $width = AttrNum($wlname, 'beamWidth', 6); # zu klein ist nicht problematisch $w = $width*$maxhours; # gesammte Breite der Ausgabe , WetterIcon braucht ca. 34px $fsize = AttrNum($wlname, 'spaceSize', 24); $maxVal = AttrNum($wlname, 'maxPV', 0); # dyn. Anpassung der Balkenhöhe oder statisch ? $show_night = AttrNum($wlname, 'showNight', 0); # alle Balken (Spalten) anzeigen ? $show_diff = AttrVal($wlname, 'showDiff', 'no'); # zusätzliche Anzeige $di{} in allen Typen $weather = AttrNum($wlname, 'showWeather', 1); $colorw = AttrVal($wlname, 'weatherColor', undef); $wlalias = AttrVal($wlname, 'alias', $wlname); $header = (AttrNum($wlname, 'showHeader', 1)) ? 1 : undef; # Icon Erstellung, mit @ ergänzen falls einfärben # Beispiel mit Farbe: $icon = FW_makeImage('light_light_dim_100.svg@green'); $icon = FW_makeImage($icon) if (defined($icon)); my $co4h = ReadingsNum($name,"L2_Next04Hours-Consumption", 0); my $coRe = ReadingsNum($name,"L3_RestOfDay-Consumption", 0); my $coTo = ReadingsNum($name,"L3_Tomorrow-Consumption", 0); my $pv4h = ReadingsNum($name,"L2_Next04Hours-PV", 0); my $pvRe = ReadingsNum($name,"L3_RestOfDay-PV", 0); my $pvTo = ReadingsNum($name,"L3_Tomorrow-PV", 0); if ($kw eq 'kW') { $co4h = sprintf("%.1f" , $co4h/1000)." kW"; $coRe = sprintf("%.1f" , $coRe/1000)." kW"; $coTo = sprintf("%.1f" , $coTo/1000)." kW"; $pv4h = sprintf("%.1f" , $pv4h/1000)." kW"; $pvRe = sprintf("%.1f" , $pvRe/1000)." kW"; $pvTo = sprintf("%.1f" , $pvTo/1000)." kW"; } else { $co4h .= " W"; $coRe .= " W"; $coTo .= " W"; $pv4h .= " W"; $pvRe .= " W"; $pvTo .= " W"; } # Headerzeile generieren my $alias = AttrVal($name, "alias", "SMA Sunny Portal"); # Linktext als Aliasname oder "SMA Sunny Portal" my $dlink = "$alias"; my $lup = ReadingsTimestamp($name, "state", "0000-00-00 00:00:00"); # letzte Updatezeit my $lupt = "last update:"; # Da der Header relativ viele Zeichen hat, müssen Stellen erlaubt werden an denen automatisch umgebrochen werden kann. # Sonst sind schmale Ausgaben nicht von den Balken bzw. deren Anzahl abhängig, sondern allein durch die Breite des Headers bestimmt if ($header) { my ($h1,$h2); if(AttrVal("global","language","EN") eq "DE") { $h1 = "Prognose [pv] - nächste 4 Stunden: $pv4h/h / Rest des Tages: $pvRe/h / Morgen: $pvTo/h"; $h2 = "Prognose [co] - nächste 4 Stunden: $co4h/h / Rest des Tages: $coRe/h / Morgen: $coTo/h"; my ($year, $month, $day, $hour, $min, $sec) = $lup =~ /(\d+)-(\d\d)-(\d\d)\s+(.*)/; $lup = "$3.$2.$1 $4"; $lupt = "letzte Aktualisierung:"; } else { $h1 = "forecast data [pv] - next 4 hours: $pv4h/h / rest of day: $pvRe / tomorrow: $pvTo/h"; $h2 = "forecast data [co] - next 4 hours: $co4h/h / rest of day: $coRe / tomorrow: $coTo/h"; } $lup = "     ($lupt $lup)"; if ($type eq 'pv') { $header = $dlink.' '.$lup.'
'.$h1; } elsif ($type eq 'co') { $header = $dlink.' '.$lup.'
'.$h2; } else { $header = $dlink.' '.$lup.'
'.$h1.'
'.$h2; } } # Werte aktuelle Stunde $pv{0} = ReadingsNum($name,"L2_ThisHour_PvMeanPower", 0); $co{0} = ReadingsNum($name,"L2_ThisHour_Consumption", 0); $di{0} = $pv{0} - $co{0}; $is{0} = (ReadingsVal($name,"L2_ThisHour_IsConsumptionRecommended",'no') eq 'yes' ) ? $icon : undef; $we{0} = $hash->{HELPER}{L2_ThisHour_WeatherId} if($weather); # für Wettericons $we{0} = $we{0}?$we{0}:0; if(AttrVal("global","language","EN") eq "DE") { (undef,undef,undef,$t{0}) = ReadingsVal($name,"L2_ThisHour_Time",'0') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/; } else { (undef,undef,undef,$t{0}) = ReadingsVal($name,"L2_ThisHour_Time",'0') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/; } $t{0} = int($t{0}); # zum Rechnen Integer ohne führende Null ########################################################### # get consumer list and display it in portalGraphics foreach (@pgCDev) { my ($itemName, undef) = split(':',$_); $itemName =~ s/^\s+|\s+$//g; #trim it, if blanks were used $_ =~ s/^\s+|\s+$//g; #trim it, if blanks were used #check if listed device is planned if (ReadingsVal($name, "L3_".$itemName."_Planned", "no") eq "yes") { #get start and end hour my ($start, $end); # werden auf Balken Pos 0 - 23 umgerechnet, nicht auf Stunde !!, Pos = 24 -> ungültige Pos = keine Anzeige if(AttrVal("global","language","EN") eq "DE") { (undef,undef,undef,$start) = ReadingsVal($name,"L3_".$itemName."_PlannedOpTimeBegin",'00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/; (undef,undef,undef,$end) = ReadingsVal($name,"L3_".$itemName."_PlannedOpTimeEnd",'00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/; } else { (undef,undef,undef,$start) = ReadingsVal($name,"L3_".$itemName."_PlannedOpTimeBegin",'0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/; (undef,undef,undef,$end) = ReadingsVal($name,"L3_".$itemName."_PlannedOpTimeEnd",'0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/; } $start = int($start); $end = int($end); my $flag = 0; # default kein Tagesverschieber #correct the hour for accurate display if ($start < $t{0}) { # consumption seems to be tomorrow $start = 24-$t{0}+$start; $flag = 1; } else { $start -= $t{0}; } if ($flag) { # consumption seems to be tomorrow $end = 24-$t{0}+$end; } else { $end -= $t{0}; } $_ .= ":".$start.":".$end; } else { $_ .= ":24:24"; } Log3($name, 4, "$name - Consumer planned data: $_"); } $maxVal = (!$maxVal) ? $pv{0} : $maxVal; # Startwert wenn kein Wert bereits via attr vorgegeben ist $maxCon = $co{0}; # für Typ co $maxDif = $di{0}; # für Typ diff $minDif = $di{0}; # für Typ diff foreach $i (1..$maxhours-1) { $pv{$i} = ReadingsNum($name,"L4_NextHour".sprintf("%02d",$i)."_PvMeanPower",0); # Erzeugung $co{$i} = ReadingsNum($name,"L4_NextHour".sprintf("%02d",$i)."_Consumption",0); # Verbrauch $di{$i} = $pv{$i} - $co{$i}; $maxVal = $pv{$i} if ($pv{$i} > $maxVal); $maxCon = $co{$i} if ($co{$i} > $maxCon); $maxDif = $di{$i} if ($di{$i} > $maxDif); $minDif = $di{$i} if ($di{$i} < $minDif); $is{$i} = (ReadingsVal($name,"L4_NextHour".sprintf("%02d",$i)."_IsConsumptionRecommended",'no') eq 'yes') ? $icon : undef; $we{$i} = $hash->{HELPER}{"L4_NextHour".sprintf("%02d",$i)."_WeatherId"} if($weather); # für Wettericons $we{$i} = $we{$i}?$we{$i}:0; if(AttrVal("global","language","EN") eq "DE") { (undef,undef,undef,$t{$i}) = ReadingsVal($name,"L4_NextHour".sprintf("%02d",$i)."_Time",'0') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/; } else { (undef,undef,undef,$t{$i}) = ReadingsVal($name,"L4_NextHour".sprintf("%02d",$i)."_Time",'0') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/; } $t{$i} = int($t{$i}); # keine führende 0 } ###################################### # Tabellen Ausgabe erzeugen # Wenn Table class=block alleine steht, zieht es bei manchen Styles die Ausgabe auf 100% Seitenbreite # lässt sich durch einbetten in eine zusätzliche Table roomoverview eindämmen # Die Tabelle ist recht schmal angelegt, aber nur so lassen sich Umbrüche erzwingen $ret = ""; $ret .= $html_start if (defined($html_start)); $ret .= ""; $ret .= ""; $ret .= "
"; $ret .= "\n"; # das \n erleichtert das Lesen der debug Quelltextausgabe if ($header) { # Header ausgeben $ret .= ""; # mit einem extra ein wenig mehr Platz machen, ergibt i.d.R. weniger als ein Zeichen $ret .= ""; } if ($legend_txt && ($legend eq 'top')) { $ret .= ""; $ret .= ""; } if ($weather) { $ret .= ""; # freier Platz am Anfang foreach $i (0..$maxhours-1) { # keine Anzeige bei Null Ertrag bzw. in der Nacht , Typ pcvo & diff haben aber immer Daten in der Nacht if ($pv{$i} || $show_night || ($type eq 'pvco') || ($type eq 'diff')) { # FHEM Wetter Icons (weather_xxx) , Skalierung und Farbe durch FHEM Bordmittel my $icon_name = weather_icon($we{$i}); # unknown -> FHEM Icon Fragezeichen im Kreis wird als Ersatz Icon ausgegeben Log3($name, 3,"$name - unknown SMA Portal weather id: ".$we{$i}.", please inform the maintainer") if($icon_name eq 'unknown'); $icon_name .='@'.$colorw if (defined($colorw)); $val = FW_makeImage($icon_name); $val ='???' if ($val eq $icon_name); # passendes Icon beim User nicht vorhanden ! ( attr web iconPath falsch/prüfen/update ? ) $ret .= ""; } else { # Kein Ertrag oder show_night = 0 $ret .= ""; $we{$i} = undef; } # mit $we{$i} = undef kann man unten leicht feststellen ob für diese Spalte bereits ein Icon ausgegeben wurde oder nicht } $ret .= ""; # freier Platz am Ende der Icon Zeile } if ($show_diff eq 'top') { # Zusätzliche Zeile Ertrag - Verbrauch $ret .= ""; # freier Platz am Anfang foreach $i (0..$maxhours-1) { $val = formatVal6($di{$i},$kw,$we{$i}); $val = ($di{$i} < 0) ? ''.$val.'' : '+'.$val; # negativ Zahlen in Fettschrift $ret .= ""; } $ret .= ""; # freier Platz am Ende } $ret .= ""; # Neue Zeile mit freiem Platz am Anfang foreach $i (0..$maxhours-1) { # Achtung Falle, Division by Zero möglich, # maxVal kann gerade bei kleineren maxhours Ausgaben in der Nacht leicht auf 0 fallen $height = 200 if (!$height); # Fallback, sollte eigentlich nicht vorkommen, außer der User setzt es auf 0 $maxVal = 1 if (!$maxVal); $maxCon = 1 if (!$maxCon); # Der zusätzliche Offset durch $fsize verhindert bei den meisten Skins # dass die Grundlinie der Balken nach unten durchbrochen wird if ($type eq 'co') { $he = int(($maxCon-$co{$i})/$maxCon*$height) + $fsize; # he - freier der Raum über den Balken. $z3 = int($height + $fsize - $he); # Resthöhe } elsif ($type eq 'pv') { $he = int(($maxVal-$pv{$i})/$maxVal*$height) + $fsize; $z3 = int($height + $fsize - $he); } elsif ($type eq 'pvco') { # Berechnung der Zonen # he - freier der Raum über den Balken. fsize wird nicht verwendet, da bei diesem Typ keine Zahlen über den Balken stehen # z2 - der Ertrag ggf mit Icon # z3 - der Verbrauch , bei zu kleinem Wert wird der Platz komplett Zone 2 zugeschlagen und nicht angezeigt # z2 und z3 nach Bedarf tauschen, wenn der Verbrauch größer als der Ertrag ist $maxVal = $maxCon if ($maxCon > $maxVal); # wer hat den größten Wert ? if ($pv{$i} > $co{$i}) { # pv oben , co unten $z2 = $pv{$i}; $z3 = $co{$i}; } else { # tauschen, Verbrauch ist größer als Ertrag $z3 = $pv{$i}; $z2 = $co{$i}; } $he = int(($maxVal-$z2)/$maxVal*$height); $z2 = int(($z2 - $z3)/$maxVal*$height); $z3 = int($height - $he - $z2); # was von maxVal noch übrig ist if ($z3 < int($fsize/2)) { # dünnen Strichbalken vermeiden / ca. halbe Zeichenhöhe $z2 += $z3; $z3 = 0; } } else { # Typ dif # Berechnung der Zonen # he - freier der Raum über den Balken , Zahl positiver Wert + fsize # z2 - positiver Balken inkl Icon # z3 - negativer Balken # z4 - Zahl negativer Wert + fsize my ($px_pos,$px_neg); my $maxPV = 0; # ToDo: maxPV noch aus Attribut maxPV ableiten if ($maxPV) { # Feste Aufteilung +/- , jeder 50 % bei maxPV = 0 $px_pos = int($height/2); $px_neg = $height - $px_pos; # Rundungsfehler vermeiden } else { # Dynamische hoch/runter Verschiebung der Null-Linie if ($minDif >= 0 ) { # keine negativen Balken vorhanden, die Positiven bekommen den gesammten Raum $px_neg = 0; $px_pos = $height; } else { if ($maxDif > 0) { $px_neg = int($height * abs($minDif) / ($maxDif + abs($minDif))); # Wieviel % entfallen auf unten ? $px_pos = $height-$px_neg; # der Rest ist oben } else { # keine positiven Balken vorhanden, die Negativen bekommen den gesammten Raum $px_neg = $height; $px_pos = 0; } } } if ($di{$i} >= 0) { # Zone 2 & 3 mit ihren direkten Werten vorbesetzen $z2 = $di{$i}; $z3 = abs($minDif); } else { $z2 = $maxDif; $z3 = abs($di{$i}); # Nur Betrag ohne Vorzeichen } # Alle vorbesetzen Werte umrechnen auf echte Ausgabe px $he = (!$px_pos) ? 0 : int(($maxDif-$z2)/$maxDif*$px_pos); # Teilung durch 0 vermeiden $z2 = ($px_pos - $he) ; $z4 = (!$px_neg) ? 0 : int((abs($minDif)-$z3)/abs($minDif)*$px_neg); # Teilung durch 0 unbedingt vermeiden $z3 = ($px_neg - $z4); # Beiden Zonen die Werte ausgeben könnten muß fsize als zusätzlicher Raum zugeschlagen werden ! $he += $fsize; $z4 += $fsize if ($z3); # komplette Grafik ohne negativ Balken, keine Ausgabe von z3 & z4 } # das style des nächsten TD bestimmt ganz wesentlich das gesammte Design # das \n erleichtert das lesen des Seitenquelltext beim debugging # vertical-align:bottom damit alle Balken und Ausgaben wirklich auf der gleichen Grundlinie sitzen $ret .=""; # Stundenwerte ohne führende 0 } $ret .= ""; ################### # Legende unten if ($legend_txt && ($legend eq 'bottom')) { $ret .= ""; $ret .= ""; } $ret .= "
$header
$legend_txt
$val
$val
\n"; if (($type eq 'pv') || ($type eq 'co')) { $v = ($type eq 'co') ? $co{$i} : $pv{$i} ; $v = 0 if (($type eq 'co') && !$pv{$i} && !$show_night); # auch bei type co die Nacht ggf. unterdrücken $val = formatVal6($v,$kw,$we{$i}); $ret .=""; # mit width=100% etwas bessere Füllung der Balken $ret .=""; $ret .=""; if ($v || $show_night) { # Balken nur einfärben wenn der User via Attr eine Farbe vorgibt, sonst bestimmt class odd von TR alleine die Farbe my $style = "style=\"padding-bottom:0px; vertical-align:top; margin-left:auto; margin-right:auto;"; $style .= (defined($colorv)) ? " background-color:#$colorv\"" : '"'; # Syntaxhilight $ret .= ""; $ret .= ""; } } elsif ($type eq 'pvco') { my ($color1, $color2, $style1, $style2); $ret .="
".$val."
"; ################################## # inject the new icon if defined my $show = 0; # wurde bereits für diese Stunde ein Geräte Icon ausgegeben ? foreach (@pgCDev) { if ($_) { my ($cons,$im,$start,$end) = split (':', $_); Log3($name, 4, "$name - Consumer to show -> $cons, relative to current time -> start: $start, end: $end") if($i<1); if ($im && ($i >= $start) && ($i <= $end)) { $ret .= FW_makeImage($im); # $show = 1; # nachher dann kein normales Icon mehr anzeigen, oder doch ? # oder noch ein extra Attr machen zum auswählen ? # eventuell den Block wieder nach unten schieben und den normalen Birnen # Vorrang geben } } } $ret .= $is{$i} if (defined ($is{$i}) && !$show); $ret .= "
\n"; # mit width=100% etwas bessere Füllung der Balken if ($he) { # der Freiraum oben kann beim größten Balken ganz entfallen $ret .=""; } if ($pv{$i} > $co{$i}) { # wer ist oben, co pder pv ? Wert und Farbe für Zone 2 & 3 vorbesetzen $val = formatVal6($pv{$i},$kw,$we{$i}); $color1 = $colorv; $style1 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; $style1 .= (defined($color1)) ? " background-color:#$color1\"" : '"'; if ($z3) { # die Zuweisung können wir uns sparen wenn Zone 3 nachher eh nicht ausgegeben wird $v = formatVal6($co{$i},$kw,$we{$i}); $color2 = $colorc; $style2 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; $style2 .= (defined($color2)) ? " background-color:#$color2\"" : '"'; } } else { $val = formatVal6($co{$i},$kw,$we{$i}); $color1 = $colorc; $style1 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; $style1 .= (defined($color1)) ? " background-color:#$color1\"" : '"'; if ($z3) { $v = formatVal6($pv{$i},$kw,$we{$i}); $color2 = $colorv; $style2 = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; $style2 .= (defined($color2)) ? " background-color:#$color2\"" : '"'; } } $ret .= ""; $ret .= ""; if ($z3) { # die Zone 3 lassen wir bei zu kleinen Werten auch ganz weg $ret .= ""; $ret .= ""; } } else { # Type dif my $style = "style=\"padding-bottom:0px; padding-top:1px; vertical-align:top; margin-left:auto; margin-right:auto;"; $ret .="
$val"; $ret .= $is{$i} if (defined $is{$i}); $ret .= "
$v
\n"; # Tipp : das nachfolgende border=0 auf 1 setzen hilft sehr Ausgabefehler zu endecken $val = ($di{$i} >= 0) ? formatVal6($di{$i},$kw,$we{$i}) : ''; $val = '   0  ' if ($di{$i} == 0); # Sonderfall , hier wird die 0 gebraucht ! if ($val) { $ret .=""; $ret .=""; } if ($di{$i} >= 0) { # mit Farbe 1 colorv füllen $style .= (defined($colorv)) ? " background-color:#$colorv\"" : '"'; $z2 = 1 if ($di{$i} == 0); # Sonderfall , 1px dünnen Strich ausgeben $ret .= ""; $ret .= ""; } else { # ohne Farbe $z2 = 2 if ($di{$i} == 0); # Sonderfall, hier wird die 0 gebraucht ! if ($z2 && $val) { # z2 weglassen wenn nicht unbedigt nötig bzw. wenn zuvor he mit val keinen Wert hatte $ret .= ""; $ret .=""; } } if ($di{$i} < 0) { # Negativ Balken anzeigen ? $style .= (defined($colorc)) ? " background-color:#$colorc\"" : '"'; # mit Farbe 2 colorc füllen $ret .= ""; $ret .= ""; } elsif ($z3) { # ohne Farbe $ret .=""; $ret .=""; } if ($z4) { # kann entfallen wenn auch z3 0 ist $val = ($di{$i} < 0) ? formatVal6($di{$i},$kw,$we{$i}) : ' '; $ret .=""; $ret .=""; } } if ($show_diff eq 'bottom') { # zusätzliche diff Anzeige $val = formatVal6($di{$i},$kw,$we{$i}); $val = ($di{$i} < 0) ? ''.$val.'' : '+'.$val; # Kommentar siehe oben bei show_diff eq top $ret .= ""; } $ret .= "
".$val."
"; $ret .= $is{$i} if (defined $is{$i}); $ret .="
".$val."
$val
"; $t{$i} = $t{$i}.$hourstyle if(defined($hourstyle)); # z.B. 10:00 statt 10 $ret .= $t{$i}."
"; $ret .= "$legend_txt
"; $ret .= $html_end if (defined($html_end)); $ret .= ""; return $ret; } ############################################################################### # Balkenbreite normieren # # Die Balkenbreite wird bestimmt durch den Wert. # Damit alle Balken die gleiche Breite bekommen, müssen die Werte auf # 6 Ausgabezeichen angeglichen werden. # "align=center" gleicht gleicht es aus, alternativ könnte man sie auch # komplett rechtsbündig ausgeben. # Es ergibt bei fast allen Styles gute Ergebnisse, Ausnahme IOS12 & 6, da diese # beiden Styles einen recht großen Font benutzen. # Wird Wetter benutzt, wird die Balkenbreite durch das Icon bestimmt ############################################################################### sub formatVal6($$;$) { my ($v,$kw,$w) = @_; my $n = ' '; # positive Zahl if ($v < 0) { $n = '-'; # negatives Vorzeichen merken $v = abs($v); } if ($kw eq 'kW') { # bei Anzeige in kW muss weniger aufgefüllt werden $v = sprintf('%.1f',($v/1000)); $v += 0; # keine 0.0 oder 6.0 etc return ($n eq '-') ? ($v*-1) : $v if defined($w) ; my $t = $v - int($v); # Nachkommstelle ? if (!$t) { # glatte Zahl ohne Nachkommastelle if(!$v) { return ' '; # 0 nicht anzeigen, passt eigentlich immer bis auf einen Fall im Typ diff } elsif ($v < 10) { return '  '.$n.$v.'  '; } else { return '  '.$n.$v.' '; } } else { # mit Nachkommastelle -> zwei Zeichen mehr .X if ($v < 10) { return ' '.$n.$v.' '; } else { return $n.$v.' '; } } } return ($n eq '-')?($v*-1):$v if defined($w); # Werte bleiben in Watt if (!$v) { return ' '; } # keine Anzeige bei Null elsif ($v < 10) { return '  '.$n.$v.'  '; } # z.B. 0 elsif ($v < 100) { return ' '.$n.$v.'  '; } elsif ($v < 1000) { return ' '.$n.$v.' '; } elsif ($v < 10000) { return $n.$v.' '; } else { return $n.$v; } # mehr als 10.000 W :) } ############################################################################### # Zuordungstabelle "WeatherId" angepasst auf FHEM Icons ############################################################################### sub weather_icon ($) { my $id = shift; my %weather_ids = ( '0' => 'weather_sun', # Sonne (klar) # vorhanden '1' => 'weather_cloudy_light', # leichte Bewölkung (1/3) # vorhanden '2' => 'weather_cloudy', # mittlere Bewölkung (2/3) # vorhanden '3' => 'weather_cloudy_heavy', # starke Bewölkung (3/3) # vorhanden '10' => 'weather_fog', # Nebel # neu '11' => 'weather_rain_fog', # Nebel mit Regen # neu '20' => 'weather_rain_heavy', # Regen (viel) # vorhanden '21' => 'weather_rain_snow_heavy', # Regen (viel) mit Schneefall # neu '30' => 'weather_rain_light', # leichter Regen (1 Tropfen) # vorhanden '31' => 'weather_rain', # leichter Regen (2 Tropfen) # vorhanden '32' => 'weather_rain_heavy', # leichter Regen (3 Tropfen) # vorhanden '40' => 'weather_rain_snow_light', # leichter Regen mit Schneefall (1 Tropfen) # neu '41' => 'weather_rain_snow', # leichter Regen mit Schneefall (3 Tropfen) # neu '50' => 'weather_snow_light', # bewölkt mit Schneefall (1 Flocke) # vorhanden '51' => 'weather_snow', # bewölkt mit Schneefall (2 Flocken) # vorhanden '52' => 'weather_snow_heavy', # bewölkt mit Schneefall (3 Flocken) # vorhanden '60' => 'weather_rain_light', # Sonne, Wolke mit Regen (1 Tropfen) # vorhanden '61' => 'weather_rain', # Sonne, Wolke mit Regen (2 Tropfen) # vorhanden '62' => 'weather_rain_heavy', # Sonne, Wolke mit Regen (3 Tropfen) # vorhanden '70' => 'weather_snow_light', # Sonne, Wolke mit Schnee (1 Flocke) # vorhanden '71' => 'weather_snow_heavy', # Sonne, Wolke mit Schnee (3 Flocken) # vorhanden '80' => 'weather_thunderstorm', # Wolke mit Blitz # vorhanden '81' => 'weather_storm', # Wolke mit Blitz und Starkregen # vorhanden '90' => 'weather_sun', # Sonne (klar) # vorhanden '91' => 'weather_sun', # Sonne (klar) wie 90 # vorhanden '100' => 'weather_night', # Mond - Nacht # neu '101' => 'weather_night_cloudy_light', # Mond mit Wolken - # neu '102' => 'weather_night_cloudy', # Wolken mittel (2/2) - Nacht # neu '103' => 'weather_night_cloudy_heavy', # Wolken stark (3/3) - Nacht # neu '110' => 'weather_night_fog', # Nebel - Nacht # neu '111' => 'weather_night_rain_fog', # Nebel mit Regen (3 Tropfen) - Nacht # neu '120' => 'weather_night_rain_heavy', # Regen (viel) - Nacht # neu '121' => 'weather_night_snow_rain_heavy', # Regen (viel) mit Schneefall - Nacht # neu '130' => 'weather_night_rain_light', # leichter Regen (1 Tropfen) - Nacht # neu '131' => 'weather_night_rain', # leichter Regen (2 Tropfen) - Nacht # neu '132' => 'weather_night_rain_heavy', # leichter Regen (3 Tropfen) - Nacht # neu '140' => 'weather_night_snow_rain_light', # leichter Regen mit Schneefall (1 Tropfen) - Nacht # neu '141' => 'weather_night_snow_rain_heavy', # leichter Regen mit Schneefall (3 Tropfen) - Nacht # neu '150' => 'weather_night_snow_light', # bewölkt mit Schneefall (1 Flocke) - Nacht # neu '151' => 'weather_night_snow', # bewölkt mit Schneefall (2 Flocken) - Nacht # neu '152' => 'weather_night_snow_heavy', # bewölkt mit Schneefall (3 Flocken) - Nacht # neu '160' => 'weather_night_rain_light', # Mond, Wolke mit Regen (1 Tropfen) - Nacht # neu '161' => 'weather_night_rain', # Mond, Wolke mit Regen (2 Tropfen) - Nacht # neu '162' => 'weather_night_rain_heavy', # Mond, Wolke mit Regen (3 Tropfen) - Nacht # neu '170' => 'weather_night_snow_rain', # Mond, Wolke mit Schnee (1 Flocke) - Nacht # neu '171' => 'weather_night_snow_heavy', # Mond, Wolke mit Schnee (3 Flocken) - Nacht # neu '180' => 'weather_night_thunderstorm_light', # Wolke mit Blitz - Nacht # neu '181' => 'weather_night_thunderstorm' # Wolke mit Blitz und Starkregen - Nacht # neu ); return $weather_ids{$id} if(defined($weather_ids{$id})); return 'unknown'; } ###################################################################################################### # Refresh eines Raumes aus $hash->{HELPER}{SPGROOM} # bzw. Longpoll von SSCam bzw. eines SMAPortalSPG Devices wenn $hash->{HELPER}{SPGDEV} gefüllt # $hash, $pload (1=Page reload), SMAPortalSPG-Event (1=Event) ###################################################################################################### sub SPGRefresh($$$) { my ($hash,$pload,$lpollspg) = @_; my $name; if (ref $hash ne "HASH") { ($name,$pload,$lpollspg) = split ",",$hash; $hash = $defs{$name}; } else { $name = $hash->{NAME}; } my $fpr = 0; # Kontext des SMAPortalSPG-Devices speichern für Refresh my $sd = $hash->{HELPER}{SPGDEV}?$hash->{HELPER}{SPGDEV}:"\"n.a.\""; # Name des aufrufenden SMAPortalSPG-Devices my $sr = $hash->{HELPER}{SPGROOM}?$hash->{HELPER}{SPGROOM}:"\"n.a.\""; # Raum aus dem das SMAPortalSPG-Device die Funktion aufrief my $sl = $hash->{HELPER}{SPGDETAIL}?$hash->{HELPER}{SPGDETAIL}:"\"n.a.\""; # Name des SMAPortalSPG-Devices (wenn Detailansicht) $fpr = AttrVal($hash->{HELPER}{SPGDEV},"forcePageRefresh",0) if($hash->{HELPER}{SPGDEV}); Log3($name, 4, "$name - Refresh - caller: $sd, callerroom: $sr, detail: $sl, pload: $pload, forcePageRefresh: $fpr, event_Spgdev: $lpollspg"); # Page-Reload if($pload && ($hash->{HELPER}{SPGROOM} && !$hash->{HELPER}{SPGDETAIL} && !$fpr)) { # trifft zu wenn in einer Raumansicht my @rooms = split(",",$hash->{HELPER}{SPGROOM}); foreach (@rooms) { my $room = $_; { map { FW_directNotify("FILTER=room=$room", "#FHEMWEB:$_", "location.reload('true')", "") } devspec2array("TYPE=FHEMWEB") } } } elsif ($pload && (!$hash->{HELPER}{SPGROOM} || $hash->{HELPER}{SPGDETAIL})) { # trifft zu bei Detailansicht oder im FLOORPLAN bzw. Dashboard oder wenn Seitenrefresh mit dem # SMAPortalSPG-Attribut "forcePageRefresh" erzwungen wird { map { FW_directNotify("#FHEMWEB:$_", "location.reload('true')", "") } devspec2array("TYPE=FHEMWEB") } } else { if($fpr) { { map { FW_directNotify("#FHEMWEB:$_", "location.reload('true')", "") } devspec2array("TYPE=FHEMWEB") } } } # parentState des SMAPortalSPG-Device updaten my @spgs = devspec2array("TYPE=SMAPortalSPG"); my $st = ReadingsVal($name, "state", "initialized"); foreach(@spgs) { if($defs{$_}{PARENT} eq $name) { next if(IsDisabled($defs{$_}{NAME})); readingsBeginUpdate($defs{$_}); readingsBulkUpdate($defs{$_},"parentState", $st); readingsBulkUpdate($defs{$_},"state", "updated"); readingsEndUpdate($defs{$_}, 1); } } return; } 1; =pod =encoding utf8 =item summary Module for communication with the SMA Sunny Portal =item summary_DE Modul zur Kommunikation mit dem SMA Sunny Portal =begin html

SMAPortal


    Is coming soon ...
=end html =begin html_DE

SMAPortal

    Mit diesem Modul können Daten aus dem SMA Sunny Portal abgerufen werden. Momentan sind es:

      • Live-Daten (Verbrauch und PV-Erzeugung)
      • Batteriedaten (In/Out)
      • Wetter-Daten von SMA für den Anlagenstandort
      • Prognosedaten (Verbrauch und PV-Erzeugung) inklusive Verbraucherempfehlung
      • die durch den Sunny Home Manager geplanten Schaltzeiten und aktuellen Status von Verbrauchern (sofern vorhanden)

    Vorbereitung

      Dieses Modul nutzt das Perl-Modul JSON welches üblicherweise nachinstalliert werden muss.
      Auf Debian-Linux basierenden Systemen kann es installiert werden mit:

      sudo apt-get install libjson-perl

      Überblick über die Perl-Module welche von SMAPortal genutzt werden:

      POSIX
      JSON
      Data::Dumper
      Time::HiRes
      Time::Local
      Blocking (FHEM-Modul)
      GPUtils (FHEM-Modul)
      FHEM::Meta (FHEM-Modul)
      LWP::UserAgent
      HTTP::Cookies
      MIME::Base64
      Encode


    Definition

      Ein SMAPortal-Device wird definiert mit:

        define <Name> SMAPortal

      Nach der Definition des Devices müssen noch die Zugangsparameter für das SMA-Portal gespeichert werden. Das geschieht mit dem Befehl:

        set <Name> credentials <Username> <Passwort>


    Set

      • set <name> createPortalGraphic <Generation | Consumption | Generation_Consumption | Differential>
      • Erstellt Devices zur grafischen Anzeige der SMA Sunny Portal Prognosedaten in verschiedenen Layouts. Das Attribut "detailLevel" muss auf den Level 4 gesetzt sein. Der Befehl setzt dieses Attribut automatisch auf den benötigten Wert.
        Mit den "Attributen des Grafikdevices" können Erscheinungsbild und Farbgebung der Prognosedaten in den erstellten Grafik-Devices angepasst werden.

      • set <name> credentials <username> <password>
      • Setzt Username / Passwort für den Zugriff zum SMA-Portal.


    Get

      • get <name> data
      • Mit diesem Befehl werden die Daten aus dem SMA-Portal manuell abgerufen.

      • get <name> storedCredentials
      • Die gespeicherten Anmeldeinformationen (Credentials) werden in einem Popup als Klartext angezeigt.


    Attribute

      • cookielifetime <Sekunden>
        Gültigkeitszeitraum für einen empfangenen Cookie (Default: 3000 Sekunden).

      • cookieLocation <Pfad/File>
        Angabe von Pfad und Datei zur Abspeicherung des empfangenen Cookies (Default: ./log/mycookies.txt).

          Beispiel:
          attr <name> cookieLocation ./log/cookies.txt

      • detailLevel
        Es wird der Umfang der zu generierenden Daten eingestellt.

          L1 - nur Live-Daten und Wetter-Daten werden generiert.
          L2 - wie L1 und zusätzlich Prognose der aktuellen und nächsten 4 Stunden sowie PV-Erzeugung und Verbrauch des aktuellen Tages
          L3 - wie L2 und zusätzlich Prognosedaten des Resttages, des Folgetages, der geplanten Einschaltzeiten von Verbrauchern und deren aktueller Status
          L4 - wie L3 und zusätzlich die detaillierte Prognose der nächsten 24 Stunden


      • disable
        Deaktiviert das Device.

      • getDataRetries <Anzahl>
        Anzahl der Wiederholungen (get data) im Fall dass keine Live-Daten vom SMA-Portal geliefert wurden (default: 3).

      • interval <Sekunden>
        Zeitintervall zum kontinuierlichen Datenabruf aus dem SMA-Portal (Default: 300 Sekunden).
        Ist "interval = 0" gesetzt, erfolgt kein automatischer Datenabruf und muss mit "get <name> data" manuell erfolgen. Wird ein Wert kleiner 120 Sekunden (und >0) angegeben, wird der Wert auf 120 Sekunden korrigiert.

        Hinweis: Das Abfrageintervall sollte nicht kleiner 120 Sekunden sein. Nach bisherigen Erfahrungen toleriert SMA ein Intervall von 120 Sekunden obwohl lt. SMA AGB der automatische Datenabruf untersagt ist.

      • showPassInLog
        Wenn gesetzt, wird das verwendete Passwort im Logfile angezeigt. (default = 0)

      • timeout <Sekunden>
        Timeout-Wert für HTTP-Aufrufe zum SMA-Portal (Default: 30 Sekunden).

      • userAgent <Kennung>
        Es kann die User-Agent-Kennung zur Identifikation gegenüber dem Portal angegeben werden.

          Beispiel:
          attr <name> userAgent Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0


=end html_DE =for :application/json;q=META.json 76_SMAPortal.pm { "abstract": "Module for communication with the SMA Sunny Portal", "x_lang": { "de": { "abstract": "Modul zur Kommunikation mit dem SMA Sunny Portal" } }, "keywords": [ "sma", "photovoltaik", "electricity", "portal", "smaportal" ], "version": "v1.1.1", "release_status": "testing", "author": [ "Heiko Maaz ", "Wzut", "XGuide", null ], "x_fhem_maintainer": [ "DS_Starter" ], "x_fhem_maintainer_github": [ "nasseeder1" ], "prereqs": { "runtime": { "requires": { "FHEM": 5.00918799, "perl": 5.014, "JSON": 0, "Encode": 0, "POSIX": 0, "Data::Dumper": 0, "Blocking": 0, "GPUtils": 0, "Time::HiRes": 0, "Time::Local": 0, "LWP": 0, "HTTP::Cookies": 0, "MIME::Base64": 0 }, "recommends": { "FHEM::Meta": 0 }, "suggests": { } } } } =end :application/json;q=META.json =cut