refactor for better support with ch341 and other unreliable rs232 adapters

This commit is contained in:
David
2019-07-27 22:34:34 +10:00
parent c9ba895410
commit ccb999b8dc
28 changed files with 800 additions and 774 deletions
Vendored
BIN
View File
Binary file not shown.
+3 -3
View File
@@ -9,10 +9,10 @@ RUN apt update && apt install -y \
mosquitto-clients mosquitto-clients
ADD sources/ /opt/ 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 mkdir bin && cmake . && make
WORKDIR /opt WORKDIR /opt
ENTRYPOINT ["/bin/bash", "/opt/voltronic-mqtt/entrypoint.sh"] ENTRYPOINT ["/bin/bash", "/opt/inverter-mqtt/entrypoint.sh"]
+24 -5
View File
@@ -21,7 +21,7 @@ _Example: My "Lovelace" dashboard using data collected from the Inverter._
- Docker - Docker
- Docker-compose - 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/) - 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 ```bash
# Clone down sources on the host you want to monitor... # 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 git clone https://github.com/ned-kelly/docker-voltronic-homeassistant.git /opt/ha-inverter-mqtt-agent
cd /opt/ha-voltronic-mqtt cd /opt/ha-inverter-mqtt-agent
# Configure the 'device=' directive (in skymax.conf) to suit for RS232 or USB..  # Configure the 'device=' directive (in inverter.conf) to suit for RS232 or USB.. 
vi config/skymax.conf 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. # 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 vi config/mqtt.json
@@ -88,6 +88,25 @@ Set other commands PEa / PDa (Enable/disable buzzer)
PEx / PDx (Enable/disable backlight) 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 ### Bonus: Lovelace Dashboard Files
_**Please refer to the screenshot above for an example of the dashboard.**_ _**Please refer to the screenshot above for an example of the dashboard.**_
+7 -4
View File
@@ -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... # The device to read from...
# Use: /dev/ttyS0 if you have a serial device or /dev/hidraw0 if you're connecting via USB. # Use: /dev/ttyS0 if you have a serial device,
device=/dev/ttyS0 # /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... # How many times per hour is the program going to run...
# This is used to calculate the PV & Load Watt Hours between runs... # This is used to calculate the PV & Load Watt Hours between runs...
@@ -11,7 +14,7 @@ device=/dev/ttyS0
# (120 = every 30 seconds)... # (120 = every 30 seconds)...
run_interval=120 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' # reading compared to measurement tools. Normally this will remain '1'
amperage_factor=1.0 amperage_factor=1.0
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"server": "10.16.10.5", "server": "10.16.10.4",
"port": "1883", "port": "1883",
"topic": "homeassistant", "topic": "homeassistant",
"devicename": "voltronic" "devicename": "voltronic"
+3 -3
View File
@@ -3,8 +3,8 @@ version: '2'
services: services:
voltronic-mqtt: voltronic-mqtt:
#build: . build: .
image: bushrangers/ha-voltronic-mqtt #image: bushrangers/ha-voltronic-mqtt
container_name: voltronic-mqtt container_name: voltronic-mqtt
@@ -12,7 +12,7 @@ services:
restart: always restart: always
volumes: volumes:
- ./config/:/etc/skymax/ - ./config/:/etc/inverter/
devices: devices:
# - "/dev/mem:/dev/mem" # - "/dev/mem:/dev/mem"
BIN
View File
Binary file not shown.
@@ -1,8 +1,8 @@
CMAKE_MINIMUM_REQUIRED(VERSION 2.6) CMAKE_MINIMUM_REQUIRED(VERSION 2.6)
PROJECT("skymax") PROJECT("inverter_poller")
set (CMAKE_CXX_FLAGS "-O2 --std=c++0x ${CMAKE_CXX_FLAGS}") set (CMAKE_CXX_FLAGS "-O2 --std=c++0x ${CMAKE_CXX_FLAGS}")
file(GLOB SOURCES *.cpp) file(GLOB SOURCES *.cpp)
ADD_EXECUTABLE(bin/skymax ${SOURCES}) ADD_EXECUTABLE(inverter_poller ${SOURCES})
target_link_libraries(bin/skymax -lpthread) target_link_libraries(inverter_poller -lpthread)
@@ -9,13 +9,12 @@
// It is not posix compliant and wont work with args like: ./program -xf filename // 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 // 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) for (int i=1; i < argc; ++i)
this->tokens.push_back(std::string(argv[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; std::vector<std::string>::const_iterator itr;
itr = std::find(this->tokens.begin(), this->tokens.end(), option); itr = std::find(this->tokens.begin(), this->tokens.end(), option);
if (itr != this->tokens.end() && ++itr != this->tokens.end()) 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(""); static const std::string empty_string("");
return 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) return std::find(this->tokens.begin(), this->tokens.end(), option)
!= this->tokens.end(); != this->tokens.end();
} }
@@ -5,8 +5,7 @@
#include <vector> #include <vector>
class InputParser class InputParser {
{
std::vector <std::string> tokens; std::vector <std::string> tokens;
public: public:
+23
View File
@@ -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
+252
View File
@@ -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);
}
+41
View File
@@ -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
+289
View File
@@ -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(&ampfactor, 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 #define ___MAIN_H
#include <atomic> #include <atomic>
#include "skymax.h" #include "inverter.h"
extern bool debugFlag; extern bool debugFlag;
extern atomic_bool ups_data_changed;
extern atomic_bool ups_status_changed; extern atomic_bool ups_status_changed;
extern atomic_bool ups_qpiws_changed;
extern atomic_bool ups_qmod_changed; extern atomic_bool ups_qmod_changed;
extern atomic_bool ups_qpiri_changed; extern atomic_bool ups_qpiri_changed;
extern atomic_bool ups_qpigs_changed; extern atomic_bool ups_qpigs_changed;
+74
View File
@@ -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 # 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... # 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) # 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... # 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... # 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_SERVER=`cat /etc/inverter/mqtt.json | jq '.server' -r`
MQTT_PORT=`cat /etc/skymax/mqtt.json | jq '.port' -r` MQTT_PORT=`cat /etc/inverter/mqtt.json | jq '.port' -r`
MQTT_TOPIC=`cat /etc/skymax/mqtt.json | jq '.topic' -r` MQTT_TOPIC=`cat /etc/inverter/mqtt.json | jq '.topic' -r`
MQTT_DEVICENAME=`cat /etc/skymax/mqtt.json | jq '.devicename' -r` MQTT_DEVICENAME=`cat /etc/inverter/mqtt.json | jq '.devicename' -r`
registerTopic () { registerTopic () {
mosquitto_pub \ mosquitto_pub \
@@ -2,10 +2,10 @@
pushMQTTData () { pushMQTTData () {
MQTT_SERVER=`cat /etc/skymax/mqtt.json | jq '.server' -r` MQTT_SERVER=`cat /etc/inverter/mqtt.json | jq '.server' -r`
MQTT_PORT=`cat /etc/skymax/mqtt.json | jq '.port' -r` MQTT_PORT=`cat /etc/inverter/mqtt.json | jq '.port' -r`
MQTT_TOPIC=`cat /etc/skymax/mqtt.json | jq '.topic' -r` MQTT_TOPIC=`cat /etc/inverter/mqtt.json | jq '.topic' -r`
MQTT_DEVICENAME=`cat /etc/skymax/mqtt.json | jq '.devicename' -r` MQTT_DEVICENAME=`cat /etc/inverter/mqtt.json | jq '.devicename' -r`
mosquitto_pub \ mosquitto_pub \
-h $MQTT_SERVER \ -h $MQTT_SERVER \
@@ -14,7 +14,7 @@ pushMQTTData () {
-m "$2" -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` Battery_redischarge_voltage=`echo $INVERTER_DATA | jq '.Battery_redischarge_voltage' -r`
[ ! -z "$Battery_redischarge_voltage" ] && pushMQTTData "Battery_redischarge_voltage" "$Battery_redischarge_voltage" [ ! -z "$Battery_redischarge_voltage" ] && pushMQTTData "Battery_redischarge_voltage" "$Battery_redischarge_voltage"
Warnings=`echo $INVERTER_DATA | jq '.Warnings' -r`
[ ! -z "$Warnings" ] && pushMQTTData "Warnings" "$Warnings"
+14
View File
@@ -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)
-54
View File
@@ -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.
-285
View File
@@ -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(&ampfactor, 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;
}
-232
View File
@@ -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);
}
-36
View File
@@ -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
-71
View File
@@ -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;
}
-14
View File
@@ -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)