From 526eccf90bc8b5d578fe7821dff58412bf08ceb6 Mon Sep 17 00:00:00 2001 From: DS_Starter Date: Sat, 20 Jun 2020 08:02:04 +0000 Subject: [PATCH] 76_SMAPortal: contrib 3.0.0 git-svn-id: https://svn.fhem.de/fhem/trunk@22203 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/contrib/DS_Starter/76_SMAPortal.pm | 1390 ++++-- .../contrib/DS_Starter/76_SMAPortal_V3.0.0.pm | 4051 ----------------- 2 files changed, 903 insertions(+), 4538 deletions(-) delete mode 100644 fhem/contrib/DS_Starter/76_SMAPortal_V3.0.0.pm diff --git a/fhem/contrib/DS_Starter/76_SMAPortal.pm b/fhem/contrib/DS_Starter/76_SMAPortal.pm index cc9da9c57..fc5b3ddc7 100644 --- a/fhem/contrib/DS_Starter/76_SMAPortal.pm +++ b/fhem/contrib/DS_Starter/76_SMAPortal.pm @@ -50,6 +50,7 @@ use HTTP::Cookies; use JSON qw(decode_json); use MIME::Base64; use Encode; +use utf8; # Run before module compilation BEGIN { @@ -135,6 +136,7 @@ BEGIN { # Versions History intern my %vNotesIntern = ( + "3.0.0" => "18.06.2020 refactored readings and subroutines, detailLevel deleted, new attr providerLevel, integrate logbook data ", "2.10.6" => "12.06.2020 add hash dataprovider ", "2.10.5" => "12.06.2020 add check login by /Templates/, deeper/mulitple choice verbose5Data ", "2.10.4" => "11.06.2020 additional L1 Readings for Battery and more ", @@ -216,25 +218,32 @@ my %statkeys = ( # Statistikdaten auszulesende Schlüs AutarkyRate => 1, ); -my %stpl = ( # Ausgangstemplate Subfunktionen der Datenprovider - balanceDayData => { doit => 0, level => 'L02', func => 'dummy' }, - liveData => { doit => 0, level => 'L00', func => '_getLiveData' }, - weatherData => { doit => 0, level => 'L01', func => 'dummy' }, - forecastData => { doit => 0, level => 'L03', func => 'dummy' }, - consumerLiveData => { doit => 0, level => 'L04', func => 'dummy' }, - consumerDayData => { doit => 0, level => 'L05', func => 'dummy' }, - consumerMonthData => { doit => 0, level => 'L06', func => 'dummy' }, - consumerYearData => { doit => 0, level => 'L07', func => 'dummy' }, +my %subs; # Arbeitskopie von %stpl +my %stpl = ( # Ausgangstemplate Subfunktionen der Datenprovider + consumerMasterdata => { doit => 1, level => 'L00', func => '_getConsumerMasterdata'}, # mandatory + plantData => { doit => 1, level => 'L00', func => '_getPlantData' }, # mandatory + liveData => { doit => 0, level => 'L01', func => '_getLiveData' }, + weatherData => { doit => 0, level => 'L02', func => '_getWeatherData' }, + balanceDayData => { doit => 0, level => 'L03', func => '_getBalanceDayData' }, + forecastData => { doit => 0, level => 'L04', func => '_getForecastData' }, + consumerCurrentdata => { doit => 0, level => 'L05', func => '_getConsumerCurrData' }, + consumerDayData => { doit => 0, level => 'L06', func => '_getConsumerDayData' }, + consumerMonthData => { doit => 0, level => 'L07', func => '_getConsumerMonthData' }, + consumerYearData => { doit => 0, level => 'L08', func => '_getConsumerYearData' }, + plantLogbook => { doit => 0, level => 'L09', func => '_getPlantLogbook' }, ); # Tags der verfügbaren Datenquellen my @pd = qw( balanceDayData - liveData - weatherData - forecastData - consumerLiveData + consumerCurrentdata + consumerMasterdata consumerDayData consumerMonthData - consumerYearData + consumerYearData + forecastData + liveData + plantData + weatherData + plantLogbook ); @@ -245,7 +254,13 @@ my @pd = qw( balanceDayData sub Initialize { my ($hash) = @_; - my $v5d = join ",", @pd; + my @pls; + for my $p (@pd) { + push @pls, $p if(!$stpl{$p}{doit}); + } + my $prov = join ",", @pls; + my $v5d = join ",", @pd; + $hash->{DefFn} = \&Define; $hash->{UndefFn} = \&Undefine; $hash->{DeleteFn} = \&Delete; @@ -254,10 +269,11 @@ sub Initialize { $hash->{GetFn} = \&Get; $hash->{DbLog_splitFn} = \&DbLog_split; $hash->{AttrList} = "cookieLocation ". - "detailLevel:1,2,3,4 ". "disable:0,1 ". "interval ". - "providerLevel:multiple-strict,".$v5d." ". + "plantLogbookTypes:multiple-strict,Info,Warning,Disturbance,Error ". + "plantLogbookApprovalState:Any,NotApproved ". + "providerLevel:multiple-strict,".$prov." ". "showPassInLog:1,0 ". "userAgent ". "verbose5Data:multiple-strict,none,loginData,".$v5d." ". @@ -401,8 +417,10 @@ sub Set { ## no critic 'complexity' 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\""; + $c = qq{This device provides a praphical output of SMA Sunny Portal values.\n}. + qq{The device "$name" needs to contain "forecastData" in attribute "providerLevel".\n}. + qq{If you want see weather conditions, this attribute has to contain "weatherData" as well. }. + qq{The attribute "providerLevel" must also contain "consumerCurrentdata" if you want switch your consumer connectet to the SMA Home Manager.}; CommandAttr($hash->{CL},"$htmldev comment $c"); # es muß nicht unbedingt jedes der möglichen userattr unbedingt vorbesetzt werden @@ -433,7 +451,6 @@ sub Set { ## no critic 'complexity' 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\"."; @@ -468,6 +485,7 @@ sub Get { if ($opt eq "data") { $hash->{HELPER}{GETTER} = "all"; $hash->{HELPER}{SETTER} = "none"; + CallInfo($hash); } elsif ($opt eq "storedCredentials") { @@ -698,6 +716,16 @@ sub CallInfo { return if(IsDisabled($name)); + for my $key (%stpl) { # festlegen welche Daten geliefert werden sollen + $subs{$name}{$key}{doit} = $stpl{$key}{doit}; + $subs{$name}{$key}{level} = $stpl{$key}{level}; + $subs{$name}{$key}{func} = $stpl{$key}{func}; + } + my @pl = split ",", AttrVal($name, "providerLevel", ""); + for my $p (@pl) { + $subs{$name}{$p}{doit} = 1; + } + if ($hash->{HELPER}{RUNNING_PID}) { BlockingKill($hash->{HELPER}{RUNNING_PID}); delete($hash->{HELPER}{RUNNING_PID}); @@ -719,9 +747,9 @@ sub CallInfo { $hash->{HELPER}{ACTCYCLE} = 1; $hash->{HELPER}{CYCLEBTIME} = (gettimeofday())[0]; } - $hash->{HELPER}{RETRIES} = 1 if(!$nr); + $hash->{HELPER}{RETRIES} = 1 if(!$nr); - my $ac = $hash->{HELPER}{ACTCYCLE}; + my $ac = $hash->{HELPER}{ACTCYCLE}; Log3 ($name, 3, "$name - Running data cycle: $ac of $maxcycles"); @@ -785,7 +813,6 @@ sub GetSetData { ## no cri my $cookieLocation = AttrVal($name, "cookieLocation", "./log/".$name."_cookie.txt"); my $v5d = AttrVal($name, "verbose5Data", "none"); my $verbose = AttrVal($name, "verbose", 3); - my $dl = AttrVal($name, "detailLevel", 1); # selected Detail Level my $state = "ok"; my ($st,$lc) = ("",""); my @da = (); @@ -806,7 +833,7 @@ sub GetSetData { ## no cri # Default Header Daten $ua->default_header("Accept" => "*/*", "Accept-Encoding" => "gzip, deflate, br", - "Accept-Language" => "en-US;q=0.7,en;q=0.3", + "Accept-Language" => "en-US;q=0.7,en;q=0.3", # deutsch: de,en-US;q=0.7,en;q=0.3 , englisch: en-US;q=0.7,en;q=0.3 "Connection" => "keep-alive", "Cookie" => "collapseNavi_state=shown", "DNT" => 1, @@ -868,214 +895,50 @@ sub GetSetData { ## no cri ### Daten abrufen ############################# - if($getp ne "none") { + if($getp ne "none") { - ($errstate,$state,$reread,$retry) = _getLiveData ({ name => $name, - ua => $ua, - state => $state, - daref => \@da - }); - - if($errstate) { - $st = encode_base64 ( $state,""); - return "$name|0|0|$errstate|$getp|$setp|$st"; - } - - goto &GetSetData if($reread); - - # Wiederholung Datenabruf innerhalb eines Cycle - my $retc = $hash->{HELPER}{RETRIES}; # aktuelle Retry-Zähler - - if($retry && $retc < $maxretries) { # neuer Retry im gleichen Zyklus (nicht wenn Verbraucher schalten) - $hash->{HELPER}{RETRIES}++; - if($retc == $thold) { # Schwellenwert Leseversuche erreicht -> Cookie File löschen - Log3 ($name, 3, qq{$name - Threshold reached, delete cookie and retry ...}); - sleep $sleepexc; # Threshold exceed -> Retry mit Cookie löschen - $exceed = 1; - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "RETRIES:".$hash->{HELPER}{RETRIES} ], 1); - return "$name|$exceed|$newcycle|$errstate|$getp|$setp"; - } + for my $k (keys %{$subs{$name}}) { + next if(!$subs{$name}{$k}{doit}); + no strict "refs"; ## no critic 'NoStrict' + ($errstate,$state,$reread,$retry) = &{$subs{$name}{$k}{func}} ({ name => $name, + ua => $ua, + state => $state, + daref => \@da + }); + use strict "refs"; - sleep $sleepretry; - goto &GetSetData; - } - - # Wiederholung Datenabruf in einem neuen Cycle - my $ac = $hash->{HELPER}{ACTCYCLE}; - my $maxcycles = (controlParams $name)[1]; - if($retry && $ac < $maxcycles) { # neuer Zyklus (nicht wenn Verbraucher schalten) - Log3 ($name, 3, qq{$name - Maximum retries reached, delete cookie and start new cycle ...}); - $newcycle = 1; - return "$name|$exceed|$newcycle|$errstate|$getp|$setp"; - } - - - - ### Wetterdaten - ##################### - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/Dashboard/Weather', - tag => "weatherData", - state => $state, - fnaref => [ qw( extractWeatherData ) ], - addon => "", - daref => \@da - }); - if($errstate) { - $st = encode_base64 ( $state,""); - return "$name|0|0|$errstate|$getp|$setp|$st"; - } - - - - - ### Statistic Data Tag (anchorTime beachten !) - ################################################ - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); - my $cts = fhemTimeLocal(0, 0, 0, $mday, $mon, $year); - my $offset = fhemTzOffset($cts); - my $time = int(($cts + $offset) * 1000); # add Timestamp in Millisekunden and UTC - - my $anchort = int ($time / 1000); # anchorTime -> abzurufendes Datum - my $tab = 1; # Tab 1 -> Tag , 2->Monat, 3->Jahr, 4->Gesamt - my %fields = ("Content-Type" => "application/json; charset=utf-8"); - my $cont = qq{ {"tabNumber":$tab,"anchorTime":$anchort} }; - - ($errstate,$state) = __dispatchPost ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/FixedPages/HoManEnergyRedesign.aspx/GetLegendWithValues', - tag => "balanceDayData", - state => $state, - fnaref => [ qw( extractStatisticData ) ], - fields => \%fields, - content => $cont, - addon => "Day", - daref => \@da - }); - if($errstate) { - $st = encode_base64 ( $state,""); - return "$name|0|0|$errstate|$getp|$setp|$st"; - } - - - - ### Forecast Daten abrufen - ########################### - if($dl > 1) { - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/HoMan/Forecast/LoadRecommendationData', - tag => "forecastData", - state => $state, - fnaref => [ qw( extractPlantData extractForecastData extractConsumerData ) ], - addon => "", - daref => \@da - }); - if($errstate) { - $st = encode_base64 ( $state,""); - return "$name|0|0|$errstate|$getp|$setp|$st"; - } - } - - - - - ### Consumer Livedaten und historische Energiedaten - ##################################################### - if($dl > 2) { - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetLiveProxyValues', - tag => "consumerLiveData", - state => $state, - fnaref => [ qw( extractConsumerLiveData ) ], - addon => "", - daref => \@da - }); if($errstate) { $st = encode_base64 ( $state,""); return "$name|0|0|$errstate|$getp|$setp|$st"; } + goto &GetSetData if($reread); - if($hash->{HELPER}{PLANTOID}) { - my $PlantOid = $hash->{HELPER}{PLANTOID}; - my $dds = (split(/\s+/x, TimeNow()))[0]; - my $dde = (split(/\s+/x, FmtDateTime(time()+86400)))[0]; - - my ($mds,$me,$ye,$mde,$yds,$yde); - if($dds =~ /(.*)-(.*)-(.*)/x) { - $mds = "$1-$2-01"; - $me = (($2+1)<=12) ? $2+1 : 1; - $me = sprintf("%02d", $me); - $ye = ($2>$me) ? $1+1 : $1; - $mde = "$ye-$me-01"; - $yds = "$1-01-01"; - $yde = ($1+1)."-01-01"; + # Wiederholung Datenabruf innerhalb eines Cycle + my $retc = $hash->{HELPER}{RETRIES}; # aktuelle Retry-Zähler + + if($retry && $retc < $maxretries) { # neuer Retry im gleichen Zyklus (nicht wenn Verbraucher schalten) + $hash->{HELPER}{RETRIES}++; + if($retc == $thold) { # Schwellenwert Leseversuche erreicht -> Cookie File löschen + Log3 ($name, 3, qq{$name - Threshold reached, delete cookie and retry ...}); + sleep $sleepexc; # Threshold exceed -> Retry mit Cookie löschen + $exceed = 1; + BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "RETRIES:".$hash->{HELPER}{RETRIES} ], 1); + return "$name|$exceed|$newcycle|$errstate|$getp|$setp"; } - my $ccdd = 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetMeasuredValues?IntervalId=2&'.$PlantOid.'&StartTime='.$dds.'&EndTime='.$dde.''; - my $ccmd = 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetMeasuredValues?IntervalId=4&'.$PlantOid.'&StartTime='.$mds.'&EndTime='.$mde.''; - my $ccyd = 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetMeasuredValues?IntervalId=5&'.$PlantOid.'&StartTime='.$yds.'&EndTime='.$yde.''; - - # Energiedaten aktueller Tag - Log3 ($name, 4, "$name - Getting consumer energy data of current day"); - Log3 ($name, 4, "$name - Request date -> start: $dds, end: $dde"); - Log3 ($name, 5, "$name - Request consumer current day data string ->\n$ccdd"); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => $ccdd, - tag => "consumerDayData", - state => $state, - fnaref => [ qw( extractConsumerHistData ) ], - addon => "day", - daref => \@da - }); - if($errstate) { - $st = encode_base64 ( $state,""); - return "$name|0|0|$errstate|$getp|$setp|$st"; - } + sleep $sleepretry; + goto &GetSetData; + } - # Energiedaten aktueller Monat - Log3 ($name, 4, "$name - Getting consumer energy data of current month"); - Log3 ($name, 4, "$name - Request date -> start: $mds, end: $mde"); - Log3 ($name, 5, "$name - Request consumer current month data string ->\n$ccmd"); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => $ccmd, - tag => "consumerMonthData", - state => $state, - fnaref => [ qw( extractConsumerHistData ) ], - addon => "month", - daref => \@da - }); - if($errstate) { - $st = encode_base64 ( $state,""); - return "$name|0|0|$errstate|$getp|$setp|$st"; - } - - # Energiedaten aktuelles Jahr - Log3 ($name, 4, "$name - Getting consumer energy data of current year"); - Log3 ($name, 4, "$name - Request date -> start: $yds, end: $yde"); - Log3 ($name, 5, "$name - Request consumer current year data string ->\n$ccyd"); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => $ccyd, - tag => "consumerYearData", - state => $state, - fnaref => [ qw( extractConsumerHistData ) ], - addon => "year", - daref => \@da - }); - if($errstate) { - $st = encode_base64 ( $state,""); - return "$name|0|0|$errstate|$getp|$setp|$st"; - } - } + # Wiederholung Datenabruf in einem neuen Cycle + my $ac = $hash->{HELPER}{ACTCYCLE}; + my $maxcycles = (controlParams $name)[1]; + if($retry && $ac < $maxcycles) { # neuer Zyklus (nicht wenn Verbraucher schalten) + Log3 ($name, 3, qq{$name - Maximum retries reached, delete cookie and start new cycle ...}); + $newcycle = 1; + return "$name|$exceed|$newcycle|$errstate|$getp|$setp"; + } } } @@ -1156,7 +1019,7 @@ sub _checkLogin { Log3 ($name, 3, "$name - Login into SMA-Portal successfully done with user: $logname"); handleCounter ($name, "dailyIssueCookieCounter"); # Cookie Ausstellungszähler setzen - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "L1_Login-Status:successful", "oldlogintime:".(gettimeofday())[0] ], 1); + BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "loginState:successful", "oldlogintime:".(gettimeofday())[0] ], 1); $errstate = 0; } else { @@ -1191,13 +1054,12 @@ return ($state, $errstate); ################################################################ # Abruf Live Daten ################################################################ -sub _getLiveData { +sub _getLiveData { ## no critic "not used" my $paref = shift; my $name = $paref->{name}; my $ua = $paref->{ua}; # LWP Useragent my $state = $paref->{state}; my $daref = $paref->{daref}; # Referenz zum Datenarray - my $hash = $defs{$name}; my ($reread,$retry,$errstate) = (0,0,0); @@ -1219,6 +1081,361 @@ sub _getLiveData { return ($errstate,$state,$reread,$retry); } +################################################################ +# Abruf Wetterdaten +################################################################ +sub _getWeatherData { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $ua = $paref->{ua}; # LWP Useragent + my $state = $paref->{state}; + my $daref = $paref->{daref}; # Referenz zum Datenarray + + my ($reread,$retry,$errstate) = (0,0,0); + + ($errstate,$state) = __dispatchGet ({ name => $name, + ua => $ua, + call => 'https://www.sunnyportal.com/Dashboard/Weather', + tag => "weatherData", + state => $state, + fnaref => [ qw( extractWeatherData ) ], + addon => "", + daref => $daref + }); + +return ($errstate,$state,$reread,$retry); +} + +################################################################ +# Abruf Anlagen Stammdaten +################################################################ +sub _getPlantData { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $ua = $paref->{ua}; # LWP Useragent + my $state = $paref->{state}; + my $daref = $paref->{daref}; # Referenz zum Datenarray + + my ($reread,$retry,$errstate) = (0,0,0); + + ($errstate,$state) = __dispatchGet ({ name => $name, + ua => $ua, + call => 'https://www.sunnyportal.com/HoMan/Forecast/LoadRecommendationData', + tag => "plantData", + state => $state, + fnaref => [ qw( extractPlantData ) ], + addon => "", + daref => $daref + }); + +return ($errstate,$state,$reread,$retry); +} + +################################################################ +# Abruf Consumer Stammdaten +################################################################ +sub _getConsumerMasterdata { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $ua = $paref->{ua}; # LWP Useragent + my $state = $paref->{state}; + my $daref = $paref->{daref}; # Referenz zum Datenarray + + my ($reread,$retry,$errstate) = (0,0,0); + + ($errstate,$state) = __dispatchGet ({ name => $name, + ua => $ua, + call => 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetLiveProxyValues', + tag => "consumerMasterdata", + state => $state, + fnaref => [ qw( extractConsumerMasterdata ) ], + addon => "", + daref => $daref + }); + +return ($errstate,$state,$reread,$retry); +} + +################################################################ +# Abruf Consumer current Data +################################################################ +sub _getConsumerCurrData { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $ua = $paref->{ua}; # LWP Useragent + my $state = $paref->{state}; + my $daref = $paref->{daref}; # Referenz zum Datenarray + + my ($reread,$retry,$errstate) = (0,0,0); + + ($errstate,$state) = __dispatchGet ({ name => $name, + ua => $ua, + call => 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetLiveProxyValues', + tag => "consumerCurrentdata", + state => $state, + fnaref => [ qw( extractConsumerCurrentdata ) ], + addon => "", + daref => $daref + }); + +return ($errstate,$state,$reread,$retry); +} + +################################################################ +# Abruf Consumer Tagesdaten +################################################################ +sub _getConsumerDayData { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $ua = $paref->{ua}; # LWP Useragent + my $state = $paref->{state}; + my $daref = $paref->{daref}; # Referenz zum Datenarray + my $hash = $defs{$name}; + + my ($reread,$retry,$errstate) = (0,0,0); + + if(!$hash->{HELPER}{PLANTOID}) { + $errstate = 1; + $state = qq{The consumer data cannot be retrieved because the plant ID isn't set.}; + Log3 $name, 2, "$name - $state"; + return ($errstate,$state,$reread,$retry); + } + + my $PlantOid = $hash->{HELPER}{PLANTOID}; + my $dds = (split(/\s+/x, TimeNow()))[0]; + my $dde = (split(/\s+/x, FmtDateTime(time()+86400)))[0]; + + my $ccdd = 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetMeasuredValues?IntervalId=2&'.$PlantOid.'&StartTime='.$dds.'&EndTime='.$dde.''; + + # Energiedaten aktueller Tag + Log3 ($name, 4, "$name - Getting consumer energy data of current day"); + Log3 ($name, 4, "$name - Request date -> start: $dds, end: $dde"); + Log3 ($name, 5, "$name - Request consumer current day data string ->\n$ccdd"); + + ($errstate,$state) = __dispatchGet ({ name => $name, + ua => $ua, + call => $ccdd, + tag => "consumerDayData", + state => $state, + fnaref => [ qw( extractConsumerHistData ) ], + addon => "day", + daref => $daref + }); + +return ($errstate,$state,$reread,$retry); +} + +################################################################ +# Abruf Consumer Monatsdaten +################################################################ +sub _getConsumerMonthData { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $ua = $paref->{ua}; # LWP Useragent + my $state = $paref->{state}; + my $daref = $paref->{daref}; # Referenz zum Datenarray + my $hash = $defs{$name}; + + my ($reread,$retry,$errstate) = (0,0,0); + + if(!$hash->{HELPER}{PLANTOID}) { + $errstate = 1; + $state = qq{The consumer data cannot be retrieved because the plant ID isn't set.}; + Log3 $name, 2, "$name - $state"; + return ($errstate,$state,$reread,$retry); + } + + my $PlantOid = $hash->{HELPER}{PLANTOID}; + my $dds = (split(/\s+/x, TimeNow()))[0]; + my $dde = (split(/\s+/x, FmtDateTime(time()+86400)))[0]; + + my ($mds,$me,$ye,$mde); + if($dds =~ /(.*)-(.*)-(.*)/x) { + $mds = "$1-$2-01"; + $me = (($2+1)<=12) ? $2+1 : 1; + $me = sprintf("%02d", $me); + $ye = ($2>$me) ? $1+1 : $1; + $mde = "$ye-$me-01"; + } + + my $ccmd = 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetMeasuredValues?IntervalId=4&'.$PlantOid.'&StartTime='.$mds.'&EndTime='.$mde.''; + + # Energiedaten aktueller Monat + Log3 ($name, 4, "$name - Getting consumer energy data of current month"); + Log3 ($name, 4, "$name - Request date -> start: $mds, end: $mde"); + Log3 ($name, 5, "$name - Request consumer current month data string ->\n$ccmd"); + + ($errstate,$state) = __dispatchGet ({ name => $name, + ua => $ua, + call => $ccmd, + tag => "consumerMonthData", + state => $state, + fnaref => [ qw( extractConsumerHistData ) ], + addon => "month", + daref => $daref + }); + +return ($errstate,$state,$reread,$retry); +} + +################################################################ +# Abruf Consumer Jahresdaten +################################################################ +sub _getConsumerYearData { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $ua = $paref->{ua}; # LWP Useragent + my $state = $paref->{state}; + my $daref = $paref->{daref}; # Referenz zum Datenarray + my $hash = $defs{$name}; + + my ($reread,$retry,$errstate) = (0,0,0); + + if(!$hash->{HELPER}{PLANTOID}) { + $errstate = 1; + $state = qq{The consumer data cannot be retrieved because of the plant ID isn't set.}; + Log3 $name, 2, "$name - $state"; + return ($errstate,$state,$reread,$retry); + } + + my $PlantOid = $hash->{HELPER}{PLANTOID}; + my $dds = (split(/\s+/x, TimeNow()))[0]; + my $dde = (split(/\s+/x, FmtDateTime(time()+86400)))[0]; + + my ($mds,$me,$ye,$mde,$yds,$yde); + if($dds =~ /(.*)-(.*)-(.*)/x) { + $mds = "$1-$2-01"; + $me = (($2+1)<=12) ? $2+1 : 1; + $me = sprintf("%02d", $me); + $ye = ($2>$me) ? $1+1 : $1; + $mde = "$ye-$me-01"; + $yds = "$1-01-01"; + $yde = ($1+1)."-01-01"; + } + + my $ccyd = 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetMeasuredValues?IntervalId=5&'.$PlantOid.'&StartTime='.$yds.'&EndTime='.$yde.''; + + # Energiedaten aktuelles Jahr + Log3 ($name, 4, "$name - Getting consumer energy data of current year"); + Log3 ($name, 4, "$name - Request date -> start: $yds, end: $yde"); + Log3 ($name, 5, "$name - Request consumer current year data string ->\n$ccyd"); + + ($errstate,$state) = __dispatchGet ({ name => $name, + ua => $ua, + call => $ccyd, + tag => "consumerYearData", + state => $state, + fnaref => [ qw( extractConsumerHistData ) ], + addon => "year", + daref => $daref + }); + +return ($errstate,$state,$reread,$retry); +} + +################################################################ +# Abruf Vorhersage Daten +################################################################ +sub _getForecastData { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $ua = $paref->{ua}; # LWP Useragent + my $state = $paref->{state}; + my $daref = $paref->{daref}; # Referenz zum Datenarray + + my ($reread,$retry,$errstate) = (0,0,0); + + ($errstate,$state) = __dispatchGet ({ name => $name, + ua => $ua, + call => 'https://www.sunnyportal.com/HoMan/Forecast/LoadRecommendationData', + tag => "forecastData", + state => $state, + fnaref => [ qw( extractForecastData extractConsumerPlanData ) ], + addon => "", + daref => $daref + }); + +return ($errstate,$state,$reread,$retry); +} + +################################################################ +# Abruf Statistik Daten Day +# (anchorTime beachten !) +################################################################ +sub _getBalanceDayData { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $ua = $paref->{ua}; # LWP Useragent + my $state = $paref->{state}; + my $daref = $paref->{daref}; # Referenz zum Datenarray + + my ($reread,$retry,$errstate) = (0,0,0); + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); + my $cts = fhemTimeLocal(0, 0, 0, $mday, $mon, $year); + my $offset = fhemTzOffset($cts); + my $anchort = int($cts + $offset); # anchorTime in UTC -> abzurufendes Datum + + my $tab = 1; # Tab 1 -> Tag , 2->Monat, 3->Jahr, 4->Gesamt + my %fields = ("Content-Type" => "application/json; charset=utf-8"); + my $cont = qq{ {"tabNumber":$tab,"anchorTime":$anchort} }; + + ($errstate,$state) = __dispatchPost ({ name => $name, + ua => $ua, + call => 'https://www.sunnyportal.com/FixedPages/HoManEnergyRedesign.aspx/GetLegendWithValues', + tag => "balanceDayData", + state => $state, + fnaref => [ qw( extractStatisticData ) ], + fields => \%fields, + content => $cont, + addon => "Day", + daref => $daref + }); + +return ($errstate,$state,$reread,$retry); +} + +################################################################ +# Abruf Anlagen Logbuch +################################################################ +sub _getPlantLogbook { ## no critic "not used" + my $paref = shift; + my $name = $paref->{name}; + my $ua = $paref->{ua}; # LWP Useragent + my $state = $paref->{state}; + my $daref = $paref->{daref}; # Referenz zum Datenarray + my $hash = $defs{$name}; + + my ($reread,$retry,$errstate) = (0,0,0); + + if(!$hash->{HELPER}{PLANTOID}) { + $errstate = 1; + $state = qq{The logbook cannot be retrieved because of the plant ID isn't set.}; + Log3 $name, 2, "$name - $state"; + return ($errstate,$state,$reread,$retry); + } + + my $PlantOid = $hash->{HELPER}{PLANTOID}; + my $msgtypes = AttrVal($name, "plantLogbookTypes", "Warning,Disturbance,Error"); # möglich: Warning,Info,Disturbance,Error + my $appstate = AttrVal($name, "plantLogbookApprovalState", "Any"); + my $date = (split(/\s+/x, TimeNow()))[0]; + my $call = 'https://www.sunnyportal.com/Plants/'.$PlantOid.'/Log/Get?MessageTypes='.$msgtypes.'&ApprovalState='.$appstate.'&Device=None&MaxDateTime='.$date.'&Ticks=0'; + + Log3 ($name, 4, "$name - Retrieving the logbook data up to the date: $date"); + + ($errstate,$state) = __dispatchGet ({ name => $name, + ua => $ua, + call => $call, + tag => "plantLogbook", + state => $state, + fnaref => [ qw( extractPlantLogbook ) ], + addon => "", + daref => $daref + }); + +return ($errstate,$state,$reread,$retry); +} + ################################################################ # Dispatcher GET ################################################################ @@ -1326,7 +1543,7 @@ sub ___getData { Log3 ($name, 4, "$name - Getting $tag"); - if($verbose == 5 && $v5d =~ /$tag/) { + if($verbose == 5 && $v5d =~ /$tag/x) { $ua->add_handler( request_send => sub { shift->dump; return } ); # for debugging $ua->add_handler( response_done => sub { shift->dump; return } ); } @@ -1336,7 +1553,7 @@ sub ___getData { $cont = eval{decode_json($dcont)} or do { $cont = $dcont }; - if($v5d =~ /$tag/) { + if($v5d =~ /$tag/x) { Log3 ($name, 5, "$name - Return Code: ".$data->code); Log3 ($name, 5, "$name - $tag received:\n".Dumper $cont); } @@ -1366,7 +1583,7 @@ sub ___postData { Log3 ($name, 4, "$name - Getting $tag"); - if($verbose == 5 && $v5d =~ /$tag/) { + if($verbose == 5 && $v5d =~ /$tag/x) { $ua->add_handler( request_send => sub { shift->dump; return } ); # for debugging $ua->add_handler( response_done => sub { shift->dump; return } ); } @@ -1377,7 +1594,7 @@ sub ___postData { $cont = eval{decode_json($dcont)} or do { $cont = $dcont }; - if($v5d =~ /$tag/) { + if($v5d =~ /$tag/x) { Log3 ($name, 5, "$name - Return Code: ".$data->code); Log3 ($name, 5, "$name - $tag received:\n".Dumper $cont); } @@ -1391,7 +1608,7 @@ return ($data,$dcont); ################################################################ # analysiere abgerufene Daten ################################################################ -sub ___analyzeData { ## no critic 'complexity' +sub ___analyzeData { ## no critic 'complexity' my $paref = shift; my $name = $paref->{name}; my $errstate = $paref->{errstate}; @@ -1400,12 +1617,20 @@ sub ___analyzeData { ## my $hash = $defs{$name}; my ($reread,$retry) = (0,0); my $data = ""; - my @da = (); my $v5d = AttrVal($name, "verbose5Data", "none"); my $ad_content = encode("utf8", $ad->decoded_content); my $act = $hash->{HELPER}{RETRIES}; # Index aktueller Wiederholungsversuch my $attstr = "Attempts read data again ... ($act of $maxretries)"; # Log vorbereiten + + my $wm1e = qq{Updating of the live data was interrupted}; + my $wm1d = qq{Die Aktualisierung der Live-Daten wurde unterbrochen}; + my $wm2e = qq{The current consumption could not be determined. The current purchased electricity is unknown}; + my $wm2d = qq{Der aktuelle Verbrauch konnte nicht ermittelt werden. Der aktuelle Netzbezug ist unbekannt}; + my $em1e = qq{Communication with the Sunny Home Manager is currently not possible}; + my $em1d = qq{Kommunikation mit dem Sunny Home Manager derzeit nicht m}; + my $em2e = qq{The current data cannot be retrieved from the PV system. Check the cabling and configuration}; + my $em2d = qq{Die aktuellen Daten .*? nicht von der Anlage abgerufen werden.*? Sie die Verkabelung und Konfiguration}; $data = eval{decode_json($ad_content)} or do { $data = $ad_content }; @@ -1413,10 +1638,12 @@ sub ___analyzeData { ## for my $k (keys %{$data}) { my $val = $data->{$k}; next if(!defined $val); - + + my @da; + if(ref $val eq "ARRAY") { - for my $a (@{$val}) { - push @da, $a; + for my $a (@{$val}) { + push @da, $a if(!ref $a); } } @@ -1427,27 +1654,27 @@ sub ___analyzeData { ## } $val = join " ", @da if(@da); - + if ($val && $k !~ /__type/ix) { - if($k =~ m/WarningMessages/x && $val =~ /Updating of the live data was interrupted/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! - Log3 $name, 3, "$name - Updating of the live data was interrupted. $attstr"; + if($k =~ m/WarningMessages/x && $val =~ /$wm1e|$wm1d/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! + Log3 ($name, 3, "$name - Updating of the live data was interrupted. $attstr"); $retry = 1; return ($reread,$retry,$errstate,$state); } - if($k =~ m/WarningMessages/x && $val =~ /The current consumption could not be determined. The current purchased electricity is unknown/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! - Log3 $name, 3, "$name - The current consumption could not be determined. The current purchased electricity is unknown. $attstr"; + if($k =~ m/WarningMessages/x && $val =~ /$wm2e|$wm2d/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! + Log3 ($name, 3, "$name - The current consumption could not be determined. The current purchased electricity is unknown. $attstr"); $retry = 1; return ($reread,$retry,$errstate,$state); } - if($k =~ m/ErrorMessages/x && $val =~ /Communication with the Sunny Home Manager is currently not possible/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! + if($k =~ m/ErrorMessages/x && $val =~ /$em1e|$em1d/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! # Energiedaten konnten nicht ermittelt werden, Daten neu lesen mit Zeitverzögerung - Log3 $name, 3, "$name - Communication with the Sunny Home Manager currently impossible. $attstr"; + Log3 ($name, 3, "$name - Communication with the Sunny Home Manager currently impossible. $attstr"); $retry = 1; return ($reread,$retry,$errstate,$state); } - if($k =~ m/ErrorMessages/x && $val =~ /The current data cannot be retrieved from the PV system. Check the cabling and configuration of the following energy meters/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! + if($k =~ m/ErrorMessages/x && $val =~ /$em2e|$em2d/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! # Energiedaten konnten nicht ermittelt werden, Daten neu lesen mit Zeitverzögerung - Log3 $name, 3, "$name - The current data cannot be retrieved from the PV system. $attstr"; + Log3 ($name, 3, "$name - The current data cannot be retrieved from the PV system. $attstr"); $retry = 1; return ($reread,$retry,$errstate,$state); } @@ -1455,7 +1682,7 @@ sub ___analyzeData { ## } } else { - my $njdat = $ad->as_string; + my $njdat = encode("utf8", $ad->as_string); if($njdat =~ /401\s-\sUnauthorized/x) { Log3 ($name, 2, "$name - ERROR - User logged in but unauthorized"); @@ -1523,9 +1750,7 @@ sub ParseData { ## no critic @da = split "###", $lc; } - - my $dl = AttrVal($name, "detailLevel", 1); - delread($hash, $dl+1); + delread($hash, 1); readingsBeginUpdate($hash); @@ -1536,12 +1761,16 @@ sub ParseData { ## no critic readingsEndUpdate($hash, 1); - 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; + my $ldlv = $stpl{liveData}{level}; + my $cclv = $stpl{consumerCurrentdata}{level}; + my $lddo = $subs{$name}{liveData}{doit}; - if(!$errstate && !$pv && !$fi && !$gc) { + my $pv = ReadingsNum($name, "${ldlv}_PV" , 0); + my $fi = ReadingsNum($name, "${ldlv}_FeedIn" , 0); + my $gc = ReadingsNum($name, "${ldlv}_GridConsumption", 0); + my $sum = $fi-$gc; + + if(!$errstate && $lddo && !$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"); @@ -1553,13 +1782,13 @@ sub ParseData { ## no critic if($setp ne "none") { my ($d,$op) = split(":",$setp); $op = ($op eq "auto") ? "off (automatic)" : $op; - readingsBulkUpdate($hash, "L3_${d}_Switch", $op); + readingsBulkUpdate($hash, "${cclv}_${d}_Switch", $op); } - readingsBulkUpdate($hash, "lastCycleTime", $ctime) if($ctime > 0); - readingsBulkUpdate($hash, "summary" , "$sum W"); + readingsBulkUpdate($hash, "lastCycleTime", $ctime ) if($ctime > 0); + readingsBulkUpdate($hash, "summary" , $sum." W") if($subs{$name}{liveData}{doit}); } else { - readingsBulkUpdate($hash, "L1_Login-Status", "failed"); + readingsBulkUpdate($hash, "loginState", "failed"); } readingsBulkUpdate($hash, "state", $state); @@ -1631,30 +1860,31 @@ sub extractLiveData { ## no critic 'complexity' }; my ($errMsg,$warnMsg,$infoMsg) = (0,0,0); + my $lv = $stpl{liveData}{level}; if (ref $live eq "HASH") { - push @$daref, "L1_FeedIn:" .($live->{FeedIn} // 0)." W"; - push @$daref, "L1_GridConsumption:" .($live->{GridConsumption} // 0)." W"; - push @$daref, "L1_PV:" .($live->{PV} // 0)." W"; - push @$daref, "L1_AutarkyQuote:" .($live->{AutarkyQuote} // 0)." %"; - push @$daref, "L1_SelfConsumption:" .($live->{SelfConsumption} // 0)." W"; - push @$daref, "L1_SelfConsumptionQuote:" .($live->{SelfConsumptionQuote} // 0)." %"; - push @$daref, "L1_SelfSupply:" .($live->{SelfSupply} // 0)." W"; - push @$daref, "L1_TotalConsumption:" .($live->{TotalConsumption} // 0)." W"; + push @$daref, "${lv}_FeedIn:" .($live->{FeedIn} // 0)." W"; + push @$daref, "${lv}_GridConsumption:" .($live->{GridConsumption} // 0)." W"; + push @$daref, "${lv}_PV:" .($live->{PV} // 0)." W"; + push @$daref, "${lv}_AutarkyQuote:" .($live->{AutarkyQuote} // 0)." %"; + push @$daref, "${lv}_SelfConsumption:" .($live->{SelfConsumption} // 0)." W"; + push @$daref, "${lv}_SelfConsumptionQuote:" .($live->{SelfConsumptionQuote} // 0)." %"; + push @$daref, "${lv}_SelfSupply:" .($live->{SelfSupply} // 0)." W"; + push @$daref, "${lv}_TotalConsumption:" .($live->{TotalConsumption} // 0)." W"; - push @$daref, "L1_BatteryIn:" .$live->{BatteryIn}. " W" if(defined $live->{BatteryIn}); - push @$daref, "L1_BatteryOut:" .$live->{BatteryOut}." W" if(defined $live->{BatteryOut}); - push @$daref, "L1_BatteryMode:" .$live->{BatteryMode}."" if(defined $live->{BatteryMode}); - push @$daref, "L1_BatteryStateOfHealth:" .$live->{BatteryStateOfHealth}."" if(defined $live->{BatteryStateOfHealth}); - push @$daref, "L1_BatteryChargeStatus:" .$live->{BatteryChargeStatus}." %" if(defined $live->{BatteryChargeStatus}); - push @$daref, "L1_DirectConsumption:" .$live->{DirectConsumption}." W" if(defined $live->{DirectConsumption}); - push @$daref, "L1_DirectConsumptionQuote:" .$live->{DirectConsumptionQuote}." %" if(defined $live->{DirectConsumptionQuote}); + push @$daref, "${lv}_BatteryIn:" .$live->{BatteryIn}. " W" if(defined $live->{BatteryIn}); + push @$daref, "${lv}_BatteryOut:" .$live->{BatteryOut}." W" if(defined $live->{BatteryOut}); + push @$daref, "${lv}_BatteryMode:" .$live->{BatteryMode}."" if(defined $live->{BatteryMode}); + push @$daref, "${lv}_BatteryStateOfHealth:" .$live->{BatteryStateOfHealth}."" if(defined $live->{BatteryStateOfHealth}); + push @$daref, "${lv}_BatteryChargeStatus:" .$live->{BatteryChargeStatus}." %" if(defined $live->{BatteryChargeStatus}); + push @$daref, "${lv}_DirectConsumption:" .$live->{DirectConsumption}." W" if(defined $live->{DirectConsumption}); + push @$daref, "${lv}_DirectConsumptionQuote:" .$live->{DirectConsumptionQuote}." %" if(defined $live->{DirectConsumptionQuote}); - push @$daref, "L1_ModuleTemperature:" .$live->{ModuleTemperature}."" if(defined $live->{ModuleTemperature}); - push @$daref, "L1_OperationHealth:" .$live->{OperationHealth}."" if(defined $live->{OperationHealth}); - push @$daref, "L1_Insolation:" .$live->{Insolation}."" if(defined $live->{Insolation}); - push @$daref, "L1_WindSpeed:" .$live->{WindSpeed}."" if(defined $live->{WindSpeed}); - push @$daref, "L1_EnvironmentTemperature:" .$live->{EnvironmentTemperature}."" if(defined $live->{EnvironmentTemperature}); + push @$daref, "${lv}_ModuleTemperature:" .$live->{ModuleTemperature}."" if(defined $live->{ModuleTemperature}); + push @$daref, "${lv}_OperationHealth:" .$live->{OperationHealth}."" if(defined $live->{OperationHealth}); + push @$daref, "${lv}_Insolation:" .$live->{Insolation}."" if(defined $live->{Insolation}); + push @$daref, "${lv}_WindSpeed:" .$live->{WindSpeed}."" if(defined $live->{WindSpeed}); + push @$daref, "${lv}_EnvironmentTemperature:" .$live->{EnvironmentTemperature}."" if(defined $live->{EnvironmentTemperature}); if($live->{ErrorMessages}[0]) { my @em; @@ -1663,7 +1893,7 @@ sub extractLiveData { ## no critic 'complexity' push @em, $a; } $val = join " ", @em if(@em); - push @$daref, "L1_ErrorMessages:".qq{Message got from SMA Sunny Portal:
$val}; + push @$daref, "${lv}_ErrorMessages:".qq{Message got from SMA Sunny Portal:
$val}; } if($live->{WarningMessages}[0]) { @@ -1673,7 +1903,7 @@ sub extractLiveData { ## no critic 'complexity' push @wm, $a; } $val = join " ", @wm if(@wm); - push @$daref, "L1_WarningMessages:".qq{Message got from SMA Sunny Portal:
$val}; + push @$daref, "${lv}_WarningMessages:".qq{Message got from SMA Sunny Portal:
$val}; } if($live->{InfoMessages}[0]) { @@ -1683,13 +1913,13 @@ sub extractLiveData { ## no critic 'complexity' push @im, $a; } $val = join " ", @im if(@im); - push @$daref, "L1_InfoMessages:".qq{Message got from SMA Sunny Portal:
$val}; + push @$daref, "${lv}_InfoMessages:".qq{Message got from SMA Sunny Portal:
$val}; } } - BlockingInformParent("FHEM::SMAPortal::delReadingFromBlocking", [$name, "L1_ErrorMessages"] , 1) if(!$errMsg); - BlockingInformParent("FHEM::SMAPortal::delReadingFromBlocking", [$name, "L1_WarningMessages"], 1) if(!$warnMsg); - BlockingInformParent("FHEM::SMAPortal::delReadingFromBlocking", [$name, "L1_InfoMessages"] , 1) if(!$infoMsg); + BlockingInformParent("FHEM::SMAPortal::delReadingFromBlocking", [$name, "${lv}_ErrorMessages"] , 1) if(!$errMsg); + BlockingInformParent("FHEM::SMAPortal::delReadingFromBlocking", [$name, "${lv}_WarningMessages"], 1) if(!$warnMsg); + BlockingInformParent("FHEM::SMAPortal::delReadingFromBlocking", [$name, "${lv}_InfoMessages"] , 1) if(!$infoMsg); return; } @@ -1703,19 +1933,18 @@ sub extractForecastData { ## no critic 'complexity' my $forecast = shift; my $name = $hash->{NAME}; - my $dl = AttrVal($name, "detailLevel", 1); - Log3 ($name, 4, "$name - ##### extracting forecast data #### "); $forecast = eval{decode_json($forecast)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); 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 $lv = $stpl{forecastData}{level}; my $PV_sum = 0; my $consum_sum = 0; my $sum = 0; @@ -1783,30 +2012,28 @@ sub extractForecastData { ## no critic 'complexity' # 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/x && $dl >= 4) { - push @$daref, "L4_${time_str}_Time:". TimeAdjust($hash,$fc_obj->{'TimeStamp'}->{'DateTime'},$tkind); - push @$daref, "L4_${time_str}_PvMeanPower:". int( $fc_obj->{'PvMeanPower'}->{'Amount'} )." Wh"; # in W als Durchschnitt geliefet, d.h. eine Stunde -> Wh - push @$daref, "L4_${time_str}_Consumption:". int( $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 )." Wh"; # {'ConsumptionForecast'}->{'Amount'} wird als J = Ws geliefert - push @$daref, "L4_${time_str}_IsConsumptionRecommended:". ($fc_obj->{'IsConsumptionRecommended'} ? "yes" : "no"); - push @$daref, "L4_${time_str}_Total:". (int($fc_obj->{'PvMeanPower'}->{'Amount'}) - int($fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600))." Wh"; - - # add WeatherId Helper to show weather icon - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "L4_".${time_str}."_WeatherId:".int($fc_obj->{'WeatherId'}) ], 1) if(defined $fc_obj->{'WeatherId'}); - } - if($time_str =~ /ThisHour/x && $dl >= 2) { - push @$daref, "L2_${time_str}_Time:". TimeAdjust($hash,$fc_obj->{'TimeStamp'}->{'DateTime'},$tkind); - push @$daref, "L2_${time_str}_PvMeanPower:". int( $fc_obj->{'PvMeanPower'}->{'Amount'} )." Wh"; # in W als Durchschnitt geliefet, d.h. eine Stunde -> Wh - push @$daref, "L2_${time_str}_Consumption:". int( $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 )." Wh"; # {'ConsumptionForecast'}->{'Amount'} wird als J = Ws geliefert - push @$daref, "L2_${time_str}_IsConsumptionRecommended:". ($fc_obj->{'IsConsumptionRecommended'} ? "yes" : "no"); - push @$daref, "L2_${time_str}_Total:". (int($fc_obj->{'PvMeanPower'}->{'Amount'}) - int($fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600))." Wh"; - - # add WeatherId Helper to show weather icon - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "L2_".${time_str}."_WeatherId:".int($fc_obj->{'WeatherId'}) ], 1) if(defined $fc_obj->{'WeatherId'}); - } + if ($obj_nr < 24) { + my $time_str = "ThisHour"; + $time_str = "NextHour".sprintf("%02d", $obj_nr) if($fc_diff_hours>0); + if($time_str =~ /NextHour/x) { + push @$daref, "${lv}_${time_str}_Time:". TimeAdjust($hash,$fc_obj->{'TimeStamp'}->{'DateTime'},$tkind); + push @$daref, "${lv}_${time_str}_PvMeanPower:". int( $fc_obj->{'PvMeanPower'}->{'Amount'} )." Wh"; # in W als Durchschnitt geliefet, d.h. eine Stunde -> Wh + push @$daref, "${lv}_${time_str}_Consumption:". int( $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 )." Wh"; # {'ConsumptionForecast'}->{'Amount'} wird als J = Ws geliefert + push @$daref, "${lv}_${time_str}_IsConsumptionRecommended:". ($fc_obj->{'IsConsumptionRecommended'} ? "yes" : "no"); + push @$daref, "${lv}_${time_str}_Total:". (int($fc_obj->{'PvMeanPower'}->{'Amount'}) - int($fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600))." Wh"; + + # add WeatherId Helper to show weather icon + BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "${lv}_".${time_str}."_WeatherId:".int($fc_obj->{'WeatherId'}) ], 1) if(defined $fc_obj->{'WeatherId'}); + } + if($time_str =~ /ThisHour/x) { + push @$daref, "${lv}_${time_str}_Time:". TimeAdjust($hash,$fc_obj->{'TimeStamp'}->{'DateTime'},$tkind); + push @$daref, "${lv}_${time_str}_PvMeanPower:". int( $fc_obj->{'PvMeanPower'}->{'Amount'} )." Wh"; # in W als Durchschnitt geliefet, d.h. eine Stunde -> Wh + push @$daref, "${lv}_${time_str}_Consumption:". int( $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 )." Wh"; # {'ConsumptionForecast'}->{'Amount'} wird als J = Ws geliefert + push @$daref, "${lv}_${time_str}_IsConsumptionRecommended:". ($fc_obj->{'IsConsumptionRecommended'} ? "yes" : "no"); + push @$daref, "${lv}_${time_str}_Total:". (int($fc_obj->{'PvMeanPower'}->{'Amount'}) - int($fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600))." Wh"; + + # add WeatherId Helper to show weather icon + BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "${lv}_".${time_str}."_WeatherId:".int($fc_obj->{'WeatherId'}) ], 1) if(defined $fc_obj->{'WeatherId'}); } } @@ -1814,22 +2041,20 @@ sub extractForecastData { ## no critic 'complexity' $obj_nr++; } - if($dl >= 2) { - push @$daref, "L2_Next04Hours_Consumption:". int( $nextFewHoursSum{'Consumption'} )." Wh"; - push @$daref, "L2_Next04Hours_PV:". int( $nextFewHoursSum{'PV'} )." Wh"; - push @$daref, "L2_Next04Hours_Total:". int( $nextFewHoursSum{'Total'} )." Wh"; - push @$daref, "L2_Next04Hours_IsConsumptionRecommended:". int( $nextFewHoursSum{'ConsumpRcmd'} )." h"; - push @$daref, "L2_ForecastToday_Consumption:". $consum_sum." Wh"; - push @$daref, "L2_ForecastToday_PV:". $PV_sum." Wh"; - push @$daref, "L2_RestOfDay_Consumption:". int( $restOfDaySum{'Consumption'} )." Wh"; - push @$daref, "L2_RestOfDay_PV:". int( $restOfDaySum{'PV'} )." Wh"; - push @$daref, "L2_RestOfDay_Total:". int( $restOfDaySum{'Total'} )." Wh"; - push @$daref, "L2_RestOfDay_IsConsumptionRecommended:". int( $restOfDaySum{'ConsumpRcmd'} )." h"; - push @$daref, "L2_Tomorrow_Consumption:". int( $tomorrowSum{'Consumption'} )." Wh"; - push @$daref, "L2_Tomorrow_PV:". int( $tomorrowSum{'PV'} )." Wh"; - push @$daref, "L2_Tomorrow_Total:". int( $tomorrowSum{'Total'} )." Wh"; - push @$daref, "L2_Tomorrow_IsConsumptionRecommended:". int( $tomorrowSum{'ConsumpRcmd'} )." h"; - } + push @$daref, "${lv}_Next04Hours_Consumption:". int( $nextFewHoursSum{'Consumption'} )." Wh"; + push @$daref, "${lv}_Next04Hours_PV:". int( $nextFewHoursSum{'PV'} )." Wh"; + push @$daref, "${lv}_Next04Hours_Total:". int( $nextFewHoursSum{'Total'} )." Wh"; + push @$daref, "${lv}_Next04Hours_IsConsumptionRecommended:". int( $nextFewHoursSum{'ConsumpRcmd'} )." h"; + push @$daref, "${lv}_ForecastToday_Consumption:". $consum_sum." Wh"; + push @$daref, "${lv}_ForecastToday_PV:". $PV_sum." Wh"; + push @$daref, "${lv}_RestOfDay_Consumption:". int( $restOfDaySum{'Consumption'} )." Wh"; + push @$daref, "${lv}_RestOfDay_PV:". int( $restOfDaySum{'PV'} )." Wh"; + push @$daref, "${lv}_RestOfDay_Total:". int( $restOfDaySum{'Total'} )." Wh"; + push @$daref, "${lv}_RestOfDay_IsConsumptionRecommended:". int( $restOfDaySum{'ConsumpRcmd'} )." h"; + push @$daref, "${lv}_Tomorrow_Consumption:". int( $tomorrowSum{'Consumption'} )." Wh"; + push @$daref, "${lv}_Tomorrow_PV:". int( $tomorrowSum{'PV'} )." Wh"; + push @$daref, "${lv}_Tomorrow_Total:". int( $tomorrowSum{'Total'} )." Wh"; + push @$daref, "${lv}_Tomorrow_IsConsumptionRecommended:". int( $tomorrowSum{'ConsumpRcmd'} )." h"; return; } @@ -1848,6 +2073,7 @@ sub extractWeatherData { $weather = eval{decode_json($weather)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); return; }; + my $lv = $stpl{weatherData}{level}; for my $k (keys %$weather) { next if(!$k); @@ -1864,8 +2090,8 @@ sub extractWeatherData { $wdesc =~ s/t/T/x if($wdesc =~ /^t/x); $day =~ s/t/T/x; - push @$daref, "L1_${day}_Temperature:$temp $symbol"; - push @$daref, "L1_${day}_WeatherDescription:$wdesc"; + push @$daref, "${lv}_${day}_Temperature:$temp $symbol"; + push @$daref, "${lv}_${day}_WeatherDescription:$wdesc"; } } @@ -1884,21 +2110,22 @@ sub extractStatisticData { my $name = $hash->{NAME}; my $sd; - Log3 ($name, 4, "$name - ##### extracting statistic data #### "); + Log3 ($name, 4, "$name - ##### extracting balance data #### "); $statistic = eval{decode_json($statistic)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); return; }; + my $lv = $stpl{balanceDayData}{level}; if(ref $statistic eq "HASH") { - $sd = decode_json ($statistic->{d}); + $sd = decode_json ( encode('UTF-8', $statistic->{d}) ); } if($sd && ref $sd eq "ARRAY") { for my $a (@$sd) { # jedes ARRAY-Element ist ein HASH my $k = $a->{Key}; my $v = $a->{Value}; - push @$daref, "L1_Today_${k}:$v" if(defined $statkeys{$k}); + push @$daref, "${lv}_Today_${k}:$v" if(defined $statkeys{$k}); } } @@ -1920,8 +2147,9 @@ sub extractPlantData { $forecast = eval{decode_json($forecast)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); return; }; - + my $lv = $stpl{plantData}{level}; my $plantOid = $forecast->{'ForecastTimeframes'}->{'PlantOid'}; + if ($plantOid) { # wichtig für erweiterte Selektionen Log3 ($name, 4, "$name - Plant ID: ".$plantOid); $hash->{HELPER}{PLANTOID} = $plantOid; @@ -1935,7 +2163,7 @@ sub extractPlantData { $amount = $forecast->{'PlantPeakPower'}{'Amount'}; $unit = $forecast->{'PlantPeakPower'}{'StandardUnit'}{'Symbol'}; - push @$daref, "L2_PlantPeakPower:$amount $unit"; + push @$daref, "${lv}_PlantPeakPower:$amount $unit"; Log3 $name, 4, "$name - Plantdata \"PlantPeakPower Amount\": $amount"; Log3 $name, 4, "$name - Plantdata \"PlantPeakPower Symbol\": $unit"; @@ -1945,9 +2173,57 @@ return; } ################################################################ -## Auswertung Consumer Data +## Auswertung Anlagenlogbuch ################################################################ -sub extractConsumerData { +sub extractPlantLogbook { + my $hash = shift; + my $daref = shift; + my $logdata = shift; + my $name = $hash->{NAME}; + + Log3 ($name, 4, "$name - ##### extracting plant logbook data #### "); + + $logdata = eval{decode_json($logdata)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); + return; + }; + my $lv = $stpl{plantLogbook}{level}; + + my %colors = ( # Farben Highlighting + "Info" => qq{}, + "Warning" => qq{}, + "Warnung" => qq{}, + "Disturbance" => qq{}, + "Störung" => qq{}, + "Error" => qq{}, + "Fehler" => qq{}, + "Unknown" => qq{}, + ); + my $eh = ""; # Endestring Highlighting + + if(ref $logdata->{aaData} eq "ARRAY") { + my @ld = @{$logdata->{aaData}}; + for my $ae (@ld) { # jedes ARRAY-Element ist ein HASH + my $dn = encode("utf8", $ae->{DeviceName} ); + my $ts = $ae->{Timestamp}; + my $dc = encode("utf8", $ae->{Description} ); + my $id = $ae->{MessageId}; + my ($mt) = $ae->{MessageType} =~ /alt='(.*?)'/x; + $mt = encode("utf8", $mt) // "Unknown"; + + my $bh = $colors{$mt}; + my $v = qq{$bh $mt $eh
$dn : $ts
$dc}; + + push @$daref, "${lv}_LogbookEntry_${id}:$v"; + } + } + +return; +} + +################################################################ +## Auswertung Consumer Plan Data (aus forecastData) +################################################################ +sub extractConsumerPlanData { my $hash = shift; my $daref = shift; my $forecast = shift; @@ -1955,18 +2231,18 @@ sub extractConsumerData { my %consumers; my ($key,$val); - my $dl = AttrVal($name, "detailLevel", 1); - - Log3 ($name, 4, "$name - ##### extracting consumer data #### "); + Log3 ($name, 4, "$name - ##### extracting consumer plan data #### "); $forecast = eval{decode_json($forecast)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); return; }; + my $lv = $stpl{forecastData}{level}; + # Schleife über alle Consumer Objekte my $i = 0; for my $c (@{$forecast->{'Consumers'}}) { - $consumers{"${i}_ConsumerName"} = encode("utf8", $c->{'ConsumerName'} ); + $consumers{"${i}_ConsumerName"} = $c->{'ConsumerName'}; $consumers{"${i}_ConsumerOid"} = $c->{'ConsumerOid'}; $i++; } @@ -1994,16 +2270,16 @@ sub extractConsumerData { if(%consumers) { for my $key (keys(%consumers)) { Log3 $name, 4, "$name - Consumer data \"$key\": ".$consumers{$key}; - if($key =~ /ConsumerName/x && $key =~ /^(\d+)_.*$/x && $dl >= 3) { + if($key =~ /ConsumerName/x && $key =~ /^(\d+)_.*$/x) { my $lfn = $1; my $cn = $consumers{"${lfn}_ConsumerName"}; # Verbrauchername next if(!$cn); $cn = replaceJunkSigns($cn); # evtl. Umlaute/Leerzeichen 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"; + my $rb = "${lv}_${cn}_PlannedOpTimeBegin"; + my $re = "${lv}_${cn}_PlannedOpTimeEnd"; + my $rp = "${lv}_${cn}_Planned"; if($pos) { push @$daref, "$rb:$pos"; push @$daref, "$rp:yes"; @@ -2024,9 +2300,9 @@ return; } ################################################################ -## Auswertung Consumer Livedata +## Auswertung Consumer Stammdaten ################################################################ -sub extractConsumerLiveData { +sub extractConsumerMasterdata { my $hash = shift; my $daref = shift; my $clivedata = shift; @@ -2034,19 +2310,19 @@ sub extractConsumerLiveData { my %consumers; my ($i,$res); - Log3 ($name, 4, "$name - ##### extracting consumer live data #### "); + Log3 ($name, 4, "$name - ##### extracting consumer master data #### "); $clivedata = eval{decode_json($clivedata)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); return; }; + my $lv = $stpl{consumerMasterdata}{level}; - # allen Consumer Objekte die ID zuordnen + # allen Consumer Objekten die ID zuordnen $i = 0; for my $c (@{$clivedata->{'MeasurementData'}}) { - $consumers{"${i}_ConsumerName"} = encode("utf8", $c->{'DeviceName'} ); + $consumers{"${i}_ConsumerName"} = $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 next if(!$cn); $cn = replaceJunkSigns($cn); @@ -2055,8 +2331,54 @@ sub extractConsumerLiveData { $hash->{HELPER}{CONSUMER}{$i}{ConsumerOid} = $consumers{"${i}_ConsumerOid"}; $hash->{HELPER}{CONSUMER}{$i}{SerialNumber} = $c->{'SerialNumber'}; $hash->{HELPER}{CONSUMER}{$i}{SUSyID} = $c->{'SUSyID'}; + + $i++; + } + + if($hash->{HELPER}{CONSUMER}) { + for my $key (keys %{$hash->{HELPER}{CONSUMER}}) { + for my $parname (keys %{$hash->{HELPER}{CONSUMER}{$key}}) { + my $val = $hash->{HELPER}{CONSUMER}{$key}{$parname}; + next if(!defined $val); + Log3 ($name, 4, "$name - CONSUMER master data: $key -> $parname = $val"); + BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "CONSUMER:$key:$parname:$val"], 1); + } + } + } + +return; +} + +################################################################ +## Auswertung Consumer Current Data +################################################################ +sub extractConsumerCurrentdata { + my $hash = shift; + my $daref = shift; + my $clivedata = shift; + my $name = $hash->{NAME}; + my %consumers; + my ($i,$res); + + Log3 ($name, 4, "$name - ##### extracting consumer current data #### "); + + $clivedata = eval{decode_json($clivedata)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); + return; + }; + my $lv = $stpl{consumerCurrentdata}{level}; + + # allen Consumer Objekten die ID zuordnen + $i = 0; + for my $c (@{$clivedata->{'MeasurementData'}}) { + $consumers{"${i}_ConsumerName"} = $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 + next if(!$cn); + $cn = replaceJunkSigns($cn); - push @$daref, "L3_${cn}_Power:".$cpower." W" if(defined $cpower); + push @$daref, "${lv}_${cn}_Power:".$cpower." W" if(defined $cpower); $i++; } @@ -2085,23 +2407,13 @@ sub extractConsumerLiveData { $res = "undefined"; } - push @$daref, "L3_${cn}_Switch:$res"; - push @$daref, "L3_${cn}_SwitchLastTime:$ltchange"; + push @$daref, "${lv}_${cn}_Switch:$res"; + push @$daref, "${lv}_${cn}_SwitchLastTime:$ltchange"; $i++; } } - if($hash->{HELPER}{CONSUMER}) { - for my $key (keys %{$hash->{HELPER}{CONSUMER}}) { - for my $parname (keys %{$hash->{HELPER}{CONSUMER}{$key}}) { - my $val = $hash->{HELPER}{CONSUMER}{$key}{$parname}; - next if(!defined $val); - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "CONSUMER:$key:$parname:$val"], 1); - } - } - } - return; } @@ -2123,13 +2435,17 @@ sub extractConsumerHistData { # $chdata = eval{decode_json($chdata)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); return; }; + my $livelvl = $stpl{liveData}{level}; + my $bataval = (defined(ReadingsNum($name,"${livelvl}_BatteryIn", undef)) || defined(ReadingsNum($name,"${livelvl}_BatteryOut", undef)))?1:0; # Identifikation ist Battery vorhanden ? - my $bataval = (defined(ReadingsNum($name,"L1_BatteryIn", undef)) || defined(ReadingsNum($name,"L1_BatteryOut", undef)))?1:0; # Identifikation ist Battery vorhanden ? + my $dlvl = $stpl{consumerDayData}{level}; + my $mlvl = $stpl{consumerMonthData}{level}; + my $ylvl = $stpl{consumerYearData}{level}; # allen Consumer Objekte die ID zuordnen $i = 0; for my $c (@{$chdata->{'Consumers'}}) { - $consumers{"${i}_ConsumerName"} = encode("utf8", $c->{'DeviceName'} ); + $consumers{"${i}_ConsumerName"} = $c->{'DeviceName'}; $consumers{"${i}_ConsumerOid"} = $c->{'ConsumerOid'}; $consumers{"${i}_ConsumerLfd"} = $i; my $cpower = $c->{'TotalEnergy'}{'Measurement'}; # Energieverbrauch im Timeframe in Wh @@ -2147,21 +2463,23 @@ sub extractConsumerHistData { # $bct = $c->{'TotalEnergyMix'}{'BatteryConsumptionTotal'}; # Anteil der Batterie-Nutzung im Timeframe am Gesamtverbrauch in Wh } - push @$daref, "L3_${cn}_EnergyTotalDay:". sprintf("%.0f", $cpower). " Wh" if(defined($cpower) && $tf eq "day"); - push @$daref, "L3_${cn}_EnergyTotalMonth:". sprintf("%.0f", $cpower). " Wh" if(defined($cpower) && $tf eq "month"); - push @$daref, "L3_${cn}_EnergyTotalYear:". sprintf("%.0f", $cpower). " Wh" if(defined($cpower) && $tf eq "year"); - push @$daref, "L3_${cn}_EnergyRelativeMonthGrid:". sprintf("%.0f", $gcr). " %" if(defined($gcr) && $tf eq "month"); - push @$daref, "L3_${cn}_EnergyTotalMonthGrid:". sprintf("%.0f", $gct). " Wh" if(defined($gct) && $tf eq "month"); - push @$daref, "L3_${cn}_EnergyRelativeMonthPV:". sprintf("%.0f", $pcr). " %" if(defined($pcr) && $tf eq "month"); - push @$daref, "L3_${cn}_EnergyTotalMonthPV:". sprintf("%.0f", $pct). " Wh" if(defined($pct) && $tf eq "month"); - push @$daref, "L3_${cn}_EnergyRelativeMonthBatt:". sprintf("%.0f", $bcr). " %" if(defined($bcr) && $bataval && $tf eq "month"); - push @$daref, "L3_${cn}_EnergyTotalMonthBatt:". sprintf("%.0f", $bct). " Wh" if(defined($bct) && $bataval && $tf eq "month"); - push @$daref, "L3_${cn}_EnergyRelativeYearGrid:". sprintf("%.0f", $gcr). " %" if(defined($gcr) && $tf eq "year"); - push @$daref, "L3_${cn}_EnergyTotalYearGrid:". sprintf("%.0f", $gct). " Wh" if(defined($gct) && $tf eq "year"); - push @$daref, "L3_${cn}_EnergyRelativeYearPV:". sprintf("%.0f", $pcr). " %" if(defined($pcr) && $tf eq "year"); - push @$daref, "L3_${cn}_EnergyTotalYearPV:". sprintf("%.0f", $pct). " Wh" if(defined($pct) && $tf eq "year"); - push @$daref, "L3_${cn}_EnergyRelativeYearBatt:". sprintf("%.0f", $bcr). " %" if(defined($bcr) && $bataval && $tf eq "year"); - push @$daref, "L3_${cn}_EnergyTotalYearBatt:". sprintf("%.0f", $bct). " Wh" if(defined($bct) && $bataval && $tf eq "year"); + push @$daref, "${dlvl}_${cn}_EnergyTotalDay:". sprintf("%.0f", $cpower). " Wh" if(defined($cpower) && $tf eq "day"); + push @$daref, "${mlvl}_${cn}_EnergyTotalMonth:". sprintf("%.0f", $cpower). " Wh" if(defined($cpower) && $tf eq "month"); + push @$daref, "${ylvl}_${cn}_EnergyTotalYear:". sprintf("%.0f", $cpower). " Wh" if(defined($cpower) && $tf eq "year"); + + push @$daref, "${mlvl}_${cn}_EnergyRelativeMonthGrid:". sprintf("%.0f", $gcr). " %" if(defined($gcr) && $tf eq "month"); + push @$daref, "${mlvl}_${cn}_EnergyTotalMonthGrid:". sprintf("%.0f", $gct). " Wh" if(defined($gct) && $tf eq "month"); + push @$daref, "${mlvl}_${cn}_EnergyRelativeMonthPV:". sprintf("%.0f", $pcr). " %" if(defined($pcr) && $tf eq "month"); + push @$daref, "${mlvl}_${cn}_EnergyTotalMonthPV:". sprintf("%.0f", $pct). " Wh" if(defined($pct) && $tf eq "month"); + push @$daref, "${mlvl}_${cn}_EnergyRelativeMonthBatt:". sprintf("%.0f", $bcr). " %" if(defined($bcr) && $bataval && $tf eq "month"); + push @$daref, "${mlvl}_${cn}_EnergyTotalMonthBatt:". sprintf("%.0f", $bct). " Wh" if(defined($bct) && $bataval && $tf eq "month"); + + push @$daref, "${ylvl}_${cn}_EnergyRelativeYearGrid:". sprintf("%.0f", $gcr). " %" if(defined($gcr) && $tf eq "year"); + push @$daref, "${ylvl}_${cn}_EnergyTotalYearGrid:". sprintf("%.0f", $gct). " Wh" if(defined($gct) && $tf eq "year"); + push @$daref, "${ylvl}_${cn}_EnergyRelativeYearPV:". sprintf("%.0f", $pcr). " %" if(defined($pcr) && $tf eq "year"); + push @$daref, "${ylvl}_${cn}_EnergyTotalYearPV:". sprintf("%.0f", $pct). " Wh" if(defined($pct) && $tf eq "year"); + push @$daref, "${ylvl}_${cn}_EnergyRelativeYearBatt:". sprintf("%.0f", $bcr). " %" if(defined($bcr) && $bataval && $tf eq "year"); + push @$daref, "${ylvl}_${cn}_EnergyTotalYearBatt:". sprintf("%.0f", $bct). " Wh" if(defined($bct) && $bataval && $tf eq "year"); $i++; } @@ -2229,26 +2547,34 @@ return; ################################################################ # delete Readings -# $dl = detailLevel ab dem das Reading gelöscht werden soll +# $conspl = providerLevel berücksichtigen ################################################################ sub delread { - my ($hash,$dl) = @_; + my $hash = shift; + my $conspl = shift; my $name = $hash->{NAME}; my @allrds = keys%{$defs{$name}{READINGS}}; - if($dl) { - # Readings ab dem angegebenen Detail-Level löschen + my $bl = "state|lastCycleTime|Counter|loginState"; # Blacklist + + if($conspl) { + # Readings löschen wenn nicht im providerLevel enthalten for my $key(@allrds) { - $key =~ m/^L(\d)_/x; - if($1 && $1 >= $dl) { - delete($defs{$name}{READINGS}{$key}); - } + my ($lvl) = $key =~ m/^(L\d+)_/x; + if($lvl) { + for my $p (keys %{$subs{$name}}) { + delete($defs{$name}{READINGS}{$key}) if($subs{$name}{$p}{level} eq $lvl && !$subs{$name}{$p}{doit}); + delete($defs{$name}{READINGS}{$key}) if($subs{$name}{plantLogbook}{level} eq $lvl); # Logbuchreadings immer löschen + } + } else { + delete($defs{$name}{READINGS}{$key}) if($key !~ /$bl/x); + } } return; } for my $key(@allrds) { - delete($defs{$name}{READINGS}{$key}) if($key ne "state"); + delete($defs{$name}{READINGS}{$key}) if($key !~ /$bl/x); } return; @@ -2398,11 +2724,17 @@ sub PortalAsHtml { $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); + my $fdo = $subs{$name}{forecastData}{doit}; + my $fmin = $subs{$name}{forecastData}{level}; # LXX Level + my $fmaj = $subs{$name}{forecastData}{level}; + my $ldlv = $subs{$name}{liveData}{level}; + my $cclv = $subs{$name}{consumerCurrentdata}{level}; - if(!$hash || !defined($defs{$wlname}) || $dl != 4 || !defined $pv0 || !defined $pv1) { + my ($pv0,$pv1); + $pv0 = ReadingsNum($name, "${fmin}_ThisHour_PvMeanPower", undef) if($fmin); + $pv1 = ReadingsNum($name, "${fmaj}_NextHour01_PvMeanPower", undef) if($fmaj); + + if(!$hash || !defined($defs{$wlname}) || !$fdo || !defined $pv0 || !defined $pv1) { $height = AttrNum($wlname, 'beamHeight', 200); $ret .= ""; $ret .= ""; @@ -2411,12 +2743,12 @@ sub PortalAsHtml { $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 (!$fdo) { + $ret .= qq{The attribute "providerLevel" of device "$name" must contain the level "forecastData" and data must be retrieved !}; } elsif (!defined $pv0) { - $ret .= "Awaiting level 2 data ..."; + $ret .= "Awaiting minor level forecast data ..."; } elsif (!defined $pv1) { - $ret .= "Awaiting level 4 data ..."; + $ret .= "Awaiting level major level forecast data ..."; } $ret .= ""; @@ -2444,7 +2776,7 @@ sub PortalAsHtml { $cmdauto = "\"ftui.setFhemStatus('set $name $txt auto')\""; } - my $swstate = ReadingsVal($name,"L3_".$txt."_Switch", "undef"); + my $swstate = ReadingsVal($name,"${cclv}_".$txt."_Switch", "undef"); my $swicon = ""; if($swstate eq "off") { $swicon = ""; @@ -2496,15 +2828,15 @@ sub PortalAsHtml { # 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,"L2_RestOfDay_Consumption", 0); - my $coTo = ReadingsNum ($name,"L2_Tomorrow_Consumption", 0); - my $coCu = ReadingsNum ($name,"L1_GridConsumption", 0); + my $co4h = ReadingsNum ($name,"${fmin}_Next04Hours_Consumption", 0); + my $coRe = ReadingsNum ($name,"${fmin}_RestOfDay_Consumption", 0); + my $coTo = ReadingsNum ($name,"${fmin}_Tomorrow_Consumption", 0); + my $coCu = ReadingsNum ($name,"${ldlv}_GridConsumption", 0); - my $pv4h = ReadingsNum($name,"L2_Next04Hours_PV", 0); - my $pvRe = ReadingsNum($name,"L2_RestOfDay_PV", 0); - my $pvTo = ReadingsNum($name,"L2_Tomorrow_PV", 0); - my $pvCu = ReadingsNum($name,"L1_PV", 0); + my $pv4h = ReadingsNum($name,"${fmin}_Next04Hours_PV", 0); + my $pvRe = ReadingsNum($name,"${fmin}_RestOfDay_PV", 0); + my $pvTo = ReadingsNum($name,"${fmin}_Tomorrow_PV", 0); + my $pvCu = ReadingsNum($name,"${ldlv}_PV", 0); if ($kw eq 'kWh') { $co4h = sprintf("%.1f" , $co4h/1000)." kWh"; @@ -2530,9 +2862,9 @@ sub PortalAsHtml { # Headerzeile generieren if ($header) { my $lang = AttrVal("global","language","EN"); - my $alias = AttrVal($name, "alias", "SMA Sunny Portal"); # Linktext als Aliasname oder "SMA Sunny Portal" + my $alias = AttrVal($name, "alias", "SMA Sunny Portal"); # Linktext als Aliasname oder "SMA Sunny Portal" my $dlink = "$alias"; - my $lup = ReadingsTimestamp($name, "L2_ForecastToday_Consumption", "0000-00-00 00:00:00"); # letzter Forecast Update + my $lup = ReadingsTimestamp($name, "${fmin}_ForecastToday_Consumption", "0000-00-00 00:00:00"); # letzter Forecast Update my $lupt = "last update:"; my $lblPv4h = "next 4h:"; @@ -2542,7 +2874,7 @@ sub PortalAsHtml { if(AttrVal("global","language","EN") eq "DE") { # Header globales Sprachschema Deutsch $lupt = "Stand:"; - $lblPv4h = "nächste 4h:"; + $lblPv4h = encode("utf8", "nächste 4h:"); $lblPvRe = "heute:"; $lblPvTo = "morgen:"; $lblPvCu = "aktuell"; @@ -2583,17 +2915,17 @@ sub PortalAsHtml { } # Werte aktuelle Stunde - $pv{0} = ReadingsNum($name,"L2_ThisHour_PvMeanPower", 0); - $co{0} = ReadingsNum($name,"L2_ThisHour_Consumption", 0); + $pv{0} = ReadingsNum($name,"${fmin}_ThisHour_PvMeanPower", 0); + $co{0} = ReadingsNum($name,"${fmin}_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 + $is{0} = (ReadingsVal($name,"${fmin}_ThisHour_IsConsumptionRecommended",'no') eq 'yes' ) ? $icon : undef; + $we{0} = $hash->{HELPER}{"${fmin}_ThisHour_WeatherId"} if($weather); # für Wettericons $we{0} = $we{0} // 999; 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})/x; + (undef,undef,undef,$t{0}) = ReadingsVal($name,"${fmin}_ThisHour_Time",'0') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; } else { - (undef,undef,undef,$t{0}) = ReadingsVal($name,"L2_ThisHour_Time",'0') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; + (undef,undef,undef,$t{0}) = ReadingsVal($name,"${fmin}_ThisHour_Time",'0') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; } $t{0} = int($t{0}); # zum Rechnen Integer ohne führende Null @@ -2606,16 +2938,16 @@ sub PortalAsHtml { $_ =~ s/^\s+|\s+$//gx; #trim it, if blanks were used #check if listed device is planned - if (ReadingsVal($name, "L3_".$itemName."_Planned", "no") eq "yes") { + if (ReadingsVal($name, "${cclv}_".$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})/x; - (undef,undef,undef,$end) = ReadingsVal($name,"L3_".$itemName."_PlannedOpTimeEnd",'00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; + (undef,undef,undef,$start) = ReadingsVal($name,"${cclv}_".$itemName."_PlannedOpTimeBegin",'00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; + (undef,undef,undef,$end) = ReadingsVal($name,"${cclv}_".$itemName."_PlannedOpTimeEnd",'00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; } else { - (undef,undef,undef,$start) = ReadingsVal($name,"L3_".$itemName."_PlannedOpTimeBegin",'0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; - (undef,undef,undef,$end) = ReadingsVal($name,"L3_".$itemName."_PlannedOpTimeEnd",'0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; + (undef,undef,undef,$start) = ReadingsVal($name,"${cclv}_".$itemName."_PlannedOpTimeBegin",'0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; + (undef,undef,undef,$end) = ReadingsVal($name,"${cclv}_".$itemName."_PlannedOpTimeEnd",'0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; } $start = int($start); @@ -2650,8 +2982,8 @@ sub PortalAsHtml { $minDif = $di{0}; # für Typ diff for my $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 + $pv{$i} = ReadingsNum($name,"${fmaj}_NextHour".sprintf("%02d",$i)."_PvMeanPower",0); # Erzeugung + $co{$i} = ReadingsNum($name,"${fmaj}_NextHour".sprintf("%02d",$i)."_Consumption",0); # Verbrauch $di{$i} = $pv{$i} - $co{$i}; $maxVal = $pv{$i} if ($pv{$i} > $maxVal); @@ -2659,14 +2991,14 @@ sub PortalAsHtml { $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 + $is{$i} = (ReadingsVal($name,"${fmaj}_NextHour".sprintf("%02d",$i)."_IsConsumptionRecommended",'no') eq 'yes') ? $icon : undef; + $we{$i} = $hash->{HELPER}{"${fmaj}_NextHour".sprintf("%02d",$i)."_WeatherId"} if($weather); # für Wettericons $we{$i} = $we{$i} // 999; 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})/x; + (undef,undef,undef,$t{$i}) = ReadingsVal($name,"${fmaj}_NextHour".sprintf("%02d",$i)."_Time",'0') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; } 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})/x; + (undef,undef,undef,$t{$i}) = ReadingsVal($name,"${fmaj}_NextHour".sprintf("%02d",$i)."_Time",'0') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; } $t{$i} = int($t{$i}); # keine führende 0 @@ -3198,6 +3530,7 @@ return;
  • Live data (Consumption and PV-Generation)
  • Battery data (In/Out) and usage data of consumers
  • +
  • Various balance data and statistical data (also of the consumers connected to the SMA Sunny Homemanager)
  • Weather data delivered from SMA for the facility location
  • Forecast data (Consumption and PV-Generation) inclusive suggestion times to switch comsumers on
  • the planned times by the Sunny Home Manager to switch consumers on and the current state of consumers (if present)
  • @@ -3231,6 +3564,7 @@ return; HTTP::Cookies
    MIME::Base64
    Encode
    + utf8


@@ -3259,12 +3593,14 @@ return;

      -
    • set <name> createPortalGraphic <Generation | Consumption | Generation_Consumption | Differential>
    • + +
    • set <name> createPortalGraphic <Generation | Consumption | Generation_Consumption | Differential>
      Creates graphical devices to show the SMA Sunny Portal forecast data in several layouts. - The attribute "detailLevel" has to be set to level 4. The command set this attribut to the needed value automatically.
      + The attribute "providerLevel" must contain "forecastData".
      With the "attributes of the graphic device" the appearance and coloration of the forecast data in the created graphic device can be adjusted.
    +
      @@ -3274,10 +3610,11 @@ return;
        -
      • set <name> <consumer name> <on | off | auto>
      • - If the attribute "detailLevel" is set to 3 or higher, consumer data are fetched from the SMA Sunny Portal. - Once these data are available, the consumer are shown in the Set and can be switched to on, off or the automatic mode (auto) - that means the consumer are controlled by the Sunny Home Manager. + +
      • set <name> <consumer name> <on | off | auto>
        + Once consumer data are available, the consumer are shown in the Set and can be switched to on, off or the automatic mode (auto) + that means the consumer are controlled by the Sunny Home Manager. +
    @@ -3287,16 +3624,21 @@ return; Get

      +
        -
      • get <name> data
      • + +
      • get <name> data
        This command fetch the data from the SMA Sunny Portal manually.
      +
        -
      • get <name> storedCredentials
      • + +
      • get <name> storedCredentials
        The saved credentials are displayed in a popup window.
      +


    @@ -3317,23 +3659,6 @@ return;

- -
  • detailLevel
    - Adjust the complexity of the generated data. -

    - -
      -
  • - - - - - -
    L1 - live data, statistic day data and weather data are generated.
    L2 - as L1 and additional forecast of the current data, the data of the next 4 hours as well as PV-Generation / Consumption the rest of day and the next day
    L3 - as L2 and additional forecast of the planned switch-on times of consumers, whose current state and energy data as well as their battery usage data
    L4 - as L3 and additional a detailed forecast of the next 24 hours
    - -
    -
    -
  • disable
    Deactivate/activate the device.

  • @@ -3344,15 +3669,61 @@ return; if the interval is set to "0", no continuous data retrieval is executed and has to be triggered manually by the "get <name> data" command.

    - Note: - The retrieval interval should not be less than 120 seconds. As of previous experiences SMA suffers an interval of - 120 seconds although the SMA terms and conditions don't permit an automatic data fetch by computer programs. +
      + Note: + The retrieval interval should not be less than 120 seconds. As of previous experiences SMA suffers an interval of + 120 seconds although the SMA terms and conditions don't permit an automatic data fetch by computer programs. +
    +
    + + +
  • plantLogbookApprovalState
    + With this attribute the entries are filtered according to their status.
    + (default: Any) +
    + +
      + + + + +
      Any - all messages
      NotApproved - unconfirmed messages
      +
    +

  • + + +
  • plantLogbookTypes
    + This attribute defines the message types of the asset logbook to be selected. + A maximum of the latest 25 logbook entries of all set types are displayed.
    + (default: Warning,Disturbance,Error) +

  • + + +
  • providerLevel
    + The scope of the data to be generated is set. Asset and consumer master data is always retrieved. +

    + +
      + + + + + + + + + + + +
      liveData - generates readings of the current generation and consumption data
      weatherData - Weather data offered by SMA are retrieved
      balanceDayData - Statistics data of the day are retrieved
      forecastData - Forecast data of generation/consumption and consumer planning data are generated
      consumerCurrentdata - current consumer data are generated
      consumerDayData - consumer data day are generated
      consumerMonthData - consumer data month are generated
      consumerYearData - consumer data year are generated
      plantLogbook - the maximum of 25 most recent entries of the plant logbook are retrieved
      +
    +

  • showPassInLog
    If set, the used password will be displayed in Logfile output. - (default = 0)

  • + (default: 0)
  • userAgent <identifier>
    @@ -3391,7 +3762,7 @@ return;
      • Live-Daten (Verbrauch und PV-Erzeugung)
      • -
      • Tagesstatistiken
      • +
      • verschiedene Bilanzdaten und Statistikdaten (auch der an den SMA Sunny Homemanager angeschlossenen Verbraucher)
      • Batteriedaten (In/Out) sowie Nutzungsdaten durch Verbraucher
      • Wetter-Daten von SMA für den Anlagenstandort
      • Prognosedaten (Verbrauch und PV-Erzeugung) inklusive Verbraucherempfehlung
      • @@ -3426,6 +3797,7 @@ return; HTTP::Cookies
        MIME::Base64
        Encode
        + utf8


      @@ -3454,26 +3826,32 @@ return;

          -
        • set <name> createPortalGraphic <Generation | Consumption | Generation_Consumption | Differential>
        • + +
        • 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.
          + Das Attribut "providerLevel" muss auf den Level "forecastData" enthalten.
          Mit den "Attributen des Grafikdevices" können Erscheinungsbild und Farbgebung der Prognosedaten in den erstellten Grafik-Devices angepasst werden.
        +
          -
        • set <name> credentials <username> <password>
        • + +
        • set <name> credentials <username> <password>
          Setzt Username / Passwort zum Login in das SMA Sunny Portal.
        +
          -
        • set <name> <Verbrauchername> <on | off | auto>
        • - Ist das Atttribut detailLevel auf 3 oder höher gesetzt, werden Verbraucherdaten aus dem SMA Sunny Portal abgerufen. - Sobald diese Werte vorliegen, werden die vorhandenen Verbraucher im Set angezeigt und können eingeschaltet, ausgeschaltet - bzw. auf die Steuerung durch den Sunny Home Manager umgeschaltet werden (auto). + +
        • set <name> <Verbrauchername> <on | off | auto>
          + Es werden die an den SMA Sunny Homemanager angeschlossene Verbraucher (Bluetooth Steckdosen) angeboten sobald sie vom + Modul erkannt wurden. + Sobald diese Daten vorliegen, werden die vorhandenen Verbraucher im Set angezeigt und können eingeschaltet, ausgeschaltet + bzw. auf die Steuerung durch den Sunny Home Manager umgeschaltet werden (auto). +
      @@ -3484,15 +3862,21 @@ return;

          -
        • get <name> data
        • + + +
        • get <name> data
          Mit diesem Befehl werden die Daten aus dem SMA Sunny Portal manuell abgerufen.
        +
          -
        • get <name> storedCredentials
        • + +
        • get <name> storedCredentials
          Die gespeicherten Anmeldeinformationen (Credentials) werden in einem Popup als Klartext angezeigt.
        + +


      @@ -3513,23 +3897,6 @@ return;

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

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

  • -
  • disable
    Deaktiviert das Device.

  • @@ -3540,15 +3907,62 @@ return; Ist interval explizit auf "0" gesetzt, erfolgt kein automatischer Datenabruf und muss mit "get <name> data" manuell erfolgen.

    - 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. +
      + 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. +
    +
    + + +
  • plantLogbookApprovalState
    + Mit diesem Attribut werden die Einträge entsprechend ihres Status gefiltert.
    + (default: Any) +
    + +
      + + + + +
      Any - alle Mitteilungen
      NotApproved - nicht bestätigte Mitteilungen
      +
    +

  • + + +
  • plantLogbookTypes
    + Mit diesem Attribut werden die zu selektierenden Mitteilungstypen des Anlagenlogbuchs festgelegt. + Es werden maximal die aktuellsten 25 Logbucheinträge aller eingestellten Typen angezeigt.
    + (default: Warning,Disturbance,Error) +

  • + + +
  • providerLevel
    + Es wird der Umfang der zu generierenden Daten eingestellt. Anlagen- und Verbraucherstammdaten werden immer abgerufen. +

    + +
      + + + + + + + + + + + +
      liveData - erzeugt Readings der aktuellen Erzeugungs- und Verbrauchsdaten
      weatherData - von SMA angebotene Wetterdaten werden abgerufen
      balanceDayData - Statistikdaten des Tages werden abgerufen
      forecastData - Vorhersagedaten der Erzeugung / Verbrauch und Verbraucherplanungsdaten werden erzeugt
      consumerCurrentdata - aktuelle Verbraucherdaten werden erzeugt
      consumerDayData - Verbraucherdaten Tag werden erzeugt
      consumerMonthData - Verbraucherdaten Monat werden erzeugt
      consumerYearData - Verbraucherdaten Jahr werden erzeugt
      plantLogbook - die maximal 25 aktuellsten Einträge des Anlagenlogbuchs werden abgerufen
      +
    +

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

  • + Wenn gesetzt, wird das verwendete Passwort im Logfile angezeigt.
    + (default: 0) +
  • userAgent <Kennung>
    @@ -3558,7 +3972,8 @@ return;
      Beispiel:
      attr <name> userAgent Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0
      -
    + +

  • @@ -3620,7 +4035,8 @@ return; "Time::Local": 0, "LWP": 0, "HTTP::Cookies": 0, - "MIME::Base64": 0 + "MIME::Base64": 0, + "utf8": 0 }, "recommends": { "FHEM::Meta": 0 diff --git a/fhem/contrib/DS_Starter/76_SMAPortal_V3.0.0.pm b/fhem/contrib/DS_Starter/76_SMAPortal_V3.0.0.pm deleted file mode 100644 index fc5b3ddc7..000000000 --- a/fhem/contrib/DS_Starter/76_SMAPortal_V3.0.0.pm +++ /dev/null @@ -1,4051 +0,0 @@ -######################################################################################################################### -# $Id: 76_SMAPortal.pm 22149 2020-06-09 20:41:58Z DS_Starter $ -######################################################################################################################### -# 76_SMAPortal.pm -# -# (c) 2019-2020 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 / add get statistic data -# Wzut/XGuide : creation of SMAPortal graphics -# -# FHEM Forum: http://forum.fhem.de/index.php/topic,27667.0.html -# -######################################################################################################################### -# -# Definition: define SMAPortal -# -######################################################################################################################### -package FHEM::SMAPortal; ## no critic 'package' -use strict; -use warnings; -use GPUtils qw(GP_Import GP_Export); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt -use POSIX; -eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' -use Data::Dumper; -use Blocking; -use Time::HiRes qw(gettimeofday time sleep); -use Time::Local; -use LWP::UserAgent; -use HTTP::Cookies; -use JSON qw(decode_json); -use MIME::Base64; -use Encode; -use utf8; - -# Run before module compilation -BEGIN { - # Import from main:: - GP_Import( - qw( - attr - AnalyzePerlCommand - AttrVal - AttrNum - addToDevAttrList - addToAttrList - BlockingCall - BlockingKill - BlockingInformParent - CommandAttr - CommandDefine - CommandDeleteAttr - CommandDeleteReading - CommandSet - defs - delFromDevAttrList - delFromAttrList - devspec2array - deviceEvents - Debug - FmtDateTime - FmtTime - fhemTzOffset - FW_makeImage - fhemTimeGm - fhemTimeLocal - getKeyValue - gettimeofday - genUUID - init_done - InternalTimer - IsDisabled - Log - Log3 - makeReadingName - modules - readingsSingleUpdate - readingsBulkUpdate - readingsBulkUpdateIfChanged - readingsBeginUpdate - readingsDelete - readingsEndUpdate - ReadingsNum - ReadingsTimestamp - ReadingsVal - RemoveInternalTimer - readingFnAttributes - setKeyValue - sortTopicNum - TimeNow - Value - json2nameValue - FW_cmd - FW_directNotify - FW_ME - FW_subdir - FW_room - FW_detail - FW_wname - ) - ); - - # Export to main context with different name - # my $pkg = caller(0); - # my $main = $pkg; - # $main =~ s/^(?:.+::)?([^:]+)$/main::$1\_/gx; - # for (@_) { - # *{ $main . $_ } = *{ $pkg . '::' . $_ }; - # } - GP_Export( - qw( - Initialize - ) - ); - -} - -# Versions History intern -my %vNotesIntern = ( - "3.0.0" => "18.06.2020 refactored readings and subroutines, detailLevel deleted, new attr providerLevel, integrate logbook data ", - "2.10.6" => "12.06.2020 add hash dataprovider ", - "2.10.5" => "12.06.2020 add check login by /Templates/, deeper/mulitple choice verbose5Data ", - "2.10.4" => "11.06.2020 additional L1 Readings for Battery and more ", - "2.10.3" => "11.06.2020 internal code changes, bug fixes show weather_icon ", - "2.10.2" => "10.06.2020 bug fixes get/switch consumers ", - "2.10.1" => "08.06.2020 internal code changes, bug fixes ", - "2.10.0" => "03.06.2020 refactored login process ", - "2.9.0" => "01.06.2020 add get today statistic data ", - "2.8.1" => "31.05.2020 attribute timeout, maxCallCycle deleted ", - "2.8.0" => "31.05.2020 refactored process logic, attribute cookielifetime & getDataRetries deleted, command delCookieFile deleted ". - "new attribute maxCallCycle ", - "2.7.2" => "28.05.2020 delete cookie file if threshold of read retries reached ", - "2.7.1" => "28.05.2020 change cookie default location to ./log/_cookie.txt ", - "2.7.0" => "27.05.2020 improve stability of data retrieval, new command delCookieFile, new readings dailyCallCounter and dailyIssueCookieCounter ". - "current PV generation and consumption available in SMA graphics, some more improvements ", - "2.6.1" => "21.04.2020 update time in portalgraphics changed to last successful live data retrieval, credentials are not shown in list device ", - "2.6.0" => "20.04.2020 change package config, improve cookie management, decouple switch consumers from livedata retrieval ". - "some improvements according to PBP ", - "2.5.0" => "25.08.2019 change switch consumer to on<->automatic only in graphic overview, Forum: https://forum.fhem.de/index.php/topic,102112.msg969002.html#msg969002", - "2.4.5" => "22.08.2019 fix some warnings, Forum: https://forum.fhem.de/index.php/topic,102112.msg968829.html#msg968829 ", - "2.4.4" => "11.07.2019 fix consinject to show multiple consumer icons if planned ", - "2.4.3" => "07.07.2019 change header design of portal graphics again ", - "2.4.2" => "02.07.2019 change header design of portal graphics ", - "2.4.1" => "01.07.2019 replace space in consumer name by a valid sign for reading creation ", - "2.4.0" => "26.06.2019 support for FTUI-Widget ", - "2.3.7" => "24.06.2019 replace suggestIcon by consumerAdviceIcon ", - "2.3.6" => "21.06.2019 revise commandref ", - "2.3.5" => "20.06.2019 subroutine consinject added to pv, pvco style ", - "2.3.4" => "19.06.2019 change some readingnames, delete L4_plantOid, next04hours_state ", - "2.3.3" => "16.06.2019 change verbose 4 output, fix warning if no weather info was got ", - "2.3.2" => "14.06.2019 add request string to verbose 5, add battery data to live and historical consumer data ", - "2.3.1" => "13.06.2019 switch Credentials read from RAM to verbose 4, changed W/h->Wh and kW/h->kWh in PortalAsHtml ", - "2.3.0" => "12.06.2019 add set on,off,automatic cmd for controlled devices ", - "2.2.0" => "10.06.2019 relocate RestOfDay and Tomorrow data from level 3 to level 2, change readings to start all with uppercase, ". - "add consumer energy data of current day/month/year, new attribute \"verbose5Data\" ", - "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 " -); - -# Voreinstellungen -my $maxretries = 6; # max. Anzahl Wiederholungen in einem Abruf-Zyklus -my $thold = int($maxretries/2); # Schwellenwert nicht erfolgreicher Leseversuche in einem Zyklus mit dem gleichen Cookie -my $sleepretry = 0.5; # Sleep zwischen Data Call Retries (ohne Threshold Überschreitung) -my $sleepexc = 2; # Sleep vor neuem Datencall nach Überschreitung Threshold (Data Calls mit gleichem Cookie) -my $defmaxcycles = 19; # Standard max. Anzahl Datenabrufzyklen abgeleitet von Interval 120 (wird bei Automatic berechnet) - -my %statkeys = ( # Statistikdaten auszulesende Schlüssel - Energy => 1, - FeedIn => 1, - GridConsumption => 1, - SelfConsumption => 1, - SelfSupply => 1, - DirectConsumption => 1, - TotalConsumption => 1, - BackupOut => 1, - BackupIn => 1, - SelfConsumptionRate => 1, - DirectConsumptionRate => 1, - AutarkyRate => 1, -); - -my %subs; # Arbeitskopie von %stpl -my %stpl = ( # Ausgangstemplate Subfunktionen der Datenprovider - consumerMasterdata => { doit => 1, level => 'L00', func => '_getConsumerMasterdata'}, # mandatory - plantData => { doit => 1, level => 'L00', func => '_getPlantData' }, # mandatory - liveData => { doit => 0, level => 'L01', func => '_getLiveData' }, - weatherData => { doit => 0, level => 'L02', func => '_getWeatherData' }, - balanceDayData => { doit => 0, level => 'L03', func => '_getBalanceDayData' }, - forecastData => { doit => 0, level => 'L04', func => '_getForecastData' }, - consumerCurrentdata => { doit => 0, level => 'L05', func => '_getConsumerCurrData' }, - consumerDayData => { doit => 0, level => 'L06', func => '_getConsumerDayData' }, - consumerMonthData => { doit => 0, level => 'L07', func => '_getConsumerMonthData' }, - consumerYearData => { doit => 0, level => 'L08', func => '_getConsumerYearData' }, - plantLogbook => { doit => 0, level => 'L09', func => '_getPlantLogbook' }, -); - # Tags der verfügbaren Datenquellen -my @pd = qw( balanceDayData - consumerCurrentdata - consumerMasterdata - consumerDayData - consumerMonthData - consumerYearData - forecastData - liveData - plantData - weatherData - plantLogbook - ); - - - -############################################################### -# SMAPortal Initialize -############################################################### -sub Initialize { - my ($hash) = @_; - - my @pls; - for my $p (@pd) { - push @pls, $p if(!$stpl{$p}{doit}); - } - my $prov = join ",", @pls; - my $v5d = join ",", @pd; - - $hash->{DefFn} = \&Define; - $hash->{UndefFn} = \&Undefine; - $hash->{DeleteFn} = \&Delete; - $hash->{AttrFn} = \&Attr; - $hash->{SetFn} = \&Set; - $hash->{GetFn} = \&Get; - $hash->{DbLog_splitFn} = \&DbLog_split; - $hash->{AttrList} = "cookieLocation ". - "disable:0,1 ". - "interval ". - "plantLogbookTypes:multiple-strict,Info,Warning,Disturbance,Error ". - "plantLogbookApprovalState:Any,NotApproved ". - "providerLevel:multiple-strict,".$prov." ". - "showPassInLog:1,0 ". - "userAgent ". - "verbose5Data:multiple-strict,none,loginData,".$v5d." ". - $readingFnAttributes; - - eval { FHEM::Meta::InitMod( __FILE__, $hash ) }; ## no critic 'eval' # für Meta.pm (https://forum.fhem.de/index.php/topic,97589.0.html) - -return; -} - -############################################################### -# SMAPortal Define -############################################################### -sub Define { - my ($hash, $def) = @_; - my @a = split(/\s+/x, $def); - - return "Wrong syntax: use \"define SMAPortal\" " if(int(@a) < 1); - - $hash->{HELPER}{MODMETAABSENT} = 1 if($modMetaAbsent); # Modul Meta.pm nicht vorhanden - - $hash->{HELPER}{GETTER} = "all"; - $hash->{HELPER}{SETTER} = "none"; - - setVersionInfo($hash); # Versionsinformationen setzen - getcredentials($hash,1); # Credentials lesen und in RAM laden ($boot=1) - CallInfo ($hash); # Start Daten Abrufschleife - -return; -} - -############################################################### -# SMAPortal Undefine -############################################################### -sub Undefine { - my ($hash, $arg) = @_; - - RemoveInternalTimer($hash); - BlockingKill($hash->{HELPER}{RUNNING_PID}) if($hash->{HELPER}{RUNNING_PID}); - -return; -} - -############################################################### -# 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; -} - -############################################################### -# SMAPortal Set -############################################################### -sub Set { ## no critic 'complexity' - 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); - my $ad = ""; - - 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($hash->{HELPER}{PLANTOID} && $hash->{HELPER}{CONSUMER}) { - my $lfd = 0; - for my $key (keys %{$hash->{HELPER}{CONSUMER}{$lfd}}) { - my $dev = $hash->{HELPER}{CONSUMER}{$lfd}{DeviceName}; - if($dev) { - $ad .= "|" if($lfd != 0); - $ad .= $dev; - } - if ($dev && $setlist !~ /$dev/x) { - $setlist .= "$dev:on,off,auto "; - } - $lfd++; - } - } - } - - 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 = qq{This device provides a praphical output of SMA Sunny Portal values.\n}. - qq{The device "$name" needs to contain "forecastData" in attribute "providerLevel".\n}. - qq{If you want see weather conditions, this attribute has to contain "weatherData" as well. }. - qq{The attribute "providerLevel" must also contain "consumerCurrentdata" if you want switch your consumer connectet to the SMA Home Manager.}; - 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 consumerAdviceIcon 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; - my $jh = json2nameValue(AttrVal('WEB','styleData','')); - if($jh && ref $jh eq "HASH") { - $color = $jh->{'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"); - - return "SMA Portal Graphics device \"$htmldev\" created and assigned to room \"$room\"."; - - } elsif ($opt && $ad && $opt =~ /$ad/x) { - # Verbraucher schalten - $hash->{HELPER}{GETTER} = "none"; - $hash->{HELPER}{SETTER} = "$opt:$prop"; - CallInfo($hash); - - } else { - return "$setlist"; - } - -return; -} - -############################################################### -# 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") { - $hash->{HELPER}{GETTER} = "all"; - $hash->{HELPER}{SETTER} = "none"; - - 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; -} - -############################################################### -# SMAPortal DbLog_splitFn -############################################################### -sub DbLog_split { - my ($event, $device) = @_; - my $devhash = $defs{$device}; - my ($reading, $value, $unit); - - if($event =~ m/[_\-fd]Consumption|Quote/x) { - $event =~ /^L(.*):\s(.*)\s(.*)/x; - if($1) { - $reading = "L".$1; - $value = $2; - $unit = $3; - } - } - if($event =~ m/Power|PV|FeedIn|SelfSupply|Temperature|Total|Energy|Hour:|Hour(\d\d):/x) { - $event =~ /^L(.*):\s(.*)\s(.*)/x; - if($1) { - $reading = "L".$1; - $value = $2; - $unit = $3; - } - } - if($event =~ m/Next04Hours-IsConsumption|RestOfDay-IsConsumption|Tomorrow-IsConsumption|Battery/x) { - $event =~ /^L(.*):\s(.*)\s(.*)/x; - if($1) { - $reading = "L".$1; - $value = $2; - $unit = $3; - } - } - if($event =~ m/summary/x && $event =~ /(.*):\s(.*)\s(.*)/x) { - $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; - - $hash->{CREDENTIALS} = "Set"; # "Credentials" wird als Statusbit ausgewertet. Wenn nicht gesetzt -> Warnmeldung und keine weitere Verarbeitung - $success = 1; - } - } else { # boot = 0 -> Credentials aus RAM lesen, decoden und zurückgeben - $credstr = $hash->{HELPER}{".CREDENTIALS"} // $hash->{HELPER}{CREDENTIALS}; # Kompatibilität zu Versionen vor 2.6.1 - - 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, 4, "$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 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); - } else { - InternalTimer(gettimeofday()+1.0, "FHEM::SMAPortal::CallInfo", $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/interval/x) { - unless ($aVal =~ /^\d+$/x) {return " The Value for $aName is not valid. Use only figures 0-9 !";} - } - if($aName =~ m/interval/x) { - return qq{The interval must be >= 120 seconds or 0 if you don't want use automatic updates} if($aVal > 0 && $aVal < 30); - InternalTimer(gettimeofday()+1.0, "FHEM::SMAPortal::CallInfo", $hash, 0); - } - } - -return; -} - -################################################################ -# Hauptschleife BlockingCall -# $hash->{HELPER}{GETTER} -> Flag für get Informationen -# $hash->{HELPER}{SETTER} -> Parameter für set-Befehl -# $nc = 1 wenn Cycle Zähler nicht zurückgesetzt werden soll -# $nr = 1 wenn Retry Zähler nicht zurückgesetzt werden soll -# -################################################################ -sub CallInfo { - my ($hash,$nc,$nr) = @_; - my $name = $hash->{NAME}; - my $new; - - RemoveInternalTimer($hash,"FHEM::SMAPortal::CallInfo"); - - my ($interval,$maxcycles,$timeout,$ctime) = controlParams ($name); - - 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); # Wiederholungsintervall - $hash->{MODE} = "Automatic - next polltime: ".FmtTime($new); - } - - return if(IsDisabled($name)); - - for my $key (%stpl) { # festlegen welche Daten geliefert werden sollen - $subs{$name}{$key}{doit} = $stpl{$key}{doit}; - $subs{$name}{$key}{level} = $stpl{$key}{level}; - $subs{$name}{$key}{func} = $stpl{$key}{func}; - } - my @pl = split ",", AttrVal($name, "providerLevel", ""); - for my $p (@pl) { - $subs{$name}{$p}{doit} = 1; - } - - if ($hash->{HELPER}{RUNNING_PID}) { - BlockingKill($hash->{HELPER}{RUNNING_PID}); - delete($hash->{HELPER}{RUNNING_PID}); - } - - my $getp = $hash->{HELPER}{GETTER}; - my $setp = $hash->{HELPER}{SETTER}; - - if(!$nc && !$nr) { - Log3 ($name, 3, "$name - ################################################################"); - Log3 ($name, 3, "$name - ### start new set/get data from SMA Sunny Portal ###"); - Log3 ($name, 3, "$name - ################################################################"); - Log3 ($name, 4, "$name - calculated cycles summary time: $ctime"); - Log3 ($name, 4, "$name - calculated maximum cycles: $maxcycles"); - Log3 ($name, 4, "$name - calculated timeout: $timeout"); - } - - if(!$nc) { - $hash->{HELPER}{ACTCYCLE} = 1; - $hash->{HELPER}{CYCLEBTIME} = (gettimeofday())[0]; - } - $hash->{HELPER}{RETRIES} = 1 if(!$nr); - - my $ac = $hash->{HELPER}{ACTCYCLE}; - - Log3 ($name, 3, "$name - Running data cycle: $ac of $maxcycles"); - - readingsBeginUpdate ($hash); - readingsBulkUpdateIfChanged ($hash,"state","running - call cycle $ac"); - readingsEndUpdate ($hash,1); - - $hash->{HELPER}{RUNNING_PID} = BlockingCall("FHEM::SMAPortal::GetSetData", "$name|$getp|$setp", "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; -} - -################################################################ -# Steuerparameter berechnen / festlegen -################################################################ -sub controlParams { - my $name = shift; - - # Voreinstellungen - my $timeoutdef = 290; # Standard Timeout - my $definterval = 300; # Standard Interval - my $buffer = 5; # Sicherheitspuffer zum nächsten Intervall - - my $interval = AttrVal($name, "interval", $definterval); # 0 wenn manuell gesteuert - my $maxcycles = $defmaxcycles; - - # prognostizierte Zeit eines Zyklus - my $proctime = ($maxretries * 0.1); - my $ctime = ($maxretries * $sleepretry) + $sleepexc + $proctime; - $ctime = ReadingsVal ($name, "lastCycleTime", $ctime); - $ctime = 7 if($ctime > 7); # Ausreißer ignorieren - - if(!$interval) { # manueller Datenabruf - return ($interval,$maxcycles,$timeoutdef,$ctime); - } - - # max Anzahl Zyklen - $maxcycles = int(($interval - $buffer) / $ctime) if($ctime); - - # Timeout kalkulieren - my $timeout = int(($maxcycles * $ctime) + $proctime - $buffer); - $timeout = $timeoutdef if($timeout < 10); - -return ($interval,$maxcycles,$timeout,$ctime); -} - -################################################################ -## Datenabruf SMA-Portal -## schaltet auch Verbraucher des Sunny Home Managers -################################################################ -sub GetSetData { ## no critic 'complexity' - my ($string) = @_; - my ($name,$getp,$setp) = split("\\|",$string); - my $hash = $defs{$name}; - 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/".$name."_cookie.txt"); - my $v5d = AttrVal($name, "verbose5Data", "none"); - my $verbose = AttrVal($name, "verbose", 3); - my $state = "ok"; - my ($st,$lc) = ("",""); - my @da = (); - - my ($errstate,$reread,$retry,$exceed,$newcycle) = (0,0,0,0,0); - - my ($d,$op); - if($setp ne "none") { - # Verbraucher soll in den Status $op geschaltet werden - ($d,$op) = split(":",$setp); - } - - Log3 ($name, 5, "$name - Start operation with CookieLocation: $cookieLocation and UserAgent: $useragent"); - Log3 ($name, 5, "$name - data get: $getp, data set: ".(($d && $op)?($d." ".$op):$setp)); - - my $ua = LWP::UserAgent->new; - - # Default Header Daten - $ua->default_header("Accept" => "*/*", - "Accept-Encoding" => "gzip, deflate, br", - "Accept-Language" => "en-US;q=0.7,en;q=0.3", # deutsch: de,en-US;q=0.7,en;q=0.3 , englisch: en-US;q=0.7,en;q=0.3 - "Connection" => "keep-alive", - "Cookie" => "collapseNavi_state=shown", - "DNT" => 1, - "Host" => "www.sunnyportal.com", - "Referer" => "https://www.sunnyportal.com/FixedPages/HoManLive.aspx", - "User-Agent" => $useragent, - "X-Requested-With" => "XMLHttpRequest" - ); - - # Cookies - $ua->cookie_jar(HTTP::Cookies->new( file => "$cookieLocation", - ignore_discard => 1, - autosave => 1 - ) - ); - - handleCounter ($name, "dailyCallCounter"); # Abfragezähler setzen (Anzahl tägliche Wiederholungen von GetSetData) - - ### Login - ############## - my $paref = [ $name, $ua, $state, $errstate ]; - ($state, $errstate) = _checkLogin ($paref); - - if($errstate) { - $st = encode_base64 ( $state,""); - return "$name|0|0|$errstate|$getp|$setp|$st"; - } - - ### Verbraucher schalten - ####################################### - if($setp ne "none") { - my ($serial,$id); - for my $key (keys %{$hash->{HELPER}{CONSUMER}}) { - my $h = $hash->{HELPER}{CONSUMER}{$key}{DeviceName}; - if($h && $h eq $d) { - $serial = $hash->{HELPER}{CONSUMER}{$key}{SerialNumber}; - $id = $hash->{HELPER}{CONSUMER}{$key}{SUSyID}; - } - } - my $plantOid = $hash->{HELPER}{PLANTOID}; - my $res = $ua->post('https://www.sunnyportal.com/Homan/ConsumerBalance/SetOperatingMode', { - 'mode' => $op, - 'serialNumber' => $serial, - 'SUSyID' => $id, - 'plantOid' => $plantOid - } - ); - - $res = $res->decoded_content(); - Log3 ($name, 3, "$name - Set \"$d $op\" result: ".$res); - if($res eq "true") { - $state = "ok - switched consumer $d to $op"; - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "GETTER:all" ], 1); - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "SETTER:none"], 1); - } else { - $state = "Error - couldn't switch consumer $d to $op"; - } - } - - ### Daten abrufen - ############################# - if($getp ne "none") { - - for my $k (keys %{$subs{$name}}) { - next if(!$subs{$name}{$k}{doit}); - no strict "refs"; ## no critic 'NoStrict' - ($errstate,$state,$reread,$retry) = &{$subs{$name}{$k}{func}} ({ name => $name, - ua => $ua, - state => $state, - daref => \@da - }); - use strict "refs"; - - if($errstate) { - $st = encode_base64 ( $state,""); - return "$name|0|0|$errstate|$getp|$setp|$st"; - } - - goto &GetSetData if($reread); - - # Wiederholung Datenabruf innerhalb eines Cycle - my $retc = $hash->{HELPER}{RETRIES}; # aktuelle Retry-Zähler - - if($retry && $retc < $maxretries) { # neuer Retry im gleichen Zyklus (nicht wenn Verbraucher schalten) - $hash->{HELPER}{RETRIES}++; - if($retc == $thold) { # Schwellenwert Leseversuche erreicht -> Cookie File löschen - Log3 ($name, 3, qq{$name - Threshold reached, delete cookie and retry ...}); - sleep $sleepexc; # Threshold exceed -> Retry mit Cookie löschen - $exceed = 1; - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "RETRIES:".$hash->{HELPER}{RETRIES} ], 1); - return "$name|$exceed|$newcycle|$errstate|$getp|$setp"; - } - - sleep $sleepretry; - goto &GetSetData; - } - - # Wiederholung Datenabruf in einem neuen Cycle - my $ac = $hash->{HELPER}{ACTCYCLE}; - my $maxcycles = (controlParams $name)[1]; - if($retry && $ac < $maxcycles) { # neuer Zyklus (nicht wenn Verbraucher schalten) - Log3 ($name, 3, qq{$name - Maximum retries reached, delete cookie and start new cycle ...}); - $newcycle = 1; - return "$name|$exceed|$newcycle|$errstate|$getp|$setp"; - } - } - } - - # Daten müssen als Einzeiler zurückgegeben werden - $st = encode_base64 ( $state, ""); - if(@da) { - $lc = join "###", @da; - $lc = encode_base64 ( $lc, ""); - } - -return "$name|$exceed|$newcycle|$errstate|$getp|$setp|$st|$lc"; -} - -################################################################ -# Login Status checken und ggf. einloggen -################################################################ -sub _checkLogin { - my $paref = shift; - my $name = $paref->[0]; - my $ua = $paref->[1]; - my $state = $paref->[2]; - my $errstate = $paref->[3]; - - my $hash = $defs{$name}; - my $v5d = AttrVal($name, "verbose5Data", "none"); - my $verbose = AttrVal($name, "verbose", 3); - - my $loginp = $ua->post('https://www.sunnyportal.com/Templates/Start.aspx'); - my $retcode = $loginp->code; - my $location = $loginp->header('Location') // ""; - - if($verbose == 5 && $v5d =~ /loginData/) { - $ua->add_handler( request_send => sub { shift->dump; return } ); # for debugging - $ua->add_handler( response_done => sub { shift->dump; return } ); - } - - if ($loginp->is_success) { - if($v5d =~ /loginData/) { - Log3 ($name, 5, "$name - Status Login Page: ".$loginp->status_line); - Log3 ($name, 5, "$name - Header Location: ".$location); - } - - $retcode = $loginp->code; - $location = $loginp->header('Location') // ""; - - if($location ne "/FixedPages/HoManLive.aspx" || $retcode ne "302") { # keine aktive Session -> neuer Login - Log3 ($name, 4, "$name - User not logged in. Try login with credentials ..."); - - # Credentials abrufen - my ($success, $username, $password) = getcredentials($hash,0); - - if(!$success) { - Log3($name, 1, qq{$name - Credentials couldn't be retrieved successfully - make sure you've set it with "set $name credentials "}); - $state = "Credentials couldn't be read"; - $errstate = 1; - - } else { - my $usernameField = "ctl00\$ContentPlaceHolder1\$Logincontrol1\$txtUserName"; - my $passwordField = "ctl00\$ContentPlaceHolder1\$Logincontrol1\$txtPassword"; - my $mempasswd = "ctl00\$ContentPlaceHolder1\$Logincontrol1\$MemorizePassword"; - my $loginField = "__EVENTTARGET"; - my $loginButton = "ctl00\$ContentPlaceHolder1\$Logincontrol1\$LoginBtn"; - - $loginp = $ua->post('https://www.sunnyportal.com/Templates/Start.aspx',[$usernameField => $username, $passwordField => $password, $mempasswd => "on", "__EVENTTARGET" => $loginButton]); - $retcode = $loginp->code; - $location = $loginp->header('Location') // ""; - - if($v5d =~ /loginData/) { - Log3 ($name, 5, "$name - Status Redirect Page : ".$retcode); - Log3 ($name, 5, "$name - Header Redirect Location: ".$location); - } - - my $sc = $loginp->header('Set-Cookie') // ""; - my ($logname) = $sc =~ /SunnyPortalLoginInfo=Username=(.*?)&/sx; - Log3 ($name, 5, "$name - Header Set-Cookie: ".$sc) if($v5d =~ /loginData/); - - if($logname && $logname eq $username) { # Login erfolgeich(Landing Pages können im Portal eingestellt werden!) - Log3 ($name, 3, "$name - Login into SMA-Portal successfully done with user: $logname"); - handleCounter ($name, "dailyIssueCookieCounter"); # Cookie Ausstellungszähler setzen - - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "loginState:successful", "oldlogintime:".(gettimeofday())[0] ], 1); - $errstate = 0; - - } else { - Log3 ($name, 2, "$name - ERROR - Login into SMA-Portal failed !"); - $state = "login failed - check user and password"; - $errstate = 1; - } - } - } - - } elsif($loginp->is_redirect) { - $retcode = $loginp->code; - $location = $loginp->header('Location') // ""; - if($v5d =~ /loginData/) { - Log3 ($name, 5, "$name - Redirect return code: ".$retcode); - Log3 ($name, 5, "$name - Redirect Header Location: ".$location); - } - $errstate = 0; - - } else { - $errstate = 1; - $state = $loginp->status_line; - Log3 ($name, 1, "$name - ERROR Login Page: ".$state); - } - - $ua->remove_handler('request_send'); - $ua->remove_handler('response_done'); - -return ($state, $errstate); -} - -################################################################ -# Abruf Live Daten -################################################################ -sub _getLiveData { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - - my ($reread,$retry,$errstate) = (0,0,0); - - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); - my $cts = fhemTimeLocal(0, 0, 0, $mday, $mon, $year); - my $offset = fhemTzOffset($cts); - my $time = int(($cts + $offset) * 1000); # add Timestamp in Millisekunden and UTC - - ($errstate,$state,$reread,$retry) = __dispatchGet ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/homemanager?t='.$time, - tag => "liveData", - state => $state, - fnaref => [ qw( extractLiveData ) ], - addon => "", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Abruf Wetterdaten -################################################################ -sub _getWeatherData { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - - my ($reread,$retry,$errstate) = (0,0,0); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/Dashboard/Weather', - tag => "weatherData", - state => $state, - fnaref => [ qw( extractWeatherData ) ], - addon => "", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Abruf Anlagen Stammdaten -################################################################ -sub _getPlantData { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - - my ($reread,$retry,$errstate) = (0,0,0); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/HoMan/Forecast/LoadRecommendationData', - tag => "plantData", - state => $state, - fnaref => [ qw( extractPlantData ) ], - addon => "", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Abruf Consumer Stammdaten -################################################################ -sub _getConsumerMasterdata { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - - my ($reread,$retry,$errstate) = (0,0,0); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetLiveProxyValues', - tag => "consumerMasterdata", - state => $state, - fnaref => [ qw( extractConsumerMasterdata ) ], - addon => "", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Abruf Consumer current Data -################################################################ -sub _getConsumerCurrData { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - - my ($reread,$retry,$errstate) = (0,0,0); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetLiveProxyValues', - tag => "consumerCurrentdata", - state => $state, - fnaref => [ qw( extractConsumerCurrentdata ) ], - addon => "", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Abruf Consumer Tagesdaten -################################################################ -sub _getConsumerDayData { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - my $hash = $defs{$name}; - - my ($reread,$retry,$errstate) = (0,0,0); - - if(!$hash->{HELPER}{PLANTOID}) { - $errstate = 1; - $state = qq{The consumer data cannot be retrieved because the plant ID isn't set.}; - Log3 $name, 2, "$name - $state"; - return ($errstate,$state,$reread,$retry); - } - - my $PlantOid = $hash->{HELPER}{PLANTOID}; - my $dds = (split(/\s+/x, TimeNow()))[0]; - my $dde = (split(/\s+/x, FmtDateTime(time()+86400)))[0]; - - my $ccdd = 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetMeasuredValues?IntervalId=2&'.$PlantOid.'&StartTime='.$dds.'&EndTime='.$dde.''; - - # Energiedaten aktueller Tag - Log3 ($name, 4, "$name - Getting consumer energy data of current day"); - Log3 ($name, 4, "$name - Request date -> start: $dds, end: $dde"); - Log3 ($name, 5, "$name - Request consumer current day data string ->\n$ccdd"); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => $ccdd, - tag => "consumerDayData", - state => $state, - fnaref => [ qw( extractConsumerHistData ) ], - addon => "day", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Abruf Consumer Monatsdaten -################################################################ -sub _getConsumerMonthData { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - my $hash = $defs{$name}; - - my ($reread,$retry,$errstate) = (0,0,0); - - if(!$hash->{HELPER}{PLANTOID}) { - $errstate = 1; - $state = qq{The consumer data cannot be retrieved because the plant ID isn't set.}; - Log3 $name, 2, "$name - $state"; - return ($errstate,$state,$reread,$retry); - } - - my $PlantOid = $hash->{HELPER}{PLANTOID}; - my $dds = (split(/\s+/x, TimeNow()))[0]; - my $dde = (split(/\s+/x, FmtDateTime(time()+86400)))[0]; - - my ($mds,$me,$ye,$mde); - if($dds =~ /(.*)-(.*)-(.*)/x) { - $mds = "$1-$2-01"; - $me = (($2+1)<=12) ? $2+1 : 1; - $me = sprintf("%02d", $me); - $ye = ($2>$me) ? $1+1 : $1; - $mde = "$ye-$me-01"; - } - - my $ccmd = 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetMeasuredValues?IntervalId=4&'.$PlantOid.'&StartTime='.$mds.'&EndTime='.$mde.''; - - # Energiedaten aktueller Monat - Log3 ($name, 4, "$name - Getting consumer energy data of current month"); - Log3 ($name, 4, "$name - Request date -> start: $mds, end: $mde"); - Log3 ($name, 5, "$name - Request consumer current month data string ->\n$ccmd"); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => $ccmd, - tag => "consumerMonthData", - state => $state, - fnaref => [ qw( extractConsumerHistData ) ], - addon => "month", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Abruf Consumer Jahresdaten -################################################################ -sub _getConsumerYearData { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - my $hash = $defs{$name}; - - my ($reread,$retry,$errstate) = (0,0,0); - - if(!$hash->{HELPER}{PLANTOID}) { - $errstate = 1; - $state = qq{The consumer data cannot be retrieved because of the plant ID isn't set.}; - Log3 $name, 2, "$name - $state"; - return ($errstate,$state,$reread,$retry); - } - - my $PlantOid = $hash->{HELPER}{PLANTOID}; - my $dds = (split(/\s+/x, TimeNow()))[0]; - my $dde = (split(/\s+/x, FmtDateTime(time()+86400)))[0]; - - my ($mds,$me,$ye,$mde,$yds,$yde); - if($dds =~ /(.*)-(.*)-(.*)/x) { - $mds = "$1-$2-01"; - $me = (($2+1)<=12) ? $2+1 : 1; - $me = sprintf("%02d", $me); - $ye = ($2>$me) ? $1+1 : $1; - $mde = "$ye-$me-01"; - $yds = "$1-01-01"; - $yde = ($1+1)."-01-01"; - } - - my $ccyd = 'https://www.sunnyportal.com/Homan/ConsumerBalance/GetMeasuredValues?IntervalId=5&'.$PlantOid.'&StartTime='.$yds.'&EndTime='.$yde.''; - - # Energiedaten aktuelles Jahr - Log3 ($name, 4, "$name - Getting consumer energy data of current year"); - Log3 ($name, 4, "$name - Request date -> start: $yds, end: $yde"); - Log3 ($name, 5, "$name - Request consumer current year data string ->\n$ccyd"); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => $ccyd, - tag => "consumerYearData", - state => $state, - fnaref => [ qw( extractConsumerHistData ) ], - addon => "year", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Abruf Vorhersage Daten -################################################################ -sub _getForecastData { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - - my ($reread,$retry,$errstate) = (0,0,0); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/HoMan/Forecast/LoadRecommendationData', - tag => "forecastData", - state => $state, - fnaref => [ qw( extractForecastData extractConsumerPlanData ) ], - addon => "", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Abruf Statistik Daten Day -# (anchorTime beachten !) -################################################################ -sub _getBalanceDayData { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - - my ($reread,$retry,$errstate) = (0,0,0); - - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); - my $cts = fhemTimeLocal(0, 0, 0, $mday, $mon, $year); - my $offset = fhemTzOffset($cts); - my $anchort = int($cts + $offset); # anchorTime in UTC -> abzurufendes Datum - - my $tab = 1; # Tab 1 -> Tag , 2->Monat, 3->Jahr, 4->Gesamt - my %fields = ("Content-Type" => "application/json; charset=utf-8"); - my $cont = qq{ {"tabNumber":$tab,"anchorTime":$anchort} }; - - ($errstate,$state) = __dispatchPost ({ name => $name, - ua => $ua, - call => 'https://www.sunnyportal.com/FixedPages/HoManEnergyRedesign.aspx/GetLegendWithValues', - tag => "balanceDayData", - state => $state, - fnaref => [ qw( extractStatisticData ) ], - fields => \%fields, - content => $cont, - addon => "Day", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Abruf Anlagen Logbuch -################################################################ -sub _getPlantLogbook { ## no critic "not used" - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $state = $paref->{state}; - my $daref = $paref->{daref}; # Referenz zum Datenarray - my $hash = $defs{$name}; - - my ($reread,$retry,$errstate) = (0,0,0); - - if(!$hash->{HELPER}{PLANTOID}) { - $errstate = 1; - $state = qq{The logbook cannot be retrieved because of the plant ID isn't set.}; - Log3 $name, 2, "$name - $state"; - return ($errstate,$state,$reread,$retry); - } - - my $PlantOid = $hash->{HELPER}{PLANTOID}; - my $msgtypes = AttrVal($name, "plantLogbookTypes", "Warning,Disturbance,Error"); # möglich: Warning,Info,Disturbance,Error - my $appstate = AttrVal($name, "plantLogbookApprovalState", "Any"); - my $date = (split(/\s+/x, TimeNow()))[0]; - my $call = 'https://www.sunnyportal.com/Plants/'.$PlantOid.'/Log/Get?MessageTypes='.$msgtypes.'&ApprovalState='.$appstate.'&Device=None&MaxDateTime='.$date.'&Ticks=0'; - - Log3 ($name, 4, "$name - Retrieving the logbook data up to the date: $date"); - - ($errstate,$state) = __dispatchGet ({ name => $name, - ua => $ua, - call => $call, - tag => "plantLogbook", - state => $state, - fnaref => [ qw( extractPlantLogbook ) ], - addon => "", - daref => $daref - }); - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Dispatcher GET -################################################################ -sub __dispatchGet { - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $call = $paref->{call}; # Seitenaufruf zur Datenquelle - my $tag = $paref->{tag}; # Kennzeichen der abzurufenen Daten - my $state = $paref->{state}; - my $fnref = $paref->{fnaref}; # Referenz zu Array der aufzurufenden Funktion(en) zur Datenextraktion - my $fnaddon = $paref->{addon}; # optionales Addon für aufzurufende Funktion - my $daref = $paref->{daref}; # Referenz zum Datenarray - my $hash = $defs{$name}; - - my ($reread,$retry,$errstate) = (0,0,0); - - my ($data,$data_cont) = ___getData ({ name => $name, - ua => $ua, - call => $call, - tag => $tag - }); - - ($reread,$retry,$errstate,$state) = ___analyzeData ({ name => $name, - errstate => $errstate, - state => $state, - data => $data - }); - - return ($errstate,$state,$reread,$retry) if($errstate || $reread || $retry); - - if ($data_cont && $data_cont !~ m/undefined/ix) { - my @func = @$fnref; - no strict "refs"; ## no critic 'NoStrict' - for my $fn (@func) { - &{$fn} ($hash,$daref,$data_cont,$fnaddon); - } - use strict "refs"; - } - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Dispatcher POST -################################################################ -sub __dispatchPost { - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; # LWP Useragent - my $call = $paref->{call}; # Seitenaufruf zur Datenquelle - my $tag = $paref->{tag}; # Kennzeichen der abzurufenen Daten - my $state = $paref->{state}; - my $fnref = $paref->{fnaref}; # Referenz zu Array der aufzurufenden Funktion(en) zur Datenextraktion - my $fields = $paref->{fields}; # Referenz zum Hash der zu übertragenden PUSH Header - my $cont = $paref->{content}; # Content Daten für PUSH (String) - my $fnaddon = $paref->{addon}; # optionales Addon für aufzurufende Funktion - my $daref = $paref->{daref}; # Referenz zum Datenarray - my $hash = $defs{$name}; - - my ($reread,$retry,$errstate) = (0,0,0); - - my ($data,$data_cont) = ___postData ({ name => $name, - ua => $ua, - call => $call, - tag => $tag, - fields => $fields, - content => $cont, - }); - - ($reread,$retry,$errstate,$state) = ___analyzeData ({ name => $name, - errstate => $errstate, - state => $state, - data => $data - }); - - return ($errstate,$state) if($errstate); - - if ($data_cont && $data_cont !~ m/undefined/ix) { - my @func = @$fnref; - no strict "refs"; ## no critic 'NoStrict' - for my $fn (@func) { - &{$fn} ($hash,$daref,$data_cont,$fnaddon); - } - use strict "refs"; - } - -return ($errstate,$state,$reread,$retry); -} - -################################################################ -# Standard Abruf Daten GET -################################################################ -sub ___getData { - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; - my $call = $paref->{call}; - my $tag = $paref->{tag}; - - my $v5d = AttrVal($name, "verbose5Data", "none"); - my $verbose = AttrVal($name, "verbose", 3); - - my $cont; - - Log3 ($name, 4, "$name - Getting $tag"); - - if($verbose == 5 && $v5d =~ /$tag/x) { - $ua->add_handler( request_send => sub { shift->dump; return } ); # for debugging - $ua->add_handler( response_done => sub { shift->dump; return } ); - } - - my $data = $ua->get( $call ); - my $dcont = $data->content; - - $cont = eval{decode_json($dcont)} or do { $cont = $dcont }; - - if($v5d =~ /$tag/x) { - Log3 ($name, 5, "$name - Return Code: ".$data->code); - Log3 ($name, 5, "$name - $tag received:\n".Dumper $cont); - } - - $ua->remove_handler('request_send'); - $ua->remove_handler('response_done'); - -return ($data,$dcont); -} - -################################################################ -# Standard Abruf Daten POST -################################################################ -sub ___postData { - my $paref = shift; - my $name = $paref->{name}; - my $ua = $paref->{ua}; - my $call = $paref->{call}; - my $fields = $paref->{fields}; - my $content = $paref->{content}; - my $tag = $paref->{tag}; - - my $v5d = AttrVal($name, "verbose5Data", "none"); - my $verbose = AttrVal($name, "verbose", 3); - - my $cont; - - Log3 ($name, 4, "$name - Getting $tag"); - - if($verbose == 5 && $v5d =~ /$tag/x) { - $ua->add_handler( request_send => sub { shift->dump; return } ); # for debugging - $ua->add_handler( response_done => sub { shift->dump; return } ); - } - - my $data = $ua->post( $call, %$fields, Content => $content ); - - my $dcont = $data->content; - - $cont = eval{decode_json($dcont)} or do { $cont = $dcont }; - - if($v5d =~ /$tag/x) { - Log3 ($name, 5, "$name - Return Code: ".$data->code); - Log3 ($name, 5, "$name - $tag received:\n".Dumper $cont); - } - - $ua->remove_handler('request_send'); - $ua->remove_handler('response_done'); - -return ($data,$dcont); -} - -################################################################ -# analysiere abgerufene Daten -################################################################ -sub ___analyzeData { ## no critic 'complexity' - my $paref = shift; - my $name = $paref->{name}; - my $errstate = $paref->{errstate}; - my $state = $paref->{state}; - my $ad = $paref->{data}; - my $hash = $defs{$name}; - my ($reread,$retry) = (0,0); - my $data = ""; - - my $v5d = AttrVal($name, "verbose5Data", "none"); - my $ad_content = encode("utf8", $ad->decoded_content); - my $act = $hash->{HELPER}{RETRIES}; # Index aktueller Wiederholungsversuch - my $attstr = "Attempts read data again ... ($act of $maxretries)"; # Log vorbereiten - - my $wm1e = qq{Updating of the live data was interrupted}; - my $wm1d = qq{Die Aktualisierung der Live-Daten wurde unterbrochen}; - my $wm2e = qq{The current consumption could not be determined. The current purchased electricity is unknown}; - my $wm2d = qq{Der aktuelle Verbrauch konnte nicht ermittelt werden. Der aktuelle Netzbezug ist unbekannt}; - my $em1e = qq{Communication with the Sunny Home Manager is currently not possible}; - my $em1d = qq{Kommunikation mit dem Sunny Home Manager derzeit nicht m}; - my $em2e = qq{The current data cannot be retrieved from the PV system. Check the cabling and configuration}; - my $em2d = qq{Die aktuellen Daten .*? nicht von der Anlage abgerufen werden.*? Sie die Verkabelung und Konfiguration}; - - $data = eval{decode_json($ad_content)} or do { $data = $ad_content }; - - if(ref $data eq "HASH") { - for my $k (keys %{$data}) { - my $val = $data->{$k}; - next if(!defined $val); - - my @da; - - if(ref $val eq "ARRAY") { - for my $a (@{$val}) { - push @da, $a if(!ref $a); - } - } - - if(ref $val eq "HASH") { - for my $b (keys %{$val}) { - push @da, $b; - } - } - - $val = join " ", @da if(@da); - - if ($val && $k !~ /__type/ix) { - if($k =~ m/WarningMessages/x && $val =~ /$wm1e|$wm1d/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! - Log3 ($name, 3, "$name - Updating of the live data was interrupted. $attstr"); - $retry = 1; - return ($reread,$retry,$errstate,$state); - } - if($k =~ m/WarningMessages/x && $val =~ /$wm2e|$wm2d/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! - Log3 ($name, 3, "$name - The current consumption could not be determined. The current purchased electricity is unknown. $attstr"); - $retry = 1; - return ($reread,$retry,$errstate,$state); - } - if($k =~ m/ErrorMessages/x && $val =~ /$em1e|$em1d/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! - # Energiedaten konnten nicht ermittelt werden, Daten neu lesen mit Zeitverzögerung - Log3 ($name, 3, "$name - Communication with the Sunny Home Manager currently impossible. $attstr"); - $retry = 1; - return ($reread,$retry,$errstate,$state); - } - if($k =~ m/ErrorMessages/x && $val =~ /$em2e|$em2d/) { ## no critic 'regular expression' # Regular expression without "/x" flag nicht anwenden !!! - # Energiedaten konnten nicht ermittelt werden, Daten neu lesen mit Zeitverzögerung - Log3 ($name, 3, "$name - The current data cannot be retrieved from the PV system. $attstr"); - $retry = 1; - return ($reread,$retry,$errstate,$state); - } - } - } - - } else { - my $njdat = encode("utf8", $ad->as_string); - - if($njdat =~ /401\s-\sUnauthorized/x) { - Log3 ($name, 2, "$name - ERROR - User logged in but unauthorized"); - my($p1,$p2) = $njdat =~ /

    401\s-\sUnauthorized:.(.*)?<\/h2>.*?

    (.*)?<\/h3>/sx; - $state = ($p1 // "")." ".($p2 // ""); - } - - Log3 ($name, 5, "$name - No JSON Data received:\n ".$njdat); - - $errstate = 1; - } - -return ($reread,$retry,$errstate,$state); -} - -################################################################ -## Verarbeitung empfangene Daten, setzen Readings -################################################################ -sub ParseData { ## no critic 'complexity' - my $string = shift; - my @a = split("\\|",$string); - my $hash = $defs{$a[0]}; - my $name = $hash->{NAME}; - my @da = (); - - my ($errstate,$newcycle,$getp,$setp,$state,$exceed,$lc); - - $exceed = $a[1]; - $newcycle = $a[2]; - $errstate = $a[3]; - $getp = $a[4]; - $setp = $a[5]; - - my $ac = $hash->{HELPER}{ACTCYCLE}; - my $maxcycles = (controlParams $name)[1]; - - if($exceed) { - delete($hash->{HELPER}{RUNNING_PID}); - delcookiefile ($hash); - $hash->{HELPER}{GETTER} = $getp; - $hash->{HELPER}{SETTER} = $setp; - CallInfo($hash,1,1); # neuer Versuch (nach Threshold exceed) im gleichen Cycle mit gelöschtem Cookie - return; - } - - if($newcycle && $ac < $maxcycles) { - delete($hash->{HELPER}{RUNNING_PID}); - $hash->{HELPER}{GETTER} = $getp; - $hash->{HELPER}{SETTER} = $setp; - $hash->{HELPER}{ACTCYCLE}++; - CallInfo($hash,1,0); # neuer Abrufcycle - return; - } - - # Laufzeit für einen Cycle berechnen - my $btime = $hash->{HELPER}{CYCLEBTIME}; - my $etime = (gettimeofday())[0]; - my $cycles = $hash->{HELPER}{ACTCYCLE}; - my $ctime = int(($etime - $btime) / $cycles); # durchschnittliche Laufzeit für einen Zyklus - - $state = decode_base64($a[6]); - - if($a[7]) { - $lc = decode_base64($a[7]); - @da = split "###", $lc; - } - - delread($hash, 1); - - readingsBeginUpdate($hash); - - for my $elem (@da) { - my ($rn,$rval) = split ":", $elem, 2; - readingsBulkUpdate($hash, $rn, $rval); - } - - readingsEndUpdate($hash, 1); - - my $ldlv = $stpl{liveData}{level}; - my $cclv = $stpl{consumerCurrentdata}{level}; - my $lddo = $subs{$name}{liveData}{doit}; - - my $pv = ReadingsNum($name, "${ldlv}_PV" , 0); - my $fi = ReadingsNum($name, "${ldlv}_FeedIn" , 0); - my $gc = ReadingsNum($name, "${ldlv}_GridConsumption", 0); - my $sum = $fi-$gc; - - if(!$errstate && $lddo && !$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(!$errstate) { - if($setp ne "none") { - my ($d,$op) = split(":",$setp); - $op = ($op eq "auto") ? "off (automatic)" : $op; - readingsBulkUpdate($hash, "${cclv}_${d}_Switch", $op); - } - readingsBulkUpdate($hash, "lastCycleTime", $ctime ) if($ctime > 0); - readingsBulkUpdate($hash, "summary" , $sum." W") if($subs{$name}{liveData}{doit}); - - } else { - readingsBulkUpdate($hash, "loginState", "failed"); - } - - readingsBulkUpdate($hash, "state", $state); - - readingsEndUpdate($hash, 1); - - delcookiefile ($hash); - delete($hash->{HELPER}{RUNNING_PID}); - - $hash->{HELPER}{GETTER} = "all"; - $hash->{HELPER}{SETTER} = "none"; - - 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}); - $hash->{HELPER}{GETTER} = "all"; - $hash->{HELPER}{SETTER} = "none"; - - delcookiefile ($hash); - -return; -} - -################################################################ -# Cookie-Datei löschen -################################################################ -sub delcookiefile { - my ($hash,$source) = @_; - my $name = $hash->{NAME}; - my $err = ""; - - my $cookieLocation = AttrVal($name, "cookieLocation", "./log/".$name."_cookie.txt"); - my $delfile = unlink ($cookieLocation) or $err = $!; - - if($delfile) { - Log3 $name, 3, "$name - Cookie file deleted: $cookieLocation"; - } - -return ($err); -} - -################################################################ -## Auswertung Live Daten -################################################################ -sub extractLiveData { ## no critic 'complexity' - my $hash = shift; - my $daref = shift; - my $live = shift; - my $name = $hash->{NAME}; - my $val = ""; - - Log3 ($name, 4, "$name - ##### extracting live data #### "); - - $live = eval{decode_json($live)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); - return; - }; - - my ($errMsg,$warnMsg,$infoMsg) = (0,0,0); - my $lv = $stpl{liveData}{level}; - - if (ref $live eq "HASH") { - push @$daref, "${lv}_FeedIn:" .($live->{FeedIn} // 0)." W"; - push @$daref, "${lv}_GridConsumption:" .($live->{GridConsumption} // 0)." W"; - push @$daref, "${lv}_PV:" .($live->{PV} // 0)." W"; - push @$daref, "${lv}_AutarkyQuote:" .($live->{AutarkyQuote} // 0)." %"; - push @$daref, "${lv}_SelfConsumption:" .($live->{SelfConsumption} // 0)." W"; - push @$daref, "${lv}_SelfConsumptionQuote:" .($live->{SelfConsumptionQuote} // 0)." %"; - push @$daref, "${lv}_SelfSupply:" .($live->{SelfSupply} // 0)." W"; - push @$daref, "${lv}_TotalConsumption:" .($live->{TotalConsumption} // 0)." W"; - - push @$daref, "${lv}_BatteryIn:" .$live->{BatteryIn}. " W" if(defined $live->{BatteryIn}); - push @$daref, "${lv}_BatteryOut:" .$live->{BatteryOut}." W" if(defined $live->{BatteryOut}); - push @$daref, "${lv}_BatteryMode:" .$live->{BatteryMode}."" if(defined $live->{BatteryMode}); - push @$daref, "${lv}_BatteryStateOfHealth:" .$live->{BatteryStateOfHealth}."" if(defined $live->{BatteryStateOfHealth}); - push @$daref, "${lv}_BatteryChargeStatus:" .$live->{BatteryChargeStatus}." %" if(defined $live->{BatteryChargeStatus}); - push @$daref, "${lv}_DirectConsumption:" .$live->{DirectConsumption}." W" if(defined $live->{DirectConsumption}); - push @$daref, "${lv}_DirectConsumptionQuote:" .$live->{DirectConsumptionQuote}." %" if(defined $live->{DirectConsumptionQuote}); - - push @$daref, "${lv}_ModuleTemperature:" .$live->{ModuleTemperature}."" if(defined $live->{ModuleTemperature}); - push @$daref, "${lv}_OperationHealth:" .$live->{OperationHealth}."" if(defined $live->{OperationHealth}); - push @$daref, "${lv}_Insolation:" .$live->{Insolation}."" if(defined $live->{Insolation}); - push @$daref, "${lv}_WindSpeed:" .$live->{WindSpeed}."" if(defined $live->{WindSpeed}); - push @$daref, "${lv}_EnvironmentTemperature:" .$live->{EnvironmentTemperature}."" if(defined $live->{EnvironmentTemperature}); - - if($live->{ErrorMessages}[0]) { - my @em; - $errMsg = 1; - for my $a (@{$live->{ErrorMessages}}) { - push @em, $a; - } - $val = join " ", @em if(@em); - push @$daref, "${lv}_ErrorMessages:".qq{Message got from SMA Sunny Portal:
    $val}; - } - - if($live->{WarningMessages}[0]) { - my @wm; - $warnMsg = 1; - for my $a (@{$live->{WarningMessages}}) { - push @wm, $a; - } - $val = join " ", @wm if(@wm); - push @$daref, "${lv}_WarningMessages:".qq{Message got from SMA Sunny Portal:
    $val}; - } - - if($live->{InfoMessages}[0]) { - my @im; - $infoMsg = 1; - for my $a (@{$live->{InfoMessages}}) { - push @im, $a; - } - $val = join " ", @im if(@im); - push @$daref, "${lv}_InfoMessages:".qq{Message got from SMA Sunny Portal:
    $val}; - } - } - - BlockingInformParent("FHEM::SMAPortal::delReadingFromBlocking", [$name, "${lv}_ErrorMessages"] , 1) if(!$errMsg); - BlockingInformParent("FHEM::SMAPortal::delReadingFromBlocking", [$name, "${lv}_WarningMessages"], 1) if(!$warnMsg); - BlockingInformParent("FHEM::SMAPortal::delReadingFromBlocking", [$name, "${lv}_InfoMessages"] , 1) if(!$infoMsg); - -return; -} - -################################################################ -## Auswertung Forecast Daten -################################################################ -sub extractForecastData { ## no critic 'complexity' - my $hash = shift; - my $daref = shift; - my $forecast = shift; - my $name = $hash->{NAME}; - - Log3 ($name, 4, "$name - ##### extracting forecast data #### "); - - $forecast = eval{decode_json($forecast)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); - 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 $lv = $stpl{forecastData}{level}; - my $PV_sum = 0; - my $consum_sum = 0; - my $sum = 0; - - # 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 - for 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$/x; - 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 ($obj_nr < 24) { - my $time_str = "ThisHour"; - $time_str = "NextHour".sprintf("%02d", $obj_nr) if($fc_diff_hours>0); - if($time_str =~ /NextHour/x) { - push @$daref, "${lv}_${time_str}_Time:". TimeAdjust($hash,$fc_obj->{'TimeStamp'}->{'DateTime'},$tkind); - push @$daref, "${lv}_${time_str}_PvMeanPower:". int( $fc_obj->{'PvMeanPower'}->{'Amount'} )." Wh"; # in W als Durchschnitt geliefet, d.h. eine Stunde -> Wh - push @$daref, "${lv}_${time_str}_Consumption:". int( $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 )." Wh"; # {'ConsumptionForecast'}->{'Amount'} wird als J = Ws geliefert - push @$daref, "${lv}_${time_str}_IsConsumptionRecommended:". ($fc_obj->{'IsConsumptionRecommended'} ? "yes" : "no"); - push @$daref, "${lv}_${time_str}_Total:". (int($fc_obj->{'PvMeanPower'}->{'Amount'}) - int($fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600))." Wh"; - - # add WeatherId Helper to show weather icon - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "${lv}_".${time_str}."_WeatherId:".int($fc_obj->{'WeatherId'}) ], 1) if(defined $fc_obj->{'WeatherId'}); - } - if($time_str =~ /ThisHour/x) { - push @$daref, "${lv}_${time_str}_Time:". TimeAdjust($hash,$fc_obj->{'TimeStamp'}->{'DateTime'},$tkind); - push @$daref, "${lv}_${time_str}_PvMeanPower:". int( $fc_obj->{'PvMeanPower'}->{'Amount'} )." Wh"; # in W als Durchschnitt geliefet, d.h. eine Stunde -> Wh - push @$daref, "${lv}_${time_str}_Consumption:". int( $fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600 )." Wh"; # {'ConsumptionForecast'}->{'Amount'} wird als J = Ws geliefert - push @$daref, "${lv}_${time_str}_IsConsumptionRecommended:". ($fc_obj->{'IsConsumptionRecommended'} ? "yes" : "no"); - push @$daref, "${lv}_${time_str}_Total:". (int($fc_obj->{'PvMeanPower'}->{'Amount'}) - int($fc_obj->{'ConsumptionForecast'}->{'Amount'} / 3600))." Wh"; - - # add WeatherId Helper to show weather icon - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "${lv}_".${time_str}."_WeatherId:".int($fc_obj->{'WeatherId'}) ], 1) if(defined $fc_obj->{'WeatherId'}); - } - } - - # Increment object counter - $obj_nr++; - } - - push @$daref, "${lv}_Next04Hours_Consumption:". int( $nextFewHoursSum{'Consumption'} )." Wh"; - push @$daref, "${lv}_Next04Hours_PV:". int( $nextFewHoursSum{'PV'} )." Wh"; - push @$daref, "${lv}_Next04Hours_Total:". int( $nextFewHoursSum{'Total'} )." Wh"; - push @$daref, "${lv}_Next04Hours_IsConsumptionRecommended:". int( $nextFewHoursSum{'ConsumpRcmd'} )." h"; - push @$daref, "${lv}_ForecastToday_Consumption:". $consum_sum." Wh"; - push @$daref, "${lv}_ForecastToday_PV:". $PV_sum." Wh"; - push @$daref, "${lv}_RestOfDay_Consumption:". int( $restOfDaySum{'Consumption'} )." Wh"; - push @$daref, "${lv}_RestOfDay_PV:". int( $restOfDaySum{'PV'} )." Wh"; - push @$daref, "${lv}_RestOfDay_Total:". int( $restOfDaySum{'Total'} )." Wh"; - push @$daref, "${lv}_RestOfDay_IsConsumptionRecommended:". int( $restOfDaySum{'ConsumpRcmd'} )." h"; - push @$daref, "${lv}_Tomorrow_Consumption:". int( $tomorrowSum{'Consumption'} )." Wh"; - push @$daref, "${lv}_Tomorrow_PV:". int( $tomorrowSum{'PV'} )." Wh"; - push @$daref, "${lv}_Tomorrow_Total:". int( $tomorrowSum{'Total'} )." Wh"; - push @$daref, "${lv}_Tomorrow_IsConsumptionRecommended:". int( $tomorrowSum{'ConsumpRcmd'} )." h"; - -return; -} - -################################################################ -## Auswertung Wetterdaten -################################################################ -sub extractWeatherData { - my $hash = shift; - my $daref = shift; - my $weather = shift; - my $name = $hash->{NAME}; - - Log3 ($name, 4, "$name - ##### extracting weather data #### "); - - $weather = eval{decode_json($weather)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); - return; - }; - my $lv = $stpl{weatherData}{level}; - - for my $k (keys %$weather) { - next if(!$k); - - Log3 ($name, 4, qq{$name - Weatherdata content "$k": }.Dumper $weather->{$k}); - - if (ref $weather->{$k} eq "HASH") { - my $ih = $weather->{$k}; - - my $day = $k; - my $symbol = encode("utf8", $weather->{$k}{TemperatureSymbol}); - my $temp = sprintf("%.1f", $weather->{$k}{Temperature}); - my $wdesc = $weather->{$k}{WeatherDescription}; - $wdesc =~ s/t/T/x if($wdesc =~ /^t/x); - $day =~ s/t/T/x; - - push @$daref, "${lv}_${day}_Temperature:$temp $symbol"; - push @$daref, "${lv}_${day}_WeatherDescription:$wdesc"; - } - } - -return; -} - -################################################################ -# Auswertung Statistic Daten -# $period = Day | Month | Year -################################################################ -sub extractStatisticData { - my $hash = shift; - my $daref = shift; - my $statistic = shift; - my $period = shift; - my $name = $hash->{NAME}; - my $sd; - - Log3 ($name, 4, "$name - ##### extracting balance data #### "); - - $statistic = eval{decode_json($statistic)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); - return; - }; - my $lv = $stpl{balanceDayData}{level}; - - if(ref $statistic eq "HASH") { - $sd = decode_json ( encode('UTF-8', $statistic->{d}) ); - } - - if($sd && ref $sd eq "ARRAY") { - for my $a (@$sd) { # jedes ARRAY-Element ist ein HASH - my $k = $a->{Key}; - my $v = $a->{Value}; - push @$daref, "${lv}_Today_${k}:$v" if(defined $statkeys{$k}); - } - } - -return; -} - -################################################################ -## Auswertung Anlagendaten -################################################################ -sub extractPlantData { - my $hash = shift; - my $daref = shift; - my $forecast = shift; - my $name = $hash->{NAME}; - my ($amount,$unit); - - Log3 ($name, 4, "$name - ##### extracting plant data #### "); - - $forecast = eval{decode_json($forecast)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); - return; - }; - my $lv = $stpl{plantData}{level}; - my $plantOid = $forecast->{'ForecastTimeframes'}->{'PlantOid'}; - - if ($plantOid) { # wichtig für erweiterte Selektionen - Log3 ($name, 4, "$name - Plant ID: ".$plantOid); - $hash->{HELPER}{PLANTOID} = $plantOid; - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "PLANTOID:$plantOid"], 1); - } else { - Log3 ($name, 4, "$name - Plant ID not set !"); - } - - my $ppp = $forecast->{'PlantPeakPower'}; - if($ppp) { - $amount = $forecast->{'PlantPeakPower'}{'Amount'}; - $unit = $forecast->{'PlantPeakPower'}{'StandardUnit'}{'Symbol'}; - - push @$daref, "${lv}_PlantPeakPower:$amount $unit"; - - Log3 $name, 4, "$name - Plantdata \"PlantPeakPower Amount\": $amount"; - Log3 $name, 4, "$name - Plantdata \"PlantPeakPower Symbol\": $unit"; - } - -return; -} - -################################################################ -## Auswertung Anlagenlogbuch -################################################################ -sub extractPlantLogbook { - my $hash = shift; - my $daref = shift; - my $logdata = shift; - my $name = $hash->{NAME}; - - Log3 ($name, 4, "$name - ##### extracting plant logbook data #### "); - - $logdata = eval{decode_json($logdata)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); - return; - }; - my $lv = $stpl{plantLogbook}{level}; - - my %colors = ( # Farben Highlighting - "Info" => qq{}, - "Warning" => qq{}, - "Warnung" => qq{}, - "Disturbance" => qq{}, - "Störung" => qq{}, - "Error" => qq{}, - "Fehler" => qq{}, - "Unknown" => qq{}, - ); - my $eh = ""; # Endestring Highlighting - - if(ref $logdata->{aaData} eq "ARRAY") { - my @ld = @{$logdata->{aaData}}; - for my $ae (@ld) { # jedes ARRAY-Element ist ein HASH - my $dn = encode("utf8", $ae->{DeviceName} ); - my $ts = $ae->{Timestamp}; - my $dc = encode("utf8", $ae->{Description} ); - my $id = $ae->{MessageId}; - my ($mt) = $ae->{MessageType} =~ /alt='(.*?)'/x; - $mt = encode("utf8", $mt) // "Unknown"; - - my $bh = $colors{$mt}; - my $v = qq{$bh $mt $eh
    $dn : $ts
    $dc}; - - push @$daref, "${lv}_LogbookEntry_${id}:$v"; - } - } - -return; -} - -################################################################ -## Auswertung Consumer Plan Data (aus forecastData) -################################################################ -sub extractConsumerPlanData { - my $hash = shift; - my $daref = shift; - my $forecast = shift; - my $name = $hash->{NAME}; - my %consumers; - my ($key,$val); - - Log3 ($name, 4, "$name - ##### extracting consumer plan data #### "); - - $forecast = eval{decode_json($forecast)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); - return; - }; - - my $lv = $stpl{forecastData}{level}; - - # Schleife über alle Consumer Objekte - my $i = 0; - for my $c (@{$forecast->{'Consumers'}}) { - $consumers{"${i}_ConsumerName"} = $c->{'ConsumerName'}; - $consumers{"${i}_ConsumerOid"} = $c->{'ConsumerOid'}; - $i++; - } - - if(%consumers && $forecast->{'ForecastTimeframes'}) { - # es sind Vorhersagen zu geplanten Verbraucherschaltzeiten vorhanden - # TimeFrameStart/End Kind: "Utc" - for 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'}; - for my $k (keys(%consumers)) { - $val = $consumers{$k}; - if($val eq $deviceOid && $k =~ /^(\d+)_.*$/x) { - my $lfn = $1; - $consumers{"${lfn}_PlannedOpTimeStart"} = $timeFrameStart; - $consumers{"${lfn}_PlannedOpTimeEnd"} = $timeFrameEnd; - } - } - } - } - - if(%consumers) { - for my $key (keys(%consumers)) { - Log3 $name, 4, "$name - Consumer data \"$key\": ".$consumers{$key}; - if($key =~ /ConsumerName/x && $key =~ /^(\d+)_.*$/x) { - my $lfn = $1; - my $cn = $consumers{"${lfn}_ConsumerName"}; # Verbrauchername - next if(!$cn); - $cn = replaceJunkSigns($cn); # evtl. Umlaute/Leerzeichen im Verbrauchernamen ersetzen - my $pos = $consumers{"${lfn}_PlannedOpTimeStart"}; # geplanter Start - my $poe = $consumers{"${lfn}_PlannedOpTimeEnd"}; # geplantes Ende - my $rb = "${lv}_${cn}_PlannedOpTimeBegin"; - my $re = "${lv}_${cn}_PlannedOpTimeEnd"; - my $rp = "${lv}_${cn}_Planned"; - if($pos) { - push @$daref, "$rb:$pos"; - push @$daref, "$rp:yes"; - } else { - push @$daref, "$rb:undefined"; - push @$daref, "$rp:no"; - } - if($poe) { - push @$daref, "$re:$poe"; - } else { - push @$daref, "$re:undefined"; - } - } - } - } - -return; -} - -################################################################ -## Auswertung Consumer Stammdaten -################################################################ -sub extractConsumerMasterdata { - my $hash = shift; - my $daref = shift; - my $clivedata = shift; - my $name = $hash->{NAME}; - my %consumers; - my ($i,$res); - - Log3 ($name, 4, "$name - ##### extracting consumer master data #### "); - - $clivedata = eval{decode_json($clivedata)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); - return; - }; - my $lv = $stpl{consumerMasterdata}{level}; - - # allen Consumer Objekten die ID zuordnen - $i = 0; - for my $c (@{$clivedata->{'MeasurementData'}}) { - $consumers{"${i}_ConsumerName"} = $c->{'DeviceName'}; - $consumers{"${i}_ConsumerOid"} = $c->{'Consume'}{'ConsumerOid'}; - $consumers{"${i}_ConsumerLfd"} = $i; - my $cn = $consumers{"${i}_ConsumerName"}; # Verbrauchername - next if(!$cn); - $cn = replaceJunkSigns($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'}; - - $i++; - } - - if($hash->{HELPER}{CONSUMER}) { - for my $key (keys %{$hash->{HELPER}{CONSUMER}}) { - for my $parname (keys %{$hash->{HELPER}{CONSUMER}{$key}}) { - my $val = $hash->{HELPER}{CONSUMER}{$key}{$parname}; - next if(!defined $val); - Log3 ($name, 4, "$name - CONSUMER master data: $key -> $parname = $val"); - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, "NULL", "CONSUMER:$key:$parname:$val"], 1); - } - } - } - -return; -} - -################################################################ -## Auswertung Consumer Current Data -################################################################ -sub extractConsumerCurrentdata { - my $hash = shift; - my $daref = shift; - my $clivedata = shift; - my $name = $hash->{NAME}; - my %consumers; - my ($i,$res); - - Log3 ($name, 4, "$name - ##### extracting consumer current data #### "); - - $clivedata = eval{decode_json($clivedata)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); - return; - }; - my $lv = $stpl{consumerCurrentdata}{level}; - - # allen Consumer Objekten die ID zuordnen - $i = 0; - for my $c (@{$clivedata->{'MeasurementData'}}) { - $consumers{"${i}_ConsumerName"} = $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 - next if(!$cn); - $cn = replaceJunkSigns($cn); - - push @$daref, "${lv}_${cn}_Power:".$cpower." W" if(defined $cpower); - - $i++; - } - - if(%consumers && $clivedata->{'ParameterData'}) { - # es sind Daten zu den Verbrauchern vorhanden - # Kind: "Utc" ? - $i = 0; - for my $c (@{$clivedata->{'ParameterData'}}) { - my $tkind = $c->{'Parameters'}[0]{'Timestamp'}{'Kind'}; # Zeitart: Unspecified, Utc - 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 - next if(!$cn); - $cn = replaceJunkSigns($cn); # evtl. Umlaute/Leerzeichen im Verbrauchernamen ersetzen - - if(!$GriSwStt && $GriSwAuto) { - $res = "off (automatic)"; - } elsif (!$GriSwStt && !$GriSwAuto) { - $res = "off"; - } elsif ($GriSwStt) { - $res = "on"; - } else { - $res = "undefined"; - } - - push @$daref, "${lv}_${cn}_Switch:$res"; - push @$daref, "${lv}_${cn}_SwitchLastTime:$ltchange"; - - $i++; - } - } - -return; -} - -################################################################ -## Auswertung Consumer History Energy Data -## $tf = Time Frame -################################################################ -sub extractConsumerHistData { ## no critic 'complexity' - my $hash = shift; - my $daref = shift; - my $chdata = shift; - my $tf = shift; - my $name = $hash->{NAME}; - my %consumers; - my ($i,$gcr,$gct,$pcr,$pct,$tct,$bcr,$bct); - - Log3 ($name, 4, "$name - ##### extracting consumer history data #### "); - - $chdata = eval{decode_json($chdata)} or do { Log3 ($name, 2, "$name - ERROR - can't decode JSON Data"); - return; - }; - my $livelvl = $stpl{liveData}{level}; - my $bataval = (defined(ReadingsNum($name,"${livelvl}_BatteryIn", undef)) || defined(ReadingsNum($name,"${livelvl}_BatteryOut", undef)))?1:0; # Identifikation ist Battery vorhanden ? - - my $dlvl = $stpl{consumerDayData}{level}; - my $mlvl = $stpl{consumerMonthData}{level}; - my $ylvl = $stpl{consumerYearData}{level}; - - # allen Consumer Objekte die ID zuordnen - $i = 0; - for my $c (@{$chdata->{'Consumers'}}) { - $consumers{"${i}_ConsumerName"} = $c->{'DeviceName'}; - $consumers{"${i}_ConsumerOid"} = $c->{'ConsumerOid'}; - $consumers{"${i}_ConsumerLfd"} = $i; - my $cpower = $c->{'TotalEnergy'}{'Measurement'}; # Energieverbrauch im Timeframe in Wh - my $cn = $consumers{"${i}_ConsumerName"}; # Verbrauchername - next if(!$cn); - $cn = replaceJunkSigns($cn); - - if($tf =~ /month|year/x) { - $tct = $c->{'TotalEnergyMix'}{'TotalConsumptionTotal'}; # Gesamtverbrauch im Timeframe in Wh - $gcr = $c->{'TotalEnergyMix'}{'GridConsumptionRelative'}; # Anteil des Netzbezugs im Timeframe am Gesamtverbrauch in % - $gct = $c->{'TotalEnergyMix'}{'GridConsumptionTotal'}; # Anteil des Netzbezugs im Timeframe am Gesamtverbrauch in Wh - $pcr = $c->{'TotalEnergyMix'}{'PvConsumptionRelative'}; # Anteil des PV-Nutzung im Timeframe am Gesamtverbrauch in % - $pct = $c->{'TotalEnergyMix'}{'PvConsumptionTotal'}; # Anteil des PV-Nutzung im Timeframe am Gesamtverbrauch in Wh - $bcr = $c->{'TotalEnergyMix'}{'BatteryConsumptionRelative'}; # Anteil der Batterie-Nutzung im Timeframe am Gesamtverbrauch in % - $bct = $c->{'TotalEnergyMix'}{'BatteryConsumptionTotal'}; # Anteil der Batterie-Nutzung im Timeframe am Gesamtverbrauch in Wh - } - - push @$daref, "${dlvl}_${cn}_EnergyTotalDay:". sprintf("%.0f", $cpower). " Wh" if(defined($cpower) && $tf eq "day"); - push @$daref, "${mlvl}_${cn}_EnergyTotalMonth:". sprintf("%.0f", $cpower). " Wh" if(defined($cpower) && $tf eq "month"); - push @$daref, "${ylvl}_${cn}_EnergyTotalYear:". sprintf("%.0f", $cpower). " Wh" if(defined($cpower) && $tf eq "year"); - - push @$daref, "${mlvl}_${cn}_EnergyRelativeMonthGrid:". sprintf("%.0f", $gcr). " %" if(defined($gcr) && $tf eq "month"); - push @$daref, "${mlvl}_${cn}_EnergyTotalMonthGrid:". sprintf("%.0f", $gct). " Wh" if(defined($gct) && $tf eq "month"); - push @$daref, "${mlvl}_${cn}_EnergyRelativeMonthPV:". sprintf("%.0f", $pcr). " %" if(defined($pcr) && $tf eq "month"); - push @$daref, "${mlvl}_${cn}_EnergyTotalMonthPV:". sprintf("%.0f", $pct). " Wh" if(defined($pct) && $tf eq "month"); - push @$daref, "${mlvl}_${cn}_EnergyRelativeMonthBatt:". sprintf("%.0f", $bcr). " %" if(defined($bcr) && $bataval && $tf eq "month"); - push @$daref, "${mlvl}_${cn}_EnergyTotalMonthBatt:". sprintf("%.0f", $bct). " Wh" if(defined($bct) && $bataval && $tf eq "month"); - - push @$daref, "${ylvl}_${cn}_EnergyRelativeYearGrid:". sprintf("%.0f", $gcr). " %" if(defined($gcr) && $tf eq "year"); - push @$daref, "${ylvl}_${cn}_EnergyTotalYearGrid:". sprintf("%.0f", $gct). " Wh" if(defined($gct) && $tf eq "year"); - push @$daref, "${ylvl}_${cn}_EnergyRelativeYearPV:". sprintf("%.0f", $pcr). " %" if(defined($pcr) && $tf eq "year"); - push @$daref, "${ylvl}_${cn}_EnergyTotalYearPV:". sprintf("%.0f", $pct). " Wh" if(defined($pct) && $tf eq "year"); - push @$daref, "${ylvl}_${cn}_EnergyRelativeYearBatt:". sprintf("%.0f", $bcr). " %" if(defined($bcr) && $bataval && $tf eq "year"); - push @$daref, "${ylvl}_${cn}_EnergyTotalYearBatt:". sprintf("%.0f", $bct). " Wh" if(defined($bct) && $bataval && $tf eq "year"); - - $i++; - } - -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 /\./x]} @versions; - - @sorted = map {join ".", unpack "C*", $_} - sort - map {pack "C*", split /\./x} @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: 76_SMAPortal.pm 22149 2020-06-09 20:41:58Z DS_Starter $ im Kopf komplett! vorhanden ) - $modules{$type}{META}{x_version} =~ s/1\.1\.1/$v/gx; - } else { - $modules{$type}{META}{x_version} = $v; - } - return $@ unless (FHEM::Meta::SetInternals($hash)); # FVERSION wird gesetzt ( nur gesetzt wenn $Id: 76_SMAPortal.pm 22149 2020-06-09 20:41:58Z DS_Starter $ 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' ); ## no critic 'VERSION' - } - } else { - # herkömmliche Modulstruktur - $hash->{VERSION} = $v; - } - -return; -} - -################################################################ -# delete Readings -# $conspl = providerLevel berücksichtigen -################################################################ -sub delread { - my $hash = shift; - my $conspl = shift; - my $name = $hash->{NAME}; - my @allrds = keys%{$defs{$name}{READINGS}}; - - my $bl = "state|lastCycleTime|Counter|loginState"; # Blacklist - - if($conspl) { - # Readings löschen wenn nicht im providerLevel enthalten - for my $key(@allrds) { - my ($lvl) = $key =~ m/^(L\d+)_/x; - if($lvl) { - for my $p (keys %{$subs{$name}}) { - delete($defs{$name}{READINGS}{$key}) if($subs{$name}{$p}{level} eq $lvl && !$subs{$name}{$p}{doit}); - delete($defs{$name}{READINGS}{$key}) if($subs{$name}{plantLogbook}{level} eq $lvl); # Logbuchreadings immer löschen - } - } else { - delete($defs{$name}{READINGS}{$key}) if($key !~ /$bl/x); - } - } - return; - } - - for my $key(@allrds) { - delete($defs{$name}{READINGS}{$key}) if($key !~ /$bl/x); - } - -return; -} - -################################################################ -# statistische Counter managen -# $name = Name Device -# $rd = Name des Zählerreadings -################################################################ -sub handleCounter { - my $name = shift; - my $rd = shift; - - my $cstring = ReadingsVal($name, $rd, ""); - my ($day,$count) = split(":", $cstring); - my $mday = (localtime(time))[3]; - if(!$day || $day != $mday) { - $count = 0; - $day = $mday; - Log3 ($name, 2, qq{$name - reset counter "$rd" to >0< }) if(!$defs{$name}->{HELPER}{$rd}); - $defs{$name}->{HELPER}{$rd} = 1; # nur im fork setzen um doppelten Logeintrag zu vermeiden - } - $count++; - $cstring = "$rd:$day:$count"; - BlockingInformParent("FHEM::SMAPortal::setFromBlocking", [$name, $cstring, "NULL"], 1); - -return; -} - -################################################################### -# Werte aus BlockingCall heraus setzen -# Erwartete Liste: -# @setl = $name,$setread,$retries,$helper -################################################################### -sub setFromBlocking { - my $name = shift; - my $setread = shift // "NULL"; - my $helper = shift // "NULL"; - my $hash = $defs{$name}; - - if($setread ne "NULL") { - my @cparts = split ":", $setread, 2; - readingsSingleUpdate($hash, $cparts[0], $cparts[1], 1); - } - - if($helper ne "NULL") { - my ($hnam,$k1,$k2,$k3) = split ":", $helper, 4; - if(defined $k3) { - $hash->{HELPER}{"$hnam"}{"$k1"}{"$k2"} = $k3; - } elsif (defined $k2) { - $hash->{HELPER}{"$hnam"}{"$k1"} = $k2; - } else { - $hash->{HELPER}{"$hnam"} = $k1; - } - } - -return 1; -} - -################################################################ -# Reading aus BlockingCall heraus löschen -# Erwartete Liste: -# @params = $name,$reading -################################################################ -sub delReadingFromBlocking { - my $name = shift; - my $reading = shift; - my $hash = $defs{$name}; - - readingsDelete($hash, $reading); - -return 1; -} - -################################################################ -# Timestamp korrigieren -################################################################ -sub TimeAdjust { - my ($hash,$t,$tkind) = @_; - $t =~ s/T/ /x; - my ($datehour, $rest) = split(/:/x,$t,2); - my ($year, $month, $day, $hour) = $datehour =~ /(\d+)-(\d\d)-(\d\d)\s+(\d\d)/x; - - # 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 $isdst = (localtime($epoch))[8]; - - if(lc($tkind) =~ /unspecified/x) { - if($isdst) { - $epoch = $epoch - 7200; - } else { - $epoch = $epoch - 3600; - } - } - - 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 und ungültige Zeichen für Readingerstellung ersetzen -############################################################################### -sub replaceJunkSigns { - my ($rn) = @_; - - $rn =~ s/ß/ss/gx; - $rn =~ s/ä/ae/gx; - $rn =~ s/ö/oe/gx; - $rn =~ s/ü/ue/gx; - $rn =~ s/Ä/Ae/gx; - $rn =~ s/Ö/Oe/gx; - $rn =~ s/Ü/Ue/gx; - $rn = makeReadingName($rn); - -return($rn); -} - -############################################################################### -# Subroutine für Portalgrafik -############################################################################### -sub PortalAsHtml { ## no critic 'complexity' - my ($name,$wlname,$ftui) = @_; - my $hash = $defs{$name}; - my $ret = ""; - - my ($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,$hdrDetail,$hdrAlign); - 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 $fdo = $subs{$name}{forecastData}{doit}; - my $fmin = $subs{$name}{forecastData}{level}; # LXX Level - my $fmaj = $subs{$name}{forecastData}{level}; - my $ldlv = $subs{$name}{liveData}{level}; - my $cclv = $subs{$name}{consumerCurrentdata}{level}; - - my ($pv0,$pv1); - $pv0 = ReadingsNum($name, "${fmin}_ThisHour_PvMeanPower", undef) if($fmin); - $pv1 = ReadingsNum($name, "${fmaj}_NextHour01_PvMeanPower", undef) if($fmaj); - - if(!$hash || !defined($defs{$wlname}) || !$fdo || !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 (!$fdo) { - $ret .= qq{The attribute "providerLevel" of device "$name" must contain the level "forecastData" and data must be retrieved !}; - } elsif (!defined $pv0) { - $ret .= "Awaiting minor level forecast data ..."; - } elsif (!defined $pv1) { - $ret .= "Awaiting level major level forecast 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))); - - # Verbraucherlegende und Steuerung - if ($legend) { - for (@pgCDev) { - my($txt,$im) = split(':',$_); # $txt ist der Verbrauchername - my $cmdon = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name $txt on')\""; - my $cmdoff = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name $txt off')\""; - my $cmdauto = "\"FW_cmd('$FW_ME$FW_subdir?XHR=1&cmd=set $name $txt auto')\""; - - if ($ftui && $ftui eq "ftui") { - $cmdon = "\"ftui.setFhemStatus('set $name $txt on')\""; - $cmdoff = "\"ftui.setFhemStatus('set $name $txt off')\""; - $cmdauto = "\"ftui.setFhemStatus('set $name $txt auto')\""; - } - - my $swstate = ReadingsVal($name,"${cclv}_".$txt."_Switch", "undef"); - my $swicon = ""; - if($swstate eq "off") { - $swicon = ""; - } elsif ($swstate eq "on") { - $swicon = ""; - } elsif ($swstate =~ /off.*automatic.*/ix) { - $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, 'consumerAdviceIcon', 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, 'Wh/kWh', 'Wh'); - - $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); - $hdrAlign = AttrVal($wlname, 'headerAlignment', 'center'); # ermöglicht per attr die Ausrichtung der Tabelle zu setzen - $hdrDetail = AttrVal($wlname, 'headerDetail', 'all'); # ermöglicht den Inhalt zu begrenzen, um bspw. passgenau in ftui einzubetten - - # 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,"${fmin}_Next04Hours_Consumption", 0); - my $coRe = ReadingsNum ($name,"${fmin}_RestOfDay_Consumption", 0); - my $coTo = ReadingsNum ($name,"${fmin}_Tomorrow_Consumption", 0); - my $coCu = ReadingsNum ($name,"${ldlv}_GridConsumption", 0); - - my $pv4h = ReadingsNum($name,"${fmin}_Next04Hours_PV", 0); - my $pvRe = ReadingsNum($name,"${fmin}_RestOfDay_PV", 0); - my $pvTo = ReadingsNum($name,"${fmin}_Tomorrow_PV", 0); - my $pvCu = ReadingsNum($name,"${ldlv}_PV", 0); - - if ($kw eq 'kWh') { - $co4h = sprintf("%.1f" , $co4h/1000)." kWh"; - $coRe = sprintf("%.1f" , $coRe/1000)." kWh"; - $coTo = sprintf("%.1f" , $coTo/1000)." kWh"; - $coCu = sprintf("%.1f" , $coCu/1000)." kW"; - $pv4h = sprintf("%.1f" , $pv4h/1000)." kWh"; - $pvRe = sprintf("%.1f" , $pvRe/1000)." kWh"; - $pvTo = sprintf("%.1f" , $pvTo/1000)." kWh"; - $pvCu = sprintf("%.1f" , $pvCu/1000)." kW"; - } else { - $co4h .= " Wh"; - $coRe .= " Wh"; - $coTo .= " Wh"; - $coCu .= " W"; - $pv4h .= " Wh"; - $pvRe .= " Wh"; - $pvTo .= " Wh"; - $pvCu .= " W"; - } - - - # Headerzeile generieren - if ($header) { - my $lang = AttrVal("global","language","EN"); - my $alias = AttrVal($name, "alias", "SMA Sunny Portal"); # Linktext als Aliasname oder "SMA Sunny Portal" - my $dlink = "$alias"; - my $lup = ReadingsTimestamp($name, "${fmin}_ForecastToday_Consumption", "0000-00-00 00:00:00"); # letzter Forecast Update - - my $lupt = "last update:"; - my $lblPv4h = "next 4h:"; - my $lblPvRe = "today:"; - my $lblPvTo = "tomorrow:"; - my $lblPvCu = "actual"; - - if(AttrVal("global","language","EN") eq "DE") { # Header globales Sprachschema Deutsch - $lupt = "Stand:"; - $lblPv4h = encode("utf8", "nächste 4h:"); - $lblPvRe = "heute:"; - $lblPvTo = "morgen:"; - $lblPvCu = "aktuell"; - } - - $header = ""; - - # Header Link + Status - if($hdrDetail eq "all" || $hdrDetail eq "statusLink") { - my ($year, $month, $day, $hour, $min, $sec) = $lup =~ /(\d+)-(\d\d)-(\d\d)\s+(.*)/x; - $lup = "$3.$2.$1 $4"; - $header .= ""; - } - - # Header Information pv - if($hdrDetail eq "all" || $hdrDetail eq "pv" || $hdrDetail eq "pvco") { - $header .= ""; - $header .= ""; - $header .= ""; - $header .= ""; - $header .= ""; - $header .= ""; - $header .= ""; - } - - # Header Information co - if($hdrDetail eq "all" || $hdrDetail eq "co" || $hdrDetail eq "pvco") { - $header .= ""; - $header .= ""; - $header .= ""; - $header .= ""; - $header .= ""; - $header .= ""; - $header .= ""; - } - - $header .= "
    ".$dlink."(".$lupt." ".$lup.")
    PV =>$lblPvCu $pvCu$lblPv4h $pv4h$lblPvRe $pvRe$lblPvTo $pvTo
    CO =>$lblPvCu $coCu$lblPv4h $co4h$lblPvRe $coRe$lblPvTo $coTo
    "; - } - - # Werte aktuelle Stunde - $pv{0} = ReadingsNum($name,"${fmin}_ThisHour_PvMeanPower", 0); - $co{0} = ReadingsNum($name,"${fmin}_ThisHour_Consumption", 0); - $di{0} = $pv{0} - $co{0}; - $is{0} = (ReadingsVal($name,"${fmin}_ThisHour_IsConsumptionRecommended",'no') eq 'yes' ) ? $icon : undef; - $we{0} = $hash->{HELPER}{"${fmin}_ThisHour_WeatherId"} if($weather); # für Wettericons - $we{0} = $we{0} // 999; - - if(AttrVal("global","language","EN") eq "DE") { - (undef,undef,undef,$t{0}) = ReadingsVal($name,"${fmin}_ThisHour_Time",'0') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; - } else { - (undef,undef,undef,$t{0}) = ReadingsVal($name,"${fmin}_ThisHour_Time",'0') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; - } - - $t{0} = int($t{0}); # zum Rechnen Integer ohne führende Null - - ########################################################### - # get consumer list and display it in portalGraphics - for (@pgCDev) { - my ($itemName, undef) = split(':',$_); - $itemName =~ s/^\s+|\s+$//gx; #trim it, if blanks were used - $_ =~ s/^\s+|\s+$//gx; #trim it, if blanks were used - - #check if listed device is planned - if (ReadingsVal($name, "${cclv}_".$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,"${cclv}_".$itemName."_PlannedOpTimeBegin",'00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; - (undef,undef,undef,$end) = ReadingsVal($name,"${cclv}_".$itemName."_PlannedOpTimeEnd",'00.00.0000 24') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; - } else { - (undef,undef,undef,$start) = ReadingsVal($name,"${cclv}_".$itemName."_PlannedOpTimeBegin",'0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; - (undef,undef,undef,$end) = ReadingsVal($name,"${cclv}_".$itemName."_PlannedOpTimeEnd",'0000-00-00 24') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; - } - - $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 - - for my $i (1..$maxhours-1) { - $pv{$i} = ReadingsNum($name,"${fmaj}_NextHour".sprintf("%02d",$i)."_PvMeanPower",0); # Erzeugung - $co{$i} = ReadingsNum($name,"${fmaj}_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,"${fmaj}_NextHour".sprintf("%02d",$i)."_IsConsumptionRecommended",'no') eq 'yes') ? $icon : undef; - $we{$i} = $hash->{HELPER}{"${fmaj}_NextHour".sprintf("%02d",$i)."_WeatherId"} if($weather); # für Wettericons - $we{$i} = $we{$i} // 999; - - if(AttrVal("global","language","EN") eq "DE") { - (undef,undef,undef,$t{$i}) = ReadingsVal($name,"${fmaj}_NextHour".sprintf("%02d",$i)."_Time",'0') =~ m/(\d{2}).(\d{2}).(\d{4})\s(\d{2})/x; - } else { - (undef,undef,undef,$t{$i}) = ReadingsVal($name,"${fmaj}_NextHour".sprintf("%02d",$i)."_Time",'0') =~ m/(\d{4})-(\d{2})-(\d{2})\s(\d{2})/x; - } - - $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 - - for my $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 - - for my $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 - - for my $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."
    "; - - my $sicon = 1; - $ret .= $is{$i} if (defined ($is{$i}) && $sicon); - - ################################## - # inject the new icon if defined - $ret .= consinject($hash,$i,@pgCDev) if($ret); - - $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}); - - ################################## - # inject the new icon if defined - $ret .= consinject($hash,$i,@pgCDev) if($ret); - - $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; -} - -################################################################ -# Inject consumer icon -################################################################ -sub consinject { - my ($hash,$i,@pgCDev) = @_; - my $name = $hash->{NAME}; - my $ret = ""; - - for (@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); - } - } - } - -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 'kWh') { # bei Anzeige in kWh 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 - '999' => '1px-spacer' # Dummy - keine Anzeige Wettericon # vorhanden - ); - -return $weather_ids{$id} if(defined($weather_ids{$id})); -return 'unknown'; -} - -###################################################################################################### -# Refresh eines Raumes aus $hash->{HELPER}{SPGROOM} -# bzw. Longpoll von SMAPortal 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}); - for (@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"); - for(@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

    -
      - - With this module it is possible to fetch data from the SMA Sunny Portal and switch - consumers (e.g. bluetooth plug sockets) if any are present. - At the momentent that are the following data:

      -
        -
          -
        • Live data (Consumption and PV-Generation)
        • -
        • Battery data (In/Out) and usage data of consumers
        • -
        • Various balance data and statistical data (also of the consumers connected to the SMA Sunny Homemanager)
        • -
        • Weather data delivered from SMA for the facility location
        • -
        • Forecast data (Consumption and PV-Generation) inclusive suggestion times to switch comsumers on
        • -
        • the planned times by the Sunny Home Manager to switch consumers on and the current state of consumers (if present)
        • -
        -
      -
      - - Graphic data can also be integrated into FHEM Tablet UI with the - "SMAPortalSPG Widget".
      -
      - - Preparation

      - -
        - This module use the Perl module JSON which has typically to be installed discrete.
        - On Debian linux based systems that can be done by command:

        - - sudo apt-get install libjson-perl

        - - Subsequent there are an overview of used Perl modules:

        - - 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
        - utf8
        - -

        -
      - - - Definition -
        -
        - A SMAPortal device will be defined by:

        - -
          - define <name> SMAPortal

          -
        - - After the definition of the device the credentials for the SMA Sunny Portal must be saved with the - following command:

        - -
          - set <name> credentials <Username> <Password> -
        -
      -

      - - - Set -
        -
        -
          - -
        • set <name> createPortalGraphic <Generation | Consumption | Generation_Consumption | Differential>
          - Creates graphical devices to show the SMA Sunny Portal forecast data in several layouts. - The attribute "providerLevel" must contain "forecastData".
          - With the "attributes of the graphic device" the appearance and coloration of the forecast - data in the created graphic device can be adjusted. -
        - -
        - -
          -
        • set <name> credentials <username> <password>
        • - Set Username / Password used for the login into the SMA Sunny Portal. -
        -
        - -
          - -
        • set <name> <consumer name> <on | off | auto>
          - Once consumer data are available, the consumer are shown in the Set and can be switched to on, off or the automatic mode (auto) - that means the consumer are controlled by the Sunny Home Manager. -
        • -
        - -
      -

      - - - Get -
        -
        - -
          - -
        • get <name> data
          - This command fetch the data from the SMA Sunny Portal manually. -
        - -
        - -
          - -
        • get <name> storedCredentials
          - The saved credentials are displayed in a popup window. -
        - -
      -

      - - - Attributes -
        -
        -
          - -
        • cookieLocation <Pfad/File>
          - The path and filename of received Cookies.
          - (default: ./log/<name>_cookie.txt) -

          - -
            - Example:
            - attr <name> cookieLocation ./log/cookies.txt
            -
          -

        • - - -
        • disable
          - Deactivate/activate the device.

        • - - -
        • interval <seconds>
          - Time interval for continuous data retrieval from the aus dem SMA Sunny Portal (default: 300 seconds).
          - if the interval is set to "0", no continuous data retrieval is executed and has to be triggered manually by the - "get <name> data" command.

          - -
            - Note: - The retrieval interval should not be less than 120 seconds. As of previous experiences SMA suffers an interval of - 120 seconds although the SMA terms and conditions don't permit an automatic data fetch by computer programs. -
          -

        • - - -
        • plantLogbookApprovalState
          - With this attribute the entries are filtered according to their status.
          - (default: Any) -
          - -
            - - - - -
            Any - all messages
            NotApproved - unconfirmed messages
            -
          -

        • - - -
        • plantLogbookTypes
          - This attribute defines the message types of the asset logbook to be selected. - A maximum of the latest 25 logbook entries of all set types are displayed.
          - (default: Warning,Disturbance,Error) -

        • - - -
        • providerLevel
          - The scope of the data to be generated is set. Asset and consumer master data is always retrieved. -

          - -
            - - - - - - - - - - - -
            liveData - generates readings of the current generation and consumption data
            weatherData - Weather data offered by SMA are retrieved
            balanceDayData - Statistics data of the day are retrieved
            forecastData - Forecast data of generation/consumption and consumer planning data are generated
            consumerCurrentdata - current consumer data are generated
            consumerDayData - consumer data day are generated
            consumerMonthData - consumer data month are generated
            consumerYearData - consumer data year are generated
            plantLogbook - the maximum of 25 most recent entries of the plant logbook are retrieved
            -
          -
          -

        • - - -
        • showPassInLog
          - If set, the used password will be displayed in Logfile output. - (default: 0)

        • - - -
        • userAgent <identifier>
          - An user agent identifier for identifikation against the SMA Sunny Portal can be specified. -

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

        • - - -
        • verbose5Data
          - The attribute value verbose 5 is used to generate very large amounts of data. - The verbose 5 outputs of interest can be selected specifically.
          - (default: none) -

        • - -
        -
      -
      - -
    - - -=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)
        • -
        • verschiedene Bilanzdaten und Statistikdaten (auch der an den SMA Sunny Homemanager angeschlossenen Verbraucher)
        • -
        • Batteriedaten (In/Out) sowie Nutzungsdaten durch Verbraucher
        • -
        • 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)
        • -
        -
      -
      - - Die Portalgrafik kann ebenfalls in FHEM Tablet UI mit dem - "SMAPortalSPG Widget" integriert werden.
      -
      - - 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
        - utf8
        - -

        -
      - - - Definition -
        -
        - Ein SMAPortal-Device wird definiert mit:

        - -
          - define <Name> SMAPortal

          -
        - - Nach der Definition des Devices müssen die Zugangsparameter für das SMA Sunny Portal gespeichert werden - 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 "providerLevel" muss auf den Level "forecastData" enthalten.
          - 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 zum Login in das SMA Sunny Portal. -
        - -
        - -
          - -
        • set <name> <Verbrauchername> <on | off | auto>
          - Es werden die an den SMA Sunny Homemanager angeschlossene Verbraucher (Bluetooth Steckdosen) angeboten sobald sie vom - Modul erkannt wurden. - Sobald diese Daten vorliegen, werden die vorhandenen Verbraucher im Set angezeigt und können eingeschaltet, ausgeschaltet - bzw. auf die Steuerung durch den Sunny Home Manager umgeschaltet werden (auto). -
        • -
        - -
      -

      - - - Get -
        -
        -
          - - -
        • get <name> data
          - Mit diesem Befehl werden die Daten aus dem SMA Sunny Portal manuell abgerufen. -
        - -
        - -
          - -
        • get <name> storedCredentials
          - Die gespeicherten Anmeldeinformationen (Credentials) werden in einem Popup als Klartext angezeigt. -
        - - -
      -

      - - - Attribute -
        -
        -
          - -
        • cookieLocation <Pfad/File>
          - Angabe von Pfad und Datei zur Abspeicherung des empfangenen Cookies.
          - (default: ./log/<name>_cookie.txt) -

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

        • - - -
        • disable
          - Deaktiviert das Device.

        • - - -
        • interval <Sekunden>
          - Zeitintervall zum kontinuierlichen Datenabruf aus dem SMA Sunny Portal (Default: 300 Sekunden).
          - Ist interval explizit auf "0" gesetzt, erfolgt kein automatischer Datenabruf und muss mit "get <name> data" manuell - erfolgen.

          - -
            - 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. -
          -

        • - - -
        • plantLogbookApprovalState
          - Mit diesem Attribut werden die Einträge entsprechend ihres Status gefiltert.
          - (default: Any) -
          - -
            - - - - -
            Any - alle Mitteilungen
            NotApproved - nicht bestätigte Mitteilungen
            -
          -

        • - - -
        • plantLogbookTypes
          - Mit diesem Attribut werden die zu selektierenden Mitteilungstypen des Anlagenlogbuchs festgelegt. - Es werden maximal die aktuellsten 25 Logbucheinträge aller eingestellten Typen angezeigt.
          - (default: Warning,Disturbance,Error) -

        • - - -
        • providerLevel
          - Es wird der Umfang der zu generierenden Daten eingestellt. Anlagen- und Verbraucherstammdaten werden immer abgerufen. -

          - -
            - - - - - - - - - - - -
            liveData - erzeugt Readings der aktuellen Erzeugungs- und Verbrauchsdaten
            weatherData - von SMA angebotene Wetterdaten werden abgerufen
            balanceDayData - Statistikdaten des Tages werden abgerufen
            forecastData - Vorhersagedaten der Erzeugung / Verbrauch und Verbraucherplanungsdaten werden erzeugt
            consumerCurrentdata - aktuelle Verbraucherdaten werden erzeugt
            consumerDayData - Verbraucherdaten Tag werden erzeugt
            consumerMonthData - Verbraucherdaten Monat werden erzeugt
            consumerYearData - Verbraucherdaten Jahr werden erzeugt
            plantLogbook - die maximal 25 aktuellsten Einträge des Anlagenlogbuchs werden abgerufen
            -
          -
          -

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

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

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

        • - - -
        • verbose5Data
          - Mit dem Attributwert verbose 5 werden sehr große Datenmengen generiert. - Es können gezielt die interessierenden verbose 5 Ausgaben selektiert werden.
          - (default: none) -

        • - -
        -
      -
      - -
    - -=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": "stable", - "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, - "utf8": 0 - }, - "recommends": { - "FHEM::Meta": 0 - }, - "suggests": { - } - } - } -} -=end :application/json;q=META.json - -=cut \ No newline at end of file