From 7b5834fcb1b2dd44584542eb193c36e50770d418 Mon Sep 17 00:00:00 2001 From: Rik Veenboer Date: Thu, 19 Dec 2024 15:04:50 +0100 Subject: [PATCH] snapshot inverter files --- inverter/config.ini | 366 ++++++++++++++++++++++++++++++ inverter/create_rrd.php | 78 +++++++ inverter/daemon.php | 68 ++++++ inverter/etc/logrotate.d/inverter | 10 + inverter/functions.php | 65 ++++++ inverter/inverter.php | 59 +++++ inverter/openweathermap.php | 69 ++++++ inverter/pvoutput.php | 107 +++++++++ inverter/rrd.php | 69 ++++++ inverter/test.php | 2 + inverter/weather.php | 11 + inverter/wunderground.php | 67 ++++++ 12 files changed, 971 insertions(+) create mode 100644 inverter/config.ini create mode 100755 inverter/create_rrd.php create mode 100755 inverter/daemon.php create mode 100644 inverter/etc/logrotate.d/inverter create mode 100755 inverter/functions.php create mode 100755 inverter/inverter.php create mode 100755 inverter/openweathermap.php create mode 100755 inverter/pvoutput.php create mode 100755 inverter/rrd.php create mode 100644 inverter/test.php create mode 100755 inverter/weather.php create mode 100755 inverter/wunderground.php diff --git a/inverter/config.ini b/inverter/config.ini new file mode 100644 index 0000000..e6ac81e --- /dev/null +++ b/inverter/config.ini @@ -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" diff --git a/inverter/create_rrd.php b/inverter/create_rrd.php new file mode 100755 index 0000000..e90c9db --- /dev/null +++ b/inverter/create_rrd.php @@ -0,0 +1,78 @@ +#!/usr/bin/env php + 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); + } + } +} diff --git a/inverter/daemon.php b/inverter/daemon.php new file mode 100755 index 0000000..b3cc0ab --- /dev/null +++ b/inverter/daemon.php @@ -0,0 +1,68 @@ +> /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(); +} diff --git a/inverter/etc/logrotate.d/inverter b/inverter/etc/logrotate.d/inverter new file mode 100644 index 0000000..d222f7d --- /dev/null +++ b/inverter/etc/logrotate.d/inverter @@ -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 +} diff --git a/inverter/functions.php b/inverter/functions.php new file mode 100755 index 0000000..a8e249c --- /dev/null +++ b/inverter/functions.php @@ -0,0 +1,65 @@ + /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)); +} diff --git a/inverter/openweathermap.php b/inverter/openweathermap.php new file mode 100755 index 0000000..6ac5c87 --- /dev/null +++ b/inverter/openweathermap.php @@ -0,0 +1,69 @@ + $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); +} diff --git a/inverter/pvoutput.php b/inverter/pvoutput.php new file mode 100755 index 0000000..e4e04bd --- /dev/null +++ b/inverter/pvoutput.php @@ -0,0 +1,107 @@ +#!/usr/bin/env php + 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); +} diff --git a/inverter/rrd.php b/inverter/rrd.php new file mode 100755 index 0000000..bfbf411 --- /dev/null +++ b/inverter/rrd.php @@ -0,0 +1,69 @@ + 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); + } +} \ No newline at end of file diff --git a/inverter/test.php b/inverter/test.php new file mode 100644 index 0000000..09500e9 --- /dev/null +++ b/inverter/test.php @@ -0,0 +1,2 @@ + $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); +}