From b4a3a9ff52ec1be4cb0d07b8bdaffb724729f39e Mon Sep 17 00:00:00 2001 From: pizmus <> Date: Mon, 23 Sep 2019 20:47:21 +0000 Subject: [PATCH] 70_SolarEdgeAPI: source code formatting git-svn-id: https://svn.fhem.de/fhem/trunk@20234 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/70_SolarEdgeAPI.pm | 1058 ++++++++++++++++++---------------- 2 files changed, 554 insertions(+), 505 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index b6a55a87f..ec4fb3524 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - change: 70_SolarEdgeAPI: source code formatting - bugfix: 89_FULLY: Support Fully version 1.34 - change: 93_DbRep: comma can be shown in sqlCmdHistory, Forum: #103908 - feature: 49_SSCamSTRM: new attribute "hideAudio" diff --git a/fhem/FHEM/70_SolarEdgeAPI.pm b/fhem/FHEM/70_SolarEdgeAPI.pm index 5b431dd24..a1f6b5b60 100644 --- a/fhem/FHEM/70_SolarEdgeAPI.pm +++ b/fhem/FHEM/70_SolarEdgeAPI.pm @@ -74,17 +74,24 @@ eval "use JSON;1" or $solarEdgeAPI_missingModul .= "JSON "; # 1.0.0 initial version as copied from https://github.com/felixmartens/fhem # with minimal changes to be able to submit it to FHEM SVN # -# 1.1.0 Detect that site does not support the "currentPowerFlow" API. +# 1.1.0beta Detect that site does not support the "currentPowerFlow" API. # Read "overview" API to get the current power. # Added attributes enableStatusReadings, enableAggregatesReadings, # and enableOverviewReadings. +# Note: This version was released by accident with "beta" in the +# version string. +# +# 1.1.1 source code formatting +# added TODOs in the source code # ############################################################################### -my $solarEdgeAPI_version = "1.1.0beta"; +# TODO move into hash +my $solarEdgeAPI_version = "1.1.1"; ############################################################################### +# TODO Are these necessary? # Declare functions sub SolarEdgeAPI_Attr(@); sub SolarEdgeAPI_Define($$); @@ -101,542 +108,583 @@ sub SolarEdgeAPI_ErrorHandling($$$); sub SolarEdgeAPI_WriteReadings($$$); sub SolarEdgeAPI_Timer_GetData($); +# TODO move into hash +# TODO why does one of the paths have a ".json" and the others do not? +# TODO understand why reading names differ from API names my %solarEdgeAPI_paths = ( 'status' => 'currentPowerFlow.json', 'aggregates' => 'energyDetails', 'overview' => 'overview' ); -sub SolarEdgeAPI_Initialize($) { +sub SolarEdgeAPI_Initialize($) +{ + my ($hash) = @_; - my ($hash) = @_; - - # Consumer - $hash->{GetFn} = "SolarEdgeAPI_Get"; - $hash->{DefFn} = "SolarEdgeAPI_Define"; - $hash->{UndefFn} = "SolarEdgeAPI_Undef"; - $hash->{NotifyFn} = "SolarEdgeAPI_Notify"; - - $hash->{AttrFn} = "SolarEdgeAPI_Attr"; - $hash->{AttrList} = "interval ". - "disable:1 ". - "enableStatusReadings:1,0 " . - "enableAggregatesReadings:1,0 " . - "enableOverviewReadings:1,0 " . - $readingFnAttributes; - - foreach my $d(sort keys %{$modules{SolarEdgeAPI}{defptr}}) { - - my $hash = $modules{SolarEdgeAPI}{defptr}{$d}; - $hash->{VERSION} = $solarEdgeAPI_version; - } + $hash->{GetFn} = "SolarEdgeAPI_Get"; + $hash->{DefFn} = "SolarEdgeAPI_Define"; + $hash->{UndefFn} = "SolarEdgeAPI_Undef"; + $hash->{NotifyFn} = "SolarEdgeAPI_Notify"; + $hash->{AttrFn} = "SolarEdgeAPI_Attr"; + $hash->{AttrList} = "interval ". + "disable:1 ". + "enableStatusReadings:1,0 " . + "enableAggregatesReadings:1,0 " . + "enableOverviewReadings:1,0 " . + $readingFnAttributes; + + # TODO Is this required? Is it the right place? + foreach my $d(sort keys %{$modules{SolarEdgeAPI}{defptr}}) + { + my $hash = $modules{SolarEdgeAPI}{defptr}{$d}; + $hash->{VERSION} = $solarEdgeAPI_version; + } } -sub SolarEdgeAPI_Define($$) { +sub SolarEdgeAPI_Define($$) +{ + my ($hash, $def) = @_; - my ( $hash, $def ) = @_; - - my @a = split( "[ \t][ \t]*", $def ); + my @a = split( "[ \t][ \t]*", $def ); - - return "too few parameters: define SolarEdgeAPI " if(int(@a) != 5); - return "Cannot define a SolarEdgeAPI device. Perl modul $solarEdgeAPI_missingModul is missing." if ( $solarEdgeAPI_missingModul ); - - my $name = $a[0]; - - my $apikey = $a[2]; - my $siteid = $a[3]; - my $interval = $a[4]||'auto'; - - $hash->{APIKEY} = $apikey; - $hash->{SITEID} = $siteid; - - $hash->{INTERVAL} = $interval; - $hash->{PORT} = 80; - $hash->{VERSION} = $solarEdgeAPI_version; - $hash->{NOTIFYDEV} = "global"; - $hash->{actionQueue} = []; - - - $attr{$name}{room} = "Photovoltaik" if( !defined( $attr{$name}{room} ) ); - - Log3 $name, 3, "SolarEdgeAPI ($name) - defined SolarEdgeAPI Device with SiteID $hash->{SITEID} and Interval $hash->{INTERVAL}"; - - $modules{SolarEdgeAPI}{defptr}{SITEID} = $hash; - - return undef; -} - -sub SolarEdgeAPI_Undef($$) { - - my ( $hash, $arg ) = @_; - - my $name = $hash->{NAME}; - - - Log3 $name, 3, "SolarEdgeAPI ($name) - Device $name deleted"; - delete $modules{SolarEdgeAPI}{defptr}{SITEID} if( defined($modules{SolarEdgeAPI}{defptr}{SITEID}) and $hash->{SITEID} ); - - return undef; -} - -sub SolarEdgeAPI_Attr(@) { - - my ( $cmd, $name, $attrName, $attrVal ) = @_; - my $hash = $defs{$name}; - - - if( $attrName eq "disable" ) { - if( $cmd eq "set" and $attrVal eq "1" ) { - RemoveInternalTimer($hash); - readingsSingleUpdate ( $hash, "state", "disabled", 1 ); - Log3 $name, 3, "SolarEdgeAPI ($name) - disabled"; - - } elsif( $cmd eq "del" ) { - Log3 $name, 3, "SolarEdgeAPI ($name) - enabled"; - } - } - - if( $attrName eq "disabledForIntervals" ) { - if( $cmd eq "set" ) { - return "check disabledForIntervals Syntax HH:MM-HH:MM or 'HH:MM-HH:MM HH:MM-HH:MM ...'" - unless($attrVal =~ /^((\d{2}:\d{2})-(\d{2}:\d{2})\s?)+$/); - Log3 $name, 3, "SolarEdgeAPI ($name) - disabledForIntervals"; - readingsSingleUpdate ( $hash, "state", "disabled", 1 ); - - } elsif( $cmd eq "del" ) { - Log3 $name, 3, "SolarEdgeAPI ($name) - enabled"; - readingsSingleUpdate( $hash, "state", "active", 1 ); - } - } - - if( $attrName eq "interval" ) { - if( $cmd eq "set" ) { - if($attrVal eq "auto" || $attrVal > 120) { - RemoveInternalTimer($hash); - $hash->{INTERVAL} = $attrVal; - Log3 $name, 3, "SolarEdgeAPI ($name) - set interval to $attrVal"; - SolarEdgeAPI_Timer_GetData($hash); - } else { - Log3 $name, 3, "SolarEdgeAPI ($name) - interval too small, please use something >= 120 (sec), default is 300 (sec)"; - return "interval too small, please use something >= 120 (sec), default is 300 (sec) daytime and 1200 (sec) nighttime"; - } - } elsif( $cmd eq "del" ) { - RemoveInternalTimer($hash); - $hash->{INTERVAL} = 'auto'; - Log3 $name, 3, "SolarEdgeAPI ($name) - set interval to default"; - SolarEdgeAPI_Timer_GetData($hash); - } - } - - if ($attrName eq "enableStatusReadings") - { - if($cmd eq "set") - { - if (not (($attrVal eq "0") || ($attrVal eq "1"))) - { - my $message = "illegal value for enableStatusReadings"; - Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; - return $message; - } - } - } - - if ($attrName eq "enableAggregatesReadings") - { - if($cmd eq "set") - { - if (not (($attrVal eq "0") || ($attrVal eq "1"))) - { - my $message = "illegal value for enableAggregatesReadings"; - Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; - return $message; - } - } - } + if (int(@a) != 5) + { + return "too few parameters: define SolarEdgeAPI "; + } - if ($attrName eq "enableOverviewReadings") + if ($solarEdgeAPI_missingModul) + { + return "Cannot define a SolarEdgeAPI device. Perl modul $solarEdgeAPI_missingModul is missing."; + } + + my $name = $a[0]; + my $apikey = $a[2]; + my $siteid = $a[3]; + my $interval = $a[4] || 'auto'; + + $hash->{APIKEY} = $apikey; + $hash->{SITEID} = $siteid; + $hash->{INTERVAL} = $interval; + $hash->{PORT} = 80; + $hash->{VERSION} = $solarEdgeAPI_version; + $hash->{NOTIFYDEV} = "global"; + $hash->{actionQueue} = []; + + # TODO Remove this? + $attr{$name}{room} = "Photovoltaik" if( !defined( $attr{$name}{room} ) ); + + Log3 $name, 3, "SolarEdgeAPI ($name) - defined, SiteID $hash->{SITEID}, Interval $hash->{INTERVAL}"; + + $modules{SolarEdgeAPI}{defptr}{SITEID} = $hash; + + return undef; +} + +sub SolarEdgeAPI_Undef($$) +{ + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 3, "SolarEdgeAPI ($name) - deleted"; + delete $modules{SolarEdgeAPI}{defptr}{SITEID} if( defined($modules{SolarEdgeAPI}{defptr}{SITEID}) and $hash->{SITEID} ); + + return undef; +} + +sub SolarEdgeAPI_Attr(@) +{ + my ($cmd, $name, $attrName, $attrVal) = @_; + my $hash = $defs{$name}; + + if ($attrName eq "disable") + { + if (($cmd eq "set") and ($attrVal eq "1")) { - if($cmd eq "set") - { - if (not (($attrVal eq "0") || ($attrVal eq "1"))) - { - my $message = "illegal value for enableOverviewReadings"; - Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; - return $message; - } - } + RemoveInternalTimer($hash); + readingsSingleUpdate ( $hash, "state", "disabled", 1); + Log3 $name, 3, "SolarEdgeAPI ($name) - disabled"; + } + elsif ($cmd eq "del") + { + Log3 $name, 3, "SolarEdgeAPI ($name) - enabled"; } + } + + # TODO Is this the common/intended implementation of this feature? + if ($attrName eq "disabledForIntervals") + { + if ($cmd eq "set") + { + return "check disabledForIntervals Syntax HH:MM-HH:MM or 'HH:MM-HH:MM HH:MM-HH:MM ...'" + unless($attrVal =~ /^((\d{2}:\d{2})-(\d{2}:\d{2})\s?)+$/); + Log3 $name, 3, "SolarEdgeAPI ($name) - disabledForIntervals"; + readingsSingleUpdate ( $hash, "state", "disabled", 1 ); + } + elsif ($cmd eq "del") + { + Log3 $name, 3, "SolarEdgeAPI ($name) - enabled"; + readingsSingleUpdate( $hash, "state", "active", 1 ); + } + } + + if ($attrName eq "interval") + { + if ($cmd eq "set") + { + if (($attrVal eq "auto") || ($attrVal > 120)) + { + RemoveInternalTimer($hash); + $hash->{INTERVAL} = $attrVal; + Log3 $name, 3, "SolarEdgeAPI ($name) - set interval to $attrVal"; + SolarEdgeAPI_Timer_GetData($hash); + } + else + { + Log3 $name, 3, "SolarEdgeAPI ($name) - interval too small, please use something >= 120 (sec), default is 300 (sec)"; + return "interval too small, please use something >= 120 (sec), default is 300 (sec) daytime and 1200 (sec) nighttime"; + } + } + elsif ($cmd eq "del") + { + RemoveInternalTimer($hash); + $hash->{INTERVAL} = 'auto'; + Log3 $name, 3, "SolarEdgeAPI ($name) - set interval to default"; + SolarEdgeAPI_Timer_GetData($hash); + } + } + + if ($attrName eq "enableStatusReadings") + { + if($cmd eq "set") + { + if (not (($attrVal eq "0") || ($attrVal eq "1"))) + { + my $message = "illegal value for enableStatusReadings"; + Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; + return $message; + } + } + } + + if ($attrName eq "enableAggregatesReadings") + { + if($cmd eq "set") + { + if (not (($attrVal eq "0") || ($attrVal eq "1"))) + { + my $message = "illegal value for enableAggregatesReadings"; + Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; + return $message; + } + } + } + + if ($attrName eq "enableOverviewReadings") + { + if($cmd eq "set") + { + if (not (($attrVal eq "0") || ($attrVal eq "1"))) + { + my $message = "illegal value for enableOverviewReadings"; + Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; + return $message; + } + } + } + + return undef; +} + +sub SolarEdgeAPI_Notify($$) +{ + my ($hash,$dev) = @_; + my $name = $hash->{NAME}; - return undef; + return if (IsDisabled($name)); + + my $devname = $dev->{NAME}; + my $devtype = $dev->{TYPE}; + my $events = deviceEvents($dev,1); + return if (!$events); + + if ((grep /^INITIALIZED$/,@{$events}) or + (grep /^DELETEATTR.$name.disable$/,@{$events}) or + (grep /^DELETEATTR.$name.interval$/,@{$events}) or + ((grep /^DEFINED.$name$/,@{$events}) and $init_done)) + { + SolarEdgeAPI_Timer_GetData($hash); + } + + return; } -sub SolarEdgeAPI_Notify($$) { - - my ($hash,$dev) = @_; - my $name = $hash->{NAME}; - return if (IsDisabled($name)); +sub SolarEdgeAPI_Get($@) +{ + my ($hash, $name, $cmd) = @_; + + # TODO rework + my $arg; + if ($cmd eq 'status') + { + $arg = lc($cmd); + } + elsif ($cmd eq 'aggregates') + { + $arg = lc($cmd); + } + elsif ($cmd eq 'overview') + { + $arg = lc($cmd); + } + else + { + my $list = 'status:noArg aggregates:noArg overview:noArg'; + return "Unknown argument $cmd, choose one of $list"; + } - my $devname = $dev->{NAME}; - my $devtype = $dev->{TYPE}; - my $events = deviceEvents($dev,1); - return if (!$events); + if ((defined($hash->{actionQueue})) and (scalar(@{$hash->{actionQueue}}) > 0)) + { + return 'There are still path commands in the action queue'; + } + + unshift( @{$hash->{actionQueue}}, $arg ); + SolarEdgeAPI_GetData($hash); - - SolarEdgeAPI_Timer_GetData($hash) if( grep /^INITIALIZED$/,@{$events} - or grep /^DELETEATTR.$name.disable$/,@{$events} - or grep /^DELETEATTR.$name.interval$/,@{$events} - or (grep /^DEFINED.$name$/,@{$events} and $init_done) ); - return; + return undef; } -sub SolarEdgeAPI_Get($@) { - - my ($hash, $name, $cmd) = @_; - my $arg; +# TODO rename? +sub SolarEdgeAPI_Timer_GetData($) +{ + my $hash = shift; + my $name = $hash->{NAME}; + my $interval = $hash->{INTERVAL}; - if( $cmd eq 'status' ) { - - $arg = lc($cmd); - - } elsif( $cmd eq 'aggregates' ) { - - $arg = lc($cmd); - - } elsif( $cmd eq 'overview' ) { - - $arg = lc($cmd); - - } else { - - my $list = 'status:noArg aggregates:noArg overview:noArg'; - - return "Unknown argument $cmd, choose one of $list"; - } - - return 'There are still path commands in the action queue' - if( defined($hash->{actionQueue}) and scalar(@{$hash->{actionQueue}}) > 0 ); - - unshift( @{$hash->{actionQueue}}, $arg ); - SolarEdgeAPI_GetData($hash); - - return undef; -} - -sub SolarEdgeAPI_Timer_GetData($) { - - my $hash = shift; - my $name = $hash->{NAME}; - my $interval = $hash->{INTERVAL}; - - if( defined($hash->{actionQueue}) and scalar(@{$hash->{actionQueue}}) == 0 ) { - if( not IsDisabled($name) ) { - while( my $obj = each %solarEdgeAPI_paths ) - { - if ( (($obj eq "status") and (AttrVal($name, "enableStatusReadings", 1))) or - (($obj eq "aggregates") and (AttrVal($name, "enableAggregatesReadings", 1))) or - (($obj eq "overview") and (AttrVal($name, "enableOverviewReadings", 0))) ) - { - unshift( @{$hash->{actionQueue}}, $obj ); - } - } - - SolarEdgeAPI_GetData($hash); - - } else { - readingsSingleUpdate($hash,'state','disabled',1); - } - } - - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = - localtime(time); - - if($interval eq "auto"){ - if ($hour > 6&& $hour < 22) { $interval = 300;} - else { $interval = 1200;} - } - - InternalTimer( gettimeofday()+$interval, 'SolarEdgeAPI_Timer_GetData', $hash ); - Log3 $name, 4, "SolarEdgeAPI ($name) - Call InternalTimer SolarEdgeAPI_Timer_GetData with interval $interval"; -} - -sub SolarEdgeAPI_GetData($) { - - my ($hash) = @_; - - my $name = $hash->{NAME}; - my $siteid = $hash->{SITEID}; - - my $host = "monitoringapi.solaredge.com/site/" . $siteid; - my $apikey = $hash->{APIKEY}; - my $path = pop( @{$hash->{actionQueue}} ); - my $params = ""; - if($path eq "aggregates" ){ - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = - localtime(time); - $params= "&timeUnit=QUARTER_OF_AN_HOUR&startTime=" . (1900+$year) . "-" . (1+$mon) . "-" . $mday . "%2000:00:00&endTime=" . (1900+$year) . "-" . (1+$mon) . "-" . $mday . "%20" . $hour . ":" . $min . ":" . $sec; - } - my $uri = $host . '/' . $solarEdgeAPI_paths{$path} . "?api_key=" . $apikey.$params; - - - - - readingsSingleUpdate($hash,'state','fetch data - ' . scalar(@{$hash->{actionQueue}}) . ' entries in the Queue',1); - - HttpUtils_NonblockingGet( - { - url => "https://" . $uri, - timeout => 5, - method => 'GET', - hash => $hash, - setCmd => $path, - doTrigger => 1, - callback => \&SolarEdgeAPI_ErrorHandling, - } - ); - - Log3 $name, 4, "SolarEdgeAPI ($name) - Send with URI: http://$uri"; -} - -sub SolarEdgeAPI_ErrorHandling($$$) { - - my ($param,$err,$data) = @_; - - my $hash = $param->{hash}; - my $name = $hash->{NAME}; - - - ### Begin Error Handling - - if( defined( $err ) ) { - if( $err ne "" ) { - - readingsBeginUpdate( $hash ); - readingsBulkUpdate( $hash, 'state', $err, 1); - readingsBulkUpdate( $hash, 'lastRequestError', $err, 1 ); - readingsEndUpdate( $hash, 1 ); - - Log3 $name, 3, "SolarEdgeAPI ($name) - RequestERROR: $err"; - - $hash->{actionQueue} = []; - return; - } - } - - if( $data eq "" and exists( $param->{code} ) && $param->{code} ne 200 ) { - - readingsBeginUpdate( $hash ); - readingsBulkUpdate( $hash, 'state', $param->{code}, 1 ); - - readingsBulkUpdate( $hash, 'lastRequestError', $param->{code}, 1 ); - - Log3 $name, 3, "SolarEdgeAPI ($name) - RequestERROR: ".$param->{code}; - - readingsEndUpdate( $hash, 1 ); - - Log3 $name, 5, "SolarEdgeAPI ($name) - RequestERROR: received http code ".$param->{code}." without any data after requesting"; - - $hash->{actionQueue} = []; - return; - } - - if( ( $data =~ /Error/i ) and exists( $param->{code} ) ) { - - readingsBeginUpdate( $hash ); - - readingsBulkUpdate( $hash, 'state', $param->{code}, 1 ); - readingsBulkUpdate( $hash, "lastRequestError", $param->{code}, 1 ); - - readingsEndUpdate( $hash, 1 ); - - Log3 $name, 3, "SolarEdgeAPI ($name) - statusRequestERROR: http error ".$param->{code}; - - $hash->{actionQueue} = []; - return; - ### End Error Handling - } - - SolarEdgeAPI_GetData($hash) - if( defined($hash->{actionQueue}) and scalar(@{$hash->{actionQueue}}) > 0 ); - - Log3 $name, 4, "SolarEdgeAPI ($name) - Receive JSON data: $data"; - - SolarEdgeAPI_ResponseProcessing($hash,$param->{setCmd},$data); -} - -sub SolarEdgeAPI_ResponseProcessing($$$) { - - my ($hash,$path,$json) = @_; - - my $name = $hash->{NAME}; - my $decode_json; - my $readings; - - - $decode_json = eval{decode_json($json)}; - if($@){ - Log3 $name, 4, "SolarEdgeAPI ($name) - error while request: $@"; - readingsBeginUpdate($hash); - readingsBulkUpdate($hash, 'JSON Error', $@); - readingsBulkUpdate($hash, 'state', 'JSON error'); - readingsEndUpdate($hash,1); - return; - } - - #### Verarbeitung der Readings zum passenden Path - - if( $path eq 'aggregates') { - $readings = SolarEdgeAPI_ReadingsProcessing_Aggregates($hash,$decode_json); - - } elsif( $path eq 'status') { - $readings = SolarEdgeAPI_ReadingsProcessing_Status($hash,$decode_json); - - } elsif( $path eq 'overview') { - $readings = SolarEdgeAPI_ReadingsProcessing_Overview($hash,$decode_json); - - } else { - $readings = $decode_json; - } - - SolarEdgeAPI_WriteReadings($hash,$path,$readings); -} - -sub SolarEdgeAPI_WriteReadings($$$) { - - my ($hash,$path,$readings) = @_; - - my $name = $hash->{NAME}; - - - Log3 $name, 4, "SolarEdgeAPI ($name) - Write Readings"; - - - readingsBeginUpdate($hash); - while( my ($r,$v) = each %{$readings} ) { - readingsBulkUpdate($hash,$path.'-'.$r,$v); - } - - readingsBulkUpdateIfChanged($hash,'actionQueue',scalar(@{$hash->{actionQueue}}) . ' entries in the Queue'); - readingsBulkUpdateIfChanged($hash,'state',(defined($hash->{actionQueue}) and scalar(@{$hash->{actionQueue}}) == 0 ? 'ready' : 'fetch data - ' . scalar(@{$hash->{actionQueue}}) . ' paths in actionQueue')); - readingsEndUpdate($hash,1); -} - -sub SolarEdgeAPI_ReadingsProcessing_Aggregates($$) { - - my ($hash,$decode_json) = @_; - - my $name = $hash->{NAME}; - my %readings; - - - if( ref($decode_json) eq "HASH" ) { - my $data = $decode_json->{'energyDetails'}; - $readings{'unit'} = $data->{'unit'}||"Error Reading Response"; - $readings{'timeUnit'} = $data->{'timeUnit'}||"Error Reading Response"; - $data = $decode_json->{'energyDetails'}->{'meters'}; - - my $meter_type = ""; - my $meter_cum = 0; - my $meter_val = 0; - - foreach my $meter ( @{$decode_json->{'energyDetails'}->{'meters'}}) { - # Meters - $meter_type = $meter->{'type'}; - $meter_cum = 0; - $meter_val = 0; - foreach my $meterTelemetry (@{$meter -> {'values'}}) { - my $v = $meterTelemetry->{'value'}; - $meter_cum = $meter_cum + $v; - $meter_val = $v; - } - $readings{$meter_type . "-recent15min"} = $meter_val; - $readings{$meter_type . "-cumToday"} = $meter_cum; - } - - } else { - $readings{'error'} = 'aggregates response is not a Hash'; - } - - return \%readings; -} - -sub SolarEdgeAPI_ReadingsProcessing_Status($$) { - - my ($hash,$decode_json) = @_; - - my $name = $hash->{NAME}; - my %readings; - my $data = $decode_json->{'siteCurrentPowerFlow'}; - - if ((defined $data) && (!defined $data->{'unit'})) + if ((defined($hash->{actionQueue})) and (scalar(@{$hash->{actionQueue}}) == 0)) + { + if (not IsDisabled($name)) { - Log3 $name, 3, "SolarEdgeAPI ($name) - API currentPowerFlow is not supported. Avoid unsuccessful server queries by setting attribute enableStatusReadings=0."; - $readings{'error'} = 'API currentPowerFlow is not supported by site.'; + while (my $obj = each %solarEdgeAPI_paths) + { + if ((($obj eq "status") and (AttrVal($name, "enableStatusReadings", 1))) or + (($obj eq "aggregates") and (AttrVal($name, "enableAggregatesReadings", 1))) or + (($obj eq "overview") and (AttrVal($name, "enableOverviewReadings", 0)))) + { + unshift( @{$hash->{actionQueue}}, $obj ); + } + } + SolarEdgeAPI_GetData($hash); + } + else + { + # TODO is this needed? + # TODO avoid inverted condition above + readingsSingleUpdate($hash,'state','disabled',1); + } + } + + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); + + if ($interval eq "auto") + { + if ($hour > 6&& $hour < 22) + { + $interval = 300; } else { - $readings{'unit'} = $data->{'unit'}||"Error Reading Response"; - $readings{'updateRefreshRate'} = $data->{'updateRefreshRate'}||"Error Reading Response"; - - # Connections / Directions - - my $pv2load = 0; - my $pv2storage = 0; - my $load2storage = 0; - my $storage2load = 0; - my $load2grid = 0; - my $grid2load = 0; - foreach my $connection ( @{ $data->{'connections'} }) { - my $from = lc($connection->{'from'}); - my $to = lc($connection->{'to'}); - if($from eq 'grid'&&$to eq "load"){ $grid2load = 1;} - if($from eq "load"&&$to eq 'grid') {$load2grid = 1;} - if($from eq 'load'&&$to eq "storage"){ $load2storage = 1;} - if($from eq 'pv'&&$to eq "storage") {$pv2storage = 1;} - if($from eq 'pv'&&$to eq "load") {$pv2load = 1;} - if($from eq 'storage'&&$to eq "load"){ $storage2load = 1;} - } - - # GRID - - $readings{'grid_status'} = $data->{'GRID'}->{"status"}||"Error Reading Response"; - $readings{'grid_power'} = ($load2grid >0 ? "-" : "") . $data->{'GRID'}->{"currentPower"}; - - # LOAD - - $readings{'load_status'} = $data->{'LOAD'}->{"status"}||"Error Reading Response"; - $readings{'load_power'} = $data->{'LOAD'}->{"currentPower"}; - - # PV - - $readings{'pv_status'} = $data->{'PV'}->{"status"}||"Error Reading Response"; - $readings{'pv_power'} = $data->{'PV'}->{"currentPower"}; - - # Storage - - $readings{'storage_status'} = $data->{'STORAGE'}->{"status"}||"No storage found"; - if ($readings{'storage_status'} ne "No storage found") - { - $readings{'storage_power'} = ($storage2load >0 ? "-" : "") . $data->{'STORAGE'}->{"currentPower"}; - $readings{'storage_level'} = $data->{'STORAGE'}->{"chargeLevel"}||"Error Reading Response"; - $readings{'storage_critical'} = $data->{'STORAGE'}->{"critical"}; - } + $interval = 1200; } - - return \%readings; + } + + InternalTimer( gettimeofday()+$interval, 'SolarEdgeAPI_Timer_GetData', $hash ); + Log3 $name, 4, "SolarEdgeAPI ($name) - Call InternalTimer SolarEdgeAPI_Timer_GetData with interval $interval"; } -sub SolarEdgeAPI_ReadingsProcessing_Overview($$) { - my ($hash, $decode_json) = @_; - my $name = $hash->{NAME}; +# TODO rename +sub SolarEdgeAPI_GetData($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + my $siteid = $hash->{SITEID}; + my $host = "monitoringapi.solaredge.com/site/" . $siteid; + my $apikey = $hash->{APIKEY}; + my $path = pop(@{$hash->{actionQueue}}); + + # TODO explain + my $params = ""; + if ($path eq "aggregates") + { + my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); + $params= "&timeUnit=QUARTER_OF_AN_HOUR&startTime=".(1900+$year)."-".(1+$mon)."-".$mday."%2000:00:00&endTime=".(1900+$year)."-".(1+$mon)."-".$mday."%20".$hour.":".$min.":".$sec; + } + + my $uri = $host . '/' . $solarEdgeAPI_paths{$path} . "?api_key=" . $apikey.$params; + + # TODO remove this? + readingsSingleUpdate($hash, 'state', 'fetch data - '.scalar(@{$hash->{actionQueue}}).' entries in the Queue',1); + + HttpUtils_NonblockingGet( + { + url => "https://".$uri, + timeout => 5, + method => 'GET', + hash => $hash, + setCmd => $path, + doTrigger => 1, + callback => \&SolarEdgeAPI_ErrorHandling, + } + ); - my %readings; - my $data = $decode_json->{'overview'}; + Log3 $name, 4, "SolarEdgeAPI ($name) - Send with URI: http://$uri"; +} + +# TODO rename +sub SolarEdgeAPI_ErrorHandling($$$) +{ + my ($param,$err,$data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + + # TODO factor out error handing + ### Begin Error Handling - $readings{'power'} = $data->{'currentPower'}->{"power"}; + if (defined($err) and ($err ne "")) + { + # TODO do error reporting via Log3 only!? + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, 'state', $err, 1); + readingsBulkUpdate($hash, 'lastRequestError', $err, 1); + readingsEndUpdate($hash, 1); + + Log3 $name, 3, "SolarEdgeAPI ($name) - RequestERROR: $err"; + + $hash->{actionQueue} = []; + return; + } + + if (($data eq "") and (exists($param->{code})) and ($param->{code} ne 200)) + { + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, 'state', $param->{code}, 1); + readingsBulkUpdate($hash, 'lastRequestError', $param->{code}, 1); + readingsEndUpdate($hash, 1); - return \%readings; + Log3 $name, 3, "SolarEdgeAPI ($name) - RequestERROR: ".$param->{code}; + Log3 $name, 5, "SolarEdgeAPI ($name) - RequestERROR: received http code ".$param->{code}." without any data after requesting"; + + $hash->{actionQueue} = []; + return; + } + + if (($data =~ /Error/i) and (exists( $param->{code}))) + { + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, 'state', $param->{code}, 1); + readingsBulkUpdate($hash, "lastRequestError", $param->{code}, 1); + readingsEndUpdate($hash, 1); + + Log3 $name, 3, "SolarEdgeAPI ($name) - statusRequestERROR: http error ".$param->{code}; + + $hash->{actionQueue} = []; + return; + } + + ### End Error Handling + + # TODO Is the order ok: first sending the next request the processing the response (below)!? + if (defined($hash->{actionQueue}) and scalar(@{$hash->{actionQueue}}) > 0) + { + SolarEdgeAPI_GetData($hash); + } + + Log3 $name, 4, "SolarEdgeAPI ($name) - Receive JSON data: $data"; + + SolarEdgeAPI_ResponseProcessing($hash, $param->{setCmd}, $data); +} + +sub SolarEdgeAPI_ResponseProcessing($$$) +{ + my ($hash, $path, $json) = @_; + my $name = $hash->{NAME}; + + my $readings; + + my $decode_json; # TODO rename, here and in other functions + $decode_json = eval{decode_json($json)}; + if ($@) + { + # TODO error reporting via Log3 only!? + Log3 $name, 4, "SolarEdgeAPI ($name) - error while request: $@"; + readingsBeginUpdate($hash); + readingsBulkUpdate($hash, 'JSON Error', $@); + readingsBulkUpdate($hash, 'state', 'JSON error'); + readingsEndUpdate($hash,1); + return; + } + + # TODO english + #### Verarbeitung der Readings zum passenden Path + + if ($path eq 'aggregates') + { + $readings = SolarEdgeAPI_ReadingsProcessing_Aggregates($hash,$decode_json); + } + elsif ($path eq 'status') + { + $readings = SolarEdgeAPI_ReadingsProcessing_Status($hash,$decode_json); + } + elsif ($path eq 'overview') + { + $readings = SolarEdgeAPI_ReadingsProcessing_Overview($hash,$decode_json); + } + else + { + # TODO Does this make sense? Understand what this does. + $readings = $decode_json; + } + + SolarEdgeAPI_WriteReadings($hash, $path, $readings); +} + +sub SolarEdgeAPI_WriteReadings($$$) +{ + my ($hash, $path, $readings) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 4, "SolarEdgeAPI ($name) - Write Readings"; + + readingsBeginUpdate($hash); + while (my ($r,$v) = each %{$readings}) + { + readingsBulkUpdate($hash,$path.'-'.$r,$v); + } + # TODO remove this reading? Replace by logging. + readingsBulkUpdateIfChanged($hash, 'actionQueue', scalar(@{$hash->{actionQueue}}).' entries in the Queue'); + # TODO remove this reading? + readingsBulkUpdateIfChanged($hash, 'state', ((defined($hash->{actionQueue}) and (scalar(@{$hash->{actionQueue}}) == 0)) ? 'ready' : 'fetch data - '.scalar(@{$hash->{actionQueue}}).' paths in actionQueue')); + readingsEndUpdate($hash, 1); +} + +sub SolarEdgeAPI_ReadingsProcessing_Aggregates($$) +{ + my ($hash, $decode_json) = @_; + my $name = $hash->{NAME}; + + my %readings; + + if (ref($decode_json) eq "HASH") + { + my $data = $decode_json->{'energyDetails'}; + $readings{'unit'} = $data->{'unit'} || "Error Reading Response"; + $readings{'timeUnit'} = $data->{'timeUnit'} || "Error Reading Response"; + + $data = $decode_json->{'energyDetails'}->{'meters'}; + my $meter_type = ""; + my $meter_cum = 0; + my $meter_val = 0; + foreach my $meter (@{$decode_json->{'energyDetails'}->{'meters'}}) + { + # meters + $meter_type = $meter->{'type'}; + $meter_cum = 0; + $meter_val = 0; + foreach my $meterTelemetry (@{$meter->{'values'}}) + { + my $v = $meterTelemetry->{'value'}; + $meter_cum = $meter_cum + $v; + $meter_val = $v; + } + $readings{$meter_type."-recent15min"} = $meter_val; + $readings{$meter_type."-cumToday"} = $meter_cum; + } + } + else + { + # TODO do error reporting via Log3 only + $readings{'error'} = 'aggregates response is not a Hash'; + } + + return \%readings; +} + +sub SolarEdgeAPI_ReadingsProcessing_Status($$) +{ + my ($hash, $decode_json) = @_; + my $name = $hash->{NAME}; + + my %readings; + my $data = $decode_json->{'siteCurrentPowerFlow'}; + + if ((defined $data) && (!defined $data->{'unit'})) + { + Log3 $name, 3, "SolarEdgeAPI ($name) - API currentPowerFlow is not supported. Avoid unsuccessful server queries by setting attribute enableStatusReadings=0."; + $readings{'error'} = 'API currentPowerFlow is not supported by site.'; + } + else + { + $readings{'unit'} = $data->{'unit'} || "Error Reading Response"; + $readings{'updateRefreshRate'} = $data->{'updateRefreshRate'} || "Error Reading Response"; + + # Connections / Directions + my $pv2load = 0; + my $pv2storage = 0; + my $load2storage = 0; + my $storage2load = 0; + my $load2grid = 0; + my $grid2load = 0; + foreach my $connection ( @{ $data->{'connections'} }) { + my $from = lc($connection->{'from'}); + my $to = lc($connection->{'to'}); + # TODO don't mix " and ' + if (($from eq 'grid') and ($to eq "load")) { $grid2load = 1; } + if (($from eq "load") and ($to eq 'grid')) { $load2grid = 1; } + if (($from eq 'load') and ($to eq "storage")) { $load2storage = 1; } + if (($from eq 'pv') and ($to eq "storage")) { $pv2storage = 1; } + if (($from eq 'pv') and ($to eq "load")) { $pv2load = 1; } + if (($from eq 'storage') and ($to eq "load")) { $storage2load = 1; } + } + + # GRID + $readings{'grid_status'} = $data->{'GRID'}->{"status"} || "Error Reading Response"; # TODO rethink error reporting via readings + $readings{'grid_power'} = (($load2grid > 0) ? "-" : "").$data->{'GRID'}->{"currentPower"}; + + # LOAD + $readings{'load_status'} = $data->{'LOAD'}->{"status"} || "Error Reading Response"; + $readings{'load_power'} = $data->{'LOAD'}->{"currentPower"}; + + # PV + $readings{'pv_status'} = $data->{'PV'}->{"status"} || "Error Reading Response"; + $readings{'pv_power'} = $data->{'PV'}->{"currentPower"}; + + # Storage + $readings{'storage_status'} = $data->{'STORAGE'}->{"status"} || "No storage found"; + if ($readings{'storage_status'} ne "No storage found") + { + $readings{'storage_power'} = (($storage2load > 0) ? "-" : "").$data->{'STORAGE'}->{"currentPower"}; + $readings{'storage_level'} = $data->{'STORAGE'}->{"chargeLevel"} || "Error Reading Response"; + $readings{'storage_critical'} = $data->{'STORAGE'}->{"critical"}; + } + } + + return \%readings; +} + +sub SolarEdgeAPI_ReadingsProcessing_Overview($$) +{ + my ($hash, $decode_json) = @_; + my $name = $hash->{NAME}; + + my %readings; + my $data = $decode_json->{'overview'}; + + $readings{'power'} = $data->{'currentPower'}->{"power"}; + + # TODO generate more readings from the overview API. Some readings might only be relevant once per day. + + return \%readings; }