From 7a6b39bf1892233fade5787510eb07465add1ac6 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 28 May 2019 14:27:57 +1000 Subject: [PATCH] initial docker support on x86 --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 7 + Dockerfile | 18 ++ README.md | 13 + config/mqtt.json | 6 + config/skymax.conf | 20 ++ docker-compose.yml | 26 ++ sources/.DS_Store | Bin 0 -> 6148 bytes sources/voltronic-cli/CMakeLists.txt | 8 + sources/voltronic-cli/COPYING | 340 ++++++++++++++++++++++ sources/voltronic-cli/README | 54 ++++ sources/voltronic-cli/inputparser.cpp | 34 +++ sources/voltronic-cli/inputparser.h | 18 ++ sources/voltronic-cli/main.cpp | 285 ++++++++++++++++++ sources/voltronic-cli/main.h | 13 + sources/voltronic-cli/skymax.cpp | 232 +++++++++++++++ sources/voltronic-cli/skymax.h | 36 +++ sources/voltronic-cli/tools.cpp | 71 +++++ sources/voltronic-cli/tools.h | 9 + sources/voltronic-mqtt/entrypoint.sh | 11 + sources/voltronic-mqtt/mqtt-init.sh | 68 +++++ sources/voltronic-mqtt/mqtt-push.sh | 120 ++++++++ sources/voltronic-mqtt/mqtt-subscriber.sh | 14 + 23 files changed, 1403 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 config/mqtt.json create mode 100644 config/skymax.conf create mode 100644 docker-compose.yml create mode 100644 sources/.DS_Store create mode 100644 sources/voltronic-cli/CMakeLists.txt create mode 100644 sources/voltronic-cli/COPYING create mode 100644 sources/voltronic-cli/README create mode 100644 sources/voltronic-cli/inputparser.cpp create mode 100644 sources/voltronic-cli/inputparser.h create mode 100644 sources/voltronic-cli/main.cpp create mode 100644 sources/voltronic-cli/main.h create mode 100644 sources/voltronic-cli/skymax.cpp create mode 100644 sources/voltronic-cli/skymax.h create mode 100644 sources/voltronic-cli/tools.cpp create mode 100644 sources/voltronic-cli/tools.h create mode 100755 sources/voltronic-mqtt/entrypoint.sh create mode 100755 sources/voltronic-mqtt/mqtt-init.sh create mode 100755 sources/voltronic-mqtt/mqtt-push.sh create mode 100755 sources/voltronic-mqtt/mqtt-subscriber.sh diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5b11eb24e5a7eded955e315dd3d0b312283aebe1 GIT binary patch literal 6148 zcmeHK%Sr=55Ukdq0WUdvoL}${mJq+d9}tr$LU6$)_dVsi{4}c{2yv5x2QOL;JvGzQ zHN(_ldmDf)_nSLl0bovd#KDKD`M&$aE-K<^amFiNHp6=N)c3nd_UV9gudqgsH$3AT z{~7}~_IPK)7Q+vZK7FC0Qa}nw0VyB_{<#9I*>3ZjM2%8F3P^!31^oNa=#E|Cm>8c9 z4$%S-XAFmN9=!yyd4Sjzj){!WEUCn#TD2IKbjDldb%kSM(qVBk ZOIuwiB8E=se z>k>6e0V!~-z-2BMUjOguXZrtRl2%ec3j8SrY_@t>E%~IXt&_)jt!?y2y61e*-8c^l nhbYIyD92oQIlhgg%xga9epfgq2A%Ps6ZJFTy2zx!Z!7Q#9RnNP literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ee06c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +voltronic-cli/bin/skymax.bak +voltronic-cli/bin/skymax +voltronic-cli/test/ +CMakeFiles +CMakeCache.txt +cmake_install.cmake +Makefile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8fdd01c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM debian:stretch + +RUN apt update && apt install -y \ + curl \ + git \ + build-essential \ + cmake \ + jq \ + mosquitto-clients + +ADD sources/ /opt/ +ADD config/ /etc/skymax/ + +RUN cd /opt/voltronic-cli && \ + mkdir bin && cmake . && make + +WORKDIR /opt +ENTRYPOINT ["/bin/bash", "/opt/voltronic-mqtt/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..249dc4b --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +## A Docker based Home Assistant interface for Voltronic Solar Inverters + +This project [was derived](https://github.com/leithhobson/skymax-demo-Original) from the 'skymax' [C based monitoring application](https://skyboo.net/2017/03/monitoring-voltronic-power-axpert-mex-inverter-under-linux/) designed to take the monitoring data from Voltronic, Axpert, Mppsolar PIP, Voltacon, Effekta, and other branded OEM Inverters and send it to a Home Assistant MQTT server for ingestion... + +The program can also receive commands from Home Assistant (via MQTT) to change the state of the inverter remotely. + +By remotely setting values via MQTT you can for example, change the power mode to '_solar only_' during the day, but then change back to '_grid mode charging_' for your AGM batteries in the evenings - But if it's raining (based on data from your weather station), Set the charge mode to `PCP02` _(Charge based on 'Solar and Utility')_... + +The program is designed to be run in a Docker Container, and can be deployed on a lightweight SBC next to your Inverter (i.e. an Orange Pi Zero running Arabian), and read data via the RS232 or USB ports on the back of the Inverter. + +---- + + diff --git a/config/mqtt.json b/config/mqtt.json new file mode 100644 index 0000000..c0f2428 --- /dev/null +++ b/config/mqtt.json @@ -0,0 +1,6 @@ +{ + "server": "10.16.10.5", + "port": "1883", + "topic": "homeassistant", + "devicename": "voltronic" +} \ No newline at end of file diff --git a/config/skymax.conf b/config/skymax.conf new file mode 100644 index 0000000..46ffd7c --- /dev/null +++ b/config/skymax.conf @@ -0,0 +1,20 @@ +#This is the settings file, all comment lines should start with a hash mark. + +# 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 + +# 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 allos 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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5f6f0ee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +version: '2' + +services: + voltronic-mqtt: + + build: . + container_name: voltronic-mqtt + + privileged: true + restart: always + + volumes: + - ./config/:/etc/skymax/ + + devices: + # - "/dev/mem:/dev/mem" + + # USB Port Mapping + - /dev/bus/usb:/dev/bus/usb:rwm + - /dev/ttyUSB0:/dev/ttyUSB0:rwm + - /dev/ttyUSB1:/dev/ttyUSB1:rwm + + # Serial Port Mapping... + - /dev/ttyS0:/dev/ttyS0 + - /dev/ttyS1:/dev/ttyS1 + - /dev/ttyS2:/dev/ttyS2 diff --git a/sources/.DS_Store b/sources/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2543583e22b82df96842321bd21799187b722cc0 GIT binary patch literal 6148 zcmeHK%Sr=55Ukc50)ph|aelyqe=vmj1^EF%L4>%$5Ody>-{q%S{XkeYf)_7R4c#@< z+cm@1VS5{Zt>2&SfE9ox-4P!i=H}1cXLeB;Bhq=s0ecL1!6R;G)#nq=y~c^W9gY+J zE8dtnp0oU0CB-|7}qgN5Su56y>LuqhGt16Ce>=hu%t8Js;(D~iAjgm@L_ec)r4YkJI`-X z4(o}EQa}n!6}Zgp%KQI4{fGH~O43dWNP&N)fGsxL&6=-Ny><3--fJ8Ef$lY*bT_Vp o!VvA4810xFZ^t)Lly%M5eBKMk#Go@DbfSI+To;)X_-h5e0J9?(5dZ)H literal 0 HcmV?d00001 diff --git a/sources/voltronic-cli/CMakeLists.txt b/sources/voltronic-cli/CMakeLists.txt new file mode 100644 index 0000000..1fe817e --- /dev/null +++ b/sources/voltronic-cli/CMakeLists.txt @@ -0,0 +1,8 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 2.6) +PROJECT("skymax") + +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) diff --git a/sources/voltronic-cli/COPYING b/sources/voltronic-cli/COPYING new file mode 100644 index 0000000..f90922e --- /dev/null +++ b/sources/voltronic-cli/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/sources/voltronic-cli/README b/sources/voltronic-cli/README new file mode 100644 index 0000000..8ee547c --- /dev/null +++ b/sources/voltronic-cli/README @@ -0,0 +1,54 @@ +-------------------------------------------------------------------------------------- +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/inputparser.cpp b/sources/voltronic-cli/inputparser.cpp new file mode 100644 index 0000000..3d98463 --- /dev/null +++ b/sources/voltronic-cli/inputparser.cpp @@ -0,0 +1,34 @@ +// @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/voltronic-cli/inputparser.h new file mode 100644 index 0000000..49c1604 --- /dev/null +++ b/sources/voltronic-cli/inputparser.h @@ -0,0 +1,18 @@ +// 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/voltronic-cli/main.cpp b/sources/voltronic-cli/main.cpp new file mode 100644 index 0000000..8867244 --- /dev/null +++ b/sources/voltronic-cli/main.cpp @@ -0,0 +1,285 @@ +#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/main.h b/sources/voltronic-cli/main.h new file mode 100644 index 0000000..8acae20 --- /dev/null +++ b/sources/voltronic-cli/main.h @@ -0,0 +1,13 @@ +#ifndef ___MAIN_H +#define ___MAIN_H + +#include +#include "skymax.h" + +extern bool debugFlag; +extern atomic_bool ups_status_changed; +extern atomic_bool ups_qmod_changed; +extern atomic_bool ups_qpiri_changed; +extern atomic_bool ups_qpigs_changed; + +#endif // ___MAIN_H diff --git a/sources/voltronic-cli/skymax.cpp b/sources/voltronic-cli/skymax.cpp new file mode 100644 index 0000000..370130f --- /dev/null +++ b/sources/voltronic-cli/skymax.cpp @@ -0,0 +1,232 @@ +#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 new file mode 100644 index 0000000..e3d1073 --- /dev/null +++ b/sources/voltronic-cli/skymax.h @@ -0,0 +1,36 @@ +#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 new file mode 100644 index 0000000..b5317d8 --- /dev/null +++ b/sources/voltronic-cli/tools.cpp @@ -0,0 +1,71 @@ + #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-cli/tools.h b/sources/voltronic-cli/tools.h new file mode 100644 index 0000000..ab36bf7 --- /dev/null +++ b/sources/voltronic-cli/tools.h @@ -0,0 +1,9 @@ +#ifndef ___TOOLS_H +#define ___TOOLS_H + +#define LOG_FILE "/dev/null" + +void lprintf(const char *format, ...); +int print_help(); + +#endif // ___TOOLS_H diff --git a/sources/voltronic-mqtt/entrypoint.sh b/sources/voltronic-mqtt/entrypoint.sh new file mode 100755 index 0000000..0fb2d3a --- /dev/null +++ b/sources/voltronic-mqtt/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +export TERM=xterm + +# Init the mqtt server for the first time... +bash /opt/voltronic-mqtt/mqtt-init.sh + +# 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 & + +# execute exactly ever minute... +watch -n 30 /opt/voltronic-mqtt/mqtt-push.sh # > /dev/null 2>&1 diff --git a/sources/voltronic-mqtt/mqtt-init.sh b/sources/voltronic-mqtt/mqtt-init.sh new file mode 100755 index 0000000..03135c8 --- /dev/null +++ b/sources/voltronic-mqtt/mqtt-init.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# +# 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` + +registerTopic () { + mosquitto_pub \ + -h $MQTT_SERVER \ + -p $MQTT_PORT \ + -t "$MQTT_TOPIC/sensor/"$MQTT_DEVICENAME"_$1/config" \ + -m "{ + \"name\": \""$MQTT_DEVICENAME"_$1\", + \"unit_of_measurement\": \"$2\", + \"state_topic\": \"$MQTT_TOPIC/sensor/"$MQTT_DEVICENAME"_$1\", + \"icon\": \"mdi:$3\" + }" +} + +registerInverterRawCMD () { + mosquitto_pub \ + -h $MQTT_SERVER \ + -p $MQTT_PORT \ + -t "$MQTT_TOPIC/sensor/$MQTT_DEVICENAME/config" \ + -m "{ + \"name\": \""$MQTT_DEVICENAME"\", + \"state_topic\": \"$MQTT_TOPIC/sensor/$MQTT_DEVICENAME\" + }" +} + +registerTopic "Inverter_mode" "" "mdi-solar-power" # 1 = Power_On, 2 = Standby, 3 = Line, 4 = Battery, 5 = Fault, 6 = Power_Saving, 7 = Unknown +registerTopic "AC_grid_voltage" "V" "mdi-power-plug" +registerTopic "AC_grid_frequency" "Hz" "mdi-current-ac" +registerTopic "AC_out_voltage" "V" "mdi-power-plug" +registerTopic "AC_out_frequency" "Hz" "mdi-current-ac" +registerTopic "PV_in_voltage" "V" "mdi-solar-panel-large" +registerTopic "PV_in_current" "A" "mdi-solar-panel-large" +registerTopic "PV_in_watts" "W" "mdi-solar-panel-large" +registerTopic "PV_in_watthour" "Wh" "mdi-solar-panel-large" +registerTopic "SCC_voltage" "V" "mdi-current-dc" +registerTopic "Load_pct" "%" "mdi-brightness-percent" +registerTopic "Load_watt" "W" "mdi-chart-bell-curve" +registerTopic "Load_watthour" "Wh" "mdi-chart-bell-curve" +registerTopic "Load_va" "VA" "mdi-chart-bell-curve" +registerTopic "Bus_voltage" "V" "mdi-details" +registerTopic "Heatsink_temperature" "" "mdi-details" +registerTopic "Battery_capacity" "%" "mdi-battery-outline" +registerTopic "Battery_voltage" "V" "mdi-battery-outline" +registerTopic "Battery_charge_current" "A" "mdi-current-dc" +registerTopic "Battery_discharge_current" "A" "mdi-current-dc" +registerTopic "Load_status_on" "" "mdi-power" +registerTopic "SCC_charge_on" "" "mdi-power" +registerTopic "AC_charge_on" "" "mdi-power" +registerTopic "Battery_recharge_voltage" "V" "mdi-current-dc" +registerTopic "Battery_under_voltage" "V" "mdi-current-dc" +registerTopic "Battery_bulk_voltage" "V" "mdi-current-dc" +registerTopic "Battery_float_voltage" "V" "mdi-current-dc" +registerTopic "Max_grid_charge_current" "A" "mdi-current-ac" +registerTopic "Max_charge_current" "A" "mdi-current-ac" +registerTopic "Out_source_priority" "" "mdi-grid" +registerTopic "Charger_source_priority" "" "mdi-solar-power" +registerTopic "Battery_redischarge_voltage" "V" "mdi-battery-negative" + +# Add in a separate topic so we can send raw commands from assistant back to the inverter via MQTT (such as changing power modes etc)... +registerInverterRawCMD diff --git a/sources/voltronic-mqtt/mqtt-push.sh b/sources/voltronic-mqtt/mqtt-push.sh new file mode 100755 index 0000000..f23a377 --- /dev/null +++ b/sources/voltronic-mqtt/mqtt-push.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +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` + + mosquitto_pub \ + -h $MQTT_SERVER \ + -p $MQTT_PORT \ + -t "$MQTT_TOPIC/sensor/"$MQTT_DEVICENAME"_$1" \ + -m "$2" +} + +INVERTER_DATA=`timeout 10 /opt/voltronic-cli/bin/skymax` + +##################################################################################### + +Inverter_mode=`echo $INVERTER_DATA | jq '.Inverter_mode' -r` +[ ! -z "$Inverter_mode" ] && pushMQTTData "Inverter_mode" "$Inverter_mode" + +AC_grid_voltage=`echo $INVERTER_DATA | jq '.AC_grid_voltage' -r` +[ ! -z "$AC_grid_voltage" ] && pushMQTTData "AC_grid_voltage" "$AC_grid_voltage" + +AC_grid_frequency=`echo $INVERTER_DATA | jq '.AC_grid_frequency' -r` +[ ! -z "$AC_grid_frequency" ] && pushMQTTData "AC_grid_frequency" "$AC_grid_frequency" + +AC_out_voltage=`echo $INVERTER_DATA | jq '.AC_out_voltage' -r` +[ ! -z "$AC_out_voltage" ] && pushMQTTData "AC_out_voltage" "$AC_out_voltage" + +AC_out_frequency=`echo $INVERTER_DATA | jq '.AC_out_frequency' -r` +[ ! -z "$AC_out_frequency" ] && pushMQTTData "AC_out_frequency" "$AC_out_frequency" + +PV_in_voltage=`echo $INVERTER_DATA | jq '.PV_in_voltage' -r` +[ ! -z "$PV_in_voltage" ] && pushMQTTData "PV_in_voltage" "$PV_in_voltage" + +PV_in_current=`echo $INVERTER_DATA | jq '.PV_in_current' -r` +[ ! -z "$PV_in_current" ] && pushMQTTData "PV_in_current" "$PV_in_current" + +PV_in_watts=`echo $INVERTER_DATA | jq '.PV_in_watts' -r` +[ ! -z "$PV_in_watts" ] && pushMQTTData "PV_in_watts" "$PV_in_watts" + +PV_in_watthour=`echo $INVERTER_DATA | jq '.PV_in_watthour' -r` +[ ! -z "$PV_in_watthour" ] && pushMQTTData "PV_in_watthour" "$PV_in_watthour" + +SCC_voltage=`echo $INVERTER_DATA | jq '.SCC_voltage' -r` +[ ! -z "$SCC_voltage" ] && pushMQTTData "SCC_voltage" "$SCC_voltage" + +Load_pct=`echo $INVERTER_DATA | jq '.Load_pct' -r` +[ ! -z "$Load_pct" ] && pushMQTTData "Load_pct" "$Load_pct" + +Load_watt=`echo $INVERTER_DATA | jq '.Load_watt' -r` +[ ! -z "$Load_watt" ] && pushMQTTData "Load_watt" "$Load_watt" + +Load_watthour=`echo $INVERTER_DATA | jq '.Load_watthour' -r` +[ ! -z "$Load_watthour" ] && pushMQTTData "Load_watthour" "$Load_watthour" + +Load_va=`echo $INVERTER_DATA | jq '.Load_va' -r` +[ ! -z "$Load_va" ] && pushMQTTData "Load_va" "$Load_va" + +Bus_voltage=`echo $INVERTER_DATA | jq '.Bus_voltage' -r` +[ ! -z "$Bus_voltage" ] && pushMQTTData "Bus_voltage" "$Bus_voltage" + +Heatsink_temperature=`echo $INVERTER_DATA | jq '.Heatsink_temperature' -r` +[ ! -z "$Heatsink_temperature" ] && pushMQTTData "Heatsink_temperature" "$Heatsink_temperature" + +Battery_capacity=`echo $INVERTER_DATA | jq '.Battery_capacity' -r` +[ ! -z "$Battery_capacity" ] && pushMQTTData "Battery_capacity" "$Battery_capacity" + +Battery_voltage=`echo $INVERTER_DATA | jq '.Battery_voltage' -r` +[ ! -z "$Battery_voltage" ] && pushMQTTData "Battery_voltage" "$Battery_voltage" + +Battery_charge_current=`echo $INVERTER_DATA | jq '.Battery_charge_current' -r` +[ ! -z "$Battery_charge_current" ] && pushMQTTData "Battery_charge_current" "$Battery_charge_current" + +Battery_discharge_current=`echo $INVERTER_DATA | jq '.Battery_discharge_current' -r` +[ ! -z "$Battery_discharge_current" ] && pushMQTTData "Battery_discharge_current" "$Battery_discharge_current" + +Load_status_on=`echo $INVERTER_DATA | jq '.Load_status_on' -r` +[ ! -z "$Load_status_on" ] && pushMQTTData "Load_status_on" "$Load_status_on" + +SCC_charge_on=`echo $INVERTER_DATA | jq '.SCC_charge_on' -r` +[ ! -z "$SCC_charge_on" ] && pushMQTTData "SCC_charge_on" "$SCC_charge_on" + +AC_charge_on=`echo $INVERTER_DATA | jq '.AC_charge_on' -r` +[ ! -z "$AC_charge_on" ] && pushMQTTData "AC_charge_on" "$AC_charge_on" + +Battery_recharge_voltage=`echo $INVERTER_DATA | jq '.Battery_recharge_voltage' -r` +[ ! -z "$Battery_recharge_voltage" ] && pushMQTTData "Battery_recharge_voltage" "$Battery_recharge_voltage" + +Battery_under_voltage=`echo $INVERTER_DATA | jq '.Battery_under_voltage' -r` +[ ! -z "$Battery_under_voltage" ] && pushMQTTData "Battery_under_voltage" "$Battery_under_voltage" + +Battery_under_voltage=`echo $INVERTER_DATA | jq '.Battery_under_voltage' -r` +[ ! -z "$Battery_under_voltage" ] && pushMQTTData "Battery_under_voltage" "$Battery_under_voltage" + +Battery_bulk_voltage=`echo $INVERTER_DATA | jq '.Battery_bulk_voltage' -r` +[ ! -z "$Battery_bulk_voltage" ] && pushMQTTData "Battery_bulk_voltage" "$Battery_bulk_voltage" + +Battery_float_voltage=`echo $INVERTER_DATA | jq '.Battery_float_voltage' -r` +[ ! -z "$Battery_float_voltage" ] && pushMQTTData "Battery_float_voltage" "$Battery_float_voltage" + +Max_grid_charge_current=`echo $INVERTER_DATA | jq '.Max_grid_charge_current' -r` +[ ! -z "$Max_grid_charge_current" ] && pushMQTTData "Max_grid_charge_current" "$Max_grid_charge_current" + +Max_charge_current=`echo $INVERTER_DATA | jq '.Max_charge_current' -r` +[ ! -z "$Max_charge_current" ] && pushMQTTData "Max_charge_current" "$Max_charge_current" + +Out_source_priority=`echo $INVERTER_DATA | jq '.Out_source_priority' -r` +[ ! -z "$Out_source_priority" ] && pushMQTTData "Out_source_priority" "$Out_source_priority" + +Charger_source_priority=`echo $INVERTER_DATA | jq '.Charger_source_priority' -r` +[ ! -z "$Charger_source_priority" ] && pushMQTTData "Charger_source_priority" "$Charger_source_priority" + +Battery_redischarge_voltage=`echo $INVERTER_DATA | jq '.Battery_redischarge_voltage' -r` +[ ! -z "$Battery_redischarge_voltage" ] && pushMQTTData "Battery_redischarge_voltage" "$Battery_redischarge_voltage" + + diff --git a/sources/voltronic-mqtt/mqtt-subscriber.sh b/sources/voltronic-mqtt/mqtt-subscriber.sh new file mode 100755 index 0000000..24131c8 --- /dev/null +++ b/sources/voltronic-mqtt/mqtt-subscriber.sh @@ -0,0 +1,14 @@ +#!/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)