############################################################################### # # $Id$ # # By (c) 2019 FHEM user 'pizmus' (pizmus at web de) # # Based on 70_SolarEdgeAPI.pm from https://github.com/felixmartens/fhem by # (c) 2018 Felix Martens (felix at martensmail dot de) # # Based on 46_TeslaPowerwall2AC by # (c) 2017 Copyright: Marko Oldenburg (leongaultier at gmail dot com) # # All rights reserved # # This script is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # any later version. # # The GNU General Public License can be found at # http://www.gnu.org/copyleft/gpl.html. # A copy is found in the textfile GPL.txt and important notices to the license # from the author is found in LICENSE.txt distributed with these scripts. # # This script is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # ############################################################################### package main; use strict; use warnings; use HttpUtils; ############################################################################### # # Note: Always call the JSON module via "eval": # # $data = eval{decode_json($data)}; # if($@){ # Log3($SELF, 2, "$TYPE ($SELF) - error while request: $@"); # readingsSingleUpdate($hash, "state", "error", 1); # return; # } # ############################################################################### my $solarEdgeAPI_missingModul = ""; eval "use JSON;1" or $solarEdgeAPI_missingModul .= "JSON "; ############################################################################### # # versioning scheme: ..[betaXYZ] # # The is incremented for changes which are not backward compatible. # A change of the may require adaptations on the user side, for # some or all users, e.g. because a reading is removed or has a new meaning. # # The is incremented for changes which are backward compatible, # e.g. added functionality which does not impact old functionality. # # The is incremented for small bug fixes, changes of source code # comments or documentation. # # A string starting with "beta" is attached for release candidates which are # distributed for testing. If no issues are found in a beta version, the "beta" # string is removed and the source file is submitted. # ############################################################################### # # 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.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 # # 1.2.0 added internals that count requests, successful responses and error # responses # added "set restartTimer" and "set resetDebugCounters" # added attributes: # intervalAtNightTime # dayTimeStartHour # nightTimeStartHour # enableDebugReadings # added internal NUMBER_OF_REQUESTS_PER_DAY that shows the # theoretical number of http requests per day, based on current # attribute settings # Parameter "interval" of the "define" function is now optional. If # it is not provided the default value "auto" is used. # If the new attributes are not set by the user, the default values # are chosen so that behavior is same as in previous versions. # Restart periodic timer during _Define instead of _Notify. # # 1.3.0 show SolarEdge logo to comply with requirement from API documentation # # 1.4.0 new reading groups: dailyAggregates, storage, dailyStorage, # dailyDetails, dailyOverview # # 2.0.0 changes which are not backward compatible: # - "define" does not assign attribute room="Photovoltaik" anymore # reason: different users organize rooms differently, it is not the business of the FHEM module # impact: attribute room has to be assigned by user for new devices # - remove parameter "interval" of "define" function # reason: The interval and other related settings are configured via attributes. # Attributes are easy to change while the device is alive. Making the same setting # via define is redundant and increases complexity. # impact: existing devices will fail after update/restart if the optional parameter # was used. The device definition has to be changed by removing the last parameter. # - rename attribute "interval" to "intervalAtDayTime" # reason: make names consistent (intervalAtDayTime/intervalAtNightTime) # impact: All users who have specified attribute "interval" must change it to "intervalAtDayTime". # - default values of attributes: # "enableStatusReadings" -> change from 1 to 0 # "enableAggregatesReadings" -> change from 1 to 0 # "enableOverviewReadings" -> change from 0 to 1 # "enableDailyDetailsReadings" -> change from 0 to 1 # "enableDailyOverviewReadings" -> change from 0 to 1 # "dayTimeStartHour" -> change from 7 to 6 # "intervalAtDayTime" -> change from 300 to 215 # reason: provide a simple default configuration that works as a good starting point for new users # impact: Users that have started with older versions, and who rely on default values, have to set attributes. # - do not show number of queue entries in readings "state" and "actionQueue". # example of state value (old behavior): "fetch data - 2 entries in the Queue" # reason: not a good value of "state" to trigger on # impact: Most likely none. # - Do not show http errors in readings "state" and "lastRequestError", write error message to log file instead. # reason: not a good value of "state" to trigger on. Information should be in the log file. # impact: Most likely none. From now on look at log file for error messages. # - Do not show JSON errors in readings "JSON Error" and "state", write error message to log file instead. # reason: not a good value of "state" to trigger on. Information should be in the log file. # impact: Most likely none. From now on look at log file for error messages. # - Do not report "aggregates response is not a Hash" via reading "error". # Do not report "API currentPowerFlow is not supported by site." via reading "error". # reason: Information should be in the log file. # impact: Most likely none. # - Do not assign text messages to *_status readings of "status" readings group. # Use "-" instead if no data is available. # reason: Simplify automatic processing of readings. # impact: User needs to change e.g. "notify" definitions, if any. # - Set the internal "STATE" and the reading "state" to: # - "error" if the last http request has shown an error condition # - "disabled" if the device is disabled # - "active" otherwise # # 2.0.1 tolerate empty field in energyDetails response # # 2.1.0 generate daily readings at ~23:59 instead of 22:00 # # 2.2.0 allow smaller values for attributes intervalAtDayTime and intervalAtNightTime # ############################################################################### sub SolarEdgeAPI_SetVersion($) { my ($hash) = @_; $hash->{VERSION} = "2.2.0"; } ############################################################################### # module interface functions ############################################################################### sub SolarEdgeAPI_Initialize($) { my ($hash) = @_; $hash->{GetFn} = "SolarEdgeAPI_Get"; $hash->{SetFn} = "SolarEdgeAPI_Set"; $hash->{DefFn} = "SolarEdgeAPI_Define"; $hash->{UndefFn} = "SolarEdgeAPI_Undef"; $hash->{AttrFn} = "SolarEdgeAPI_Attr"; $hash->{AttrList} = "intervalAtDayTime ". "intervalAtNightTime ". "dayTimeStartHour ". "nightTimeStartHour ". "disable:1 ". "enableStatusReadings:1,0 ". "enableAggregatesReadings:1,0 ". "enableOverviewReadings:1,0 ". "enableStorageReadings:1,0 ". "enableDailyDetailsReadings:1,0 ". "enableDailyStorageReadings:1,0 ". "enableDailyAggregatesReadings:1,0 ". "enableDailyOverviewReadings:1,0 ". "enableDebugReadings:1,0 ". $readingFnAttributes; $hash->{FW_detailFn} = "SolarEdgeAPI_fhemwebFn"; } sub SolarEdgeAPI_Define($$) { my ($hash, $def) = @_; my @a = split( "[ \t][ \t]*", $def ); if (int(@a) != 4) { return "incorrect number of parameters: define SolarEdgeAPI "; } if ($solarEdgeAPI_missingModul) { return "Cannot define a SolarEdgeAPI device. Perl modul $solarEdgeAPI_missingModul is missing."; } my $name = $a[0]; $hash->{APIKEY} = $a[2]; $hash->{SITEID} = $a[3]; $hash->{DEFAULT_DAY_TIME_INTERVAL} = 215; $hash->{DEFAULT_NIGHT_TIME_INTERVAL} = 1200; $hash->{DEFAULT_DAY_TIME_START_HOUR} = 6; $hash->{DEFAULT_NIGHT_TIME_START_HOUR} = 22; $hash->{PORT} = 80; $hash->{NOTIFYDEV} = "global"; $hash->{actionQueue} = []; SolarEdgeAPI_ResetDebugCounters($hash); SolarEdgeAPI_SetVersion($hash); Log3 $name, 3, "SolarEdgeAPI ($name) - defined"; my %paths = ( 'status' => 'currentPowerFlow.json', 'aggregates' => 'energyDetails.json', 'overview' => 'overview.json', 'storage' => 'storageData.json', 'dailyDetails' => 'details.json', 'dailyStorage' => 'storageData.json', 'dailyOverview' => 'overview.json', 'dailyAggregates' => 'energyDetails.json' ); $hash->{PATHS} = \%paths; # remove any active timer RemoveInternalTimer($hash); # initiate periodic readings InternalTimer(gettimeofday() + 60, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); SolarEdgeAPI_UpdateState($hash, "active"); return undef; } sub SolarEdgeAPI_ResetDebugCounters($) { my ($hash) = @_; my $name = $hash->{NAME}; $hash->{NUMBER_OF_REQUESTS} = 0; $hash->{NUMBER_OF_GOOD_RESPONSES} = 0; $hash->{NUMBER_OF_ERROR_1} = 0; $hash->{NUMBER_OF_ERROR_2} = 0; $hash->{NUMBER_OF_ERROR_3} = 0; $hash->{NUMBER_OF_JSON_ERRORS} = 0; $hash->{NUMBER_OF_REQUESTS_PER_DAY} = 0; if (AttrVal($name, "enableDebugReadings", undef)) { readingsSingleUpdate($hash, 'debugNumRequests', $hash->{NUMBER_OF_REQUESTS}, 1); readingsSingleUpdate($hash, 'debugNumGoodResponses', $hash->{NUMBER_OF_GOOD_RESPONSES}, 1); readingsSingleUpdate($hash, 'debugNumJsonErrors', $hash->{NUMBER_OF_JSON_ERRORS}, 1); readingsSingleUpdate($hash, 'debugNumError1', $hash->{NUMBER_OF_ERROR_1}, 1); readingsSingleUpdate($hash, 'debugNumError2', $hash->{NUMBER_OF_ERROR_2}, 1); readingsSingleUpdate($hash, 'debugNumError3', $hash->{NUMBER_OF_ERROR_3}, 1); } } sub SolarEdgeAPI_Undef($$) { my ($hash, $arg) = @_; my $name = $hash->{NAME}; Log3 $name, 3, "SolarEdgeAPI ($name) - deleted"; # remove any active timer RemoveInternalTimer($hash); return undef; } sub SolarEdgeAPI_Attr(@) { my ($cmd, $name, $attrName, $attrVal) = @_; my $hash = $defs{$name}; if ($attrName eq "disable") { if ($cmd eq "set") { if ($attrVal eq "1") { RemoveInternalTimer($hash); SolarEdgeAPI_UpdateState($hash, "disabled"); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute disable=1"; } elsif ($attrVal eq "0") { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); SolarEdgeAPI_UpdateState($hash, "active"); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute disable=0"; } else { my $message = "unexpected value for attribute disable"; Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; return $message; } } elsif ($cmd eq "del") { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); SolarEdgeAPI_UpdateState($hash, "active"); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute disable deleted"; } } 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?)+$/); SolarEdgeAPI_UpdateState($hash, "disabled"); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute disabledForIntervals set"; } elsif ($cmd eq "del") { SolarEdgeAPI_UpdateState($hash, "active"); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute disabledForIntervals deleted"; } } if ($attrName eq "intervalAtDayTime") { if ($cmd eq "set") { if (($attrVal eq "auto") || ($attrVal >= 1)) { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute intervalAtDayTime set to $attrVal"; } else { my $message = "intervalAtDayTime is out of range"; Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; return $message; } } elsif ($cmd eq "del") { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute intervalAtDayTime deleted"; } } if ($attrName eq "intervalAtNightTime") { if ($cmd eq "set") { if (($attrVal < 1) or ($attrVal > 3600)) { my $message = "intervalAtNightTime is out of range"; Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; return $message; } else { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute intervalAtNightTime set to $attrVal"; } } elsif ($cmd eq "del") { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute intervalAtNightTime deleted"; } } if ($attrName eq "dayTimeStartHour") { if ($cmd eq "set") { if (($attrVal < 3) or ($attrVal > 10)) { my $message = "dayTimeStartHour is out of range"; Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; return $message; } else { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute dayTimeStartHour set to $attrVal"; } } elsif ($cmd eq "del") { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute dayTimeStartHour deleted"; } } if ($attrName eq "nightTimeStartHour") { if ($cmd eq "set") { if (($attrVal < 14) or ($attrVal > 22)) { my $message = "nightTimeStartHour is out of range"; Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; return $message; } else { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute nightTimeStartHour set to $attrVal"; } } elsif ($cmd eq "del") { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); Log3 $name, 3, "SolarEdgeAPI ($name) - attribute nightTimeStartHour deleted"; } } if (($attrName eq "enableStatusReadings") or ($attrName eq "enableAggregatesReadings") or ($attrName eq "enableOverviewReadings") or ($attrName eq "enableStorageReadings") or ($attrName eq "enableDailyDetailsReadings") or ($attrName eq "enableDailyStorageReadings") or ($attrName eq "enableDailyOverviewReadings") or ($attrName eq "enableDailyAggregatesReadings")) { if($cmd eq "set") { if (not (($attrVal eq "0") || ($attrVal eq "1"))) { my $message = "illegal value for $attrName"; Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; return $message; } else { InternalTimer(gettimeofday() + 5, 'SolarEdgeAPI_RestartHttpRequestTimers', $hash); } } } if ($attrName eq "enableDebugReadings") { if($cmd eq "set") { if (not (($attrVal eq "0") || ($attrVal eq "1"))) { my $message = "illegal value for enableDebugReadings"; Log3 $name, 3, "SolarEdgeAPI ($name) - ".$message; return $message; } } } return undef; } sub SolarEdgeAPI_Set($$) { my ($hash, @parameters) = @_; my $name = $parameters[0]; my $what = $parameters[1]; if ($what eq "restartTimer") { Log3 $name, 3, "SolarEdgeAPI ($name) - set restartTimer"; SolarEdgeAPI_RestartHttpRequestTimers($hash); } elsif ($what eq "resetDebugCounters") { Log3 $name, 3, "SolarEdgeAPI ($name) - set resetDebugCounters"; SolarEdgeAPI_ResetDebugCounters($hash); } elsif ($what eq "?") { my $message = "unknown argument $what, choose one of restartTimer:noArg resetDebugCounters:noArg"; return $message; } else { my $message = "unknown argument $what, choose one of restartTimer resetDebugCounters"; Log3 $name, 1, "SolarEdgeAPI ($name) - ".$message; return $message; } return undef; } sub SolarEdgeAPI_Get($@) { my ($hash, $name, $cmd) = @_; if (($cmd eq 'status') or ($cmd eq 'aggregates') or ($cmd eq 'overview') or ($cmd eq 'dailyOverview') or ($cmd eq 'storage') or ($cmd eq 'dailyDetails') or ($cmd eq 'dailyStorage') or ($cmd eq 'dailyAggregates')) { Log3 $name, 3, "SolarEdgeAPI ($name) - get command: ".$cmd; if ((defined($hash->{actionQueue})) and (scalar(@{$hash->{actionQueue}}) > 0)) { Log3 $name, 3, "SolarEdgeAPI ($name) - get command ".$cmd." ignored because actionQueue is not empty"; return 'There are still path commands in the action queue'; } unshift( @{$hash->{actionQueue}}, $cmd ); SolarEdgeAPI_SendHttpRequest($hash); } elsif ($cmd eq 'numberOfRequests') { my $daytimeInterval = AttrVal($name, "intervalAtDayTime", $hash->{DEFAULT_DAY_TIME_INTERVAL}); my $nighttimeInterval = AttrVal($name, "intervalAtNightTime", $hash->{DEFAULT_NIGHT_TIME_INTERVAL}); my $dayTimeStartHour = AttrVal($name, "dayTimeStartHour", $hash->{DEFAULT_DAY_TIME_START_HOUR}); my $nightTimeStartHour = AttrVal($name, "nightTimeStartHour", $hash->{DEFAULT_NIGHT_TIME_START_HOUR}); my $numberOfDaytimeHours = $nightTimeStartHour - $dayTimeStartHour; my $numberOfNighttimeHours = 24 - $numberOfDaytimeHours; my $numberOfPeriodicHttpRequests = 0; if (AttrVal($name, "enableStatusReadings", 0)) { $numberOfPeriodicHttpRequests += 1; } if (AttrVal($name, "enableAggregatesReadings", 0)) { $numberOfPeriodicHttpRequests += 1; } if (AttrVal($name, "enableOverviewReadings", 1)) { $numberOfPeriodicHttpRequests += 1; } if (AttrVal($name, "enableStorageReadings", 0)) { $numberOfPeriodicHttpRequests += 1; } $hash->{NUMBER_OF_REQUESTS_PER_DAY} = (($numberOfDaytimeHours * 3600 / $daytimeInterval + $numberOfNighttimeHours * 3600 / $nighttimeInterval) * $numberOfPeriodicHttpRequests) + (AttrVal($name, "enableDailyStorageReadings", 0)) + (AttrVal($name, "enableDailyOverviewReadings", 1)) + (AttrVal($name, "enableDailyDetailsReadings", 1)) + (AttrVal($name, "enableDailyAggregatesReadings", 0)); return $hash->{NUMBER_OF_REQUESTS_PER_DAY}; } else { my $list = 'status:noArg aggregates:noArg overview:noArg dailyOverview:noArg storage:noArg dailyDetails:noArg dailyStorage:noArg dailyAggregates:noArg numberOfRequests:noArg'; return "Unknown argument $cmd, choose one of $list"; } return undef; } ############################################################################### # HTTP request generation ############################################################################### # precondition: There must be at least one entry in actionQueue. sub SolarEdgeAPI_SendHttpRequest($) { 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}}); # some API require additional parameters, e.g. the time frame and time # resolution to use with the query my $params = ""; if ($path eq "aggregates") { # request data for the timeframe from midnight until now 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; } elsif ($path eq "dailyAggregates") { # request data for the timeframe from January 1st until today my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); $params = "&timeUnit=DAY&startTime=".(1900+$year)."-1-1"."%20"."00:00:00&endTime=".(1900+$year)."-".(1+$mon)."-".$mday."%20".$hour.":".$min.":".$sec; } elsif (($path eq "storage") or ($path eq "dailyStorage")) { # request data for the last 1/2 hour my ($sec1,$min1,$hour1,$mday1,$mon1,$year1,$wday1,$yday1,$isdst1) = localtime(time()); my ($sec2,$min2,$hour2,$mday2,$mon2,$year2,$wday2,$yday2,$isdst2) = localtime(time() - (30 * 60)); $params = "&startTime=".(1900+$year2)."-".(1+$mon2)."-".$mday2."%20".$hour2.":".$min2.":".$sec2. "&endTime=".(1900+$year1)."-".(1+$mon1)."-".$mday1."%20".$hour1.":".$min1.":".$sec1; } my $pathsRef = $hash->{PATHS}; my %paths = %$pathsRef; my $uri = $host . '/' . $paths{$path} . "?api_key=" . $apikey.$params; HttpUtils_NonblockingGet( { url => "https://".$uri, timeout => 5, method => 'GET', hash => $hash, setCmd => $path, doTrigger => 1, callback => \&SolarEdgeAPI_HandleHttpResponse, } ); # update debug counter $hash->{NUMBER_OF_REQUESTS} = $hash->{NUMBER_OF_REQUESTS} + 1; if (AttrVal($name, "enableDebugReadings", undef)) { readingsSingleUpdate($hash, 'debugNumRequests', $hash->{NUMBER_OF_REQUESTS}, 1); } Log3 $name, 4, "SolarEdgeAPI ($name) - SolarEdgeAPI_SendHttpRequest path: $path / $paths{$path}"; Log3 $name, 5, "SolarEdgeAPI ($name) - request: http://$uri"; } sub SolarEdgeAPI_PeriodicHttpRequestTimerFunction($) { my $hash = shift; my $name = $hash->{NAME}; Log3 $name, 4, "SolarEdgeAPI ($name) - periodic timer expired"; my $pathsRef = $hash->{PATHS}; my %paths = %$pathsRef; if ((defined($hash->{actionQueue})) and (scalar(@{$hash->{actionQueue}}) < 100)) { if (not IsDisabled($name)) { while (my $obj = each %paths) { if ((($obj eq "status") and (AttrVal($name, "enableStatusReadings", 0))) or (($obj eq "aggregates") and (AttrVal($name, "enableAggregatesReadings", 0))) or (($obj eq "overview") and (AttrVal($name, "enableOverviewReadings", 1))) or (($obj eq "storage") and (AttrVal($name, "enableStorageReadings", 0)))) { Log3 $name, 4, "SolarEdgeAPI ($name) - adding periodic request to actionQueue: ".$obj; unshift( @{$hash->{actionQueue}}, $obj ); } } SolarEdgeAPI_SendHttpRequest($hash); } else { SolarEdgeAPI_UpdateState($hash, "disabled"); } } InternalTimer(SolarEdgeAPI_GetTimeOfNextReading($hash), 'SolarEdgeAPI_PeriodicHttpRequestTimerFunction', $hash); } sub SolarEdgeAPI_DailyHttpRequestTimerFunction($) { my $hash = shift; my $name = $hash->{NAME}; Log3 $name, 4, "SolarEdgeAPI ($name) - daily timer expired"; my $pathsRef = $hash->{PATHS}; my %paths = %$pathsRef; if ((defined($hash->{actionQueue})) and (scalar(@{$hash->{actionQueue}}) < 100)) { if (not IsDisabled($name)) { while (my $obj = each %paths) { if ((($obj eq "dailyDetails") and (AttrVal($name, "enableDailyDetailsReadings", 1))) or (($obj eq "dailyStorage") and (AttrVal($name, "enableDailyStorageReadings", 0))) or (($obj eq "dailyOverview") and (AttrVal($name, "enableDailyOverviewReadings", 1))) or (($obj eq "dailyAggregates") and (AttrVal($name, "enableDailyAggregatesReadings", 0)))) { Log3 $name, 4, "SolarEdgeAPI ($name) - adding daily request to actionQueue: ".$obj; unshift( @{$hash->{actionQueue}}, $obj ); } } SolarEdgeAPI_SendHttpRequest($hash); } else { SolarEdgeAPI_UpdateState($hash, "disabled"); } } InternalTimer(SolarEdgeAPI_GetTimeOfNextDailyReading($hash), 'SolarEdgeAPI_DailyHttpRequestTimerFunction', $hash); } sub SolarEdgeAPI_RestartHttpRequestTimers($) { my $hash = shift; my $name = $hash->{NAME}; Log3 $name, 3, "SolarEdgeAPI ($name) - restarting timer"; # remove all active timers RemoveInternalTimer($hash); # Do the next http request now. This will start a timer for the next one. SolarEdgeAPI_PeriodicHttpRequestTimerFunction($hash); # Schedule the first daily request now. This will start a timer for the next one. InternalTimer(SolarEdgeAPI_GetTimeOfNextDailyReading($hash), 'SolarEdgeAPI_DailyHttpRequestTimerFunction', $hash); } sub SolarEdgeAPI_GetTimeOfNextDailyReading($) { my ($hash) = @_; my $name = $hash->{NAME}; my $epoch = time(); my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($epoch); if (($hour >= 23) and ($min >= 59)) { # If it is 23:59 the next reading should occur tomorrow. # add 24 hours to epoch to get a time during the following day $epoch += 24 * 60 * 60; # convert again ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($epoch); } # change hour:minute to 23:59 and convert to epoch $epoch = fhemTimeLocal(0, 59, 23, $mday, $mon, $year); # $sec, $min, $hour, $mday, $month, $year return $epoch; } sub SolarEdgeAPI_GetTimeOfNextReading($) { my ($hash) = @_; my $name = $hash->{NAME}; my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); my $dayTimeStartHour = AttrVal($name, "dayTimeStartHour", $hash->{DEFAULT_DAY_TIME_START_HOUR}); my $nightTimeStartHour = AttrVal($name, "nightTimeStartHour", $hash->{DEFAULT_NIGHT_TIME_START_HOUR}); my $daytimeInterval = AttrVal($name, "intervalAtDayTime", $hash->{DEFAULT_DAY_TIME_INTERVAL}); my $nighttimeInterval = AttrVal($name, "intervalAtNightTime", $hash->{DEFAULT_NIGHT_TIME_INTERVAL}); # select the interval to use now my $interval; if (($hour >= $dayTimeStartHour) && ($hour < $nightTimeStartHour)) { $interval = $daytimeInterval; } else { $interval = $nighttimeInterval; } # TODO if the next night time interval ends after dayTimeStartHour change interval so # that the next request goes out at dayTimeStartHour my $newTriggerTime = gettimeofday() + $interval; Log3 $name, 4, "SolarEdgeAPI ($name) - next reading in $interval seconds"; return $newTriggerTime; } ############################################################################### # HTTP response handling ############################################################################### sub SolarEdgeAPI_CheckHttpError($$$$) { my ($hash, $param, $err, $data) = @_; my $name = $hash->{NAME}; if (defined($err) and ($err ne "")) { # update debug counter $hash->{NUMBER_OF_ERROR_1} = $hash->{NUMBER_OF_ERROR_1} + 1; if (AttrVal($name, "enableDebugReadings", undef)) { readingsSingleUpdate($hash, 'debugNumError1', $hash->{NUMBER_OF_ERROR_1}, 1); } Log3 $name, 3, "SolarEdgeAPI ($name) - error (1) in http response: $err"; SolarEdgeAPI_UpdateState($hash, "error"); # drop all outstanding requests $hash->{actionQueue} = []; return 1; } if (($data eq "") and (exists($param->{code})) and ($param->{code} ne 200)) { # update debug counter $hash->{NUMBER_OF_ERROR_2} = $hash->{NUMBER_OF_ERROR_2} + 1; if (AttrVal($name, "enableDebugReadings", undef)) { readingsSingleUpdate($hash, 'debugNumError2', $hash->{NUMBER_OF_ERROR_2}, 1); } Log3 $name, 3, "SolarEdgeAPI ($name) - error (2) in http response, no data, code: ".$param->{code}; SolarEdgeAPI_UpdateState($hash, "error"); # drop all outstanding requests $hash->{actionQueue} = []; return 2; } if (($data =~ /Error/i) and (exists( $param->{code}))) { # update debug counter $hash->{NUMBER_OF_ERROR_3} = $hash->{NUMBER_OF_ERROR_3} + 1; if (AttrVal($name, "enableDebugReadings", undef)) { readingsSingleUpdate($hash, 'debugNumError3', $hash->{NUMBER_OF_ERROR_3}, 1); } Log3 $name, 3, "SolarEdgeAPI ($name) - error (3) in http response, code: ".$param->{code}; SolarEdgeAPI_UpdateState($hash, "error"); # drop all outstanding requests $hash->{actionQueue} = []; return 3; } return undef; } sub SolarEdgeAPI_HandleHttpResponse($$$) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; Log3 $name, 4, "SolarEdgeAPI ($name) - SolarEdgeAPI_HandleHttpResponse"; if (SolarEdgeAPI_CheckHttpError($hash, $param, $err, $data)) { return; } Log3 $name, 5, "SolarEdgeAPI ($name) - received JSON data: $data"; # update debug counter $hash->{NUMBER_OF_GOOD_RESPONSES} = $hash->{NUMBER_OF_GOOD_RESPONSES} + 1; if (AttrVal($name, "enableDebugReadings", undef)) { readingsSingleUpdate($hash, 'debugNumGoodResponses', $hash->{NUMBER_OF_GOOD_RESPONSES}, 1); } SolarEdgeAPI_ProcessResponse($hash, $param->{setCmd}, $data); if (defined($hash->{actionQueue}) and scalar(@{$hash->{actionQueue}}) > 0) { SolarEdgeAPI_SendHttpRequest($hash); } } sub SolarEdgeAPI_ProcessResponse($$$) { my ($hash, $path, $data) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "SolarEdgeAPI ($name) - SolarEdgeAPI_ProcessResponse: $path"; my $readings; # generate fake data for storage data API for debug purposes my $generateFakeStorageData = 0; if ((($path eq 'storage') or ($path eq 'dailyStorage')) and ($generateFakeStorageData)) { $data = '{"storageData":{"batteryCount":1,"batteries":[{ "nameplate":9800.0,"serialNumber":"R155XXX","modelNumber":"R155XXX","telemetryCount":4,"telemetries": [{"timeStamp":"2019-11-15 00:02:35","power":100.0,"batteryState":3,"lifeTimeEnergyDischarged":2275121,"lifeTimeEnergyCharged":2646795,"batteryPercentageState":8.999232,"fullPackEnergyAvailable":9999.0,"internalTemp":21.1,"ACGridCharging":0.0}, {"timeStamp":"2019-11-15 00:07:34","power":100.0,"batteryState":3,"lifeTimeEnergyDischarged":2275122,"lifeTimeEnergyCharged":2646795,"batteryPercentageState":8.999232,"fullPackEnergyAvailable":9999.0,"internalTemp":21.0,"ACGridCharging":0.0}, {"timeStamp":"2019-11-15 00:12:34","power":100.0,"batteryState":3,"lifeTimeEnergyDischarged":2275123,"lifeTimeEnergyCharged":2646795,"batteryPercentageState":8.999232,"fullPackEnergyAvailable":9999.0,"internalTemp":21.0,"ACGridCharging":0.0}, {"timeStamp":"2019-11-15 00:17:33","power":100.0,"batteryState":3,"lifeTimeEnergyDischarged":2276198,"lifeTimeEnergyCharged":2648149,"batteryPercentageState":10.99419,"fullPackEnergyAvailable":9999.0,"internalTemp":20.6,"ACGridCharging":0.0} ]}]}}'; } my $decodedJsonData = eval{decode_json($data)}; if ($@) { # update debug counter $hash->{NUMBER_OF_JSON_ERRORS} = $hash->{NUMBER_OF_JSON_ERRORS} + 1; if (AttrVal($name, "enableDebugReadings", undef)) { readingsSingleUpdate($hash, 'debugNumJsonErrors', $hash->{NUMBER_OF_JSON_ERRORS}, 1); } Log3 $name, 3, "SolarEdgeAPI ($name) - JSON error: $@"; SolarEdgeAPI_UpdateState($hash, "error"); return; } if ($path eq 'aggregates') { $readings = SolarEdgeAPI_ReadingsProcessing_Aggregates($hash, $decodedJsonData); } elsif ($path eq 'status') { $readings = SolarEdgeAPI_ReadingsProcessing_Status($hash, $decodedJsonData); } elsif ($path eq 'overview') { $readings = SolarEdgeAPI_ReadingsProcessing_Overview($hash, $decodedJsonData, 0); } elsif ($path eq 'storage') { $readings = SolarEdgeAPI_ReadingsProcessing_Storage($hash, $decodedJsonData, 0); } elsif ($path eq 'dailyDetails') { $readings = SolarEdgeAPI_ReadingsProcessing_DailyDetails($hash, $decodedJsonData); } elsif ($path eq 'dailyStorage') { $readings = SolarEdgeAPI_ReadingsProcessing_Storage($hash, $decodedJsonData, 1); } elsif ($path eq 'dailyOverview') { $readings = SolarEdgeAPI_ReadingsProcessing_Overview($hash, $decodedJsonData, 1); } elsif ($path eq 'dailyAggregates') { $readings = SolarEdgeAPI_ReadingsProcessing_DailyAggregates($hash, $decodedJsonData); } else { Log3 $name, 3, "SolarEdgeAPI ($name) - unknown type of response: $path"; } SolarEdgeAPI_UpdateReadings($hash, $path, $readings); SolarEdgeAPI_UpdateState($hash, "active"); } sub SolarEdgeAPI_ReadingsProcessing_Aggregates($$) { my ($hash, $decodedJsonData) = @_; my $name = $hash->{NAME}; my %readings; if (not (ref($decodedJsonData) eq "HASH")) { Log3 $name, 3, "SolarEdgeAPI ($name) - aggregates response is not a hash"; return \%readings; } foreach my $meter ( @{$decodedJsonData->{'energyDetails'}->{'meters'}}) { my $meterType = $meter->{'type'}; my $meterCum = 0; my $meterRecent15Min = 0; foreach my $meterData (@{$meter -> {'values'}}) { my $value = $meterData->{'value'}; if (defined $value) { $meterCum = $meterCum + $value; $meterRecent15Min = $value; } } $readings{$meterType . "-cumToday"} = $meterCum; $readings{$meterType . "-recent15min"} = $meterRecent15Min; } return \%readings; } sub SolarEdgeAPI_IsLastDayOfMonth($) { my ($hash) = @_; my $name = $hash->{NAME}; my $isLastDayOfMonth = 0; my $epoch = time(); my ($sec1, $min1, $hour1, $mday1, $mon1, $year1, $wday1, $yday1, $isdst1) = localtime($epoch); my $epochOneDayLater = $epoch + 24 * 60 * 60; my ($sec2, $min2, $hour2, $mday2, $mon2, $year2, $wday2, $yday2, $isdst2) = localtime($epochOneDayLater); if ($mon1 != $mon2) { $isLastDayOfMonth = 1; } my $month = $mon1 + 1; my $day = $mday1; Log3 $name, 4, "SolarEdgeAPI ($name) - day $day month $month isLastDayOfMonth $isLastDayOfMonth"; return $isLastDayOfMonth; } sub SolarEdgeAPI_IsLastDayOfYear($) { my ($hash) = @_; my $name = $hash->{NAME}; my $isLastDayOfYear = 0; my $epoch = time(); my ($sec1, $min1, $hour1, $mday1, $mon1, $year1, $wday1, $yday1, $isdst1) = localtime($epoch); my $epochOneDayLater = $epoch + 24 * 60 * 60; my ($sec2, $min2, $hour2, $mday2, $mon2, $year2, $wday2, $yday2, $isdst2) = localtime($epochOneDayLater); if ($year1 != $year2) { $isLastDayOfYear = 1; } my $month = $mon1 + 1; my $day = $mday1; Log3 $name, 4, "SolarEdgeAPI ($name) - day $day month $month isLastDayOfYear $isLastDayOfYear"; return $isLastDayOfYear; } sub SolarEdgeAPI_ReadingsProcessing_DailyAggregates($$) { my ($hash, $decodedJsonData) = @_; my $name = $hash->{NAME}; my %readings; if (not (ref($decodedJsonData) eq "HASH")) { Log3 $name, 3, "SolarEdgeAPI ($name) - daily aggregates response is not a hash"; return \%readings; } my ($sec, $min, $hour, $day, $month, $year, $wday, $yday, $isdst) = localtime(time()); $month = $month + 1; # iterate over day for different meters foreach my $meter ( @{$decodedJsonData->{'energyDetails'}->{'meters'}}) { my $meterType = $meter->{'type'}; my $cumYear = 0; my $cumMonth = 0; my $cumToday = 0; Log3 $name, 4, "SolarEdgeAPI ($name) - meterType $meterType"; # accumulate values of one meter foreach my $meterData (@{$meter -> {'values'}}) { my $value = $meterData->{'value'}; # decode timestamp, example: "2015-10-19 00:00:00" my $timestamp = $meterData->{'date'}; my $timestampMonth = -1; my $timestampDay = -1; if (!($timestamp =~ m/^([0-9]+)\-([0-9]+)\-([0-9]+)/)) { Log3 $name, 3, "SolarEdgeAPI ($name) - invalid timestamp in energyDetails response"; } else { $timestampMonth = $2; $timestampDay = $3; } Log3 $name, 4, "SolarEdgeAPI ($name) - $timestamp $value - timestampMonth $timestampMonth timestampDay $timestampDay"; # cumulate for all days of this year $cumYear += $value; # cumulate for all days of this month if ($timestampMonth == $month) { $cumMonth += $value; # detect the cumulated value for today if ($timestampDay == $day) { $cumToday = $value; } } } $readings{$meterType."-cumYear"} = $cumYear / 1000.0; $readings{$meterType."-cumMonth"} = $cumMonth / 1000.0; $readings{$meterType."-cumDay"} = $cumToday; if (SolarEdgeAPI_IsLastDayOfMonth($hash)) { $readings{$meterType."-cumMonthOnce"} = $cumMonth / 1000.0; } if (SolarEdgeAPI_IsLastDayOfYear($hash)) { $readings{$meterType."-cumYearOnce"} = $cumYear / 1000.0; } } return \%readings; } sub SolarEdgeAPI_ReadingsProcessing_Status($$) { my ($hash, $decodedJsonData) = @_; my $name = $hash->{NAME}; my %readings; my $data = $decodedJsonData->{'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."; } 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') 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"} || "-"; $readings{'grid_power'} = (($load2grid > 0) ? "-" : "").$data->{'GRID'}->{"currentPower"}; # LOAD $readings{'load_status'} = $data->{'LOAD'}->{"status"} || "-"; $readings{'load_power'} = $data->{'LOAD'}->{"currentPower"}; # PV $readings{'pv_status'} = $data->{'PV'}->{"status"} || "-"; $readings{'pv_power'} = $data->{'PV'}->{"currentPower"}; # Storage $readings{'storage_status'} = $data->{'STORAGE'}->{"status"} || "-"; if ($readings{'storage_status'} ne "-") { $readings{'storage_power'} = (($storage2load > 0) ? "-" : "").$data->{'STORAGE'}->{"currentPower"}; $readings{'storage_level'} = $data->{'STORAGE'}->{"chargeLevel"} || "-"; $readings{'storage_critical'} = $data->{'STORAGE'}->{"critical"}; } } return \%readings; } sub SolarEdgeAPI_ReadingsProcessing_Overview($$$) { my ($hash, $decodedJsonData, $daily) = @_; my $name = $hash->{NAME}; my %readings; my $data = $decodedJsonData->{'overview'}; if (not $daily) { $readings{'power'} = $data->{'currentPower'}->{"power"}; } $readings{'energyLifetime'} = $data->{'lifeTimeData'}->{"energy"} / 1000.0 / 1000.0; my $energyYear = $data->{'lastYearData'}->{"energy"} / 1000.0; $readings{'energyYear'} = $energyYear; my $energyMonth = $data->{'lastMonthData'}->{"energy"} / 1000.0; $readings{'energyMonth'} = $energyMonth; my $energyDay = $data->{'lastDayData'}->{"energy"}; $readings{'energyDay'} = $energyDay; if (SolarEdgeAPI_IsLastDayOfMonth($hash)) { $readings{"energyMonthOnce"} = $energyMonth; } if (SolarEdgeAPI_IsLastDayOfYear($hash)) { $readings{"energyYearOnce"} = $energyYear; } return \%readings; } sub SolarEdgeAPI_decodeBatteryState($) { my ($code) = @_; my $result = "$code"."_"; if ($code == 0) { $result .= "Off"; } elsif ($code == 1) { $result .= "Standby"; } elsif ($code == 2) { $result .= "Init"; } elsif ($code == 3) { $result .= "Charge"; } elsif ($code == 4) { $result .= "Discharge"; } elsif ($code == 5) { $result .= "Fault"; } elsif ($code == 7) { $result .= "Idle"; } else { $result .= "Unknown"; } return $result; } sub SolarEdgeAPI_ReadingsProcessing_Storage($$$) { my ($hash, $decodedJsonData, $daily) = @_; my $name = $hash->{NAME}; my %readings; if (not (ref($decodedJsonData) eq "HASH")) { Log3 $name, 3, "SolarEdgeAPI ($name) - storageData response is not a hash"; return \%readings; } foreach my $batteryData ( @{$decodedJsonData->{'storageData'}->{'batteries'}}) { my $serialNumber = $batteryData->{'serialNumber'}; Log3 $name, 4, "SolarEdgeAPI ($name) - serialNumber $serialNumber"; my $power = 0; my $batteryState = -1; my $lifeTimeEnergyCharged = 0; my $lifeTimeEnergyDischarged = 0; my $fullPackEnergyAvailable = 0; my $internalTemp = 0; my $batteryPercentageState = 0; my $acGridCharging = 0; foreach my $dataset (@{$batteryData -> {'telemetries'}}) { my $newPower = $dataset->{'power'}; my $newBatteryState = $dataset->{'batteryState'}; my $newLifeTimeEnergyCharged = $dataset->{'lifeTimeEnergyCharged'}; my $newLifeTimeEnergyDischarged = $dataset->{'lifeTimeEnergyDischarged'}; my $newFullPackEnergyAvailable = $dataset->{'fullPackEnergyAvailable'}; my $newInternalTemp = $dataset->{'internalTemp'}; my $newBatteryPercentageState = $dataset->{'batteryPercentageState'}; my $newAcGridCharging = $dataset->{'ACGridCharging'}; if (($newPower > -100000) and ($newPower < 100000)) { $power = $newPower; } if (($newBatteryState >= 0) and ($newBatteryState <= 10)) { $batteryState = $newBatteryState; } if ($newLifeTimeEnergyCharged > 0) { $lifeTimeEnergyCharged = $newLifeTimeEnergyCharged; } if ($newLifeTimeEnergyDischarged > 0) { $lifeTimeEnergyDischarged = $newLifeTimeEnergyDischarged; } if ($newFullPackEnergyAvailable > 0) { $fullPackEnergyAvailable = $newFullPackEnergyAvailable; } if (($newInternalTemp > 0) and ($newInternalTemp < 200)) { $internalTemp = $newInternalTemp; } if (($newBatteryPercentageState >= 0) and ($newBatteryPercentageState <= 100)) { $batteryPercentageState = $newBatteryPercentageState; } if ($newAcGridCharging >= 0) { $acGridCharging = $newAcGridCharging; } Log3 $name, 4, "SolarEdgeAPI ($name) - new: $newPower $newBatteryState $newLifeTimeEnergyCharged $newLifeTimeEnergyDischarged $newFullPackEnergyAvailable $newInternalTemp $newBatteryPercentageState $newAcGridCharging"; Log3 $name, 4, "SolarEdgeAPI ($name) - $power $batteryState $lifeTimeEnergyCharged $lifeTimeEnergyDischarged $fullPackEnergyAvailable $internalTemp $batteryPercentageState $acGridCharging"; } if ($daily) { $readings{$serialNumber."-lifeTimeEnergyCharged"} = $lifeTimeEnergyCharged / 1000.0 / 1000.0; $readings{$serialNumber."-lifeTimeEnergyDischarged"} = $lifeTimeEnergyDischarged / 1000.0 / 1000.0; $readings{$serialNumber."-fullPackEnergyAvailable"} = $fullPackEnergyAvailable; } else { $readings{$serialNumber."-power"} = $power; $readings{$serialNumber."-batteryState"} = $batteryState; $readings{$serialNumber."-batteryStateDecoded"} = SolarEdgeAPI_decodeBatteryState($batteryState); $readings{$serialNumber."-lifeTimeEnergyCharged"} = $lifeTimeEnergyCharged; $readings{$serialNumber."-lifeTimeEnergyDischarged"} = $lifeTimeEnergyDischarged; $readings{$serialNumber."-internalTemp"} = $internalTemp; $readings{$serialNumber."-batteryPercentageState"} = $batteryPercentageState; $readings{$serialNumber."-ACGridCharging"} = $acGridCharging; } } return \%readings; } sub SolarEdgeAPI_ReadingsProcessing_DailyDetails($$) { my ($hash, $decodedJsonData) = @_; my $name = $hash->{NAME}; my %readings; my $data = $decodedJsonData->{'details'}; # documented but not in the response: #$readings{'alertQuantity'} = $data->{'alertQuantity'}; #$readings{'alertSeverity'} = $data->{'alertSeverity'}; $readings{'peakPower'} = $data->{'peakPower'}; $readings{'status'} = $data->{'status'}; return \%readings; } sub SolarEdgeAPI_UpdateReadings($$$) { my ($hash, $path, $readings) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "SolarEdgeAPI ($name) - SolarEdgeAPI_UpdateReadings"; readingsBeginUpdate($hash); while (my ($r,$v) = each %{$readings}) { readingsBulkUpdate($hash,$path.'-'.$r,$v); } readingsEndUpdate($hash, 1); } ############################################################################### # update "state" ############################################################################### sub SolarEdgeAPI_UpdateState($$) { my ($hash, $newState) = @_; my $name = $hash->{NAME}; Log3 $name, 4, "SolarEdgeAPI ($name) - new state: $newState"; $hash->{STATE} = $newState; readingsSingleUpdate($hash, "state", $newState, 1); } ############################################################################### # show SolarEdge logo ############################################################################### sub SolarEdgeAPI_fhemwebFn($$$) { my ($FW_wname, $d, $room) = @_; return << 'EOF'

EOF } 1; =pod =item device =item summary Retrieves data from a SolarEdge PV system via the SolarEdge Monitoring API =item summary_DE =begin html

SolarEdgeAPI

    This module retrieves data from a SolarEdge PV system via the SolarEdge Server Monitoring API.

    Data is retrieved from the server periodically. The interval during day time is typically higher
    compared to night time. According to the API documentation the total number of server queries per
    day is limited to 300.
    The intervals as well as the start of day time and night time can be controlled by attributes.
    In each interval each enabled group of readings is generated once. You can reduce the number of
    server queries by disabling groups of readings and by increasing the interval time.

    Note: Features marked as "deprecated" or "debug only" may change or disappear in future versions.

    Define
      define <name> SolarEdgeAPI <API Key> <Site ID>
      The <API Key> and the <Site ID> can be retrieved from the SolarEdge
      Monitoring Portal. The <API Key> has to be enabled in the "Admin" Secion
      of the web portal.

    Readings
      All reading names start with the name of the group of readings followed by "-".
      All readings that belong to the same group have the same timing: Some groups of readings are generated
      periodically. The period is defined by attributes intervalAtDayTime, intervalAtNighttime, dayTimeStartHour and
      nightTimeStartHour. Other readings are generated once per day only. Reading groups which are updated
      once per day have a name starting with "daily". Each update of a group of readings requires on http
      request to the SolarEdge server. The number of queries is limited to 300 per day, according to API
      documentation.

      Groups of readings:

    • overview - readings generated from "overview" API response
      • overview-power [W]
      • overview-energyLifetime [MWh]
      • overview-energyYear [kWh]
      • overview-energyMonth [kWh]
      • overview-energyDay [Wh]
    • dailyOverview - readings generated from "overview" API response once per day
      • dailyOverview-energyDay [Wh] - This reading is derived. It depends on the latest dailyOverview-energyMonth reading.
      • dailyOverview-energyMonth [kWh]
      • dailyOverview-energyYear [kWh]
      • dailyOverview-energyLifetime [MWh]
      • dailyOverview-energyMonthOnce [kWh] generated on the last day of the month only
      • dailyOverview-energyYearOnce [kWh] generated on the last day of the year only
    • aggregates - readings generated from "energyDetails" API response
      • aggregates-<meterType>-cumToday [Wh]
      • aggregates-<meterType>-recent15min [Wh](deprecated)
    • dailyAggregates - readings generated from "energyDetails" API response once per day
      • dailyAggregates-<meterType>-cumDayOnce [Wh]
      • dailyAggregates-<meterType>-cumMonthDaily [kWh]
      • dailyAggregates-<meterType>-cumYearDaily [kWh]
      • dailyAggregates-<meterType>-cumMonthOnce [kWh] generated on the last day of the month only
      • dailyAggregates-<meterType>-cumYearOnce [kWh] generated on the last day of the year only
    • storage - readings generated from "storageData" API response
      • storage-<serial>-power [W]
      • storage-<serial>-batteryState
      • storage-<serial>-batteryStateDecoded [text]
      • storage-<serial>-lifetimeEnergyCharged
      • storage-<serial>-lifetimeEnergyDischarged
      • storage-<serial>-internalTemp [degrees C]
      • storage-<serial>-batteryPercentageState [percent]
    • dailyStorage - readings generated from "storageData" API response once per day
      • dailyStorage-<serial>-lifetimeEnergyCharged [MWh]
      • dailyStorage-<serial>-lifetimeEnergyDischarged [MWh]
      • dailyStorage-<serial>-fullPackEnergyAvailable [kWh]
    • dailyDetails - readings generated from "details" API response once per day
      • dailyDetails-peakPower [W]
      • dailyDetails-status [text]
    • status - readings generated from "currentPowerFlow" API response. This API is not supported by all sites.
      • status-grid_status [?]
      • status-grid_power [W]
      • status-load_status [?]
      • status-load_power [W]
      • status-pv_status [?]
      • status-pv_power [W]
      • status-storage_status [?]
      • status-storage_power [W]
      • status-storage_level [?]
      • status-storage_critical [?]
    • debug - debug data about successful and failing http requests (for debug only)
    • actionQueue - information about the entries in the action queue (for debug only)

    Get
    • numberOfRequests - get the expected number of requests per day with current attribute settings (for debug only)
    • status - fetch corresponding group of readings (for debug only)
    • aggregates - fetch corresponding group of readings (for debug only)
    • overview - fetch corresponding group of readings (for debug only)
    • storage - fetch corresponding group of readings (for debug only)
    • dailyDetails - fetch corresponding group of readings (for debug only)
    • dailyStorage - fetch corresponding group of readings (for debug only)
    • dailyAggregates - fetch corresponding group of readings (for debug only)
    • dailyOverview - fetch corresponding group of readings (for debug only)

    Set
    • restartTimer - restart periodic http requests (for debug only)
    • resetDebugCounters - reset debug counters (internals and optional debug* readings) (for debug only)

    Attributes
    • intervalAtDayTime - interval of http requests during day time (default: 215 (seconds))
    • intervalAtNightTime - interval of http requests during night time (default: 1200 (seconds))
    • dayTimeStartHour - start of daytime, default 6 (= 6:00am)
    • nightTimeStartHour - start of night time, default 22 (= 10:00pm)
    • enableStatusReadings - enable the corresponding group of readings, default: 0
    • enableAggregatesReadings - enable the corresponding group of readings, default: 0
    • enableOverviewReadings - enable the corresponding group of readings, default: 1
    • enableStorageReadings - enable the corresponding group of readings, default: 0
    • enableDailyDetailsReadings - enable the corresponding group of readings, default: 1
    • enableDailyStorageReadings - enable the corresponding group of readings, default: 0
    • enableDailyAggregatesReadings - enable the corresponding group of readings, default: 0
    • enableDailyOverviewReadings - enable the corresponding group of readings, default: 1
    • enableDebugReadings Enable the debug* readings. These debug readings do not cause additional http requests. Default: 0

=end html =cut