snapshot inverter files
This commit is contained in:
366
inverter/config.ini
Normal file
366
inverter/config.ini
Normal 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
78
inverter/create_rrd.php
Executable 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
68
inverter/daemon.php
Executable 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();
|
||||
}
|
||||
10
inverter/etc/logrotate.d/inverter
Normal file
10
inverter/etc/logrotate.d/inverter
Normal 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
65
inverter/functions.php
Executable 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
59
inverter/inverter.php
Executable 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
69
inverter/openweathermap.php
Executable 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
107
inverter/pvoutput.php
Executable 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
69
inverter/rrd.php
Executable 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
2
inverter/test.php
Normal file
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
$rCurl = curl_init();
|
||||
11
inverter/weather.php
Executable file
11
inverter/weather.php
Executable 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
67
inverter/wunderground.php
Executable 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);
|
||||
}
|
||||
Reference in New Issue
Block a user