refactor for better support with ch341 and other unreliable rs232 adapters
This commit is contained in:
+3
-3
@@ -9,10 +9,10 @@ RUN apt update && apt install -y \
|
||||
mosquitto-clients
|
||||
|
||||
ADD sources/ /opt/
|
||||
ADD config/ /etc/skymax/
|
||||
ADD config/ /etc/inverter/
|
||||
|
||||
RUN cd /opt/voltronic-cli && \
|
||||
RUN cd /opt/inverter-cli && \
|
||||
mkdir bin && cmake . && make
|
||||
|
||||
WORKDIR /opt
|
||||
ENTRYPOINT ["/bin/bash", "/opt/voltronic-mqtt/entrypoint.sh"]
|
||||
ENTRYPOINT ["/bin/bash", "/opt/inverter-mqtt/entrypoint.sh"]
|
||||
@@ -21,7 +21,7 @@ _Example: My "Lovelace" dashboard using data collected from the Inverter._
|
||||
|
||||
- Docker
|
||||
- Docker-compose
|
||||
- [Voltronic](https://www.ebay.com.au/sch/i.html?_from=R40&_trksid=p2334524.m570.l1313.TR11.TRC1.A0.H0.Xaxpert+inverter.TRS0&_nkw=axpert+inverter&_sacat=0&LH_TitleDesc=0&LH_PrefLoc=2&_osacat=0&_odkw=solar+inverter&LH_TitleDesc=0) based inverter that you want to monitor
|
||||
- [Voltronic/Axpert/MPPSolar](https://www.ebay.com.au/sch/i.html?_from=R40&_trksid=p2334524.m570.l1313.TR11.TRC1.A0.H0.Xaxpert+inverter.TRS0&_nkw=axpert+inverter&_sacat=0&LH_TitleDesc=0&LH_PrefLoc=2&_osacat=0&_odkw=solar+inverter&LH_TitleDesc=0) based inverter that you want to monitor
|
||||
- Home Assistant [running with a MQTT Server](https://www.home-assistant.io/components/mqtt/)
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@ It's pretty straightforward, just clone down the sources and set the configurati
|
||||
|
||||
```bash
|
||||
# Clone down sources on the host you want to monitor...
|
||||
git clone https://github.com/ned-kelly/docker-voltronic-homeassistant.git /opt/ha-voltronic-mqtt
|
||||
cd /opt/ha-voltronic-mqtt
|
||||
git clone https://github.com/ned-kelly/docker-voltronic-homeassistant.git /opt/ha-inverter-mqtt-agent
|
||||
cd /opt/ha-inverter-mqtt-agent
|
||||
|
||||
# Configure the 'device=' directive (in skymax.conf) to suit for RS232 or USB..
|
||||
vi config/skymax.conf
|
||||
# Configure the 'device=' directive (in inverter.conf) to suit for RS232 or USB..
|
||||
vi config/inverter.conf
|
||||
|
||||
# Configure your MQTT server host, port, Home Assistant topic, and name of the Inverter that you want displayed in Home Assistant.
|
||||
vi config/mqtt.json
|
||||
@@ -88,6 +88,25 @@ Set other commands PEa / PDa (Enable/disable buzzer)
|
||||
PEx / PDx (Enable/disable backlight)
|
||||
```
|
||||
|
||||
### Using `inverter_poller` binary directly
|
||||
|
||||
This project uses heavily modified sources, from [manio's](https://github.com/manio/skymax-demo) original demo, and be compiled to run standalone on Linux, Mac, and Windows (via Cygwin).
|
||||
|
||||
Just head to the `sources/inverter-cli` directory and build it directly using: `cmake . && make`.
|
||||
|
||||
Basic arguments supported are:
|
||||
|
||||
```
|
||||
USAGE: ./inverter_poller <args> [-r <command>], [-h | --help], [-1 | --run-once]
|
||||
|
||||
SUPPORTED ARGUMENTS:
|
||||
-r <raw-command> TX 'raw' command to the inverter
|
||||
-h | --help This Help Message
|
||||
-1 | --run-once Runs one iteration on the inverter, and then exits
|
||||
-d Additional debugging
|
||||
|
||||
```
|
||||
|
||||
### Bonus: Lovelace Dashboard Files
|
||||
|
||||
_**Please refer to the screenshot above for an example of the dashboard.**_
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#This is the settings file, all comment lines should start with a hash mark.
|
||||
# Basic configuration options for the actual inverter polling process...
|
||||
|
||||
# The device to read from...
|
||||
# Use: /dev/ttyS0 if you have a serial device or /dev/hidraw0 if you're connecting via USB.
|
||||
device=/dev/ttyS0
|
||||
# Use: /dev/ttyS0 if you have a serial device,
|
||||
# /dev/ttyUSB0 if a USB<>Serial,
|
||||
# /dev/hidraw0 if you're connecting via the USB port on the inverter.
|
||||
|
||||
device=/dev/ttyUSB0
|
||||
|
||||
# How many times per hour is the program going to run...
|
||||
# This is used to calculate the PV & Load Watt Hours between runs...
|
||||
@@ -11,7 +14,7 @@ device=/dev/ttyS0
|
||||
# (120 = every 30 seconds)...
|
||||
run_interval=120
|
||||
|
||||
# This allos you to modify the amperage in case the inverter is giving an incorrect
|
||||
# This allows you to modify the amperage in case the inverter is giving an incorrect
|
||||
# reading compared to measurement tools. Normally this will remain '1'
|
||||
amperage_factor=1.0
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"server": "10.16.10.5",
|
||||
"server": "10.16.10.4",
|
||||
"port": "1883",
|
||||
"topic": "homeassistant",
|
||||
"devicename": "voltronic"
|
||||
|
||||
+3
-3
@@ -3,8 +3,8 @@ version: '2'
|
||||
services:
|
||||
voltronic-mqtt:
|
||||
|
||||
#build: .
|
||||
image: bushrangers/ha-voltronic-mqtt
|
||||
build: .
|
||||
#image: bushrangers/ha-voltronic-mqtt
|
||||
|
||||
container_name: voltronic-mqtt
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
- ./config/:/etc/skymax/
|
||||
- ./config/:/etc/inverter/
|
||||
|
||||
devices:
|
||||
# - "/dev/mem:/dev/mem"
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -1,8 +1,8 @@
|
||||
CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
|
||||
PROJECT("skymax")
|
||||
PROJECT("inverter_poller")
|
||||
|
||||
set (CMAKE_CXX_FLAGS "-O2 --std=c++0x ${CMAKE_CXX_FLAGS}")
|
||||
|
||||
file(GLOB SOURCES *.cpp)
|
||||
ADD_EXECUTABLE(bin/skymax ${SOURCES})
|
||||
target_link_libraries(bin/skymax -lpthread)
|
||||
ADD_EXECUTABLE(inverter_poller ${SOURCES})
|
||||
target_link_libraries(inverter_poller -lpthread)
|
||||
@@ -9,13 +9,12 @@
|
||||
// It is not posix compliant and wont work with args like: ./program -xf filename
|
||||
// You must place each arg after its own seperate dash like: ./program -x -f filename
|
||||
|
||||
InputParser::InputParser (int &argc, char **argv)
|
||||
{
|
||||
InputParser::InputParser (int &argc, char **argv) {
|
||||
for (int i=1; i < argc; ++i)
|
||||
this->tokens.push_back(std::string(argv[i]));
|
||||
}
|
||||
const std::string& InputParser::getCmdOption(const std::string &option) const
|
||||
{
|
||||
|
||||
const std::string& InputParser::getCmdOption(const std::string &option) const {
|
||||
std::vector<std::string>::const_iterator itr;
|
||||
itr = std::find(this->tokens.begin(), this->tokens.end(), option);
|
||||
if (itr != this->tokens.end() && ++itr != this->tokens.end())
|
||||
@@ -25,8 +24,8 @@ const std::string& InputParser::getCmdOption(const std::string &option) const
|
||||
static const std::string empty_string("");
|
||||
return empty_string;
|
||||
}
|
||||
bool InputParser::cmdOptionExists(const std::string &option) const
|
||||
{
|
||||
|
||||
bool InputParser::cmdOptionExists(const std::string &option) const {
|
||||
return std::find(this->tokens.begin(), this->tokens.end(), option)
|
||||
!= this->tokens.end();
|
||||
}
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
class InputParser
|
||||
{
|
||||
class InputParser {
|
||||
std::vector <std::string> tokens;
|
||||
|
||||
public:
|
||||
@@ -0,0 +1,23 @@
|
||||
# Basic configuration options for the actual inverter polling process...
|
||||
|
||||
# The device to read from...
|
||||
# Use: /dev/ttyS0 if you have a serial device,
|
||||
# /dev/ttyUSB0 if a USB<>Serial,
|
||||
# /dev/hidraw0 if you're connecting via the USB port on the inverter.
|
||||
|
||||
device=/dev/ttyUSB0
|
||||
|
||||
# How many times per hour is the program going to run...
|
||||
# This is used to calculate the PV & Load Watt Hours between runs...
|
||||
# If unsure, leave as default - it will run every minute...
|
||||
|
||||
# (120 = every 30 seconds)...
|
||||
run_interval=120
|
||||
|
||||
# This allows you to modify the amperage in case the inverter is giving an incorrect
|
||||
# reading compared to measurement tools. Normally this will remain '1'
|
||||
amperage_factor=1.0
|
||||
|
||||
# This allos you to modify the wattage in case the inverter is giving an incorrect
|
||||
# reading compared to measurement tools. Normally this will remain '1'
|
||||
watt_factor=1.01
|
||||
@@ -0,0 +1,252 @@
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include "inverter.h"
|
||||
#include "tools.h"
|
||||
#include "main.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <termios.h>
|
||||
|
||||
cInverter::cInverter(std::string devicename) {
|
||||
device = devicename;
|
||||
status1[0] = 0;
|
||||
status2[0] = 0;
|
||||
warnings[0] = 0;
|
||||
mode = 0;
|
||||
}
|
||||
|
||||
string *cInverter::GetQpigsStatus() {
|
||||
m.lock();
|
||||
string *result = new string(status1);
|
||||
m.unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
string *cInverter::GetQpiriStatus() {
|
||||
m.lock();
|
||||
string *result = new string(status2);
|
||||
m.unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
string *cInverter::GetWarnings() {
|
||||
m.lock();
|
||||
string *result = new string(warnings);
|
||||
m.unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
void cInverter::SetMode(char newmode) {
|
||||
m.lock();
|
||||
if (mode && newmode != mode)
|
||||
ups_status_changed = true;
|
||||
mode = newmode;
|
||||
m.unlock();
|
||||
}
|
||||
|
||||
int cInverter::GetMode() {
|
||||
int result;
|
||||
m.lock();
|
||||
|
||||
switch (mode) {
|
||||
case 'P': result = 1; break; // Power_On
|
||||
case 'S': result = 2; break; // Standby
|
||||
case 'L': result = 3; break; // Line
|
||||
case 'B': result = 4; break; // Battery
|
||||
case 'F': result = 5; break; // Fault
|
||||
case 'H': result = 6; break; // Power_Saving
|
||||
default: result = 0; break; // Unknown
|
||||
}
|
||||
|
||||
m.unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool cInverter::query(const char *cmd, int replysize) {
|
||||
time_t started;
|
||||
int fd;
|
||||
int i=0, n;
|
||||
|
||||
fd = open(this->device.data(), O_RDWR | O_NONBLOCK);
|
||||
if (fd == -1) {
|
||||
lprintf("INVERTER: Unable to open device file (errno=%d %s)", errno, strerror(errno));
|
||||
sleep(5);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Once connected, set the baud rate and other serial config (Don't rely on this being correct on the system by default...)
|
||||
speed_t baud = B2400;
|
||||
|
||||
// Speed settings (in this case, 2400 8N1)
|
||||
struct termios settings;
|
||||
tcgetattr(fd, &settings);
|
||||
|
||||
cfsetospeed(&settings, baud); // baud rate
|
||||
settings.c_cflag &= ~PARENB; // no parity
|
||||
settings.c_cflag &= ~CSTOPB; // 1 stop bit
|
||||
settings.c_cflag &= ~CSIZE;
|
||||
settings.c_cflag |= CS8 | CLOCAL; // 8 bits
|
||||
// settings.c_lflag = ICANON; // canonical mode
|
||||
settings.c_oflag &= ~OPOST; // raw output
|
||||
|
||||
tcsetattr(fd, TCSANOW, &settings); // apply the settings
|
||||
tcflush(fd, TCOFLUSH);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// Generating CRC for a command
|
||||
uint16_t crc = cal_crc_half((uint8_t*)cmd, strlen(cmd));
|
||||
n = strlen(cmd);
|
||||
memcpy(&buf, cmd, n);
|
||||
lprintf("INVERTER: Current CRC: %X %X", crc >> 8, crc & 0xff);
|
||||
|
||||
buf[n++] = crc >> 8;
|
||||
buf[n++] = crc & 0xff;
|
||||
buf[n++] = 0x0d;
|
||||
|
||||
//send a command
|
||||
write(fd, &buf, n);
|
||||
time(&started);
|
||||
|
||||
do {
|
||||
n = read(fd, (void*)buf+i, replysize-i);
|
||||
if (n < 0) {
|
||||
if (time(NULL) - started > 2) {
|
||||
lprintf("INVERTER: %s read timeout", cmd);
|
||||
break;
|
||||
} else {
|
||||
usleep(10);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
i += n;
|
||||
} while (i<replysize);
|
||||
close(fd);
|
||||
|
||||
if (i==replysize) {
|
||||
|
||||
lprintf("INVERTER: %s reply size (%d bytes)", cmd, i);
|
||||
|
||||
if (buf[0]!='(' || buf[replysize-1]!=0x0d) {
|
||||
lprintf("INVERTER: %s: incorrect start/stop bytes. Buffer: %s", cmd, buf);
|
||||
return false;
|
||||
}
|
||||
if (!(CheckCRC(buf, replysize))) {
|
||||
lprintf("INVERTER: %s: CRC Failed! Reply size: %d Buffer: %s", cmd, replysize, buf);
|
||||
return false;
|
||||
}
|
||||
|
||||
buf[i-3] = '\0'; //nullterminating on first CRC byte
|
||||
lprintf("INVERTER: %s: %d bytes read: %s", cmd, i, buf);
|
||||
|
||||
lprintf("INVERTER: %s query finished", cmd);
|
||||
return true;
|
||||
} else {
|
||||
lprintf("INVERTER: %s reply too short (%d bytes)", cmd, i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void cInverter::poll() {
|
||||
int n,j;
|
||||
|
||||
while (true) {
|
||||
|
||||
// Reading mode
|
||||
if (!ups_qmod_changed) {
|
||||
if (query("QMOD", 5)) {
|
||||
SetMode(buf[1]);
|
||||
ups_qmod_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// reading status (QPIGS)
|
||||
if (!ups_qpigs_changed) {
|
||||
if (query("QPIGS", 110)) {
|
||||
m.lock();
|
||||
strcpy(status1, (const char*)buf+1);
|
||||
m.unlock();
|
||||
ups_qpigs_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Reading QPIRI status
|
||||
if (!ups_qpiri_changed) {
|
||||
if (query("QPIRI", 97)) {
|
||||
m.lock();
|
||||
strcpy(status2, (const char*)buf+1);
|
||||
m.unlock();
|
||||
ups_qpiri_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Get any device warnings...
|
||||
if (!ups_qpiws_changed) {
|
||||
if (query("QPIWS", 36)) {
|
||||
m.lock();
|
||||
strcpy(warnings, (const char*)buf+1);
|
||||
m.unlock();
|
||||
ups_qpiws_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
sleep(5);
|
||||
}
|
||||
}
|
||||
|
||||
void cInverter::ExecuteCmd(const string cmd) {
|
||||
// Sending any command raw
|
||||
if (query(cmd.data(), 7)) {
|
||||
m.lock();
|
||||
strcpy(status2, (const char*)buf+1);
|
||||
m.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t cInverter::cal_crc_half(uint8_t *pin, uint8_t len) {
|
||||
uint16_t crc;
|
||||
|
||||
uint8_t da;
|
||||
uint8_t *ptr;
|
||||
uint8_t bCRCHign;
|
||||
uint8_t bCRCLow;
|
||||
|
||||
uint16_t crc_ta[16]= {
|
||||
0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
|
||||
0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef
|
||||
};
|
||||
|
||||
ptr=pin;
|
||||
crc=0;
|
||||
|
||||
while(len--!=0) {
|
||||
da=((uint8_t)(crc>>8))>>4;
|
||||
crc<<=4;
|
||||
crc^=crc_ta[da^(*ptr>>4)];
|
||||
da=((uint8_t)(crc>>8))>>4;
|
||||
crc<<=4;
|
||||
crc^=crc_ta[da^(*ptr&0x0f)];
|
||||
ptr++;
|
||||
}
|
||||
|
||||
bCRCLow = crc;
|
||||
bCRCHign= (uint8_t)(crc>>8);
|
||||
|
||||
if(bCRCLow==0x28||bCRCLow==0x0d||bCRCLow==0x0a)
|
||||
bCRCLow++;
|
||||
if(bCRCHign==0x28||bCRCHign==0x0d||bCRCHign==0x0a)
|
||||
bCRCHign++;
|
||||
|
||||
crc = ((uint16_t)bCRCHign)<<8;
|
||||
crc += bCRCLow;
|
||||
return(crc);
|
||||
}
|
||||
|
||||
bool cInverter::CheckCRC(unsigned char *data, int len) {
|
||||
uint16_t crc = cal_crc_half(data, len-3);
|
||||
return data[len-3]==(crc>>8) && data[len-2]==(crc&0xff);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef ___INVERTER_H
|
||||
#define ___INVERTER_H
|
||||
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
|
||||
using namespace std;
|
||||
|
||||
class cInverter {
|
||||
unsigned char buf[1024]; //internal work buffer
|
||||
|
||||
char warnings[1024];
|
||||
char status1[1024];
|
||||
char status2[1024];
|
||||
char mode;
|
||||
|
||||
std::string device;
|
||||
std::mutex m;
|
||||
|
||||
void SetMode(char newmode);
|
||||
bool CheckCRC(unsigned char *buff, int len);
|
||||
bool query(const char *cmd, int replysize);
|
||||
uint16_t cal_crc_half(uint8_t *pin, uint8_t len);
|
||||
|
||||
public:
|
||||
cInverter(std::string devicename);
|
||||
void poll();
|
||||
void runMultiThread() {
|
||||
std::thread t1(&cInverter::poll, this);
|
||||
t1.detach();
|
||||
}
|
||||
|
||||
string *GetQpiriStatus();
|
||||
string *GetQpigsStatus();
|
||||
string *GetWarnings();
|
||||
|
||||
int GetMode();
|
||||
void ExecuteCmd(const std::string cmd);
|
||||
};
|
||||
|
||||
#endif // ___INVERTER_H
|
||||
@@ -0,0 +1,289 @@
|
||||
// Lightweight program to take the sensor data from a Voltronic Axpert, Mppsolar PIP, Voltacon, Effekta, and other branded OEM Inverters and send it to a MQTT server for ingestion...
|
||||
// Adapted from "Maio's" C application here: https://skyboo.net/2017/03/monitoring-voltronic-power-axpert-mex-inverter-under-linux/
|
||||
//
|
||||
// Please feel free to adapt this code and add more parameters -- See the following forum for a breakdown on the RS323 protocol: http://forums.aeva.asn.au/viewtopic.php?t=4332
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <thread>
|
||||
|
||||
#include "main.h"
|
||||
#include "tools.h"
|
||||
#include "inputparser.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
|
||||
|
||||
bool debugFlag = false;
|
||||
bool runOnce = false;
|
||||
|
||||
cInverter *ups = NULL;
|
||||
|
||||
atomic_bool ups_status_changed(false);
|
||||
atomic_bool ups_qmod_changed(false);
|
||||
atomic_bool ups_qpiri_changed(false);
|
||||
atomic_bool ups_qpigs_changed(false);
|
||||
atomic_bool ups_qpiws_changed(false);
|
||||
atomic_bool ups_cmd_executed(false);
|
||||
|
||||
|
||||
// ---------------------------------------
|
||||
// Global configs read from 'inverter.conf'
|
||||
|
||||
string devicename;
|
||||
int runinterval;
|
||||
float ampfactor;
|
||||
float wattfactor;
|
||||
|
||||
// ---------------------------------------
|
||||
|
||||
void attemptAddSetting(int *addTo, string addFrom) {
|
||||
try {
|
||||
*addTo = stof(addFrom);
|
||||
} catch (exception e) {
|
||||
cout << e.what() << '\n';
|
||||
cout << "There's probably a string in the settings file where an int should be.\n";
|
||||
}
|
||||
}
|
||||
|
||||
void attemptAddSetting(float *addTo, string addFrom) {
|
||||
try {
|
||||
*addTo = stof(addFrom);
|
||||
} catch (exception e) {
|
||||
cout << e.what() << '\n';
|
||||
cout << "There's probably a string in the settings file where a floating point should be.\n";
|
||||
}
|
||||
}
|
||||
|
||||
void getSettingsFile(string filename) {
|
||||
|
||||
try {
|
||||
string fileline, linepart1, linepart2;
|
||||
ifstream infile;
|
||||
infile.open(filename);
|
||||
|
||||
while(!infile.eof()) {
|
||||
getline(infile, fileline);
|
||||
size_t firstpos = fileline.find("#");
|
||||
|
||||
if(firstpos != 0 && fileline.length() != 0) { // Ignore lines starting with # (comment lines)
|
||||
size_t delimiter = fileline.find("=");
|
||||
linepart1 = fileline.substr(0, delimiter);
|
||||
linepart2 = fileline.substr(delimiter+1, string::npos - delimiter);
|
||||
|
||||
if(linepart1 == "device")
|
||||
devicename = linepart2;
|
||||
else if(linepart1 == "run_interval")
|
||||
attemptAddSetting(&runinterval, linepart2);
|
||||
else if(linepart1 == "amperage_factor")
|
||||
attemptAddSetting(&factor, linepart2);
|
||||
else if(linepart1 == "watt_factor")
|
||||
attemptAddSetting(&wattfactor, linepart2);
|
||||
else if(linepart1 == "watt_factor")
|
||||
attemptAddSetting(&wattfactor, linepart2);
|
||||
else
|
||||
continue;
|
||||
}
|
||||
}
|
||||
infile.close();
|
||||
} catch (...) {
|
||||
cout << "Settings could not be read properly...\n";
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
|
||||
// Reply1
|
||||
float voltage_grid;
|
||||
float freq_grid;
|
||||
float voltage_out;
|
||||
float freq_out;
|
||||
int load_va;
|
||||
int load_watt;
|
||||
int load_percent;
|
||||
int voltage_bus;
|
||||
float voltage_batt;
|
||||
int batt_charge_current;
|
||||
int batt_capacity;
|
||||
int temp_heatsink;
|
||||
float pv_input_current;
|
||||
float pv_input_voltage;
|
||||
float pv_input_watts;
|
||||
float pv_input_watthour;
|
||||
float load_watthour = 0;
|
||||
float scc_voltage;
|
||||
int batt_discharge_current;
|
||||
char device_status[9];
|
||||
|
||||
// Reply2
|
||||
float grid_voltage_rating;
|
||||
float grid_current_rating;
|
||||
float out_voltage_rating;
|
||||
float out_freq_rating;
|
||||
float out_current_rating;
|
||||
int out_va_rating;
|
||||
int out_watt_rating;
|
||||
float batt_rating;
|
||||
float batt_recharge_voltage;
|
||||
float batt_under_voltage;
|
||||
float batt_bulk_voltage;
|
||||
float batt_float_voltage;
|
||||
int batt_type;
|
||||
int max_grid_charge_current;
|
||||
int max_charge_current;
|
||||
int in_voltage_range;
|
||||
int out_source_priority;
|
||||
int charger_source_priority;
|
||||
int machine_type;
|
||||
int topology;
|
||||
int out_mode;
|
||||
float batt_redischarge_voltage;
|
||||
|
||||
// Get command flag settings from the arguments (if any)
|
||||
InputParser cmdArgs(argc, argv);
|
||||
const string &rawcmd = cmdArgs.getCmdOption("-r");
|
||||
|
||||
if(cmdArgs.cmdOptionExists("-h") || cmdArgs.cmdOptionExists("--help")) {
|
||||
return print_help();
|
||||
}
|
||||
if(cmdArgs.cmdOptionExists("-d")) {
|
||||
debugFlag = true;
|
||||
}
|
||||
if(cmdArgs.cmdOptionExists("-1") || cmdArgs.cmdOptionExists("--run-once")) {
|
||||
runOnce = true;
|
||||
}
|
||||
lprintf("INVERTER: Debug set");
|
||||
|
||||
// Get the rest of the settings from the conf file
|
||||
if( access( "./inverter.conf", F_OK ) != -1 ) { // file exists
|
||||
getSettingsFile("./inverter.conf");
|
||||
} else { // file doesn't exist
|
||||
getSettingsFile("/etc/inverter/inverter.conf");
|
||||
}
|
||||
|
||||
bool ups_status_changed(false);
|
||||
ups = new cInverter(devicename);
|
||||
|
||||
// Logic to send 'raw commands' to the inverter..
|
||||
if (!rawcmd.empty()) {
|
||||
ups->ExecuteCmd(rawcmd);
|
||||
// We're piggybacking off the qpri status response...
|
||||
printf("Reply: %s\n", ups->GetQpiriStatus()->c_str());
|
||||
exit(0);
|
||||
} else {
|
||||
ups->runMultiThread();
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (ups_status_changed) {
|
||||
int mode = ups->GetMode();
|
||||
|
||||
if (mode)
|
||||
lprintf("INVERTER: Mode Currently set to: %d", mode);
|
||||
|
||||
ups_status_changed = false;
|
||||
}
|
||||
|
||||
if (ups_qmod_changed && ups_qpiri_changed && ups_qpigs_changed) {
|
||||
|
||||
ups_qmod_changed = false;
|
||||
ups_qpiri_changed = false;
|
||||
ups_qpigs_changed = false;
|
||||
|
||||
int mode = ups->GetMode();
|
||||
string *reply1 = ups->GetQpigsStatus();
|
||||
string *reply2 = ups->GetQpiriStatus();
|
||||
string *warnings = ups->GetWarnings();
|
||||
|
||||
if (reply1 && reply2 && warnings) {
|
||||
|
||||
// Parse and display values
|
||||
sscanf(reply1->c_str(), "%f %f %f %f %d %d %d %d %f %d %d %d %f %f %f %d %s", &voltage_grid, &freq_grid, &voltage_out, &freq_out, &load_va, &load_watt, &load_percent, &voltage_bus, &voltage_batt, &batt_charge_current, &batt_capacity, &temp_heatsink, &pv_input_current, &pv_input_voltage, &scc_voltage, &batt_discharge_current, &device_status);
|
||||
sscanf(reply2->c_str(), "%f %f %f %f %f %d %d %f %f %f %f %f %d %d %d %d %d %d - %d %d %d %f", &grid_voltage_rating, &grid_current_rating, &out_voltage_rating, &out_freq_rating, &out_current_rating, &out_va_rating, &out_watt_rating, &batt_rating, &batt_recharge_voltage, &batt_under_voltage, &batt_bulk_voltage, &batt_float_voltage, &batt_type, &max_grid_charge_current, &max_charge_current, &in_voltage_range, &out_source_priority, &charger_source_priority, &machine_type, &topology, &out_mode, &batt_redischarge_voltage);
|
||||
|
||||
// There appears to be a discrepancy in actual DMM measured current vs what the meter is
|
||||
// telling me it's getting, so lets add a variable we can multiply/divide by to adjust if
|
||||
// needed. This should be set in the config so it can be changed without program recompile.
|
||||
if (debugFlag) {
|
||||
printf("INVERTER: ampfactor from config is %.2f\n", ampfactor);
|
||||
printf("INVERTER: wattfactor from config is %.2f\n", wattfactor);
|
||||
}
|
||||
|
||||
pv_input_current = pv_input_current * ampfactor;
|
||||
|
||||
// It appears on further inspection of the documentation, that the input current is actually
|
||||
// current that is going out to the battery at battery voltage (NOT at PV voltage). This
|
||||
// would explain the larger discrepancy we saw before.
|
||||
|
||||
pv_input_watts = (scc_voltage * pv_input_current) * wattfactor;
|
||||
|
||||
// Calculate watt-hours generated per run interval period (given as program argument)
|
||||
pv_input_watthour = pv_input_watts / (3600 / runinterval);
|
||||
load_watthour = (float)load_watt / (3600 / runinterval);
|
||||
|
||||
// Print as JSON (output is expected to be parsed by another tool...)
|
||||
printf("{\n");
|
||||
|
||||
printf(" \"Inverter_mode\":%d,\n", mode);
|
||||
printf(" \"AC_grid_voltage\":%.1f,\n", voltage_grid);
|
||||
printf(" \"AC_grid_frequency\":%.1f,\n", freq_grid);
|
||||
printf(" \"AC_out_voltage\":%.1f,\n", voltage_out);
|
||||
printf(" \"AC_out_frequency\":%.1f,\n", freq_out);
|
||||
printf(" \"PV_in_voltage\":%.1f,\n", pv_input_voltage);
|
||||
printf(" \"PV_in_current\":%.1f,\n", pv_input_current);
|
||||
printf(" \"PV_in_watts\":%.1f,\n", pv_input_watts);
|
||||
printf(" \"PV_in_watthour\":%.4f,\n", pv_input_watthour);
|
||||
printf(" \"SCC_voltage\":%.4f,\n", scc_voltage);
|
||||
printf(" \"Load_pct\":%d,\n", load_percent);
|
||||
printf(" \"Load_watt\":%d,\n", load_watt);
|
||||
printf(" \"Load_watthour\":%.4f,\n", load_watthour);
|
||||
printf(" \"Load_va\":%d,\n", load_va);
|
||||
printf(" \"Bus_voltage\":%d,\n", voltage_bus);
|
||||
printf(" \"Heatsink_temperature\":%d,\n", temp_heatsink);
|
||||
printf(" \"Battery_capacity\":%d,\n", batt_capacity);
|
||||
printf(" \"Battery_voltage\":%.2f,\n", voltage_batt);
|
||||
printf(" \"Battery_charge_current\":%d,\n", batt_charge_current);
|
||||
printf(" \"Battery_discharge_current\":%d,\n", batt_discharge_current);
|
||||
printf(" \"Load_status_on\":%c,\n", device_status[3]);
|
||||
printf(" \"SCC_charge_on\":%c,\n", device_status[6]);
|
||||
printf(" \"AC_charge_on\":%c,\n", device_status[7]);
|
||||
printf(" \"Battery_recharge_voltage\":%.1f,\n", batt_recharge_voltage);
|
||||
printf(" \"Battery_under_voltage\":%.1f,\n", batt_under_voltage);
|
||||
printf(" \"Battery_bulk_voltage\":%.1f,\n", batt_bulk_voltage);
|
||||
printf(" \"Battery_float_voltage\":%.1f,\n", batt_float_voltage);
|
||||
printf(" \"Max_grid_charge_current\":%d,\n", max_grid_charge_current);
|
||||
printf(" \"Max_charge_current\":%d,\n", max_charge_current);
|
||||
printf(" \"Out_source_priority\":%d,\n", out_source_priority);
|
||||
printf(" \"Charger_source_priority\":%d,\n", charger_source_priority);
|
||||
printf(" \"Battery_redischarge_voltage\":%.1f\n", batt_redischarge_voltage);
|
||||
printf(" \"Warnings\":\"%s\"\n", warnings->c_str());
|
||||
printf("}\n");
|
||||
|
||||
// Delete reply string so we can update with new data when polled again...
|
||||
delete reply1;
|
||||
delete reply2;
|
||||
|
||||
if(runOnce) {
|
||||
// Do once and exit instead of loop endlessly
|
||||
lprintf("INVERTER: All queries complete, exiting loop.");
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
if (ups)
|
||||
delete ups;
|
||||
return 0;
|
||||
}
|
||||
@@ -2,10 +2,13 @@
|
||||
#define ___MAIN_H
|
||||
|
||||
#include <atomic>
|
||||
#include "skymax.h"
|
||||
#include "inverter.h"
|
||||
|
||||
extern bool debugFlag;
|
||||
extern atomic_bool ups_data_changed;
|
||||
|
||||
extern atomic_bool ups_status_changed;
|
||||
extern atomic_bool ups_qpiws_changed;
|
||||
extern atomic_bool ups_qmod_changed;
|
||||
extern atomic_bool ups_qpiri_changed;
|
||||
extern atomic_bool ups_qpigs_changed;
|
||||
@@ -0,0 +1,74 @@
|
||||
#include <mutex>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <sys/time.h>
|
||||
#include <string>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include "main.h"
|
||||
#include "tools.h"
|
||||
|
||||
std::mutex log_mutex;
|
||||
|
||||
void lprintf(const char *format, ...) {
|
||||
// Only print if debug flag is set, else do nothing
|
||||
if (debugFlag) {
|
||||
va_list ap;
|
||||
char fmt[2048];
|
||||
|
||||
//actual time
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
time(&rawtime);
|
||||
timeinfo = localtime(&rawtime);
|
||||
char buf[256];
|
||||
strcpy(buf, asctime(timeinfo));
|
||||
buf[strlen(buf)-1] = 0;
|
||||
|
||||
//connect with args
|
||||
snprintf(fmt, sizeof(fmt), "%s %s\n", buf, format);
|
||||
|
||||
//put on screen:
|
||||
va_start(ap, format);
|
||||
vprintf(fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
//to the logfile:
|
||||
static FILE *log;
|
||||
log_mutex.lock();
|
||||
log = fopen(LOG_FILE, "a");
|
||||
va_start(ap, format);
|
||||
vfprintf(log, fmt, ap);
|
||||
va_end(ap);
|
||||
fclose(log);
|
||||
log_mutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
int print_help() {
|
||||
printf("\nUSAGE: ./inverter_poller <args> [-r <command>], [-h | --help], [-1 | --run-once]\n\n");
|
||||
|
||||
printf("SUPPORTED ARGUMENTS:\n");
|
||||
printf(" -r <raw-command> TX 'raw' command to the inverter\n");
|
||||
printf(" -h | --help This Help Message\n");
|
||||
printf(" -1 | --run-once Runs one iteration on the inverter, and then exits\n");
|
||||
printf(" -d Additional debugging\n\n");
|
||||
|
||||
printf("RAW COMMAND EXAMPLES (see protocol manual for complete list):\n");
|
||||
printf("Set output source priority POP00 (Utility first)\n");
|
||||
printf(" POP01 (Solar first)\n");
|
||||
printf(" POP02 (SBU)\n");
|
||||
printf("Set charger priority PCP00 (Utility first)\n");
|
||||
printf(" PCP01 (Solar first)\n");
|
||||
printf(" PCP02 (Solar and utility)\n");
|
||||
printf(" PCP03 (Solar only)\n");
|
||||
printf("Set other commands PEa / PDa (Enable/disable buzzer)\n");
|
||||
printf(" PEb / PDb (Enable/disable overload bypass)\n");
|
||||
printf(" PEj / PDj (Enable/disable power saving)\n");
|
||||
printf(" PEu / PDu (Enable/disable overload restart)\n");
|
||||
printf(" PEx / PDx (Enable/disable backlight)\n\n");
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -3,10 +3,10 @@ export TERM=xterm
|
||||
|
||||
# Init the mqtt server for the first time, then every 5 minutes
|
||||
# This will re-create the auto-created topics in the MQTT server if HA is restarted...
|
||||
watch -n 300 /opt/voltronic-mqtt/mqtt-init.sh > /dev/null 2>&1 &
|
||||
watch -n 300 /opt/inverter-mqtt/mqtt-init.sh > /dev/null 2>&1 &
|
||||
|
||||
# Run the MQTT Subscriber process in the background (so that way we can change the configuration on the inverter from home assistant)
|
||||
/opt/voltronic-mqtt/mqtt-subscriber.sh &
|
||||
/opt/inverter-mqtt/mqtt-subscriber.sh &
|
||||
|
||||
# execute exactly every 30 seconds...
|
||||
watch -n 30 /opt/voltronic-mqtt/mqtt-push.sh > /dev/null 2>&1
|
||||
watch -n 30 /opt/inverter-mqtt/mqtt-push.sh > /dev/null 2>&1
|
||||
@@ -2,10 +2,10 @@
|
||||
#
|
||||
# Simple script to register the MQTT topics when the container starts for the first time...
|
||||
|
||||
MQTT_SERVER=`cat /etc/skymax/mqtt.json | jq '.server' -r`
|
||||
MQTT_PORT=`cat /etc/skymax/mqtt.json | jq '.port' -r`
|
||||
MQTT_TOPIC=`cat /etc/skymax/mqtt.json | jq '.topic' -r`
|
||||
MQTT_DEVICENAME=`cat /etc/skymax/mqtt.json | jq '.devicename' -r`
|
||||
MQTT_SERVER=`cat /etc/inverter/mqtt.json | jq '.server' -r`
|
||||
MQTT_PORT=`cat /etc/inverter/mqtt.json | jq '.port' -r`
|
||||
MQTT_TOPIC=`cat /etc/inverter/mqtt.json | jq '.topic' -r`
|
||||
MQTT_DEVICENAME=`cat /etc/inverter/mqtt.json | jq '.devicename' -r`
|
||||
|
||||
registerTopic () {
|
||||
mosquitto_pub \
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
pushMQTTData () {
|
||||
|
||||
MQTT_SERVER=`cat /etc/skymax/mqtt.json | jq '.server' -r`
|
||||
MQTT_PORT=`cat /etc/skymax/mqtt.json | jq '.port' -r`
|
||||
MQTT_TOPIC=`cat /etc/skymax/mqtt.json | jq '.topic' -r`
|
||||
MQTT_DEVICENAME=`cat /etc/skymax/mqtt.json | jq '.devicename' -r`
|
||||
MQTT_SERVER=`cat /etc/inverter/mqtt.json | jq '.server' -r`
|
||||
MQTT_PORT=`cat /etc/inverter/mqtt.json | jq '.port' -r`
|
||||
MQTT_TOPIC=`cat /etc/inverter/mqtt.json | jq '.topic' -r`
|
||||
MQTT_DEVICENAME=`cat /etc/inverter/mqtt.json | jq '.devicename' -r`
|
||||
|
||||
mosquitto_pub \
|
||||
-h $MQTT_SERVER \
|
||||
@@ -14,7 +14,7 @@ pushMQTTData () {
|
||||
-m "$2"
|
||||
}
|
||||
|
||||
INVERTER_DATA=`timeout 10 /opt/voltronic-cli/bin/skymax`
|
||||
INVERTER_DATA=`timeout 10 /opt/voltronic-cli/bin/inverter_poller`
|
||||
|
||||
#####################################################################################
|
||||
|
||||
@@ -120,4 +120,6 @@ Charger_source_priority=`echo $INVERTER_DATA | jq '.Charger_source_priority' -r`
|
||||
Battery_redischarge_voltage=`echo $INVERTER_DATA | jq '.Battery_redischarge_voltage' -r`
|
||||
[ ! -z "$Battery_redischarge_voltage" ] && pushMQTTData "Battery_redischarge_voltage" "$Battery_redischarge_voltage"
|
||||
|
||||
Warnings=`echo $INVERTER_DATA | jq '.Warnings' -r`
|
||||
[ ! -z "$Warnings" ] && pushMQTTData "Warnings" "$Warnings"
|
||||
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
MQTT_SERVER=`cat /etc/inverter/mqtt.json | jq '.server' -r`
|
||||
MQTT_PORT=`cat /etc/inverter/mqtt.json | jq '.port' -r`
|
||||
MQTT_TOPIC=`cat /etc/inverter/mqtt.json | jq '.topic' -r`
|
||||
MQTT_DEVICENAME=`cat /etc/inverter/mqtt.json | jq '.devicename' -r`
|
||||
|
||||
while read rawcmd;
|
||||
do
|
||||
|
||||
echo "Incoming request send: [$rawcmd] to inverter."
|
||||
/opt/voltronic-cli/bin/inverter_poller -r $rawcmd;
|
||||
|
||||
done < <(mosquitto_sub -h $MQTT_SERVER -p $MQTT_PORT -t "$MQTT_TOPIC/sensor/$MQTT_DEVICENAME" -q 1)
|
||||
@@ -1,54 +0,0 @@
|
||||
--------------------------------------------------------------------------------------
|
||||
skymax
|
||||
--------------------------------------------------------------------------------------
|
||||
This project was forked from manio/skymax-demo on git since it allows me to query my
|
||||
own Axpert solar inverter. A company in Taiwan called Voltronic sells a hardware plat-
|
||||
form used in many different brands of inverter. My brand just happens to be one of
|
||||
those Voltronic clones so all the same commands apply.
|
||||
|
||||
--------------------------------------------------------------------------------------
|
||||
compilation / running
|
||||
--------------------------------------------------------------------------------------
|
||||
(You will need cmake, make and gcc already installed. Use apt-get to get them)
|
||||
|
||||
Sample build/compilation procedure:
|
||||
$ git clone https://github.com/nrm21/skymax-demo
|
||||
$ cd skymax-demo
|
||||
$ mkdir out
|
||||
$ cd out
|
||||
$ cmake ..
|
||||
$ make
|
||||
|
||||
Then you need to get the real hidraw device name (probably hidraw0):
|
||||
$ dmesg | grep hidraw
|
||||
|
||||
and then run the program like this (assuming it is located in '/opt/skymax/out' dir):
|
||||
$ /opt/skymax/out/skymax
|
||||
|
||||
The program will run once, it will spit out values it receives from the inverter in
|
||||
JSON format and exits. It was intended to be run by telegraf using the exec plugin
|
||||
every 15 seconds, and that output data will be imported into an InfluxDB instance
|
||||
where it can be easily graphed by grafana (or some other "TIG or TICK stack"-like
|
||||
software suite).
|
||||
|
||||
The program can also be made to send raw commands to the inverter if you choose:
|
||||
$ /opt/skymax/out/skymax -r POP00 # set utility output mode
|
||||
$ /opt/skymax/out/skymax -r POP02 # set SBU output mode
|
||||
$ /opt/skymax/out/skymax -r PCP00 # set utility charging mode
|
||||
$ /opt/skymax/out/skymax -r PCP03 # set solar only charging mode
|
||||
etc...
|
||||
|
||||
These commands can of course be scripted and put into a cron job to run them at a
|
||||
certain time of day/week as well (say, every morning at 8am switch to solar charging
|
||||
mode only and SBU output mode).
|
||||
|
||||
See this protocol manual for more commands that can be sent:
|
||||
http://forums.aeva.asn.au/uploads/293/HS_MS_MSX_RS232_Protocol_20140822_after_current_upgrade.pdf
|
||||
|
||||
--------------------------------------------------------------------------------------
|
||||
GNU License
|
||||
--------------------------------------------------------------------------------------
|
||||
This program 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 (at your option) any later version. See the file
|
||||
COPYING for more information.
|
||||
@@ -1,285 +0,0 @@
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include "main.h"
|
||||
#include "inputparser.h"
|
||||
#include "tools.h"
|
||||
|
||||
bool debugFlag = false;
|
||||
cSkymax *ups = NULL;
|
||||
atomic_bool ups_status_changed(false);
|
||||
atomic_bool ups_qmod_changed(false);
|
||||
atomic_bool ups_qpiri_changed(false);
|
||||
atomic_bool ups_qpigs_changed(false);
|
||||
atomic_bool ups_cmd_executed(false);
|
||||
|
||||
// ---------------------------------------
|
||||
// Global configs read from 'skymax.conf'
|
||||
|
||||
string devicename;
|
||||
int runinterval;
|
||||
float ampfactor;
|
||||
float wattfactor;
|
||||
|
||||
// ---------------------------------------
|
||||
|
||||
void attemptAddSetting(int *addTo, string addFrom)
|
||||
{
|
||||
try
|
||||
{
|
||||
*addTo = stof(addFrom);
|
||||
}
|
||||
catch (exception e)
|
||||
{
|
||||
cout << e.what() << '\n';
|
||||
cout << "There's probably a string in the settings file where an int should be.\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void attemptAddSetting(float *addTo, string addFrom)
|
||||
{
|
||||
try
|
||||
{
|
||||
*addTo = stof(addFrom);
|
||||
}
|
||||
catch (exception e)
|
||||
{
|
||||
cout << e.what() << '\n';
|
||||
cout << "There's probably a string in the settings file where a floating point should be.\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void getSettingsFile(string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
string fileline, linepart1, linepart2;
|
||||
ifstream infile;
|
||||
infile.open(filename);
|
||||
while(!infile.eof())
|
||||
{
|
||||
getline(infile, fileline);
|
||||
size_t firstpos = fileline.find("#");
|
||||
if(firstpos != 0 && fileline.length() != 0) // Ignore lines starting with # (comment lines)
|
||||
{
|
||||
size_t delimiter = fileline.find("=");
|
||||
linepart1 = fileline.substr(0, delimiter);
|
||||
linepart2 = fileline.substr(delimiter+1, string::npos - delimiter);
|
||||
|
||||
if(linepart1 == "device")
|
||||
devicename = linepart2;
|
||||
else if(linepart1 == "run_interval")
|
||||
attemptAddSetting(&runinterval, linepart2);
|
||||
else if(linepart1 == "amperage_factor")
|
||||
attemptAddSetting(&factor, linepart2);
|
||||
else if(linepart1 == "watt_factor")
|
||||
attemptAddSetting(&wattfactor, linepart2);
|
||||
else if(linepart1 == "watt_factor")
|
||||
attemptAddSetting(&wattfactor, linepart2);
|
||||
|
||||
else
|
||||
continue;
|
||||
}
|
||||
}
|
||||
infile.close();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
cout << "Settings could not be read properly...\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Reply1
|
||||
float voltage_grid;
|
||||
float freq_grid;
|
||||
float voltage_out;
|
||||
float freq_out;
|
||||
int load_va;
|
||||
int load_watt;
|
||||
int load_percent;
|
||||
int voltage_bus;
|
||||
float voltage_batt;
|
||||
int batt_charge_current;
|
||||
int batt_capacity;
|
||||
int temp_heatsink;
|
||||
float pv_input_current;
|
||||
float pv_input_voltage;
|
||||
float pv_input_watts;
|
||||
float pv_input_watthour;
|
||||
float load_watthour = 0;
|
||||
float scc_voltage;
|
||||
int batt_discharge_current;
|
||||
char device_status[9];
|
||||
|
||||
// Reply2
|
||||
float grid_voltage_rating;
|
||||
float grid_current_rating;
|
||||
float out_voltage_rating;
|
||||
float out_freq_rating;
|
||||
float out_current_rating;
|
||||
int out_va_rating;
|
||||
int out_watt_rating;
|
||||
float batt_rating;
|
||||
float batt_recharge_voltage;
|
||||
float batt_under_voltage;
|
||||
float batt_bulk_voltage;
|
||||
float batt_float_voltage;
|
||||
int batt_type;
|
||||
int max_grid_charge_current;
|
||||
int max_charge_current;
|
||||
int in_voltage_range;
|
||||
int out_source_priority;
|
||||
int charger_source_priority;
|
||||
int machine_type;
|
||||
int topology;
|
||||
int out_mode;
|
||||
float batt_redischarge_voltage;
|
||||
|
||||
// Get command flag settings from the arguments (if any)
|
||||
InputParser cmdArgs(argc, argv);
|
||||
const string &rawcmd = cmdArgs.getCmdOption("-r");
|
||||
if(cmdArgs.cmdOptionExists("-h") || cmdArgs.cmdOptionExists("--help"))
|
||||
{
|
||||
return print_help();
|
||||
}
|
||||
if(cmdArgs.cmdOptionExists("-d"))
|
||||
{
|
||||
debugFlag = true;
|
||||
}
|
||||
lprintf("SKYMAX: Debug set");
|
||||
|
||||
// Get the rest of the settings from the conf file
|
||||
if( access( "./skymax.conf", F_OK ) != -1 ) { // file exists
|
||||
getSettingsFile("./skymax.conf");
|
||||
} else { // file doesn't exist
|
||||
getSettingsFile("/etc/skymax/skymax.conf");
|
||||
}
|
||||
|
||||
bool ups_status_changed(false);
|
||||
ups = new cSkymax(devicename);
|
||||
|
||||
if (!rawcmd.empty())
|
||||
{
|
||||
ups->ExecuteCmd(rawcmd);
|
||||
// We can piggyback on either GetStatus() function to return our result, it doesn't matter which
|
||||
printf("Reply: %s\n", ups->GetQpiriStatus()->c_str());
|
||||
}
|
||||
else // No command being sent so just run normally
|
||||
{
|
||||
ups->runMultiThread();
|
||||
|
||||
while (true)
|
||||
{
|
||||
lprintf("SKYMAX: Start loop");
|
||||
// If inverter mode changes print it to screen
|
||||
if (ups_status_changed)
|
||||
{
|
||||
int mode = ups->GetMode();
|
||||
if (mode)
|
||||
lprintf("SKYMAX: %d", mode);
|
||||
ups_status_changed = false;
|
||||
}
|
||||
|
||||
// Once we receive all queries print it to screen
|
||||
if (ups_qmod_changed && ups_qpiri_changed && ups_qpigs_changed)
|
||||
{
|
||||
ups_qmod_changed = false;
|
||||
ups_qpiri_changed = false;
|
||||
ups_qpigs_changed = false;
|
||||
|
||||
int mode = ups->GetMode();
|
||||
string *reply1 = ups->GetQpigsStatus();
|
||||
string *reply2 = ups->GetQpiriStatus();
|
||||
if (reply1 && reply2)
|
||||
{
|
||||
// Parse and display values
|
||||
sscanf(reply1->c_str(), "%f %f %f %f %d %d %d %d %f %d %d %d %f %f %f %d %s", &voltage_grid, &freq_grid, &voltage_out, &freq_out, &load_va, &load_watt, &load_percent, &voltage_bus, &voltage_batt, &batt_charge_current, &batt_capacity, &temp_heatsink, &pv_input_current, &pv_input_voltage, &scc_voltage, &batt_discharge_current, &device_status);
|
||||
sscanf(reply2->c_str(), "%f %f %f %f %f %d %d %f %f %f %f %f %d %d %d %d %d %d - %d %d %d %f", &grid_voltage_rating, &grid_current_rating, &out_voltage_rating, &out_freq_rating, &out_current_rating, &out_va_rating, &out_watt_rating, &batt_rating, &batt_recharge_voltage, &batt_under_voltage, &batt_bulk_voltage, &batt_float_voltage, &batt_type, &max_grid_charge_current, &max_charge_current, &in_voltage_range, &out_source_priority, &charger_source_priority, &machine_type, &topology, &out_mode, &batt_redischarge_voltage);
|
||||
|
||||
// There appears to be a discrepancy in actual DMM measured current vs what the meter is
|
||||
// telling me it's getting, so lets add a variable we can multiply/divide by to adjust if
|
||||
// needed. This should be set in the config so it can be changed without program recompile.
|
||||
if (debugFlag) {
|
||||
printf("SKYMAX: ampfactor from config is %.2f\n", ampfactor);
|
||||
printf("SKYMAX: wattfactor from config is %.2f\n", wattfactor);
|
||||
}
|
||||
pv_input_current = pv_input_current * ampfactor;
|
||||
// It appears on further inspection of the documentation, that the input current is actually
|
||||
// current that is going out to the battery at battery voltage (NOT at PV voltage). This
|
||||
// would explain the larger discrepancy we saw before.
|
||||
pv_input_watts = (scc_voltage * pv_input_current) * wattfactor;
|
||||
|
||||
// Calculate watt-hours generated per run interval period (given as program argument)
|
||||
pv_input_watthour = pv_input_watts / (3600 / runinterval);
|
||||
|
||||
// Only calculate load watt-hours if we are in battery mode (line mode doesn't count towards money savings)
|
||||
if (mode == 4)
|
||||
load_watthour = (float)load_watt / (3600 / runinterval);
|
||||
|
||||
// Print as JSON (output is expected to be use by telegraf to send to influxdb)
|
||||
printf("{\n");
|
||||
printf("\"Inverter_mode\":%d,\n", mode);
|
||||
printf("\"AC_grid_voltage\":%.1f,\n", voltage_grid);
|
||||
printf("\"AC_grid_frequency\":%.1f,\n", freq_grid);
|
||||
printf("\"AC_out_voltage\":%.1f,\n", voltage_out);
|
||||
printf("\"AC_out_frequency\":%.1f,\n", freq_out);
|
||||
printf("\"PV_in_voltage\":%.1f,\n", pv_input_voltage);
|
||||
printf("\"PV_in_current\":%.1f,\n", pv_input_current);
|
||||
printf("\"PV_in_watts\":%.1f,\n", pv_input_watts);
|
||||
printf("\"PV_in_watthour\":%.4f,\n", pv_input_watthour);
|
||||
printf("\"SCC_voltage\":%.4f,\n", scc_voltage);
|
||||
printf("\"Load_pct\":%d,\n", load_percent);
|
||||
printf("\"Load_watt\":%d,\n", load_watt);
|
||||
printf("\"Load_watthour\":%.4f,\n", load_watthour);
|
||||
printf("\"Load_va\":%d,\n", load_va);
|
||||
printf("\"Bus_voltage\":%d,\n", voltage_bus);
|
||||
printf("\"Heatsink_temperature\":%d,\n", temp_heatsink);
|
||||
printf("\"Battery_capacity\":%d,\n", batt_capacity);
|
||||
printf("\"Battery_voltage\":%.2f,\n", voltage_batt);
|
||||
printf("\"Battery_charge_current\":%d,\n", batt_charge_current);
|
||||
printf("\"Battery_discharge_current\":%d,\n", batt_discharge_current);
|
||||
printf("\"Load_status_on\":%c,\n", device_status[3]);
|
||||
printf("\"SCC_charge_on\":%c,\n", device_status[6]);
|
||||
printf("\"AC_charge_on\":%c,\n", device_status[7]);
|
||||
printf("\"Battery_recharge_voltage\":%.1f,\n", batt_recharge_voltage);
|
||||
printf("\"Battery_under_voltage\":%.1f,\n", batt_under_voltage);
|
||||
printf("\"Battery_bulk_voltage\":%.1f,\n", batt_bulk_voltage);
|
||||
printf("\"Battery_float_voltage\":%.1f,\n", batt_float_voltage);
|
||||
printf("\"Max_grid_charge_current\":%d,\n", max_grid_charge_current);
|
||||
printf("\"Max_charge_current\":%d,\n", max_charge_current);
|
||||
printf("\"Out_source_priority\":%d,\n", out_source_priority);
|
||||
printf("\"Charger_source_priority\":%d,\n", charger_source_priority);
|
||||
printf("\"Battery_redischarge_voltage\":%.1f\n", batt_redischarge_voltage);
|
||||
printf("}\n");
|
||||
|
||||
delete reply1;
|
||||
delete reply2;
|
||||
|
||||
// Do once and exit instead of loop endlessly
|
||||
lprintf("SKYMAX: All queries complete, exiting using goto");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (ups)
|
||||
delete ups;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include "skymax.h"
|
||||
#include "tools.h"
|
||||
#include "main.h"
|
||||
|
||||
cSkymax::cSkymax(std::string devicename)
|
||||
{
|
||||
device = devicename;
|
||||
status1[0] = 0;
|
||||
status2[0] = 0;
|
||||
mode = 0;
|
||||
}
|
||||
|
||||
string *cSkymax::GetQpigsStatus()
|
||||
{
|
||||
m.lock();
|
||||
string *result = new string(status1);
|
||||
m.unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
string *cSkymax::GetQpiriStatus()
|
||||
{
|
||||
m.lock();
|
||||
string *result = new string(status2);
|
||||
m.unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
void cSkymax::SetMode(char newmode)
|
||||
{
|
||||
m.lock();
|
||||
if (mode && newmode != mode)
|
||||
ups_status_changed = true;
|
||||
mode = newmode;
|
||||
m.unlock();
|
||||
}
|
||||
|
||||
int cSkymax::GetMode()
|
||||
{
|
||||
int result;
|
||||
m.lock();
|
||||
switch (mode)
|
||||
{
|
||||
case 'P': result = 1; break; // Power_On
|
||||
case 'S': result = 2; break; // Standby
|
||||
case 'L': result = 3; break; // Line
|
||||
case 'B': result = 4; break; // Battery
|
||||
case 'F': result = 5; break; // Fault
|
||||
case 'H': result = 6; break; // Power_Saving
|
||||
default: result = 0; break; // Unknown
|
||||
}
|
||||
m.unlock();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool cSkymax::query(const char *cmd)
|
||||
{
|
||||
time_t started;
|
||||
int fd;
|
||||
int i = 0, n;
|
||||
|
||||
fd = open(this->device.data(), O_RDWR | O_NONBLOCK); // device is provided by program arg (usually /dev/hidraw0)
|
||||
if (fd == -1)
|
||||
{
|
||||
lprintf("Skymax: Unable to open device file (errno=%d %s)", errno, strerror(errno));
|
||||
sleep(10);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generating CRC for a command
|
||||
uint16_t crc = cal_crc_half((uint8_t*)cmd, strlen(cmd));
|
||||
n = strlen(cmd);
|
||||
memcpy(&buf, cmd, n);
|
||||
lprintf("SKYMAX: Current CRC: %X %X", crc >> 8, crc & 0xff);
|
||||
buf[n++] = crc >> 8;
|
||||
buf[n++] = crc & 0xff;
|
||||
buf[n++] = 0x0d;
|
||||
|
||||
// Send a command
|
||||
write(fd, &buf, n);
|
||||
time(&started);
|
||||
|
||||
// Instead of using a fixed size for expected response length, lets find it
|
||||
// by searching for the first returned <cr> char instead.
|
||||
char *startbuf = 0;
|
||||
char *endbuf = 0;
|
||||
do
|
||||
{
|
||||
// According to protocol manual, it appears no query should ever exceed 150 byte size in response
|
||||
n = read(fd, (void*)buf+i, 120 - i);
|
||||
if (n < 0)
|
||||
{
|
||||
if (time(NULL) - started > 8) // Wait 8 secs before timeout
|
||||
{
|
||||
lprintf("SKYMAX: %s read timeout", cmd);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
usleep(10);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
i += n;
|
||||
|
||||
startbuf = (char *)&buf[0];
|
||||
endbuf = strchr(startbuf, '\r');
|
||||
//lprintf("SKYMAX: %s Current buffer: %s", cmd, startbuf);
|
||||
} while (endbuf == NULL); // Still haven't found end <cr> char as long as pointer is null
|
||||
close(fd);
|
||||
|
||||
int replysize = endbuf - startbuf + 1;
|
||||
lprintf("SKYMAX: Found <cr> at byte: %d", replysize);
|
||||
|
||||
if (buf[0]!='(' || buf[replysize-1]!=0x0d)
|
||||
{
|
||||
lprintf("SKYMAX: %s: incorrect start/stop bytes. Buffer: %s", cmd, buf);
|
||||
return false;
|
||||
}
|
||||
if (!(CheckCRC(buf, replysize)))
|
||||
{
|
||||
lprintf("SKYMAX: %s: CRC Failed! Reply size: %d Buffer: %s", cmd, replysize, buf);
|
||||
return false;
|
||||
}
|
||||
buf[replysize-3] = '\0'; // Null-terminating on first CRC byte
|
||||
lprintf("SKYMAX: %s: %d bytes read: %s", cmd, i, buf);
|
||||
|
||||
lprintf("SKYMAX: %s query finished", cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
void cSkymax::poll()
|
||||
{
|
||||
int n,j;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Reading mode
|
||||
if (!ups_qmod_changed)
|
||||
{
|
||||
if (query("QMOD"))
|
||||
{
|
||||
SetMode(buf[1]);
|
||||
ups_qmod_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Reading QPIGS status
|
||||
if (!ups_qpigs_changed)
|
||||
{
|
||||
if (query("QPIGS"))
|
||||
{
|
||||
m.lock();
|
||||
strcpy(status1, (const char*)buf+1);
|
||||
m.unlock();
|
||||
ups_qpigs_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Reading QPIRI status
|
||||
if (!ups_qpiri_changed)
|
||||
{
|
||||
if (query("QPIRI"))
|
||||
{
|
||||
m.lock();
|
||||
strcpy(status2, (const char*)buf+1);
|
||||
m.unlock();
|
||||
ups_qpiri_changed = true;
|
||||
}
|
||||
}
|
||||
sleep(5);
|
||||
}
|
||||
}
|
||||
|
||||
void cSkymax::ExecuteCmd(const string cmd)
|
||||
{
|
||||
// Sending any command raw
|
||||
if (query(cmd.data()))
|
||||
{
|
||||
m.lock();
|
||||
strcpy(status2, (const char*)buf+1);
|
||||
m.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t cSkymax::cal_crc_half(uint8_t *pin, uint8_t len)
|
||||
{
|
||||
uint16_t crc;
|
||||
|
||||
uint8_t da;
|
||||
uint8_t *ptr;
|
||||
uint8_t bCRCHign;
|
||||
uint8_t bCRCLow;
|
||||
|
||||
uint16_t crc_ta[16]=
|
||||
{
|
||||
0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
|
||||
0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef
|
||||
};
|
||||
ptr=pin;
|
||||
crc=0;
|
||||
|
||||
while(len--!=0)
|
||||
{
|
||||
da=((uint8_t)(crc>>8))>>4;
|
||||
crc<<=4;
|
||||
crc^=crc_ta[da^(*ptr>>4)];
|
||||
da=((uint8_t)(crc>>8))>>4;
|
||||
crc<<=4;
|
||||
crc^=crc_ta[da^(*ptr&0x0f)];
|
||||
ptr++;
|
||||
}
|
||||
bCRCLow = crc;
|
||||
bCRCHign= (uint8_t)(crc>>8);
|
||||
if(bCRCLow==0x28||bCRCLow==0x0d||bCRCLow==0x0a)
|
||||
bCRCLow++;
|
||||
if(bCRCHign==0x28||bCRCHign==0x0d||bCRCHign==0x0a)
|
||||
bCRCHign++;
|
||||
crc = ((uint16_t)bCRCHign)<<8;
|
||||
crc += bCRCLow;
|
||||
return(crc);
|
||||
}
|
||||
|
||||
bool cSkymax::CheckCRC(unsigned char *data, int len)
|
||||
{
|
||||
uint16_t crc = cal_crc_half(data, len-3);
|
||||
return data[len-3]==(crc>>8) && data[len-2]==(crc&0xff);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#ifndef ___SKYMAX_H
|
||||
#define ___SKYMAX_H
|
||||
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
|
||||
using namespace std;
|
||||
|
||||
class cSkymax
|
||||
{
|
||||
unsigned char buf[1024]; //internal work buffer
|
||||
char status1[1024];
|
||||
char status2[1024];
|
||||
char mode;
|
||||
std::string device;
|
||||
std::mutex m;
|
||||
void SetMode(char newmode);
|
||||
bool CheckCRC(unsigned char *buff, int len);
|
||||
bool query(const char *cmd);
|
||||
uint16_t cal_crc_half(uint8_t *pin, uint8_t len);
|
||||
|
||||
public:
|
||||
cSkymax(std::string devicename);
|
||||
void poll();
|
||||
void runMultiThread()
|
||||
{
|
||||
std::thread t1(&cSkymax::poll, this);
|
||||
t1.detach();
|
||||
}
|
||||
string *GetQpiriStatus();
|
||||
string *GetQpigsStatus();
|
||||
int GetMode();
|
||||
void ExecuteCmd(const std::string cmd);
|
||||
};
|
||||
|
||||
#endif // ___SKYMAX_H
|
||||
@@ -1,71 +0,0 @@
|
||||
#include <mutex>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <sys/time.h>
|
||||
#include <string>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include "main.h"
|
||||
#include "tools.h"
|
||||
|
||||
std::mutex log_mutex;
|
||||
|
||||
void lprintf(const char *format, ...)
|
||||
{
|
||||
// Only print if debug flag is set, else do nothing
|
||||
if (debugFlag) {
|
||||
va_list ap;
|
||||
char fmt[2048];
|
||||
|
||||
//actual time
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
time(&rawtime);
|
||||
timeinfo = localtime(&rawtime);
|
||||
char buf[256];
|
||||
strcpy(buf, asctime(timeinfo));
|
||||
buf[strlen(buf)-1] = 0;
|
||||
|
||||
//connect with args
|
||||
snprintf(fmt, sizeof(fmt), "%s %s\n", buf, format);
|
||||
|
||||
//put on screen:
|
||||
va_start(ap, format);
|
||||
vprintf(fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
//to the logfile:
|
||||
static FILE *log;
|
||||
log_mutex.lock();
|
||||
log = fopen(LOG_FILE, "a");
|
||||
va_start(ap, format);
|
||||
vfprintf(log, fmt, ap);
|
||||
va_end(ap);
|
||||
fclose(log);
|
||||
log_mutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int print_help()
|
||||
{
|
||||
printf("USAGE: skymax [-r <raw command>] | [-h | --help]\n\n");
|
||||
printf("RAW COMMAND EXAMPLES (see protocol manual for complete list):\n");
|
||||
printf("Set output source priority POP00 (Utility first)\n");
|
||||
printf(" POP01 (Solar first)\n");
|
||||
printf(" POP02 (SBU)\n");
|
||||
printf("Set charger priority PCP00 (Utility first)\n");
|
||||
printf(" PCP01 (Solar first)\n");
|
||||
printf(" PCP02 (Solar and utility)\n");
|
||||
printf(" PCP03 (Solar only)\n");
|
||||
printf("Set other commands PEa / PDa (Enable/disable buzzer)\n");
|
||||
printf(" PEb / PDb (Enable/disable overload bypass)\n");
|
||||
printf(" PEj / PDj (Enable/disable power saving)\n");
|
||||
printf(" PEu / PDu (Enable/disable overload restart)\n");
|
||||
printf(" PEx / PDx (Enable/disable backlight)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
MQTT_SERVER=`cat /etc/skymax/mqtt.json | jq '.server' -r`
|
||||
MQTT_PORT=`cat /etc/skymax/mqtt.json | jq '.port' -r`
|
||||
MQTT_TOPIC=`cat /etc/skymax/mqtt.json | jq '.topic' -r`
|
||||
MQTT_DEVICENAME=`cat /etc/skymax/mqtt.json | jq '.devicename' -r`
|
||||
|
||||
while read rawcmd;
|
||||
do
|
||||
|
||||
echo "Incoming request send: [$rawcmd] to inverter."
|
||||
/opt/voltronic-cli/bin/skymax -r $rawcmd;
|
||||
|
||||
done < <(mosquitto_sub -h $MQTT_SERVER -p $MQTT_PORT -t "$MQTT_TOPIC/sensor/$MQTT_DEVICENAME" -q 1)
|
||||
Reference in New Issue
Block a user