fhem-mirror/FHEM/70_SolarEdgeAPI.pm
2020-08-01 10:19:18 +00:00

1548 lines
54 KiB
Perl

###############################################################################
#
# $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: <majorVersion>.<minorVersion>.<patchVersion>[betaXYZ]
#
# The <majorVersion> is incremented for changes which are not backward compatible.
# A change of the <majorVersion> 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 <minorVersion> is incremented for changes which are backward compatible,
# e.g. added functionality which does not impact old functionality.
#
# The <patchVersion> 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 <name> SolarEdgeAPI <API-Key> <Site-ID>";
}
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'
<br>
<a href="https://www.solaredge.com">
<img src="https://www.solaredge.com/sites/default/files/SolarEdge_logo_header_new.png">
</a>
<br>
EOF
}
1;
=pod
=item device
=item summary Retrieves data from a SolarEdge PV system via the SolarEdge Monitoring API
=item summary_DE
=begin html
<a name="SolarEdgeAPI"></a>
<h3>SolarEdgeAPI</h3>
<ul>
This module retrieves data from a SolarEdge PV system via the SolarEdge Server Monitoring API.<br>
<br>
Data is retrieved from the server periodically. The interval during day time is typically higher<br>
compared to night time. According to the API documentation the total number of server queries per<br>
day is limited to 300.<br>
The intervals as well as the start of day time and night time can be controlled by attributes.<br>
In each interval each enabled group of readings is generated once. You can reduce the number of<br>
server queries by disabling groups of readings and by increasing the interval time.<br>
<br>
Note: Features marked as "deprecated" or "debug only" may change or disappear in future versions.<br>
<br>
<a name="SolarEdgeAPI_Define"></a>
<b>Define</b>
<ul>
<code>define &lt;name&gt; SolarEdgeAPI &lt;API Key&gt; &lt;Site ID&gt;</code><br>
The &lt;API Key&gt; and the &lt;Site ID&gt can be retrieved from the SolarEdge<br>
Monitoring Portal. The &lt;API Key&gt; has to be enabled in the "Admin" Secion<br>
of the web portal.<br>
</ul>
<br>
<a name="SolarEdgeAPI_Readings"></a>
<b>Readings</b>
<ul>
All reading names start with the name of the group of readings followed by "-".<br>
All readings that belong to the same group have the same timing: Some groups of readings are generated<br>
periodically. The period is defined by attributes intervalAtDayTime, intervalAtNighttime, dayTimeStartHour and<br>
nightTimeStartHour. Other readings are generated once per day only. Reading groups which are updated<br>
once per day have a name starting with "daily". Each update of a group of readings requires on http<br>
request to the SolarEdge server. The number of queries is limited to 300 per day, according to API<br>
documentation.<br>
<br>
Groups of readings:<br>
<br>
<li>overview - readings generated from "overview" API response
<ul>
<li>overview-power [W]</li>
<li>overview-energyLifetime [MWh]</li>
<li>overview-energyYear [kWh]</li>
<li>overview-energyMonth [kWh]</li>
<li>overview-energyDay [Wh]</li>
</ul>
</li>
<li>dailyOverview - readings generated from "overview" API response once per day
<ul>
<li>dailyOverview-energyDay [Wh] - This reading is derived. It depends on the latest dailyOverview-energyMonth reading.</li>
<li>dailyOverview-energyMonth [kWh]</li>
<li>dailyOverview-energyYear [kWh]</li>
<li>dailyOverview-energyLifetime [MWh]</li>
<li>dailyOverview-energyMonthOnce [kWh] generated on the last day of the month only</li>
<li>dailyOverview-energyYearOnce [kWh] generated on the last day of the year only</li>
</ul>
</li>
<li>aggregates - readings generated from "energyDetails" API response<br>
<ul>
<li>aggregates-&lt;meterType&gt;-cumToday [Wh]</li>
<li>aggregates-&lt;meterType&gt;-recent15min [Wh](deprecated) </li>
</ul>
</li>
<li>dailyAggregates - readings generated from "energyDetails" API response once per day<br>
<ul>
<li>dailyAggregates-&lt;meterType&gt;-cumDayOnce [Wh]</li>
<li>dailyAggregates-&lt;meterType&gt;-cumMonthDaily [kWh]</li>
<li>dailyAggregates-&lt;meterType&gt;-cumYearDaily [kWh]</li>
<li>dailyAggregates-&lt;meterType&gt;-cumMonthOnce [kWh] generated on the last day of the month only</li>
<li>dailyAggregates-&lt;meterType&gt;-cumYearOnce [kWh] generated on the last day of the year only</li>
</ul>
</li>
<li>storage - readings generated from "storageData" API response<br>
<ul>
<li>storage-&lt;serial&gt;-power [W]</li>
<li>storage-&lt;serial&gt;-batteryState</li>
<li>storage-&lt;serial&gt;-batteryStateDecoded [text]</li>
<li>storage-&lt;serial&gt;-lifetimeEnergyCharged</li>
<li>storage-&lt;serial&gt;-lifetimeEnergyDischarged</li>
<li>storage-&lt;serial&gt;-internalTemp [degrees C]</li>
<li>storage-&lt;serial&gt;-batteryPercentageState [percent]</li>
</ul>
</li>
<li>dailyStorage - readings generated from "storageData" API response once per day<br>
<ul>
<li>dailyStorage-&lt;serial&gt;-lifetimeEnergyCharged [MWh]</li>
<li>dailyStorage-&lt;serial&gt;-lifetimeEnergyDischarged [MWh]</li>
<li>dailyStorage-&lt;serial&gt;-fullPackEnergyAvailable [kWh]</li>
</ul>
</li>
<li>dailyDetails - readings generated from "details" API response once per day<br>
<ul>
<li>dailyDetails-peakPower [W]</li>
<li>dailyDetails-status [text]</li>
</ul>
</li>
<li>status - readings generated from "currentPowerFlow" API response. This API is not supported by all sites.<br>
<ul>
<li>status-grid_status [?]</li>
<li>status-grid_power [W]</li>
<li>status-load_status [?]</li>
<li>status-load_power [W]</li>
<li>status-pv_status [?]</li>
<li>status-pv_power [W]</li>
<li>status-storage_status [?]</li>
<li>status-storage_power [W]</li>
<li>status-storage_level [?]</li>
<li>status-storage_critical [?]</li>
</ul>
</li>
<li>debug - debug data about successful and failing http requests (for debug only)</li>
<li>actionQueue - information about the entries in the action queue (for debug only)</li>
</ul>
<br>
<a name="SolarEdgeAPI_Get"></a>
<b>Get</b>
<ul>
<li>numberOfRequests - get the expected number of requests per day with current attribute settings (for debug only)</li>
<li>status - fetch corresponding group of readings (for debug only)</li>
<li>aggregates - fetch corresponding group of readings (for debug only)</li>
<li>overview - fetch corresponding group of readings (for debug only)</li>
<li>storage - fetch corresponding group of readings (for debug only)</li>
<li>dailyDetails - fetch corresponding group of readings (for debug only)</li>
<li>dailyStorage - fetch corresponding group of readings (for debug only)</li>
<li>dailyAggregates - fetch corresponding group of readings (for debug only)</li>
<li>dailyOverview - fetch corresponding group of readings (for debug only)</li>
</ul>
<br>
<a name="SolarEdgeAPI_Set"></a>
<b>Set</b>
<ul>
<li>restartTimer - restart periodic http requests (for debug only)</li>
<li>resetDebugCounters - reset debug counters (internals and optional debug* readings) (for debug only)</li>
</ul>
<br>
<a name="SolarEdgeAPI_Attributes"></a>
<b>Attributes</b>
<ul>
<li>intervalAtDayTime - interval of http requests during day time (default: 215 (seconds))</li>
<li>intervalAtNightTime - interval of http requests during night time (default: 1200 (seconds))</li>
<li>dayTimeStartHour - start of daytime, default 6 (= 6:00am)</li>
<li>nightTimeStartHour - start of night time, default 22 (= 10:00pm)</li>
<li>enableStatusReadings - enable the corresponding group of readings, default: 0</li>
<li>enableAggregatesReadings - enable the corresponding group of readings, default: 0</li>
<li>enableOverviewReadings - enable the corresponding group of readings, default: 1</li>
<li>enableStorageReadings - enable the corresponding group of readings, default: 0</li>
<li>enableDailyDetailsReadings - enable the corresponding group of readings, default: 1</li>
<li>enableDailyStorageReadings - enable the corresponding group of readings, default: 0</li>
<li>enableDailyAggregatesReadings - enable the corresponding group of readings, default: 0</li>
<li>enableDailyOverviewReadings - enable the corresponding group of readings, default: 1</li>
<li>enableDebugReadings Enable the debug* readings. These debug readings do not cause additional http requests. Default: 0</li>
</ul>
<br>
</ul>
=end html
=cut