snapshot inverter files

This commit is contained in:
2024-12-19 15:04:50 +01:00
parent c53336ed28
commit 7b5834fcb1
12 changed files with 971 additions and 0 deletions

366
inverter/config.ini Normal file
View File

@@ -0,0 +1,366 @@
#-----------------------------------------
# CONFIGURATION FILE FOR INVERTER MONITOR
#-----------------------------------------
# Should work with PHOENIXTEC manufactured inverters: CMS / Sun Ezy / Orion / Eaton et al.
#
# Should at least work with:
# * CMS2000 (CMS 2000)
# * CMS10000 (CMS 10000) - requires more testing though
# * SE2800 (SunEzy 2800)
# * SE4600 (SunEzy 600E)
# * ETN2000 (Eaton 2000)
#
# NOTE: http://pvoutput.org capable of accepting 60 data updates per hour,
# but will only keep 1 every 5-10mins depending on your setting.
#-------
# flags
#-------
[flags]
debug = 0 # 0 = NO, 1 = YES
use_pvoutput = 1 # 0 = NO, 1 = YES to export data to http://pvoutput.org
use_rrdtool = 1 # 0 = NO, 1 = YES to export data to rrdtool for graphing
#-------------------
# number of seconds
#-------------------
[secs]
datapoll_freq = 5
pvoutput_freq = 300 # every 5-10mins per your setting in http://pvoutput.org
timeout = 20
reinit = 10 # -1 = infinite num of times (ie dont die)
#------------------
# file path to use
#------------------
[paths]
windows = "C:/solar" # windows
other = "/opt/inverter/data" # unix/linux
#-------------------------
# script and binary files
#-------------------------
[scripts]
pvoutput = "perl pvoutput.pl" # to export data to http://pvoutput.org
pvoutput_php = "/opt/inverter/pvoutput.php" # to export data to http://pvoutput.org
create_rrd = "/opt/inverter/create_rrd.php" # to export data to rrdtool for graphing
rrdtool_exe_win = "rrdtool" # windows
rrdtool_exe_oth = "/usr/bin/rrdtool" # unix/linux
#----------------------
# serial port settings
#----------------------
[serial]
baud = 9600
port_win = "COM5" # windows, COM port
#port_oth = "/dev/ttyS0" # unix/linux, serial port
#port_oth = "/dev/ttyUSB1" # unix/linux, USB port
#port_oth = "/dev/serial/by-path/pci-0000:00:14.0-usb-0:1:1.0-port0" # unix/linux, USB port
port_oth = "/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller-if00-port0"
#port_oth = "/dev/serial/by-id/usb-1a86_USB_Single_Serial_5539013192-if00"
#port_oth = "/dev/rfcomm0" # unix/linux, bluetooth port
parity = "none"
databits = 8
stopbits = 1
handshake = "none"
datatype = 'raw'
#------------------------------------------------
# hex start indeces and lengths for certain data
#------------------------------------------------
[hex]
data_to_follow_index = 8
capacity_index = 20
capacity_length = 12
firmware_index = 32
firmware_length = 14
model_index = 46
model_length = 28
manuf_index = 74
manuf_length = 32
serial_index = 106
serial_length = 20
other_index = 138
other_length = 8
confserial_index = 18
#-----------------------------------------------
# hex packet codes - SEND (request to inverter)
#-----------------------------------------------
[sendhex]
initialise = "aaaa010000000004000159"
serial = "aaaa010000000000000155"
conf_serial1 = "aaaa0100000000010b"
conf_serial2 = "01"
version = "aaaa01000001010300015a"
paramfmt = "aaaa010000010101000158"
param = "aaaa01000001010400015b"
datafmt = "aaaa010000010100000157"
data = "aaaa010000010102000159"
#------------------------------------------------
# hex packet codes - RECV (response to inverter)
#------------------------------------------------
[recvhex]
serial = "aaaa0000010000800a"
conf_serial = "aaaa000101000081"
version = "aaaa000101000183"
paramfmt = "aaaa000101000181"
param = "aaaa000101000184"
datafmt = "aaaa000101000180"
data = "aaaa000101000182"
#---------------------
# inverter parameters
#---------------------
[param_vpvstart]
hexcode = "40"
multiply = 0.1
measure = "V"
index = -1
descr = "PV Start-up voltage"
[param_tstart]
hexcode = "41"
multiply = 1
measure = "Sec"
index = -1
descr = "Time to connect grid"
[param_vacmin]
hexcode = "44"
multiply = 0.1
measure = "V"
index = -1
descr = "Minimum operational grid voltage"
[param_vacmax]
hexcode = "45"
multiply = 0.1
measure = "V"
index = -1
descr = "Maximum operational grid voltage"
[param_facmin]
hexcode = "46"
multiply = 0.01
measure = "Hz"
index = -1
descr = "Minimum operational frequency"
[param_facmax]
hexcode = "47"
multiply = 0.01
measure = "Hz"
index = -1
descr = "Maximum operational frequency"
[param_zacmax]
hexcode = "48"
multiply = 1
measure = "mOhm"
index = -1
descr = "Maximum operational grid impendance"
[param_dzac]
hexcode = "49"
multiply = 1
measure = "mOhm"
index = -1
descr = "Allowable Delta Zac of operation"
#---------------
# inverter data
#---------------
[data_temp]
hexcode = "00"
multiply = 0.1
measure = "deg C"
index = -1
descr = "Internal Temperature"
[data_vpv1]
hexcode = "01"
multiply = 0.1
measure = "V"
index = -1
descr = "Panel 1 Voltage"
[data_vpv2]
hexcode = "02"
multiply = 0.1
measure = "V"
index = -1
descr = "Panel 2 Voltage"
[data_vpv3]
hexcode = "03"
multiply = 0.1
measure = "V"
index = -1
descr = "Panel 3 Voltage"
[data_ipv1]
hexcode = "04"
multiply = 0.1
measure = "A"
index = -1
descr = "Panel 1 DC Current"
[data_ipv2]
hexcode = "05"
multiply = 0.1
measure = "A"
index = -1
descr = "Panel 2 DC Current"
[data_ipv3]
hexcode = "06"
multiply = 0.1
measure = "A"
index = -1
descr = "Panel 3 DC Current"
[data_etoday]
hexcode = "0d"
multiply = 0.01
measure = "kWh"
index = -1
descr = "Accumulated Energy Today"
[data_vpv]
hexcode = "40"
multiply = 0.1
measure = "V"
index = -1
descr = "Panel Voltage"
[data_iac]
hexcode = "41"
multiply = 0.1
measure = "A"
index = -1
descr = "Grid Current"
[data_vac]
hexcode = "42"
multiply = 0.1
measure = "V"
index = -1
descr = "Grid Voltage"
[data_fac]
hexcode = "43"
multiply = 0.01
measure = "Hz"
index = -1
descr = "Grid Frequency"
[data_pac]
hexcode = "44" # "0b" for 3phase
multiply = 1
measure = "W"
index = -1
descr = "Output Power"
[data_zac]
hexcode = "45"
multiply = 1
measure = "mOhm"
index = -1
descr = "Grid Impedance"
[data_etotalh]
hexcode = "47" # "07" for 3phase
multiply = 256
measure = "kWh"
index = -1
descr = "Accumulated Energy (high bit)"
[data_etotall]
hexcode = "48" # "08" for 3phase
multiply = 0.1
measure = "kWh"
index = -1
descr = "Accumulated Energy (low bit)"
[data_htotalh]
hexcode = "49" # "09" for 3phase
multiply = 256
measure = "hrs"
index = -1
descr = "Working Hours (high bit)"
[data_htotall]
hexcode = "4a" # "0a" for 3phase
multiply = 1
measure = "hrs"
index = -1
descr = "Working Hours (low bit)"
[data_mode]
hexcode = "4c" # "0c" for 3phase
multiply = 1
measure = " "
index = -1
descr = "Operating Mode"
[data_errgv]
hexcode = "78"
multiply = 1
measure = " "
index = -1
descr = "Error message: GV fault value"
[data_errgf]
hexcode = "79"
multiply = 1
measure = " "
index = -1
descr = "Error message: GF fault value"
[data_errgz]
hexcode = "7a"
multiply = 1
measure = " "
index = -1
descr = "Error message: GZ fault value"
[data_errtemp]
hexcode = "7b"
multiply = 1
measure = " "
index = -1
descr = "Error message: Tmp fault value"
[data_errpv1]
hexcode = "7c"
multiply = 1
measure = " "
index = -1
descr = "Error message: PV1 fault value"
[data_errgfc1]
hexcode = "7d"
multiply = 1
measure = " "
index = -1
descr = "Error message: GFC1 fault value"
[data_errmode]
hexcode = "7e"
multiply = 1
measure = " "
index = -1
descr = "Error mode"

78
inverter/create_rrd.php Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env php
<?php
require_once 'rrd.php';
$sDataDirectory = '/opt/inverter/data/';
$aRRDFiles = array(
'inverter.rrd' => array(5, '
DS:TEMP:GAUGE:120:U:U
DS:VPV:GAUGE:120:U:U
DS:IAC:GAUGE:120:U:U
DS:VAC:GAUGE:120:U:U
DS:FAC:GAUGE:120:U:U
DS:PAC:GAUGE:120:0:U
DS:ETOTAL:GAUGE:120:0:U
DS:ETODAY:GAUGE:120:0:U
RRA:MIN:0.5:1:720
RRA:MIN:0.5:17:1017
RRA:MIN:0.5:120:1008
RRA:MIN:0.5:535:1002
RRA:MIN:0.5:6324:1001
RRA:MAX:0.5:1:720
RRA:MAX:0.5:17:1017
RRA:MAX:0.5:120:1008
RRA:MAX:0.5:535:1002
RRA:MAX:0.5:6324:1001
RRA:AVERAGE:0.5:1:720
RRA:AVERAGE:0.5:17:1017
RRA:AVERAGE:0.5:120:1008
RRA:AVERAGE:0.5:535:1002
RRA:AVERAGE:0.5:6324:1001'),
'today.rrd' => array(5, '
DS:PAC:GAUGE:120:0:U
DS:ETODAY:GAUGE:120:0:U
RRA:AVERAGE:0.5:1:17280'));
$bFirst = true;
$aRRDKeys = array();
$i = 0;
foreach (glob($sDataDirectory . '*.csv') as $sFile) {
/* Extract header from csv file */
$aData = explode("\n", trim(file_get_contents($sFile)));
$sHeader = array_shift($aData);
if ($bFirst) {
$aHeader = array_flip(explode(',', $sHeader));
foreach ($aRRDFiles as $sFile => $aRRD) {
/* Determine fields to update in RRD database */
$sContents = $aRRD[1];
preg_match_all('~DS:([^:]+):~', $sContents, $aMatches);
$aKeys = array();
foreach ($aMatches[1] as $sField) {
$aKeys[] = $aHeader[$sField] - 1;
}
$aRRDKeys[$sFile] = array_flip($aKeys);
}
}
foreach ($aData as $sEntry) {
$aValues = array_slice(explode(',', $sEntry), 1, 12);
$iTime = $aValues[0];
if ($bFirst) {
foreach ($aRRDFiles as $sFile => $aRRD) {
/* Create RRD database */
$iStep = $aRRD[0];
$iStart = $iTime - 1;
$sContents = $aRRD[1];
RRD::create($sFile, $iStep, $iStart, $sContents);
}
$bFirst = false;
}
++$i;
foreach ($aRRDFiles as $sFile => $aRRD) {
/* Update relevant fields in RRD database */
$aValues = array_intersect_key($aValues, $aRRDKeys[$sFile]);
RRD::update($sFile, $iTime, $aValues);
}
}
}

68
inverter/daemon.php Executable file
View File

@@ -0,0 +1,68 @@
<?php
require_once 'System/Daemon.php'; // pear install -f System_Daemon
define('NAME', 'inverter');
define('TASK', '/opt/inverter/inverter.pl >> /var/log/task.log');
define('CWD', '/opt/inverter/');
define('FILE_DAEMON_START', 'daemon_start.sh');
define('FILE_DAEMON_STOP', 'daemon_stop.sh');
define('MODE', 0755);
define('PROCESS_POLL', 30);
function daemon_init() {
global $sName;
/* Daemon options */
System_Daemon::setOptions(array(
'appName' => NAME,
'appDescription' => '',
'authorName' => '',
'authorEmail' => ''));
/* Derive process name */
$sName = basename(substr(TASK, 0, strpos(TASK, ' ')));
}
function daemon_install() {
global $argv;
System_Daemon::writeAutoRun(); // update-rc.d %s defaults
/* Write scripts for scheduling with at */
if (isset($argv[2]) && $argv[2] == 'schedule') {
if (!file_exists(FILE_DAEMON_START)) {
file_put_contents(FILE_DAEMON_START, sprintf("#!/bin/bash\nservice %s start", NAME));
chmod(FILE_DAEMON_START, MODE);
}
if (!file_exists(FILE_DAEMON_STOP)) {
file_put_contents(FILE_DAEMON_STOP, sprintf("#!/bin/bash\nservice %s stop", NAME));
chmod(FILE_DAEMON_STOP, MODE);
}
}
}
function daemon_run() {
global $rProcess;
/* Hook onto daemon termination handler */
System_Daemon::setSigHandler(SIGTERM, 'daemon_sigterm_handler');
/* Start deamon */
System_Daemon::start();
while (!System_Daemon::isDying()) {
System_Daemon::info('Open process');
$rProcess = proc_open(TASK, array(), $aPipes);
do {
System_Daemon::isRunning(); // required for deamon to respond properly
sleep(PROCESS_POLL); // gets interrupted on process termination
$aStatus = proc_get_status($rProcess);
} while ($aStatus['running']);
System_Daemon::info('Process ended');
}
}
function daemon_sigterm_handler($iSigNo) {
global $sName;
system(sprintf('pkill %s', $sName));
System_Daemon::stop();
}

View File

@@ -0,0 +1,10 @@
/var/log/*.log {
rotate 5
compress
missingok
notifempty
sharedscripts
postrotate
/bin/kill -HUP `cat /var/run/inverter/inverter.pid 2>/dev/null` 2> /dev/null || true
endscript
}

65
inverter/functions.php Executable file
View File

@@ -0,0 +1,65 @@
<?php
// require_once 'wunderground.php';
require_once 'openweathermap.php';
define('DEFAULT_WAKE', '6:00');
define('DEFAULT_SLEEP', '22:00');
define('TWILIGHT_FILE', '/opt/inverter/static/twilight_%d.csv');
define('STATION', 'INOORDHO104');
define('CITY', 2745978);
function getHour($sTime = null) {
if (!is_numeric($sTime)) {
$iTime = !isset($sTime) ? time() : strtotime($sTime);
} else {
$iTime = $sTime;
}
return date('H', $iTime) + date('i', $iTime) / 60;
}
function getTwilight($iYear, $iDay) {
$sTwilightFile = sprintf(TWILIGHT_FILE, $iYear);
if (file_exists($sTwilightFile)) {
$aDays = explode("\n", file_get_contents($sTwilightFile));
if (isset($aDays[$iDay])) {
$aDay = explode(',', $aDays[$iDay]);
if ($aDay[0] == $iDay) {
return $aDay;
}
}
}
return null;
}
function getWake(&$aTwilight = null) {
if (!isset($aTwilight)) {
$aTwilight = getTwilight(date('Y'), date('z') + 1);
}
return strtotime($sWake = isset($aTwilight) ? $aTwilight[1] : DEFAULT_WAKE);
}
function getSleep(&$aTwilight = null) {
if (!isset($aTwilight)) {
$aTwilight = getTwilight(date('Y'), date('z'));
}
return strtotime($sWake = isset($aTwilight) ? $aTwilight[3] : DEFAULT_WAKE);
}
function getTemperature($sStation = STATION, $iCity = CITY) {
// $aData = wunderground('conditions', sprintf('pws:%s', STATION));
// return isset($aData['current_observation']['temp_c']) ? $aData['current_observation']['temp_c'] : null;
$aData = openweathermap(CITY);
return isset($aData['main']['temp']) ? floatval($aData['main']['temp']) - 273.15 : null;
}
function command($sCommand) {
ob_start();
system($sCommand);
return ob_get_clean();
}
function clean() {
clearstatcache();
gc_collect_cycles();
}

59
inverter/inverter.php Executable file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/php
<?php
require_once 'functions.php';
require_once 'daemon.php';
/* Initialize */
chdir(CWD);
daemon_init();
/* Install daemon */
if (isset($argv[1]) && $argv[1] == 'install') {
daemon_install();
}
/* Remove previous at entries */
foreach (explode("\n", trim(command('atq 2> /dev/null'))) as $sJob) {
$sId = substr($sJob, 0, strpos($sJob, "\t"));
$sJob = command(sprintf('at -c %s 2> /dev/null' . "\n", $sId));
$aJob = explode("\n", trim(command(sprintf('at -c %s 2> /dev/null', $sId))));
if (strpos(array_pop($aJob), NAME) !== false) {
command(sprintf('atrm %s', $sId));
}
}
/* Wake at sunrise, sleep at sunset */
$fWake = getHour(getWake($aTwilight));
$fSleep = getHour($sSleep = getSleep($aTwilight));
$sWake = $aTwilight[1];
$sSleep = $aTwilight[3];
System_Daemon::info(sprintf('Be awake between %s and %s', $sWake, $sSleep));
/* Check appropriate state */
$fNow = getHour();
if (!($bAwake = $fNow >= $fWake)) {
System_Daemon::info('Too early to wake!');
} else if ($bSleep = $fNow >= $fSleep) {
System_Daemon::info('Time to sleep!');
}
schedule_wake();
if ($bAwake && !$bSleep) {
schedule_sleep();
daemon_run();
}
function schedule_wake() {
global $sWake;
$sTime = date('H:i', strtotime($sWake)); // ignore slight deviation for next day
System_Daemon::info(sprintf('Schedule wake at %s', $sTime));
command(sprintf('at -f %s %s 2> /dev/null', FILE_DAEMON_START, $sTime));
}
function schedule_sleep() {
global $sSleep;
$sTime = date('H:i', strtotime($sSleep));
System_Daemon::info(sprintf('Schedule sleep at %s', $sTime));
command(sprintf('at -f %s %s 2> /dev/null', FILE_DAEMON_STOP, $sTime));
}

69
inverter/openweathermap.php Executable file
View File

@@ -0,0 +1,69 @@
<?php
define('KEY', 'e8f868de4eb21a7c6a877f8197cc3ed3');
define('LIMIT_MINUTE', 10);
define('LIMIT_DAY', 500);
define('LIMIT_FILE', '/opt/inverter/data/openweathermap.json');
function openweathermap($iCity, $bDebug = false) {
/* Get current date values */
$iMinute = date('i');
$iDay = date('z');
if (file_exists(LIMIT_FILE)) {
/* Read number of calls used */
$sJSON = file_get_contents(LIMIT_FILE);
$aJSON = json_decode($sJSON, true);
$aCount = array(
'minute' => $iMinute != $aJSON['minute'][0] ? 0 : $aJSON['minute'][1],
'day' => $iDay != $aJSON['day'][0] ? 0 : $aJSON['day'][1]);
} else {
/* Initialise to zero */
$aCount = array(
'minute' => 0,
'day' => 0);
}
/* Check call limits */
$iWait = 0;
if ($aCount['minute'] >= LIMIT_MINUTE) {
$iWait = 60 - date('s');
if ($bDebug === true) {
printf("Minute limit (%d) reached, wait %d seconds\n", LIMIT_MINUTE, $iWait);
}
$aCount['minute'] = 0;
} else if ($aCount['day'] >= LIMIT_DAY) {
$iWait = strtotime('00:00 + 1 day') - time();
if ($bDebug === true) {
printf("Daily limit (%d) reached, wait %d seconds\n", LIMIT_DAY, $iWait);
}
$aCount['day'] = 0;
}
/* Prevent from exceeding call limits */
if ($iWait > 0) {
//die("Try again later!\n");
return null;
}
/* Update call counts */
++$aCount['minute'];
++$aCount['day'];
/* Report number of calls used */
if ($bDebug === true) {
printf("Used %d/%d minutely and %d/%d daily calls\n", $aCount['minute'], LIMIT_MINUTE, $aCount['day'], LIMIT_DAY);
}
/* Write number of calls used to file */
$aJSON = array(
'minute' => array($iMinute, $aCount['minute']),
'day' => array($iDay, $aCount['day']));
file_put_contents(LIMIT_FILE, json_encode($aJSON));
/* Perform actual call */
$sUrl = sprintf('https://api.openweathermap.org/data/2.5/weather?id=%d&appid=%s', $iCity, KEY);
$sUrl = sprintf('https://api.openweathermap.org/data/2.5/weather?id=%d&appid=%s', $iCity, KEY);
// $sUrl = 'https://samples.openweathermap.org/data/2.5/weather?q=Uitgeeddst&appid=5fc7ebf9168bfbe9745920438e3b1';
$sJSON = file_get_contents($sUrl);
return json_decode($sJSON, true);
}

107
inverter/pvoutput.php Executable file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env php
<?php
require_once 'functions.php';
require_once 'rrd.php';
define('RRD_FILE', '/opt/inverter/data/inverter_%s_today.rrd');
define('PVOUTPUT_URL', 'http://pvoutput.org/service/r1/addstatus.jsp');
define('TODAY_FILE', '/opt/inverter/data/today_%s.csv');
define('FIELD', 'PAC');
define('RESOLUTION', 5);
define('TRESHOLD_CORRECT', 3); // h
define('MARGIN_ENERGY', 0.2);
define('MARGIN_TEMPERATURE', 0.4);
$aSystems = array(
'1206DS0163' => array('16e7a916d69656e354d00461a4da1d2e40cfa4f1', '12419')
);
/* Fetch command line arguments */
$fToday = floatval($argv[1]); // Wh
$fPower = floatval($argv[2]); // W
$fVoltage = floatval($argv[3]); // V
$sSerial = $argv[4];
/* Fetch temperature */
$fTemperature = getTemperature();
/* Fetch twilight data */
$iDay = date('z');
$aTwilight = getTwilight(date('Y'), $iDay);
/* Fetch today data */
$sTodayFile = sprintf(TODAY_FILE, $sSerial);
$aToday = array();
if (file_exists($sTodayFile)) {
$aToday = explode(',', file_get_contents($sTodayFile));
$aToday[1] = floatval($aToday[1]);
}
if (count($aToday) != 3 || $aToday[0] != $iDay) {
$aToday = array($iDay, 0, strtotime($aTwilight[1]), null);
}
$iLast = $aToday[2];
/* Extract fields */
$iTime = time();
list($aFields, $aData) = RRD::fetch(sprintf(RRD_FILE, $sSerial), RESOLUTION, $iLast, $iTime, 'AVERAGE');
/* Process data */
$bFirst = true;
$fEnergy = 0;
if (isset($aFields[FIELD])) {
$iField = $aFields[FIELD] + 1;
array_shift($aData);
foreach ($aData as $aRow) {
$iDate = substr($aRow[0], 0, -1);
$iInterval = $bFirst ? (($bFirst = false) || RESOLUTION) : $iDate - $iLast; // s
if (($fValue = floatval($aRow[$iField])) > 0) { // W
$fEnergy += $iInterval * $fValue; // Ws
}
$iLast = $iDate;
}
}
/* Store today data */
$aToday[1] += $fEnergy / 3600; // Wh
$aToday[2] = $iTime;
$aToday[3] = $fTemperature;
file_put_contents($sTodayFile, implode(',', $aToday));
/* Correct today data */
$iWake = getWake($aTwilight);
if (($iTime - $iWake) / 3600 < TRESHOLD_CORRECT && abs($aToday[1] - $fToday) > (MARGIN_ENERGY * $aToday[1])) {
$fToday = $aToday[1];
}
/* Construct PVOutput data */
$aData = array(
'd' => date('Ymd', $iTime),
't' => date('H:i', $iTime),
'v1' => $fToday, // Wh
'v2' => $fPower, // W
'v6' => $fVoltage); // V
/* Add (corrected) temperature when available */
if (isset($fTemperature)) {
if (isset($aToday[3])) {
$fTemperature = abs($aToday[3] - $fTemperature) > (MARGIN_TEMPERATURE * $aToday[3]) ? $aToday[3] : $fTemperature;
}
$aData['v5'] = $fTemperature; // ignore potential flaws in first temperature of the day
file_put_contents('/opt/inverter/data/temp.csv', sprintf("%d,%f\n", $iTime, $fTemperature), FILE_APPEND);
}
/* Store debug data */
file_put_contents('/opt/inverter/data/pvoutput.debug', json_encode(array($argv, $fEnergy, $aToday, $aData)) . "\n", FILE_APPEND);
/* Send data to PVOutput */
if (isset($aSystems[$sSerial])) {
$rCurl = curl_init();
curl_setopt_array($rCurl, array(
CURLOPT_URL => PVOUTPUT_URL,
CURLOPT_HTTPHEADER => array(
sprintf('X-Pvoutput-Apikey: %s', $aSystems[$sSerial][0]),
sprintf('X-Pvoutput-SystemId: %s', $aSystems[$sSerial][1])),
CURLOPT_POSTFIELDS => http_build_query($aData),
CURLOPT_RETURNTRANSFER => true));
$sResult = curl_exec($rCurl);
}

69
inverter/rrd.php Executable file
View File

@@ -0,0 +1,69 @@
<?php
class RRD {
const CREATE = 'create %s --step %d --start %d %s';
const UPDATE = 'update %s %d:%s';
const FETCH = 'fetch %s %s -r %d -s %d -e %d';
protected static $oInstance;
protected static $rProcess;
protected static $aPipes = array();
protected function __construct() {
self::$rProcess = proc_open('rrdtool -', array(
0 => array('pipe', 'r'),
1 => array('pipe', 'w')), self::$aPipes);
stream_set_blocking(self::$aPipes[1], false);
}
static function command($sCommand) {
if (!isset(self::$rProcess)) {
self::$oInstance = new self();
}
//echo $sCommand . "\n";
fwrite(self::$aPipes[0], $sCommand . PHP_EOL);
$nNull = null;
$aRead = array(self::$aPipes[1]);
stream_select($aRead, $nNull, $nNull, 10);
return trim(stream_get_contents(self::$aPipes[1]));
}
static function create($sFile, $iStep, $iStart, $sContents) {
$sCommand = sprintf(self::CREATE, $sFile, $iStep, $iStart, str_replace("\n", ' ', trim($sContents)));
return RRD::command($sCommand);
}
static function update($sFile, $iTime, $aValues) {
$sCommand = sprintf(self::UPDATE, $sFile, $iTime, implode(':', $aValues));
return RRD::command($sCommand);
}
static function fetch($sFile, $iResolution, $iStart, $iEnd, $sType = 'AVERAGE') {
$sCommand = sprintf(self::FETCH, $sFile, $sType, $iResolution, $iStart, $iEnd);
$sData = RRD::command($sCommand);
$aData = explode("\n", trim($sData));
$aFields = preg_split("~[\s]+~", array_shift($aData));
$aFields = array_flip($aFields);
array_shift($aData);
array_pop($aData);
$aValues = array();
foreach ($aData as $sRow) {
$aRow = explode(':', $sRow);
$iTime = current($aRow);
$aRow = explode(' ', trim(next($aRow)));
foreach ($aRow as $iKey => $mValue) {
if (strpos($mValue, 'nan') !== false) {
$aRow[$iKey] = null;
}
}
$aValues[$iTime] = $aRow;
}
return array($aFields, $aValues);
}
function __destruct() {
fwrite(self::$aPipes[0], "quit\n");
fclose(self::$aPipes[0]);
fclose(self::$aPipes[1]);
proc_close(self::$rProcess);
}
}

2
inverter/test.php Normal file
View File

@@ -0,0 +1,2 @@
<?php
$rCurl = curl_init();

11
inverter/weather.php Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/php
<?php
// require_once 'wunderground.php';
require_once 'openweathermap.php';
// define('STATION', 'INHASSUM4');
define('CITY', 2745978);
// $aData = wunderground('conditions', sprintf('pws:%s', STATION));
$aData = openweathermap(CITY);
echo $fTemperature = isset($aData['main']['temp']) ? floatval($aData['main']['temp']) - 273.15 : null;

67
inverter/wunderground.php Executable file
View File

@@ -0,0 +1,67 @@
<?php
define('KEY', '5fc7ebf9168bfbe9745920438e3b17bf');
define('LIMIT_MINUTE', 10);
define('LIMIT_DAY', 500);
define('LIMIT_FILE', '/opt/inverter/data/wunderground.json');
function wunderground($sService, $sQuery, $bDebug = false) {
/* Get current date values */
$iMinute = date('i');
$iDay = date('z');
if (file_exists(LIMIT_FILE)) {
/* Read number of calls used */
$sJSON = file_get_contents(LIMIT_FILE);
$aJSON = json_decode($sJSON, true);
$aCount = array(
'minute' => $iMinute != $aJSON['minute'][0] ? 0 : $aJSON['minute'][1],
'day' => $iDay != $aJSON['day'][0] ? 0 : $aJSON['day'][1]);
} else {
/* Initialise to zero */
$aCount = array(
'minute' => 0,
'day' => 0);
}
/* Check call limits */
$iWait = 0;
if ($aCount['minute'] >= LIMIT_MINUTE) {
$iWait = 60 - date('s');
if ($bDebug === true) {
printf("Minute limit (%d) reached, wait %d seconds\n", LIMIT_MINUTE, $iWait);
}
$aCount['minute'] = 0;
} else if ($aCount['day'] >= LIMIT_DAY) {
$iWait = strtotime('00:00 + 1 day') - time();
if ($bDebug === true) {
printf("Daily limit (%d) reached, wait %d seconds\n", LIMIT_DAY, $iWait);
}
$aCount['day'] = 0;
}
/* Prevent from exceeding call limits */
if ($iWait > 0) {
//die("Try again later!\n");
return null;
}
/* Update call counts */
++$aCount['minute'];
++$aCount['day'];
/* Report number of calls used */
if ($bDebug === true) {
printf("Used %d/%d minutely and %d/%d daily calls\n", $aCount['minute'], LIMIT_MINUTE, $aCount['day'], LIMIT_DAY);
}
/* Write number of calls used to file */
$aJSON = array(
'minute' => array($iMinute, $aCount['minute']),
'day' => array($iDay, $aCount['day']));
file_put_contents(LIMIT_FILE, json_encode($aJSON));
/* Perform actual call */
$sUrl = sprintf('http://api.wunderground.com/api/%s/%s/q/%s.json', KEY, $sService, $sQuery);
$sJSON = file_get_contents($sUrl);
return json_decode($sJSON, true);
}