From ccb999b8dc4ca860c673af2a4da5aa3c47b915a0 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 27 Jul 2019 22:34:34 +1000 Subject: [PATCH] refactor for better support with ch341 and other unreliable rs232 adapters --- .DS_Store | Bin 14340 -> 14340 bytes Dockerfile | 6 +- README.md | 29 +- config/{skymax.conf => inverter.conf} | 11 +- config/mqtt.json | 2 +- docker-compose.yml | 6 +- sources/.DS_Store | Bin 6148 -> 8196 bytes .../CMakeLists.txt | 6 +- .../{voltronic-cli => inverter-cli}/COPYING | 0 .../inputparser.cpp | 65 ++-- .../inputparser.h | 33 +- sources/inverter-cli/inverter.conf | 23 ++ sources/inverter-cli/inverter.cpp | 252 +++++++++++++++ sources/inverter-cli/inverter.h | 41 +++ sources/inverter-cli/main.cpp | 289 ++++++++++++++++++ .../{voltronic-cli => inverter-cli}/main.h | 5 +- sources/inverter-cli/tools.cpp | 74 +++++ .../{voltronic-cli => inverter-cli}/tools.h | 0 .../entrypoint.sh | 6 +- .../mqtt-init.sh | 8 +- .../mqtt-push.sh | 12 +- sources/inverter-mqtt/mqtt-subscriber.sh | 14 + sources/voltronic-cli/README | 54 ---- sources/voltronic-cli/main.cpp | 285 ----------------- sources/voltronic-cli/skymax.cpp | 232 -------------- sources/voltronic-cli/skymax.h | 36 --- sources/voltronic-cli/tools.cpp | 71 ----- sources/voltronic-mqtt/mqtt-subscriber.sh | 14 - 28 files changed, 800 insertions(+), 774 deletions(-) rename config/{skymax.conf => inverter.conf} (61%) rename sources/{voltronic-cli => inverter-cli}/CMakeLists.txt (50%) rename sources/{voltronic-cli => inverter-cli}/COPYING (100%) rename sources/{voltronic-cli => inverter-cli}/inputparser.cpp (90%) rename sources/{voltronic-cli => inverter-cli}/inputparser.h (89%) create mode 100644 sources/inverter-cli/inverter.conf create mode 100644 sources/inverter-cli/inverter.cpp create mode 100644 sources/inverter-cli/inverter.h create mode 100644 sources/inverter-cli/main.cpp rename sources/{voltronic-cli => inverter-cli}/main.h (72%) create mode 100644 sources/inverter-cli/tools.cpp rename sources/{voltronic-cli => inverter-cli}/tools.h (100%) rename sources/{voltronic-mqtt => inverter-mqtt}/entrypoint.sh (67%) rename sources/{voltronic-mqtt => inverter-mqtt}/mqtt-init.sh (91%) rename sources/{voltronic-mqtt => inverter-mqtt}/mqtt-push.sh (92%) create mode 100755 sources/inverter-mqtt/mqtt-subscriber.sh delete mode 100644 sources/voltronic-cli/README delete mode 100644 sources/voltronic-cli/main.cpp delete mode 100644 sources/voltronic-cli/skymax.cpp delete mode 100644 sources/voltronic-cli/skymax.h delete mode 100644 sources/voltronic-cli/tools.cpp delete mode 100755 sources/voltronic-mqtt/mqtt-subscriber.sh diff --git a/.DS_Store b/.DS_Store index 1cda1d9859004d883b53729344e578a7ba737bf5..60e6aaed546427fc21a3b16fdeb3754e903bf725 100644 GIT binary patch delta 1023 zcma*lT}&KR6bJD07t78KUv~*CyE8>tkPqwf*)F9;3fL`RKSJ6C`H-r^&ZV7nnWZ}m zC_Z2e>Wi(=XR zo@FCuBxZ%pdEw~1w3o4~`K9GmwNEz(_jZL+1%*YN6|<7zS<^NrqGD*yG!iFAbvq&~ z!?aF}nTD_u=Ag*EbUb4U7$*GA)DKEL+;r%Cw=VJ|UvqVA-;^-Ao&v6>-~) zS^UuEX2&y)OdE;Brl#ciY&Qa%``ta7_F}sAVckv)ObbgM>zj0c=eDzsSw2yjej3Uxvj^3d&be1mC2XvKI=oWoNU(;Q>N2~NB{Y<~mI{it1(FXku7fRqk z8NBeL0gY%vJ37&YZuHlEq9AImJ+4pnCA3eU+#n^=n?efYL)bQf!5QjkBMjqYI`iiBF= zxhY?n*VDO=F;3~%!?Ni$YKSQuN_BUARwMJO{hOZqk?1WR-rPpXfgQMt^{i z3!c$Xp`jeLsKZm(fj~ckXp=B|(18#Rpbz~LCyYTnk70~ToJa=eBu*oa1ZE`C>v#ii z;w`+rX>=YJ@SgLJR-Q7i8H1umAu6 delta 100 zcmZoEXepTBFDT2vz`)GFAi%(o$dJyE%1}Hp!E|z{hA<=J83i|IYHVVg zI9Yr%ufQ$V$>+s5HeL*2X5`+?qhQ1cROg^Bw>ehvGu!44!FU#kbO7^ac7wm{08+Re AqW}N^ diff --git a/Dockerfile b/Dockerfile index 8fdd01c..1ef7593 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] \ No newline at end of file +ENTRYPOINT ["/bin/bash", "/opt/inverter-mqtt/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 950bfb9..9cef335 100644 --- a/README.md +++ b/README.md @@ -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 [-r ], [-h | --help], [-1 | --run-once] + +SUPPORTED ARGUMENTS: + -r 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.**_ diff --git a/config/skymax.conf b/config/inverter.conf similarity index 61% rename from config/skymax.conf rename to config/inverter.conf index 46ffd7c..5d45fb4 100644 --- a/config/skymax.conf +++ b/config/inverter.conf @@ -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 diff --git a/config/mqtt.json b/config/mqtt.json index c0f2428..fffce8e 100644 --- a/config/mqtt.json +++ b/config/mqtt.json @@ -1,5 +1,5 @@ { - "server": "10.16.10.5", + "server": "10.16.10.4", "port": "1883", "topic": "homeassistant", "devicename": "voltronic" diff --git a/docker-compose.yml b/docker-compose.yml index 5b64a1b..23e3977 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" diff --git a/sources/.DS_Store b/sources/.DS_Store index 2543583e22b82df96842321bd21799187b722cc0..6b48b9dc9069c414274bddbaf7762cf30b86175e 100644 GIT binary patch literal 8196 zcmeHMU2GIp6h5bvc5a~b7E6D20<1Iz#I1CzZ9yn(+vTVHq{tRppi*Xc20C$ertHk_ zLTgQ9R9?iW(U?GDVt4~I@xd5leDL33Oo*vbVu*=vJ{jMPQRBIDXAu_K2Oj(hbCY|& z`*Y5{_niC9+&!}ZfPHyA4$uSu3T1(O5fyhxVqE0gQbo{HMTn#ikOK#7Fd+kNw@HT` zp$9?_gdPYz5PBf=z<qcEmA2f~P7_aYlY)><7BX^Vd=&)() zPO;C)+q&z|vrqHJN!MR!j5kkBb#}*N(T;~Zr(@Bnct^ZD7VX;5Jw45o#@2Yxz_IM< z$*Cu&pE@Va#Nbr{wPj|XpWkN7E%Zy9D_$$v{N3L+FH#3~?cOuSdEPbl8 zhC9YBy99rR?3_-+!4<><%!y1*U5SPzTk*S zx}tB4FBSTO6Q1eV?m;8x5;Asa@v`NsRzKL@`RLZK2WRXr+J-lMtR;Dlk* zuD%e=wJ~~<0MYuGk6wX#Fy}8JdYRgGQNp#;k)=DeuN+6C-@b9jo-^1 zR+I?5SGG$fUZ>hT4=oWmcxWq*-ig4G?K^fR{}lpXTP#>xxAeaHWh+|NwsmxG?!9rk zW#@S_v3xd3y7)9I(ZoN{TevwZ>CKjx_*~BO1H?k(lp?6atEF@_vPvQLa78v-BWo0* zIIr-VY+ZyTHH9ybqw85*A@g|b z8c8yR<6MlKlaFt}MR*TBB_>{lZ{Rxo2tUKG2v~*O=U@a?T#l=84L*SDFovDjMQq%N zn=pyna0l)rMy7BG58^N$B1UF>Mvmi2%%O*QV&)7!jnCq9cqU-xEBGqDju*;!I8b7s zM7%sd4-2WBZ8`P{k`hc_a>r{X3n%H~@pLz1rRk~=GtVnnHmQ&g)7^PsfmCY}@&C5D zzyIIeQNFEI*9;hBvKEhfBZv0c>jm@zt6mTyZ8%$#~8!_ delta 115 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{MGqg=C6q~50$jCS`z4y ASO5S3 diff --git a/sources/voltronic-cli/CMakeLists.txt b/sources/inverter-cli/CMakeLists.txt similarity index 50% rename from sources/voltronic-cli/CMakeLists.txt rename to sources/inverter-cli/CMakeLists.txt index 1fe817e..6677155 100644 --- a/sources/voltronic-cli/CMakeLists.txt +++ b/sources/inverter-cli/CMakeLists.txt @@ -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) diff --git a/sources/voltronic-cli/COPYING b/sources/inverter-cli/COPYING similarity index 100% rename from sources/voltronic-cli/COPYING rename to sources/inverter-cli/COPYING diff --git a/sources/voltronic-cli/inputparser.cpp b/sources/inverter-cli/inputparser.cpp similarity index 90% rename from sources/voltronic-cli/inputparser.cpp rename to sources/inverter-cli/inputparser.cpp index 3d98463..59fbe6c 100644 --- a/sources/voltronic-cli/inputparser.cpp +++ b/sources/inverter-cli/inputparser.cpp @@ -1,34 +1,33 @@ -// @author iain - -#include -#include -#include -#include "inputparser.h" - -// This class simply finds cmd line args and parses them for use in a program. -// 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) -{ - 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 -{ - std::vector::const_iterator itr; - itr = std::find(this->tokens.begin(), this->tokens.end(), option); - if (itr != this->tokens.end() && ++itr != this->tokens.end()) - { - return *itr; - } - static const std::string empty_string(""); - return empty_string; -} -bool InputParser::cmdOptionExists(const std::string &option) const -{ - return std::find(this->tokens.begin(), this->tokens.end(), option) - != this->tokens.end(); -} - +// @author iain + +#include +#include +#include +#include "inputparser.h" + +// This class simply finds cmd line args and parses them for use in a program. +// 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) { + 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 { + std::vector::const_iterator itr; + itr = std::find(this->tokens.begin(), this->tokens.end(), option); + if (itr != this->tokens.end() && ++itr != this->tokens.end()) + { + return *itr; + } + static const std::string empty_string(""); + return empty_string; +} + +bool InputParser::cmdOptionExists(const std::string &option) const { + return std::find(this->tokens.begin(), this->tokens.end(), option) + != this->tokens.end(); +} + std::vector tokens; \ No newline at end of file diff --git a/sources/voltronic-cli/inputparser.h b/sources/inverter-cli/inputparser.h similarity index 89% rename from sources/voltronic-cli/inputparser.h rename to sources/inverter-cli/inputparser.h index 49c1604..078e9aa 100644 --- a/sources/voltronic-cli/inputparser.h +++ b/sources/inverter-cli/inputparser.h @@ -1,18 +1,17 @@ -// inputparser.h -// @author iain -#ifndef INPUTPARSER_H -#define INPUTPARSER_H - -#include - -class InputParser -{ - std::vector tokens; - - public: - InputParser (int &argc, char **argv); - const std::string& getCmdOption(const std::string &option) const; - bool cmdOptionExists(const std::string &option) const; -}; - +// inputparser.h +// @author iain +#ifndef INPUTPARSER_H +#define INPUTPARSER_H + +#include + +class InputParser { + std::vector tokens; + + public: + InputParser (int &argc, char **argv); + const std::string& getCmdOption(const std::string &option) const; + bool cmdOptionExists(const std::string &option) const; +}; + #endif // ___INPUTPARSER_H \ No newline at end of file diff --git a/sources/inverter-cli/inverter.conf b/sources/inverter-cli/inverter.conf new file mode 100644 index 0000000..5d45fb4 --- /dev/null +++ b/sources/inverter-cli/inverter.conf @@ -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 diff --git a/sources/inverter-cli/inverter.cpp b/sources/inverter-cli/inverter.cpp new file mode 100644 index 0000000..97a5ebc --- /dev/null +++ b/sources/inverter-cli/inverter.cpp @@ -0,0 +1,252 @@ +#include +#include +#include +#include +#include "inverter.h" +#include "tools.h" +#include "main.h" + +#include +#include + +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>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); +} diff --git a/sources/inverter-cli/inverter.h b/sources/inverter-cli/inverter.h new file mode 100644 index 0000000..8e3d52b --- /dev/null +++ b/sources/inverter-cli/inverter.h @@ -0,0 +1,41 @@ +#ifndef ___INVERTER_H +#define ___INVERTER_H + +#include +#include + +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 diff --git a/sources/inverter-cli/main.cpp b/sources/inverter-cli/main.cpp new file mode 100644 index 0000000..12606ae --- /dev/null +++ b/sources/inverter-cli/main.cpp @@ -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 +#include +#include +#include + +#include "main.h" +#include "tools.h" +#include "inputparser.h" + +#include +#include + +#include +#include +#include +#include +#include + + +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; +} diff --git a/sources/voltronic-cli/main.h b/sources/inverter-cli/main.h similarity index 72% rename from sources/voltronic-cli/main.h rename to sources/inverter-cli/main.h index 8acae20..61e2a2a 100644 --- a/sources/voltronic-cli/main.h +++ b/sources/inverter-cli/main.h @@ -2,10 +2,13 @@ #define ___MAIN_H #include -#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; diff --git a/sources/inverter-cli/tools.cpp b/sources/inverter-cli/tools.cpp new file mode 100644 index 0000000..95311a0 --- /dev/null +++ b/sources/inverter-cli/tools.cpp @@ -0,0 +1,74 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 [-r ], [-h | --help], [-1 | --run-once]\n\n"); + + printf("SUPPORTED ARGUMENTS:\n"); + printf(" -r 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; +} \ No newline at end of file diff --git a/sources/voltronic-cli/tools.h b/sources/inverter-cli/tools.h similarity index 100% rename from sources/voltronic-cli/tools.h rename to sources/inverter-cli/tools.h diff --git a/sources/voltronic-mqtt/entrypoint.sh b/sources/inverter-mqtt/entrypoint.sh similarity index 67% rename from sources/voltronic-mqtt/entrypoint.sh rename to sources/inverter-mqtt/entrypoint.sh index 5d7a2e5..6be9a51 100755 --- a/sources/voltronic-mqtt/entrypoint.sh +++ b/sources/inverter-mqtt/entrypoint.sh @@ -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 diff --git a/sources/voltronic-mqtt/mqtt-init.sh b/sources/inverter-mqtt/mqtt-init.sh similarity index 91% rename from sources/voltronic-mqtt/mqtt-init.sh rename to sources/inverter-mqtt/mqtt-init.sh index 2f48dcc..69afb72 100755 --- a/sources/voltronic-mqtt/mqtt-init.sh +++ b/sources/inverter-mqtt/mqtt-init.sh @@ -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 \ diff --git a/sources/voltronic-mqtt/mqtt-push.sh b/sources/inverter-mqtt/mqtt-push.sh similarity index 92% rename from sources/voltronic-mqtt/mqtt-push.sh rename to sources/inverter-mqtt/mqtt-push.sh index 757b2c9..312722a 100755 --- a/sources/voltronic-mqtt/mqtt-push.sh +++ b/sources/inverter-mqtt/mqtt-push.sh @@ -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" diff --git a/sources/inverter-mqtt/mqtt-subscriber.sh b/sources/inverter-mqtt/mqtt-subscriber.sh new file mode 100755 index 0000000..7c61289 --- /dev/null +++ b/sources/inverter-mqtt/mqtt-subscriber.sh @@ -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) diff --git a/sources/voltronic-cli/README b/sources/voltronic-cli/README deleted file mode 100644 index 8ee547c..0000000 --- a/sources/voltronic-cli/README +++ /dev/null @@ -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. diff --git a/sources/voltronic-cli/main.cpp b/sources/voltronic-cli/main.cpp deleted file mode 100644 index 8867244..0000000 --- a/sources/voltronic-cli/main.cpp +++ /dev/null @@ -1,285 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#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; -} diff --git a/sources/voltronic-cli/skymax.cpp b/sources/voltronic-cli/skymax.cpp deleted file mode 100644 index 370130f..0000000 --- a/sources/voltronic-cli/skymax.cpp +++ /dev/null @@ -1,232 +0,0 @@ -#include -#include -#include -#include -#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 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 char as long as pointer is null - close(fd); - - int replysize = endbuf - startbuf + 1; - lprintf("SKYMAX: Found 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); -} diff --git a/sources/voltronic-cli/skymax.h b/sources/voltronic-cli/skymax.h deleted file mode 100644 index e3d1073..0000000 --- a/sources/voltronic-cli/skymax.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef ___SKYMAX_H -#define ___SKYMAX_H - -#include -#include - -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 diff --git a/sources/voltronic-cli/tools.cpp b/sources/voltronic-cli/tools.cpp deleted file mode 100644 index b5317d8..0000000 --- a/sources/voltronic-cli/tools.cpp +++ /dev/null @@ -1,71 +0,0 @@ - #include - #include - #include - #include - #include - #include - #include - #include - #include - #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 ] | [-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; - } - - \ No newline at end of file diff --git a/sources/voltronic-mqtt/mqtt-subscriber.sh b/sources/voltronic-mqtt/mqtt-subscriber.sh deleted file mode 100755 index 24131c8..0000000 --- a/sources/voltronic-mqtt/mqtt-subscriber.sh +++ /dev/null @@ -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)