From 4084b4d5d8367ef28b98e56226aa74dea829715d Mon Sep 17 00:00:00 2001 From: Arkadiusz Kubacki Date: Mon, 13 Feb 2023 10:42:31 +0100 Subject: [PATCH 01/46] Add Modbus --- include/ModbusDtu.h | 18 ++++++++++ platformio.ini | 1 + src/ModbusDtu.cpp | 80 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 include/ModbusDtu.h create mode 100644 src/ModbusDtu.cpp diff --git a/include/ModbusDtu.h b/include/ModbusDtu.h new file mode 100644 index 000000000..c83d67e0f --- /dev/null +++ b/include/ModbusDtu.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include "Configuration.h" +#include +#include + +class ModbusDtuClass { +public: + void init(); + void loop(); + +private: + uint32_t _lastPublish; + uint8_t _channels; +}; + +extern ModbusDtuClass ModbusDtu; \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 12faba04e..161e6f992 100644 --- a/platformio.ini +++ b/platformio.ini @@ -27,6 +27,7 @@ lib_deps = https://github.com/bertmelis/espMqttClient.git#v1.3.3 nrf24/RF24 @ ^1.4.5 olikraus/U8g2 @ ^2.34.13 + emelianov/modbus-esp8266@^4.1.0 extra_scripts = pre:auto_firmware_version.py diff --git a/src/ModbusDtu.cpp b/src/ModbusDtu.cpp new file mode 100644 index 000000000..a56fb7cd6 --- /dev/null +++ b/src/ModbusDtu.cpp @@ -0,0 +1,80 @@ +#include "ModbusDTU.h" + + +ModbusIP mb; + +ModbusDtuClass ModbusDtu; + +void ModbusDtuClass::init() +{ + mb.server(); + const CONFIG_T& config = Configuration.get(); + mb.addHreg(0x2000, (config.Dtu_Serial >> 32) & 0xFFFF); + mb.addHreg(0x2001, (config.Dtu_Serial >> 16) & 0xFFFF); + mb.addHreg(0x2002, (config.Dtu_Serial) & 0xFFFF); + mb.addHreg(0x2003, 0); + mb.addHreg(0x2004, 0); + mb.addHreg(0x2005, 0); + + _channels = 0; + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } + _channels +=inv->Statistics()->getChannelCount(); + } + mb.addHreg(0x200, _channels); + + for (uint8_t i = 0; i <= _channels; i++) { + for(uint8_t j = 0; j < 20; j++) { + mb.addHreg(i* 20 + 0x1000 + j, 0); + } + yield(); + } + +} + +void ModbusDtuClass::loop() +{ + + uint8_t chan = 0; + if (millis() - _lastPublish > 2000 && Hoymiles.getRadio()->isIdle()) { + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } + // Loop all channels + for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { + if (c > 0) { + uint64_t serialInv = inv->serial(); + mb.Hreg(chan* 20 + 0x1000, 0x0C00 + ((serialInv >> 40) & 0xFF)); + mb.Hreg(chan* 20 + 0x1001, (serialInv >> 24) & 0xFFFF); + mb.Hreg(chan* 20 + 0x1002, (serialInv >> 8) & 0xFFFF); + mb.Hreg(chan* 20 + 0x1003, (serialInv << 8) + c); + mb.Hreg(chan* 20 + 0x1004, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_UDC)*10)); + mb.Hreg(chan* 20 + 0x1005, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_IDC)*100)); + mb.Hreg(chan* 20 + 0x1006, (uint16_t)(inv->Statistics()->getChannelFieldValue(0, FLD_UAC)*10)); + mb.Hreg(chan* 20 + 0x1007, (uint16_t)(inv->Statistics()->getChannelFieldValue(0, FLD_F)*100)); + mb.Hreg(chan* 20 + 0x1008, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_PDC)*10)); + mb.Hreg(chan* 20 + 0x1009, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_YD))); + mb.Hreg(chan* 20 + 0x100A, ((uint16_t)(((uint32_t)(inv->Statistics()->getChannelFieldValue(c, FLD_YT)*1000)) >> 16)) & 0xFFFF); + mb.Hreg(chan* 20 + 0x100B, (((uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_YT)*1000))) & 0xFFFF); + mb.Hreg(chan* 20 + 0x100C, (uint16_t)(inv->Statistics()->getChannelFieldValue(0, FLD_T)*10)); + mb.Hreg(chan* 20 + 0x100D, 3); + mb.Hreg(chan* 20 + 0x100E, 0); + mb.Hreg(chan* 20 + 0x100F, 0); + mb.Hreg(chan* 20 + 0x1010, 0x0107); + mb.Hreg(chan* 20 + 0x1011, 0); + mb.Hreg(chan* 20 + 0x1012, 0); + mb.Hreg(chan* 20 + 0x1013, 0); + chan++; + } + } + } + _lastPublish = millis(); + } + yield(); + mb.task(); +} From cc988310e2043adb2a3ccdaba50c2fa8bb1de895 Mon Sep 17 00:00:00 2001 From: Arkadiusz Kubacki Date: Mon, 13 Feb 2023 10:42:31 +0100 Subject: [PATCH 02/46] Added Modbus --- include/ModbusDtu.h | 18 ++++++++++ platformio.ini | 1 + src/ModbusDtu.cpp | 80 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 include/ModbusDtu.h create mode 100644 src/ModbusDtu.cpp diff --git a/include/ModbusDtu.h b/include/ModbusDtu.h new file mode 100644 index 000000000..c83d67e0f --- /dev/null +++ b/include/ModbusDtu.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include "Configuration.h" +#include +#include + +class ModbusDtuClass { +public: + void init(); + void loop(); + +private: + uint32_t _lastPublish; + uint8_t _channels; +}; + +extern ModbusDtuClass ModbusDtu; \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 12faba04e..161e6f992 100644 --- a/platformio.ini +++ b/platformio.ini @@ -27,6 +27,7 @@ lib_deps = https://github.com/bertmelis/espMqttClient.git#v1.3.3 nrf24/RF24 @ ^1.4.5 olikraus/U8g2 @ ^2.34.13 + emelianov/modbus-esp8266@^4.1.0 extra_scripts = pre:auto_firmware_version.py diff --git a/src/ModbusDtu.cpp b/src/ModbusDtu.cpp new file mode 100644 index 000000000..a56fb7cd6 --- /dev/null +++ b/src/ModbusDtu.cpp @@ -0,0 +1,80 @@ +#include "ModbusDTU.h" + + +ModbusIP mb; + +ModbusDtuClass ModbusDtu; + +void ModbusDtuClass::init() +{ + mb.server(); + const CONFIG_T& config = Configuration.get(); + mb.addHreg(0x2000, (config.Dtu_Serial >> 32) & 0xFFFF); + mb.addHreg(0x2001, (config.Dtu_Serial >> 16) & 0xFFFF); + mb.addHreg(0x2002, (config.Dtu_Serial) & 0xFFFF); + mb.addHreg(0x2003, 0); + mb.addHreg(0x2004, 0); + mb.addHreg(0x2005, 0); + + _channels = 0; + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } + _channels +=inv->Statistics()->getChannelCount(); + } + mb.addHreg(0x200, _channels); + + for (uint8_t i = 0; i <= _channels; i++) { + for(uint8_t j = 0; j < 20; j++) { + mb.addHreg(i* 20 + 0x1000 + j, 0); + } + yield(); + } + +} + +void ModbusDtuClass::loop() +{ + + uint8_t chan = 0; + if (millis() - _lastPublish > 2000 && Hoymiles.getRadio()->isIdle()) { + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } + // Loop all channels + for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { + if (c > 0) { + uint64_t serialInv = inv->serial(); + mb.Hreg(chan* 20 + 0x1000, 0x0C00 + ((serialInv >> 40) & 0xFF)); + mb.Hreg(chan* 20 + 0x1001, (serialInv >> 24) & 0xFFFF); + mb.Hreg(chan* 20 + 0x1002, (serialInv >> 8) & 0xFFFF); + mb.Hreg(chan* 20 + 0x1003, (serialInv << 8) + c); + mb.Hreg(chan* 20 + 0x1004, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_UDC)*10)); + mb.Hreg(chan* 20 + 0x1005, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_IDC)*100)); + mb.Hreg(chan* 20 + 0x1006, (uint16_t)(inv->Statistics()->getChannelFieldValue(0, FLD_UAC)*10)); + mb.Hreg(chan* 20 + 0x1007, (uint16_t)(inv->Statistics()->getChannelFieldValue(0, FLD_F)*100)); + mb.Hreg(chan* 20 + 0x1008, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_PDC)*10)); + mb.Hreg(chan* 20 + 0x1009, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_YD))); + mb.Hreg(chan* 20 + 0x100A, ((uint16_t)(((uint32_t)(inv->Statistics()->getChannelFieldValue(c, FLD_YT)*1000)) >> 16)) & 0xFFFF); + mb.Hreg(chan* 20 + 0x100B, (((uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_YT)*1000))) & 0xFFFF); + mb.Hreg(chan* 20 + 0x100C, (uint16_t)(inv->Statistics()->getChannelFieldValue(0, FLD_T)*10)); + mb.Hreg(chan* 20 + 0x100D, 3); + mb.Hreg(chan* 20 + 0x100E, 0); + mb.Hreg(chan* 20 + 0x100F, 0); + mb.Hreg(chan* 20 + 0x1010, 0x0107); + mb.Hreg(chan* 20 + 0x1011, 0); + mb.Hreg(chan* 20 + 0x1012, 0); + mb.Hreg(chan* 20 + 0x1013, 0); + chan++; + } + } + } + _lastPublish = millis(); + } + yield(); + mb.task(); +} From 802601aee1dddcc784c232fad97c0901b232799a Mon Sep 17 00:00:00 2001 From: Arkadiusz Kubacki Date: Wed, 15 Feb 2023 12:04:55 +0100 Subject: [PATCH 03/46] Checking that the measurement does not exceed the max Power --- src/ModbusDtu.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ModbusDtu.cpp b/src/ModbusDtu.cpp index a56fb7cd6..566b8e591 100644 --- a/src/ModbusDtu.cpp +++ b/src/ModbusDtu.cpp @@ -53,11 +53,13 @@ void ModbusDtuClass::loop() mb.Hreg(chan* 20 + 0x1001, (serialInv >> 24) & 0xFFFF); mb.Hreg(chan* 20 + 0x1002, (serialInv >> 8) & 0xFFFF); mb.Hreg(chan* 20 + 0x1003, (serialInv << 8) + c); - mb.Hreg(chan* 20 + 0x1004, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_UDC)*10)); - mb.Hreg(chan* 20 + 0x1005, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_IDC)*100)); + if(inv->Statistics()->getChannelMaxPower(c)>0 && inv->Statistics()->getChannelFieldValue(c, FLD_IRR) < 120){ + mb.Hreg(chan* 20 + 0x1004, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_UDC)*10)); + mb.Hreg(chan* 20 + 0x1005, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_IDC)*100)); + mb.Hreg(chan* 20 + 0x1008, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_PDC)*10)); + } mb.Hreg(chan* 20 + 0x1006, (uint16_t)(inv->Statistics()->getChannelFieldValue(0, FLD_UAC)*10)); mb.Hreg(chan* 20 + 0x1007, (uint16_t)(inv->Statistics()->getChannelFieldValue(0, FLD_F)*100)); - mb.Hreg(chan* 20 + 0x1008, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_PDC)*10)); mb.Hreg(chan* 20 + 0x1009, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_YD))); mb.Hreg(chan* 20 + 0x100A, ((uint16_t)(((uint32_t)(inv->Statistics()->getChannelFieldValue(c, FLD_YT)*1000)) >> 16)) & 0xFFFF); mb.Hreg(chan* 20 + 0x100B, (((uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_YT)*1000))) & 0xFFFF); From 0777a9c201827c5c53e1b0dc39bbebb456d99d5f Mon Sep 17 00:00:00 2001 From: Arkadiusz Kubacki Date: Wed, 15 Feb 2023 14:12:21 +0100 Subject: [PATCH 04/46] Modbus BugFix --- src/ModbusDtu.cpp | 6 ++++++ src/main.cpp | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/src/ModbusDtu.cpp b/src/ModbusDtu.cpp index 566b8e591..15367d24e 100644 --- a/src/ModbusDtu.cpp +++ b/src/ModbusDtu.cpp @@ -57,7 +57,13 @@ void ModbusDtuClass::loop() mb.Hreg(chan* 20 + 0x1004, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_UDC)*10)); mb.Hreg(chan* 20 + 0x1005, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_IDC)*100)); mb.Hreg(chan* 20 + 0x1008, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_PDC)*10)); + } else if (inv->Statistics()->getChannelMaxPower(c)==0) + { + mb.Hreg(chan* 20 + 0x1004, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_UDC)*10)); + mb.Hreg(chan* 20 + 0x1005, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_IDC)*100)); + mb.Hreg(chan* 20 + 0x1008, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_PDC)*10)); } + mb.Hreg(chan* 20 + 0x1006, (uint16_t)(inv->Statistics()->getChannelFieldValue(0, FLD_UAC)*10)); mb.Hreg(chan* 20 + 0x1007, (uint16_t)(inv->Statistics()->getChannelFieldValue(0, FLD_F)*100)); mb.Hreg(chan* 20 + 0x1009, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_YD))); diff --git a/src/main.cpp b/src/main.cpp index a68ac0ad1..dea1256f3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "Configuration.h" +#include "ModbusDtu.h" #include "Display_Graphic.h" #include "MessageOutput.h" #include "MqttHandleDtu.h" @@ -159,6 +160,10 @@ void setup() } else { MessageOutput.println(F("Invalid pin config")); } + // Initialize Modbus + MessageOutput.print(F("Initialize Modbus... ")); + ModbusDtu.init(); + MessageOutput.println(F("done")); } void loop() @@ -167,6 +172,8 @@ void loop() yield(); Hoymiles.loop(); yield(); + ModbusDtu.loop(); + yield(); MqttHandleDtu.loop(); yield(); MqttHandleInverter.loop(); From 53b96902b2668096eefc9dac19c531ea748772db Mon Sep 17 00:00:00 2001 From: Arkadiusz Kubacki Date: Wed, 15 Feb 2023 15:06:29 +0100 Subject: [PATCH 05/46] Modbus BugFix --- src/ModbusDtu.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ModbusDtu.cpp b/src/ModbusDtu.cpp index 15367d24e..e4c2c9a03 100644 --- a/src/ModbusDtu.cpp +++ b/src/ModbusDtu.cpp @@ -53,11 +53,11 @@ void ModbusDtuClass::loop() mb.Hreg(chan* 20 + 0x1001, (serialInv >> 24) & 0xFFFF); mb.Hreg(chan* 20 + 0x1002, (serialInv >> 8) & 0xFFFF); mb.Hreg(chan* 20 + 0x1003, (serialInv << 8) + c); - if(inv->Statistics()->getChannelMaxPower(c)>0 && inv->Statistics()->getChannelFieldValue(c, FLD_IRR) < 120){ + if(inv->Statistics()->getChannelMaxPower(c-1)>0 && inv->Statistics()->getChannelFieldValue(c, FLD_IRR) < 120){ mb.Hreg(chan* 20 + 0x1004, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_UDC)*10)); mb.Hreg(chan* 20 + 0x1005, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_IDC)*100)); mb.Hreg(chan* 20 + 0x1008, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_PDC)*10)); - } else if (inv->Statistics()->getChannelMaxPower(c)==0) + } else if (inv->Statistics()->getChannelMaxPower(c-1)==0) { mb.Hreg(chan* 20 + 0x1004, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_UDC)*10)); mb.Hreg(chan* 20 + 0x1005, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_IDC)*100)); From e13348ce802684f8f6c307e76ad06795b37a6c0e Mon Sep 17 00:00:00 2001 From: Arkadiusz Kubacki Date: Wed, 8 Mar 2023 16:46:02 +0100 Subject: [PATCH 06/46] Added WatchDog to Modbus --- include/WatchDogDtu.h | 16 ++++++++++++++++ src/ModbusDtu.cpp | 3 +-- src/WatchDog.cpp | 22 ++++++++++++++++++++++ src/main.cpp | 5 +++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 include/WatchDogDtu.h create mode 100644 src/WatchDog.cpp diff --git a/include/WatchDogDtu.h b/include/WatchDogDtu.h new file mode 100644 index 000000000..c2a162273 --- /dev/null +++ b/include/WatchDogDtu.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "Configuration.h" +#include + +class WatchDogDtuClass { +public: + void init(); + void loop(); + +private: + uint32_t _lastPublish; +}; + +extern WatchDogDtuClass WatchDogDtu; \ No newline at end of file diff --git a/src/ModbusDtu.cpp b/src/ModbusDtu.cpp index e4c2c9a03..706d26f08 100644 --- a/src/ModbusDtu.cpp +++ b/src/ModbusDtu.cpp @@ -1,6 +1,5 @@ #include "ModbusDTU.h" - ModbusIP mb; ModbusDtuClass ModbusDtu; @@ -53,7 +52,7 @@ void ModbusDtuClass::loop() mb.Hreg(chan* 20 + 0x1001, (serialInv >> 24) & 0xFFFF); mb.Hreg(chan* 20 + 0x1002, (serialInv >> 8) & 0xFFFF); mb.Hreg(chan* 20 + 0x1003, (serialInv << 8) + c); - if(inv->Statistics()->getChannelMaxPower(c-1)>0 && inv->Statistics()->getChannelFieldValue(c, FLD_IRR) < 120){ + if(inv->Statistics()->getChannelMaxPower(c-1)>0 && inv->Statistics()->getChannelFieldValue(c, FLD_IRR) < 400){ mb.Hreg(chan* 20 + 0x1004, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_UDC)*10)); mb.Hreg(chan* 20 + 0x1005, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_IDC)*100)); mb.Hreg(chan* 20 + 0x1008, (uint16_t)(inv->Statistics()->getChannelFieldValue(c, FLD_PDC)*10)); diff --git a/src/WatchDog.cpp b/src/WatchDog.cpp new file mode 100644 index 000000000..0c6b8f5a8 --- /dev/null +++ b/src/WatchDog.cpp @@ -0,0 +1,22 @@ +#include + +//3 seconds WDT +#define WDT_TIMEOUT 30 +int last = millis(); + +WatchDogDtuClass WatchDogDtu; + +void WatchDogDtuClass::init(){ + esp_task_wdt_init(WDT_TIMEOUT, true); //enable panic so ESP32 restarts + esp_task_wdt_add(NULL); //add current thread to WDT watch + +} + + +void WatchDogDtuClass::loop() { + // resetting WDT every 2s, 5 times only + if (millis() - last >= 1000) { + esp_task_wdt_reset(); + last = millis(); + } +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index dea1256f3..6c90bea75 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ */ #include "Configuration.h" #include "ModbusDtu.h" +#include "WatchDogDtu.h" #include "Display_Graphic.h" #include "MessageOutput.h" #include "MqttHandleDtu.h" @@ -163,6 +164,8 @@ void setup() // Initialize Modbus MessageOutput.print(F("Initialize Modbus... ")); ModbusDtu.init(); + MessageOutput.print(F("Initialize WatchDog... ")); + WatchDogDtu.init(); MessageOutput.println(F("done")); } @@ -186,4 +189,6 @@ void loop() yield(); MessageOutput.loop(); yield(); + WatchDogDtu.loop(); + yield(); } \ No newline at end of file From 32b9750ced7d3c233bf6a6d1dda5ed2d7a894a49 Mon Sep 17 00:00:00 2001 From: Arkadiusz Kubacki Date: Wed, 8 Mar 2023 17:07:46 +0100 Subject: [PATCH 07/46] Update from master --- .gitignore | 1 + README.md | 19 +- docs/DeviceProfiles/wt32-eth01.json | 2 +- docs/MQTT_Topics.md | 1 + docs/Web-API.md | 606 +++++++------ include/Configuration.h | 13 +- include/InverterSettings.h | 15 + include/MqttHandleHass.h | 4 +- include/MqttHandleInverter.h | 6 +- include/SunPosition.h | 30 + include/Utils.h | 1 + include/WebApi.h | 2 + include/WebApi_prometheus.h | 27 +- include/WebApi_ws_live.h | 2 +- include/defaults.h | 2 + lib/Hoymiles/src/inverters/HM_1CH.cpp | 9 +- lib/Hoymiles/src/inverters/HM_1CH.h | 44 +- lib/Hoymiles/src/inverters/HM_2CH.cpp | 9 +- lib/Hoymiles/src/inverters/HM_2CH.h | 55 +- lib/Hoymiles/src/inverters/HM_4CH.cpp | 9 +- lib/Hoymiles/src/inverters/HM_4CH.h | 87 +- lib/Hoymiles/src/inverters/HM_Abstract.cpp | 34 +- .../src/inverters/InverterAbstract.cpp | 33 +- lib/Hoymiles/src/inverters/InverterAbstract.h | 13 +- lib/Hoymiles/src/parser/DevInfoParser.cpp | 1 + lib/Hoymiles/src/parser/StatisticsParser.cpp | 161 ++-- lib/Hoymiles/src/parser/StatisticsParser.h | 61 +- lib/ResetReason/src/ResetReason.cpp | 16 +- platformio.ini | 17 +- src/Configuration.cpp | 52 +- src/Display_Graphic.cpp | 13 +- src/InverterSettings.cpp | 82 ++ src/MqttHandleHass.cpp | 46 +- src/MqttHandleInverter.cpp | 43 +- src/NetworkSettings.cpp | 2 + src/SunPosition.cpp | 90 ++ src/Utils.cpp | 21 + src/WebApi.cpp | 7 + src/WebApi_inverter.cpp | 20 +- src/WebApi_ntp.cpp | 17 +- src/WebApi_prometheus.cpp | 154 ++-- src/WebApi_ws_live.cpp | 125 +-- src/main.cpp | 14 +- webapp/package.json | 20 +- webapp/src/components/DevInfo.vue | 22 + webapp/src/components/InputElement.vue | 2 + webapp/src/components/InverterChannelInfo.vue | 27 +- webapp/src/components/NavBar.vue | 17 +- webapp/src/components/RadioInfo.vue | 32 +- webapp/src/components/StatusBadge.vue | 32 + webapp/src/components/ThemeSwitcher.vue | 106 +++ webapp/src/components/WifiApInfo.vue | 10 +- webapp/src/components/WifiStationInfo.vue | 10 +- webapp/src/locales/de.json | 230 ++--- webapp/src/locales/en.json | 30 +- webapp/src/locales/fr.json | 48 +- webapp/src/locales/index.ts | 9 - webapp/src/main.ts | 3 +- webapp/src/types/DevInfoStatus.ts | 1 + webapp/src/types/LiveDataStatus.ts | 4 +- webapp/src/types/NtpConfig.ts | 2 + webapp/src/types/NtpStatus.ts | 3 + webapp/src/views/HomeView.vue | 24 +- webapp/src/views/InverterAdminView.vue | 64 +- webapp/src/views/MqttInfoView.vue | 67 +- webapp/src/views/NtpAdminView.vue | 10 + webapp/src/views/NtpInfoView.vue | 27 +- webapp/vite.config.ts | 11 +- webapp/yarn.lock | 841 +++++++++++------- webapp_dist/js/app.js.gz | Bin 153478 -> 151150 bytes 70 files changed, 2420 insertions(+), 1198 deletions(-) create mode 100644 include/InverterSettings.h create mode 100644 include/SunPosition.h create mode 100644 src/InverterSettings.cpp create mode 100644 src/SunPosition.cpp create mode 100644 webapp/src/components/StatusBadge.vue create mode 100644 webapp/src/components/ThemeSwitcher.vue diff --git a/.gitignore b/.gitignore index 8b14dd998..8d8cf7634 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ .vscode/settings.json platformio-device-monitor*.log platformio_override.ini +.DS_Store diff --git a/README.md b/README.md index dee06188c..458272cc6 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,16 @@ Firmware version seems to play not a significant role and cannot be read from th * Build with [Vue.js](https://vuejs.org) * Source is written in TypeScript +## Breaking changes +Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | grep BREAKING` +``` +* 3b7aef6 2023-02-13 BREAKING CHANGE: Web API! +* d4c838a 2023-02-06 BREAKING CHANGE: Prometheus API! +* daf847e 2022-11-14 BREAKING CHANGE: Removed deprecated config parsing method +* 69b675b 2022-11-01 BREAKING CHANGE: Structure WebAPI /api/livedata/status changed +* 27ed4e3 2022-10-31 BREAKING: Change power factor from percent value to value between 0 and 1 +``` + ## Hardware you need ### ESP32 board @@ -216,7 +226,15 @@ A documentation of the Web API can be found here: [Web-API Documentation](docs/W * * * +* +* +* +## Available layouts for printed circuit boards +* [BreakoutBoard - sample printed circuit board for OpenDTU and Ahoy](https://github.com/dokuhn/openDTU-BreakoutBoard) +* [Board for OpenDTU with Display](https://github.com/SteffMUC/openDTU_wDisplay2) +* [OpenDTU PCB mit Display](https://github.com/turrican944/OpenDTU-PCB) +* [PCB for OpenDTU in Cable Branchbox](https://github.com/plewka/ESP-Solar_OpenDTU) ## Building * Building the WebApp @@ -250,4 +268,3 @@ A documentation of the Web API can be found here: [Web-API Documentation](docs/W - [Ahoy](https://github.com/grindylow/ahoy) - [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles) - [OpenDTU extended to talk to Victrons MPPT battery chargers (Ve.Direct)](https://github.com/helgeerbe/OpenDTU_VeDirect) -- [BreakoutBoard - sample printed circuit board for OpenDTU and Ahoy](https://github.com/dokuhn/openDTU-BreakoutBoard) diff --git a/docs/DeviceProfiles/wt32-eth01.json b/docs/DeviceProfiles/wt32-eth01.json index ebf874e7f..971dec919 100644 --- a/docs/DeviceProfiles/wt32-eth01.json +++ b/docs/DeviceProfiles/wt32-eth01.json @@ -16,7 +16,7 @@ "mdc": 23, "mdio": 18, "type": 0, - "clk_mode": 3 + "clk_mode": 0 } } ] \ No newline at end of file diff --git a/docs/MQTT_Topics.md b/docs/MQTT_Topics.md index 2c6206e39..3445486e6 100644 --- a/docs/MQTT_Topics.md +++ b/docs/MQTT_Topics.md @@ -51,6 +51,7 @@ serial will be replaced with the serial number of the inverter. | Topic | R / W | Description | Value / Unit | | --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- | | [serial]/[1-4]/current | R | DC current of specific input in ampere | Ampere (A) | +| [serial]/[1-4]/name | R | Name of the DC input channel as configured in web GUI| | | [serial]/[1-4]/irradiation | R | Ratio DC Power over set maximum power (in web GUI) | % | | [serial]/[1-4]/power | R | DC power of specific input in watt | Watt (W) | | [serial]/[1-4]/voltage | R | DC voltage of specific input in volt | Volt (V) | diff --git a/docs/Web-API.md b/docs/Web-API.md index cd6407827..dbdd8a2a5 100644 --- a/docs/Web-API.md +++ b/docs/Web-API.md @@ -9,7 +9,9 @@ may be incomplete | -------- | --- | -- | | Get | yes | /api/config/get | | Post | yes | /api/config/delete | +| Get | yes | /api/config/list | | Post | yes | /api/config/upload | +| Get+Post | yes | /api/device/config | | Get | no | /api/devinfo/status | | Get+Post | yes | /api/dtu/config | | Get | no | /api/eventlog/status?inv=inverter-serialnumber | @@ -21,6 +23,7 @@ may be incomplete | Post | yes | /api/limit/config | | Get | no | /api/limit/status | | Get | no | /api/livedata/status | +| Post | yes | /api/maintenance/reboot | | Get+Post | yes | /api/mqtt/config | | Get | no | /api/mqtt/status | | Get+Post | yes | /api/network/config | @@ -30,7 +33,9 @@ may be incomplete | Get+Post | yes | /api/ntp/time | | Get | no | /api/power/status | | Post | yes | /api/power/config | -| Get+Post | yes | /api/security/password | +| Get | no | /api/prometheus/metrics | +| Get+Post | yes | /api/security/config | +| Get | yes | /api/security/authenticate | | Get | no | /api/system/status | @@ -46,6 +51,7 @@ may be incomplete - Other API calls use e.g. `/api/limit/status` to GET data and a different URL `/api/limit/config` to POST data. - If you want to investigate the web api communication, a good tool is [Postman](https://www.postman.com/) - Settings API require username and password provided with Basic Authentication credentials +- If you disable the readonly access to the web API, every endpoint requires authentication ### Get information @@ -56,7 +62,7 @@ You can "talk" to the OpenDTU with a command line tool like `curl`. The output i ``` ~$ curl http://192.168.10.10/api/livedata/status -{"inverters":[{"serial":"11418186xxxx","name":"HM600","data_age":4,"reachable":true,"producing":true,"limit_relative":100,"limit_absolute":600,"0":{"Power":{"v":70.69999695,"u":"W","d":1},"Voltage":{"v":233,"u":"V","d":1},"Current":{"v":0.300000012,"u":"A","d":2},"Power DC":{"v":74,"u":"W","d":2},"YieldDay":{"v":23,"u":"Wh","d":2},"YieldTotal":{"v":150.5050049,"u":"kWh","d":2},"Frequency":{"v":50.02000046,"u":"Hz","d":2},"Temperature":{"v":8.300000191,"u":"°C","d":1},"PowerFactor":{"v":1,"u":"","d":3},"ReactivePower":{"v":0.100000001,"u":"var","d":1},"Efficiency":{"v":95.54053497,"u":"%","d":2}},"1":{"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":1,"u":"V","d":1},"Current":{"v":0.02,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":49.0320015,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":2}},"2":{"Power":{"v":74,"u":"W","d":1},"Voltage":{"v":42.40000153,"u":"V","d":1},"Current":{"v":1.74000001,"u":"A","d":2},"YieldDay":{"v":23,"u":"Wh","d":0},"YieldTotal":{"v":101.4729996,"u":"kWh","d":3},"Irradiation":{"v":18.04878044,"u":"%","d":2}},"events":3},{"serial":"11418180xxxx","name":"HM800","data_age":11,"reachable":true,"producing":true,"limit_relative":100,"limit_absolute":800,"0":{"Power":{"v":70.09999847,"u":"W","d":1},"Voltage":{"v":233.1000061,"u":"V","d":1},"Current":{"v":0.300000012,"u":"A","d":2},"Power DC":{"v":73.59999847,"u":"W","d":2},"YieldDay":{"v":48,"u":"Wh","d":2},"YieldTotal":{"v":48.5399971,"u":"kWh","d":2},"Frequency":{"v":50.02000046,"u":"Hz","d":2},"Temperature":{"v":11.39999962,"u":"°C","d":1},"PowerFactor":{"v":1,"u":"","d":3},"ReactivePower":{"v":0.100000001,"u":"var","d":1},"Efficiency":{"v":95.24456024,"u":"%","d":2}},"1":{"Power":{"v":36.5,"u":"W","d":1},"Voltage":{"v":39.09999847,"u":"V","d":1},"Current":{"v":0.930000007,"u":"A","d":2},"YieldDay":{"v":31,"u":"Wh","d":0},"YieldTotal":{"v":4.301000118,"u":"kWh","d":3},"Irradiation":{"v":8.902439117,"u":"%","d":2}},"2":{"Power":{"v":37.09999847,"u":"W","d":1},"Voltage":{"v":40.79999924,"u":"V","d":1},"Current":{"v":0.910000026,"u":"A","d":2},"YieldDay":{"v":17,"u":"Wh","d":0},"YieldTotal":{"v":44.23899841,"u":"kWh","d":3},"Irradiation":{"v":9.048780441,"u":"%","d":2}},"events":1}],"total":{"Power":{"v":140.7999878,"u":"W","d":1},"YieldDay":{"v":71,"u":"Wh","d":0},"YieldTotal":{"v":199.0449982,"u":"kWh","d":2}}} +{"inverters":[{"serial":"11617160xxxx","name":"Meine Solaranlage","data_age":6983,"reachable":false,"producing":false,"limit_relative":0,"limit_absolute":-1,"AC":{"0":{"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"Power DC":{"v":0,"u":"W","d":1},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Frequency":{"v":0,"u":"Hz","d":2},"PowerFactor":{"v":0,"u":"","d":3},"ReactivePower":{"v":0,"u":"var","d":1},"Efficiency":{"v":0,"u":"%","d":3}}},"DC":{"0":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"1":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"2":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"3":{"name":{"u":""},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3}}},"INV":{"0":{"Temperature":{"v":0,"u":"°C","d":1}}},"events":0},{"serial":"11417160xxxx","name":"test","data_age":6983,"reachable":false,"producing":false,"limit_relative":0,"limit_absolute":-1,"AC":{"0":{"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"Power DC":{"v":0,"u":"W","d":1},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Frequency":{"v":0,"u":"Hz","d":2},"PowerFactor":{"v":0,"u":"","d":3},"ReactivePower":{"v":0,"u":"var","d":1},"Efficiency":{"v":0,"u":"%","d":3}}},"DC":{"0":{"name":{"u":"test 1"},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}},"1":{"name":{"u":"test 2"},"Power":{"v":0,"u":"W","d":1},"Voltage":{"v":0,"u":"V","d":1},"Current":{"v":0,"u":"A","d":2},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":3},"Irradiation":{"v":0,"u":"%","d":3}}},"INV":{"0":{"Temperature":{"v":0,"u":"°C","d":1}}},"events":0}],"total":{"Power":{"v":0,"u":"W","d":1},"YieldDay":{"v":0,"u":"Wh","d":0},"YieldTotal":{"v":0,"u":"kWh","d":2}},"hints":{"time_sync":false,"radio_problem":false,"default_password":false}} ``` @@ -67,284 +73,382 @@ To enhance readability (and filter information) use the JSON command line proces { "inverters": [ { - "serial": "11418186xxxx", - "name": "HM600", - "data_age": 4, - "reachable": true, - "producing": true, - "limit_relative": 100, - "limit_absolute": 600, - "0": { - "Power": { - "v": 70.69999695, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 233, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0.300000012, - "u": "A", - "d": 2 - }, - "Power DC": { - "v": 74, - "u": "W", - "d": 2 - }, - "YieldDay": { - "v": 23, - "u": "Wh", - "d": 2 - }, - "YieldTotal": { - "v": 150.5050049, - "u": "kWh", - "d": 2 - }, - "Frequency": { - "v": 50.02000046, - "u": "Hz", - "d": 2 - }, - "Temperature": { - "v": 8.300000191, - "u": "°C", - "d": 1 - }, - "PowerFactor": { - "v": 1, - "u": "", - "d": 3 - }, - "ReactivePower": { - "v": 0.100000001, - "u": "var", - "d": 1 - }, - "Efficiency": { - "v": 95.54053497, - "u": "%", - "d": 2 + "serial": "116171603546", + "name": "Meine Solaranlage", + "data_age": 7038, + "reachable": false, + "producing": false, + "limit_relative": 0, + "limit_absolute": -1, + "AC": { + "0": { + "Power": { + "v": 0, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 0, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0, + "u": "A", + "d": 2 + }, + "Power DC": { + "v": 0, + "u": "W", + "d": 1 + }, + "YieldDay": { + "v": 0, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 0, + "u": "kWh", + "d": 3 + }, + "Frequency": { + "v": 0, + "u": "Hz", + "d": 2 + }, + "PowerFactor": { + "v": 0, + "u": "", + "d": 3 + }, + "ReactivePower": { + "v": 0, + "u": "var", + "d": 1 + }, + "Efficiency": { + "v": 0, + "u": "%", + "d": 3 + } } }, - "1": { - "Power": { - "v": 0, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 1, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0.02, - "u": "A", - "d": 2 - }, - "YieldDay": { - "v": 0, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 49.0320015, - "u": "kWh", - "d": 3 - }, - "Irradiation": { - "v": 0, - "u": "%", - "d": 2 + "DC": { + "0": { + "name": { + "u": "" + }, + "Power": { + "v": 0, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 0, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0, + "u": "A", + "d": 2 + }, + "YieldDay": { + "v": 0, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 0, + "u": "kWh", + "d": 3 + }, + "Irradiation": { + "v": 0, + "u": "%", + "d": 3 + } + }, + "1": { + "name": { + "u": "" + }, + "Power": { + "v": 0, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 0, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0, + "u": "A", + "d": 2 + }, + "YieldDay": { + "v": 0, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 0, + "u": "kWh", + "d": 3 + }, + "Irradiation": { + "v": 0, + "u": "%", + "d": 3 + } + }, + "2": { + "name": { + "u": "" + }, + "Power": { + "v": 0, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 0, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0, + "u": "A", + "d": 2 + }, + "YieldDay": { + "v": 0, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 0, + "u": "kWh", + "d": 3 + }, + "Irradiation": { + "v": 0, + "u": "%", + "d": 3 + } + }, + "3": { + "name": { + "u": "" + }, + "Power": { + "v": 0, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 0, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0, + "u": "A", + "d": 2 + }, + "YieldDay": { + "v": 0, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 0, + "u": "kWh", + "d": 3 + } } }, - "2": { - "Power": { - "v": 74, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 42.40000153, - "u": "V", - "d": 1 - }, - "Current": { - "v": 1.74000001, - "u": "A", - "d": 2 - }, - "YieldDay": { - "v": 23, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 101.4729996, - "u": "kWh", - "d": 3 - }, - "Irradiation": { - "v": 18.04878044, - "u": "%", - "d": 2 + "INV": { + "0": { + "Temperature": { + "v": 0, + "u": "°C", + "d": 1 + } } }, - "events": 3 + "events": 0 }, { - "serial": "11418180xxxx", - "name": "HM800", - "data_age": 11, - "reachable": true, - "producing": true, - "limit_relative": 100, - "limit_absolute": 800, - "0": { - "Power": { - "v": 70.09999847, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 233.1000061, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0.300000012, - "u": "A", - "d": 2 - }, - "Power DC": { - "v": 73.59999847, - "u": "W", - "d": 2 - }, - "YieldDay": { - "v": 48, - "u": "Wh", - "d": 2 - }, - "YieldTotal": { - "v": 48.5399971, - "u": "kWh", - "d": 2 - }, - "Frequency": { - "v": 50.02000046, - "u": "Hz", - "d": 2 - }, - "Temperature": { - "v": 11.39999962, - "u": "°C", - "d": 1 - }, - "PowerFactor": { - "v": 1, - "u": "", - "d": 3 - }, - "ReactivePower": { - "v": 0.100000001, - "u": "var", - "d": 1 - }, - "Efficiency": { - "v": 95.24456024, - "u": "%", - "d": 2 + "serial": "114171603548", + "name": "test", + "data_age": 7038, + "reachable": false, + "producing": false, + "limit_relative": 0, + "limit_absolute": -1, + "AC": { + "0": { + "Power": { + "v": 0, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 0, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0, + "u": "A", + "d": 2 + }, + "Power DC": { + "v": 0, + "u": "W", + "d": 1 + }, + "YieldDay": { + "v": 0, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 0, + "u": "kWh", + "d": 3 + }, + "Frequency": { + "v": 0, + "u": "Hz", + "d": 2 + }, + "PowerFactor": { + "v": 0, + "u": "", + "d": 3 + }, + "ReactivePower": { + "v": 0, + "u": "var", + "d": 1 + }, + "Efficiency": { + "v": 0, + "u": "%", + "d": 3 + } } }, - "1": { - "Power": { - "v": 36.5, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 39.09999847, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0.930000007, - "u": "A", - "d": 2 - }, - "YieldDay": { - "v": 31, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 4.301000118, - "u": "kWh", - "d": 3 - }, - "Irradiation": { - "v": 8.902439117, - "u": "%", - "d": 2 + "DC": { + "0": { + "name": { + "u": "test 1" + }, + "Power": { + "v": 0, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 0, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0, + "u": "A", + "d": 2 + }, + "YieldDay": { + "v": 0, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 0, + "u": "kWh", + "d": 3 + }, + "Irradiation": { + "v": 0, + "u": "%", + "d": 3 + } + }, + "1": { + "name": { + "u": "test 2" + }, + "Power": { + "v": 0, + "u": "W", + "d": 1 + }, + "Voltage": { + "v": 0, + "u": "V", + "d": 1 + }, + "Current": { + "v": 0, + "u": "A", + "d": 2 + }, + "YieldDay": { + "v": 0, + "u": "Wh", + "d": 0 + }, + "YieldTotal": { + "v": 0, + "u": "kWh", + "d": 3 + }, + "Irradiation": { + "v": 0, + "u": "%", + "d": 3 + } } }, - "2": { - "Power": { - "v": 37.09999847, - "u": "W", - "d": 1 - }, - "Voltage": { - "v": 40.79999924, - "u": "V", - "d": 1 - }, - "Current": { - "v": 0.910000026, - "u": "A", - "d": 2 - }, - "YieldDay": { - "v": 17, - "u": "Wh", - "d": 0 - }, - "YieldTotal": { - "v": 44.23899841, - "u": "kWh", - "d": 3 - }, - "Irradiation": { - "v": 9.048780441, - "u": "%", - "d": 2 + "INV": { + "0": { + "Temperature": { + "v": 0, + "u": "°C", + "d": 1 + } } }, - "events": 1 + "events": 0 } ], "total": { "Power": { - "v": 140.7999878, + "v": 0, "u": "W", "d": 1 }, "YieldDay": { - "v": 71, + "v": 0, "u": "Wh", "d": 0 }, "YieldTotal": { - "v": 199.0449982, + "v": 0, "u": "kWh", "d": 2 } + }, + "hints": { + "time_sync": false, + "radio_problem": false, + "default_password": false } } ``` diff --git a/include/Configuration.h b/include/Configuration.h index 7f3d7db45..8b9e75a1c 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -4,9 +4,9 @@ #include #define CONFIG_FILENAME "/config.json" -#define CONFIG_VERSION 0x00011700 // 0.1.23 // make sure to clean all after change +#define CONFIG_VERSION 0x00011800 // 0.1.24 // make sure to clean all after change -#define WIFI_MAX_SSID_STRLEN 31 +#define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_PASSWORD_STRLEN 64 #define WIFI_MAX_HOSTNAME_STRLEN 31 @@ -29,16 +29,21 @@ #define DEV_MAX_MAPPING_NAME_STRLEN 63 -#define JSON_BUFFER_SIZE 6144 +#define JSON_BUFFER_SIZE 12288 struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; char Name[CHAN_MAX_NAME_STRLEN]; + float YieldTotalOffset; }; struct INVERTER_CONFIG_T { uint64_t Serial; char Name[INV_MAX_NAME_STRLEN + 1]; + bool Poll_Enable; + bool Poll_Enable_Night; + bool Command_Enable; + bool Command_Enable_Night; CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; @@ -59,6 +64,8 @@ struct CONFIG_T { char Ntp_Server[NTP_MAX_SERVER_STRLEN + 1]; char Ntp_Timezone[NTP_MAX_TIMEZONE_STRLEN + 1]; char Ntp_TimezoneDescr[NTP_MAX_TIMEZONEDESCR_STRLEN + 1]; + double Ntp_Longitude; + double Ntp_Latitude; bool Mqtt_Enabled; uint Mqtt_Port; diff --git a/include/InverterSettings.h b/include/InverterSettings.h new file mode 100644 index 000000000..188025b14 --- /dev/null +++ b/include/InverterSettings.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class InverterSettingsClass { +public: + void init(); + void loop(); + +private: + uint32_t _lastUpdate = 0; +}; + +extern InverterSettingsClass InverterSettings; \ No newline at end of file diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index 33d9df826..45d11cfbc 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -25,7 +25,7 @@ enum { const char* const stateClasses[] = { 0, "measurement", "total_increasing" }; typedef struct { - uint8_t fieldId; // field id + FieldId_t fieldId; // field id uint8_t deviceClsId; // device class uint8_t stateClsId; // state class } byteAssign_fieldDeviceClass_t; @@ -57,7 +57,7 @@ class MqttHandleHassClass { private: void publish(const String& subtopic, const String& payload); - void publishField(std::shared_ptr inv, uint8_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear = false); + void publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear = false); void publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload); void publishInverterNumber(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, int16_t min = 1, int16_t max = 100); void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); diff --git a/include/MqttHandleInverter.h b/include/MqttHandleInverter.h index a4e49ce41..9df43cb77 100644 --- a/include/MqttHandleInverter.h +++ b/include/MqttHandleInverter.h @@ -10,16 +10,16 @@ class MqttHandleInverterClass { void init(); void loop(); - static String getTopic(std::shared_ptr inv, uint8_t channel, uint8_t fieldId); + static String getTopic(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); private: - void publishField(std::shared_ptr inv, uint8_t channel, uint8_t fieldId); + void publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); uint32_t _lastPublishStats[INV_MAX_COUNT]; uint32_t _lastPublish; - uint8_t _publishFields[14] = { + FieldId_t _publishFields[14] = { FLD_UDC, FLD_IDC, FLD_PDC, diff --git a/include/SunPosition.h b/include/SunPosition.h new file mode 100644 index 000000000..49c5c71f2 --- /dev/null +++ b/include/SunPosition.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +#define SUNPOS_UPDATE_INTERVAL 60000l + +class SunPositionClass { +public: + SunPositionClass(); + void init(); + void loop(); + + bool isDayPeriod(); + bool sunsetTime(struct tm* info); + bool sunriseTime(struct tm* info); + +private: + void updateSunData(); + + SunSet _sun; + bool _isDayPeriod = true; + uint _sunriseMinutes = 0; + uint _sunsetMinutes = 0; + + uint32_t _lastUpdate = 0; + bool _isValidInfo = false; +}; + +extern SunPositionClass SunPosition; \ No newline at end of file diff --git a/include/Utils.h b/include/Utils.h index dbcea8196..33887ff96 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -7,4 +7,5 @@ class Utils { public: static uint32_t getChipId(); static uint64_t generateDtuSerial(); + static int getTimezoneOffset(); }; diff --git a/include/WebApi.h b/include/WebApi.h index 94275aa72..ed1f386b2 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -31,6 +31,8 @@ class WebApiClass { static bool checkCredentials(AsyncWebServerRequest* request); static bool checkCredentialsReadonly(AsyncWebServerRequest* request); + static void sendTooManyRequests(AsyncWebServerRequest* request); + private: AsyncWebServer _server; AsyncEventSource _events; diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h index 60e8e1026..5eb894eec 100644 --- a/include/WebApi_prometheus.h +++ b/include/WebApi_prometheus.h @@ -3,6 +3,7 @@ #include #include +#include class WebApiPrometheusClass { public: @@ -12,7 +13,31 @@ class WebApiPrometheusClass { private: void onPrometheusMetricsGet(AsyncWebServerRequest* request); - void addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, uint8_t channel, uint8_t fieldId, const char* channelName = NULL); + void addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* channelName = NULL); AsyncWebServer* _server; + + enum { + METRIC_TYPE_NONE = 0, + METRIC_TYPE_GAUGE, + METRIC_TYPE_COUNTER, + }; + const char* _metricTypes[3] = { 0, "gauge", "counter" }; + + std::map _fieldMetricAssignment { + { FLD_UDC, METRIC_TYPE_GAUGE }, + { FLD_IDC, METRIC_TYPE_GAUGE }, + { FLD_PDC, METRIC_TYPE_GAUGE }, + { FLD_YD, METRIC_TYPE_COUNTER }, + { FLD_YT, METRIC_TYPE_COUNTER }, + { FLD_UAC, METRIC_TYPE_GAUGE }, + { FLD_IAC, METRIC_TYPE_GAUGE }, + { FLD_PAC, METRIC_TYPE_GAUGE }, + { FLD_F, METRIC_TYPE_GAUGE }, + { FLD_T, METRIC_TYPE_GAUGE }, + { FLD_PF, METRIC_TYPE_GAUGE }, + { FLD_EFF, METRIC_TYPE_GAUGE }, + { FLD_IRR, METRIC_TYPE_GAUGE }, + { FLD_PRA, METRIC_TYPE_GAUGE } + }; }; \ No newline at end of file diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 5da186408..0cf1449b5 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -13,7 +13,7 @@ class WebApiWsLiveClass { private: void generateJsonResponse(JsonVariant& root); - void addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, uint8_t channel, uint8_t fieldId, String topic = ""); + void addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, String topic = ""); void addTotalField(JsonObject& root, String name, float value, String unit, uint8_t digits); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); diff --git a/include/defaults.h b/include/defaults.h index 69ec960b0..2a86d47a3 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -23,6 +23,8 @@ #define NTP_SERVER "pool.ntp.org" #define NTP_TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3" #define NTP_TIMEZONEDESCR "Europe/Berlin" +#define NTP_LONGITUDE 10.4515f +#define NTP_LATITUDE 51.1657f #define MQTT_ENABLED false #define MQTT_HOST "" diff --git a/lib/Hoymiles/src/inverters/HM_1CH.cpp b/lib/Hoymiles/src/inverters/HM_1CH.cpp index 246339c7b..7154611db 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_1CH.cpp @@ -32,12 +32,7 @@ String HM_1CH::typeName() return F("HM-300, HM-350, HM-400"); } -const byteAssign_t* HM_1CH::getByteAssignment() +const std::list* HM_1CH::getByteAssignment() { - return byteAssignment; -} - -uint8_t HM_1CH::getAssignmentCount() -{ - return sizeof(byteAssignment) / sizeof(byteAssign_t); + return &byteAssignment; } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_1CH.h b/lib/Hoymiles/src/inverters/HM_1CH.h index 6fb345f5c..941864670 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.h +++ b/lib/Hoymiles/src/inverters/HM_1CH.h @@ -2,35 +2,37 @@ #pragma once #include "HM_Abstract.h" +#include class HM_1CH : public HM_Abstract { public: explicit HM_1CH(uint64_t serial); static bool isValidSerial(uint64_t serial); String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getAssignmentCount(); + const std::list* getByteAssignment(); private: - const byteAssign_t byteAssignment[18] = { - { CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, - { CH1, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, - { CH1, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, - { CH1, FLD_YD, UNIT_WH, 12, 2, 1, false, 0 }, - { CH1, FLD_YT, UNIT_KWH, 8, 4, 1000, false, 3 }, - { CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + const std::list byteAssignment = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 12, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 8, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, - { CH0, FLD_UAC, UNIT_V, 14, 2, 10, false, 1 }, - { CH0, FLD_IAC, UNIT_A, 22, 2, 100, false, 2 }, - { CH0, FLD_PAC, UNIT_W, 18, 2, 10, false, 1 }, - { CH0, FLD_PRA, UNIT_VA, 20, 2, 10, false, 1 }, - { CH0, FLD_F, UNIT_HZ, 16, 2, 100, false, 2 }, - { CH0, FLD_PF, UNIT_NONE, 24, 2, 1000, false, 3 }, - { CH0, FLD_T, UNIT_C, 26, 2, 10, true, 1 }, - { CH0, FLD_EVT_LOG, UNIT_NONE, 28, 2, 1, false, 0 }, - { CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 14, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 22, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 18, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_PRA, UNIT_VA, 20, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 16, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 24, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 26, 2, 10, true, 1 }, + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 28, 2, 1, false, 0 }, + + { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, + { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, + { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, + { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_2CH.cpp b/lib/Hoymiles/src/inverters/HM_2CH.cpp index ef148bed2..f93746ee9 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_2CH.cpp @@ -33,12 +33,7 @@ String HM_2CH::typeName() return F("HM-600, HM-700, HM-800"); } -const byteAssign_t* HM_2CH::getByteAssignment() +const std::list* HM_2CH::getByteAssignment() { - return byteAssignment; -} - -uint8_t HM_2CH::getAssignmentCount() -{ - return sizeof(byteAssignment) / sizeof(byteAssign_t); + return &byteAssignment; } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_2CH.h b/lib/Hoymiles/src/inverters/HM_2CH.h index d20d43f2e..6e8672ca7 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.h +++ b/lib/Hoymiles/src/inverters/HM_2CH.h @@ -8,36 +8,37 @@ class HM_2CH : public HM_Abstract { explicit HM_2CH(uint64_t serial); static bool isValidSerial(uint64_t serial); String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getAssignmentCount(); + const std::list* getByteAssignment(); private: - const byteAssign_t byteAssignment[24] = { - { CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, - { CH1, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, - { CH1, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, - { CH1, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, - { CH1, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, - { CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + const std::list byteAssignment = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, - { CH2, FLD_UDC, UNIT_V, 8, 2, 10, false, 1 }, - { CH2, FLD_IDC, UNIT_A, 10, 2, 100, false, 2 }, - { CH2, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, - { CH2, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, - { CH2, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, - { CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_UDC, UNIT_V, 8, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_IDC, UNIT_A, 10, 2, 100, false, 2 }, + { TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, + { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, - { CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 }, - { CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 }, - { CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 }, - { CH0, FLD_PRA, UNIT_VA, 32, 2, 10, false, 1 }, - { CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 }, - { CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 }, - { CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, - { CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, - { CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_PRA, UNIT_VA, 32, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, + + { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, + { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, + { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, + { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_4CH.cpp b/lib/Hoymiles/src/inverters/HM_4CH.cpp index afffbeadc..9cf5a4aff 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -32,12 +32,7 @@ String HM_4CH::typeName() return F("HM-1000, HM-1200, HM-1500"); } -const byteAssign_t* HM_4CH::getByteAssignment() +const std::list* HM_4CH::getByteAssignment() { - return byteAssignment; -} - -uint8_t HM_4CH::getAssignmentCount() -{ - return sizeof(byteAssignment) / sizeof(byteAssign_t); + return &byteAssignment; } \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_4CH.h b/lib/Hoymiles/src/inverters/HM_4CH.h index 2a950f2ee..eccc60f27 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.h +++ b/lib/Hoymiles/src/inverters/HM_4CH.h @@ -8,50 +8,51 @@ class HM_4CH : public HM_Abstract { explicit HM_4CH(uint64_t serial); static bool isValidSerial(uint64_t serial); String typeName(); - const byteAssign_t* getByteAssignment(); - uint8_t getAssignmentCount(); + const std::list* getByteAssignment(); private: - const byteAssign_t byteAssignment[36] = { - { CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, - { CH1, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, - { CH1, FLD_PDC, UNIT_W, 8, 2, 10, false, 1 }, - { CH1, FLD_YD, UNIT_WH, 20, 2, 1, false, 0 }, - { CH1, FLD_YT, UNIT_KWH, 12, 4, 1000, false, 3 }, - { CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, - - { CH2, FLD_UDC, UNIT_V, CALC_UDC_CH, CH1, CMD_CALC, false, 1 }, - { CH2, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, - { CH2, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, - { CH2, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, - { CH2, FLD_YT, UNIT_KWH, 16, 4, 1000, false, 3 }, - { CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, - - { CH3, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, - { CH3, FLD_IDC, UNIT_A, 26, 2, 100, false, 2 }, - { CH3, FLD_PDC, UNIT_W, 30, 2, 10, false, 1 }, - { CH3, FLD_YD, UNIT_WH, 42, 2, 1, false, 0 }, - { CH3, FLD_YT, UNIT_KWH, 34, 4, 1000, false, 3 }, - { CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, - - { CH4, FLD_UDC, UNIT_V, CALC_UDC_CH, CH3, CMD_CALC, false, 1 }, - { CH4, FLD_IDC, UNIT_A, 28, 2, 100, false, 2 }, - { CH4, FLD_PDC, UNIT_W, 32, 2, 10, false, 1 }, - { CH4, FLD_YD, UNIT_WH, 44, 2, 1, false, 0 }, - { CH4, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, - { CH4, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH4, CMD_CALC, false, 3 }, - - { CH0, FLD_UAC, UNIT_V, 46, 2, 10, false, 1 }, - { CH0, FLD_IAC, UNIT_A, 54, 2, 100, false, 2 }, - { CH0, FLD_PAC, UNIT_W, 50, 2, 10, false, 1 }, - { CH0, FLD_PRA, UNIT_VA, 52, 2, 10, false, 1 }, - { CH0, FLD_F, UNIT_HZ, 48, 2, 100, false, 2 }, - { CH0, FLD_PF, UNIT_NONE, 56, 2, 1000, false, 3 }, - { CH0, FLD_T, UNIT_C, 58, 2, 10, true, 1 }, - { CH0, FLD_EVT_LOG, UNIT_NONE, 60, 2, 1, false, 0 }, - { CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + const std::list byteAssignment = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 4, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 8, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 20, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 12, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + + { TYPE_DC, CH1, FLD_UDC, UNIT_V, CALC_UDC_CH, CH0, CMD_CALC, false, 1 }, + { TYPE_DC, CH1, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, + { TYPE_DC, CH1, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 16, 4, 1000, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + + { TYPE_DC, CH2, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, + { TYPE_DC, CH2, FLD_IDC, UNIT_A, 26, 2, 100, false, 2 }, + { TYPE_DC, CH2, FLD_PDC, UNIT_W, 30, 2, 10, false, 1 }, + { TYPE_DC, CH2, FLD_YD, UNIT_WH, 42, 2, 1, false, 0 }, + { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 34, 4, 1000, false, 3 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + + { TYPE_DC, CH3, FLD_UDC, UNIT_V, CALC_UDC_CH, CH2, CMD_CALC, false, 1 }, + { TYPE_DC, CH3, FLD_IDC, UNIT_A, 28, 2, 100, false, 2 }, + { TYPE_DC, CH3, FLD_PDC, UNIT_W, 32, 2, 10, false, 1 }, + { TYPE_DC, CH3, FLD_YD, UNIT_WH, 44, 2, 1, false, 0 }, + { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 46, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 54, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 50, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_PRA, UNIT_VA, 52, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 48, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 56, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 58, 2, 10, true, 1 }, + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 60, 2, 1, false, 0 }, + + { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, + { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, + { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, + { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } }; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 73e02cee5..a07dec936 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -17,6 +17,10 @@ HM_Abstract::HM_Abstract(uint64_t serial) bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio) { + if (!getEnablePolling()) { + return false; + } + struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { return false; @@ -34,20 +38,24 @@ bool HM_Abstract::sendStatsRequest(HoymilesRadio* radio) bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force) { + if (!getEnablePolling()) { + return false; + } + struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { return false; } if (!force) { - if (Statistics()->hasChannelFieldValue(CH0, FLD_EVT_LOG)) { - if ((uint8_t)Statistics()->getChannelFieldValue(CH0, FLD_EVT_LOG) == _lastAlarmLogCnt) { + if (Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { + if ((uint8_t)Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG) == _lastAlarmLogCnt) { return false; } } } - _lastAlarmLogCnt = (uint8_t)Statistics()->getChannelFieldValue(CH0, FLD_EVT_LOG); + _lastAlarmLogCnt = (uint8_t)Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG); time_t now; time(&now); @@ -62,6 +70,10 @@ bool HM_Abstract::sendAlarmLogRequest(HoymilesRadio* radio, bool force) bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio) { + if (!getEnablePolling()) { + return false; + } + struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { return false; @@ -83,6 +95,10 @@ bool HM_Abstract::sendDevInfoRequest(HoymilesRadio* radio) bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio) { + if (!getEnablePolling()) { + return false; + } + struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { return false; @@ -101,6 +117,10 @@ bool HM_Abstract::sendSystemConfigParaRequest(HoymilesRadio* radio) bool HM_Abstract::sendActivePowerControlRequest(HoymilesRadio* radio, float limit, PowerLimitControlType type) { + if (!getEnableCommands()) { + return false; + } + if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) { limit = min(100, limit); } @@ -123,6 +143,10 @@ bool HM_Abstract::resendActivePowerControlRequest(HoymilesRadio* radio) bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) { + if (!getEnableCommands()) { + return false; + } + if (turnOn) { _powerState = 1; } else { @@ -139,6 +163,10 @@ bool HM_Abstract::sendPowerControlRequest(HoymilesRadio* radio, bool turnOn) bool HM_Abstract::sendRestartControlRequest(HoymilesRadio* radio) { + if (!getEnableCommands()) { + return false; + } + _powerState = 2; PowerControlCommand* cmd = radio->enqueCommand(); diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 30da998ad..8d84cac24 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -30,7 +30,7 @@ void InverterAbstract::init() // Not possible in constructor --> virtual function // Not possible in verifyAllFragments --> Because no data if nothing is ever received // It has to be executed because otherwise the getChannelCount method in stats always returns 0 - _statisticsParser.get()->setByteAssignment(getByteAssignment(), getAssignmentCount()); + _statisticsParser.get()->setByteAssignment(getByteAssignment()); } uint64_t InverterAbstract::serial() @@ -60,16 +60,39 @@ const char* InverterAbstract::name() bool InverterAbstract::isProducing() { - if (!Statistics()->hasChannelFieldValue(CH0, FLD_PAC)) { - return false; + float totalAc = 0; + for (auto& c : Statistics()->getChannelsByType(TYPE_AC)) { + if (Statistics()->hasChannelFieldValue(TYPE_AC, c, FLD_PAC)) { + totalAc += Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC); + } } - return Statistics()->getChannelFieldValue(CH0, FLD_PAC) > 0; + return _enablePolling && totalAc > 0; } bool InverterAbstract::isReachable() { - return Statistics()->getRxFailureCount() <= MAX_ONLINE_FAILURE_COUNT; + return _enablePolling && Statistics()->getRxFailureCount() <= MAX_ONLINE_FAILURE_COUNT; +} + +void InverterAbstract::setEnablePolling(bool enabled) +{ + _enablePolling = enabled; +} + +bool InverterAbstract::getEnablePolling() +{ + return _enablePolling; +} + +void InverterAbstract::setEnableCommands(bool enabled) +{ + _enableCommands = enabled; +} + +bool InverterAbstract::getEnableCommands() +{ + return _enableCommands; } AlarmLogParser* InverterAbstract::EventLog() diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 018e06d22..079bf7f41 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -11,6 +11,7 @@ #include "types.h" #include #include +#include #define MAX_NAME_LENGTH 32 @@ -38,12 +39,17 @@ class InverterAbstract { void setName(const char* name); const char* name(); virtual String typeName() = 0; - virtual const byteAssign_t* getByteAssignment() = 0; - virtual uint8_t getAssignmentCount() = 0; + virtual const std::list* getByteAssignment() = 0; bool isProducing(); bool isReachable(); + void setEnablePolling(bool enabled); + bool getEnablePolling(); + + void setEnableCommands(bool enabled); + bool getEnableCommands(); + void clearRxFragmentBuffer(); void addRxFragment(uint8_t fragment[], uint8_t len); uint8_t verifyAllFragments(CommandAbstract* cmd); @@ -73,6 +79,9 @@ class InverterAbstract { uint8_t _rxFragmentLastPacketId = 0; uint8_t _rxFragmentRetransmitCnt = 0; + bool _enablePolling = true; + bool _enableCommands = true; + std::unique_ptr _alarmLogParser; std::unique_ptr _devInfoParser; std::unique_ptr _powerCommandParser; diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index 364b11138..24ea88331 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -17,6 +17,7 @@ typedef struct { const devInfo_t devInfo[] = { { { 0x10, 0x10, 0x10, ALL }, 300, "HM-300" }, { { 0x10, 0x10, 0x20, ALL }, 350, "HM-350" }, + { { 0x10, 0x10, 0x30, ALL }, 400, "HM-400" }, { { 0x10, 0x10, 0x40, ALL }, 400, "HM-400" }, { { 0x10, 0x11, 0x10, ALL }, 600, "HM-600" }, { { 0x10, 0x11, 0x20, ALL }, 700, "HM-700" }, diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index be7ce046d..0af50eda5 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -28,10 +28,9 @@ const calcFunc_t calcFunctions[] = { { CALC_IRR_CH, &calcIrradiation } }; -void StatisticsParser::setByteAssignment(const byteAssign_t* byteAssignment, const uint8_t count) +void StatisticsParser::setByteAssignment(const std::list* byteAssignment) { _byteAssignment = byteAssignment; - _byteAssignmentCount = count; } void StatisticsParser::clearBuffer() @@ -50,31 +49,38 @@ void StatisticsParser::appendFragment(uint8_t offset, uint8_t* payload, uint8_t _statisticLength += len; } -uint8_t StatisticsParser::getAssignIdxByChannelField(uint8_t channel, uint8_t fieldId) +const byteAssign_t* StatisticsParser::getAssignmentByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { - const byteAssign_t* b = _byteAssignment; + for (auto const& i : *_byteAssignment) { + if (i.type == type && i.ch == channel && i.fieldId == fieldId) { + return &i; + } + } + return NULL; +} - uint8_t pos; - for (pos = 0; pos < _byteAssignmentCount; pos++) { - if (b[pos].ch == channel && b[pos].fieldId == fieldId) { - return pos; +fieldSettings_t* StatisticsParser::getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +{ + for (auto& i : _fieldSettings) { + if (i.type == type && i.ch == channel && i.fieldId == fieldId) { + return &i; } } - return 0xff; + return NULL; } -float StatisticsParser::getChannelFieldValue(uint8_t channel, uint8_t fieldId) +float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { - uint8_t pos = getAssignIdxByChannelField(channel, fieldId); - if (pos == 0xff) { + const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); + fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + + if (pos == NULL) { return 0; } - const byteAssign_t* b = _byteAssignment; - - uint8_t ptr = b[pos].start; - uint8_t end = ptr + b[pos].num; - uint16_t div = b[pos].div; + uint8_t ptr = pos->start; + uint8_t end = ptr + pos->num; + uint16_t div = pos->div; if (CMD_CALC != div) { // Value is a static value @@ -85,74 +91,105 @@ float StatisticsParser::getChannelFieldValue(uint8_t channel, uint8_t fieldId) } while (++ptr != end); float result; - if (b[pos].isSigned && b[pos].num == 2) { + if (pos->isSigned && pos->num == 2) { result = static_cast(static_cast(val)); - } else if (b[pos].isSigned && b[pos].num == 4) { + } else if (pos->isSigned && pos->num == 4) { result = static_cast(static_cast(val)); } else { result = static_cast(val); } result /= static_cast(div); + if (setting != NULL) { + result += setting->offset; + } return result; } else { // Value has to be calculated - return calcFunctions[b[pos].start].func(this, b[pos].num); + return calcFunctions[pos->start].func(this, pos->num); } return 0; } -bool StatisticsParser::hasChannelFieldValue(uint8_t channel, uint8_t fieldId) +bool StatisticsParser::hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +{ + const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); + return pos != NULL; +} + +const char* StatisticsParser::getChannelFieldUnit(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +{ + const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); + return units[pos->unitId]; +} + +const char* StatisticsParser::getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { - uint8_t pos = getAssignIdxByChannelField(channel, fieldId); - return pos != 0xff; + const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); + return fields[pos->fieldId]; } -const char* StatisticsParser::getChannelFieldUnit(uint8_t channel, uint8_t fieldId) +uint8_t StatisticsParser::getChannelFieldDigits(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { - uint8_t pos = getAssignIdxByChannelField(channel, fieldId); - const byteAssign_t* b = _byteAssignment; + const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); + return pos->digits; +} - return units[b[pos].unitId]; +float StatisticsParser::getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +{ + fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (setting != NULL) { + return setting->offset; + } + return 0; } -const char* StatisticsParser::getChannelFieldName(uint8_t channel, uint8_t fieldId) +void StatisticsParser::setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset) { - uint8_t pos = getAssignIdxByChannelField(channel, fieldId); - const byteAssign_t* b = _byteAssignment; + fieldSettings_t* setting = getSettingByChannelField(type, channel, fieldId); + if (setting != NULL) { + setting->offset = offset; + } else { + _fieldSettings.push_back({ type, channel, fieldId, offset }); + } +} - return fields[b[pos].fieldId]; +std::list StatisticsParser::getChannelTypes() +{ + return { + TYPE_AC, + TYPE_DC, + TYPE_INV + }; } -uint8_t StatisticsParser::getChannelFieldDigits(uint8_t channel, uint8_t fieldId) +const char* StatisticsParser::getChannelTypeName(ChannelType_t type) { - uint8_t pos = getAssignIdxByChannelField(channel, fieldId); - return _byteAssignment[pos].digits; + return channelsTypes[type]; } -uint8_t StatisticsParser::getChannelCount() +std::list StatisticsParser::getChannelsByType(ChannelType_t type) { - const byteAssign_t* b = _byteAssignment; - uint8_t cnt = 0; - for (uint8_t pos = 0; pos < _byteAssignmentCount; pos++) { - if (b[pos].ch > cnt) { - cnt = b[pos].ch; + std::list l; + for (auto const& b : *_byteAssignment) { + if (b.type == type) { + l.push_back(b.ch); } } - - return cnt; + l.unique(); + return l; } -uint16_t StatisticsParser::getChannelMaxPower(uint8_t channel) +uint16_t StatisticsParser::getStringMaxPower(uint8_t channel) { - return _chanMaxPower[channel]; + return _stringMaxPower[channel]; } -void StatisticsParser::setChannelMaxPower(uint8_t channel, uint16_t power) +void StatisticsParser::setStringMaxPower(uint8_t channel, uint16_t power) { - if (channel < CH4) { - _chanMaxPower[channel] = power; + if (channel < sizeof(_stringMaxPower) / sizeof(_stringMaxPower[0])) { + _stringMaxPower[channel] = power; } } @@ -174,8 +211,8 @@ uint32_t StatisticsParser::getRxFailureCount() static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0) { float yield = 0; - for (uint8_t i = 1; i <= iv->getChannelCount(); i++) { - yield += iv->getChannelFieldValue(i, FLD_YT); + for (auto& channel : iv->getChannelsByType(TYPE_DC)) { + yield += iv->getChannelFieldValue(TYPE_DC, channel, FLD_YT); } return yield; } @@ -183,8 +220,8 @@ static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0) static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0) { float yield = 0; - for (uint8_t i = 1; i <= iv->getChannelCount(); i++) { - yield += iv->getChannelFieldValue(i, FLD_YD); + for (auto& channel : iv->getChannelsByType(TYPE_DC)) { + yield += iv->getChannelFieldValue(TYPE_DC, channel, FLD_YD); } return yield; } @@ -192,14 +229,14 @@ static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0) // arg0 = channel of source static float calcUdcCh(StatisticsParser* iv, uint8_t arg0) { - return iv->getChannelFieldValue(arg0, FLD_UDC); + return iv->getChannelFieldValue(TYPE_DC, static_cast(arg0), FLD_UDC); } static float calcPowerDcCh0(StatisticsParser* iv, uint8_t arg0) { float dcPower = 0; - for (uint8_t i = 1; i <= iv->getChannelCount(); i++) { - dcPower += iv->getChannelFieldValue(i, FLD_PDC); + for (auto& channel : iv->getChannelsByType(TYPE_DC)) { + dcPower += iv->getChannelFieldValue(TYPE_DC, channel, FLD_PDC); } return dcPower; } @@ -207,15 +244,19 @@ static float calcPowerDcCh0(StatisticsParser* iv, uint8_t arg0) // arg0 = channel static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0) { - float acPower = iv->getChannelFieldValue(CH0, FLD_PAC); + float acPower = 0; + for (auto& channel : iv->getChannelsByType(TYPE_AC)) { + acPower += iv->getChannelFieldValue(TYPE_AC, channel, FLD_PAC); + } + float dcPower = 0; - for (uint8_t i = 1; i <= iv->getChannelCount(); i++) { - dcPower += iv->getChannelFieldValue(i, FLD_PDC); + for (auto& channel : iv->getChannelsByType(TYPE_DC)) { + dcPower += iv->getChannelFieldValue(TYPE_DC, channel, FLD_PDC); } + if (dcPower > 0) { return acPower / dcPower * 100.0f; } - return 0.0; } @@ -223,8 +264,8 @@ static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0) static float calcIrradiation(StatisticsParser* iv, uint8_t arg0) { if (NULL != iv) { - if (iv->getChannelMaxPower(arg0 - 1) > 0) - return iv->getChannelFieldValue(arg0, FLD_PDC) / iv->getChannelMaxPower(arg0 - 1) * 100.0f; + if (iv->getStringMaxPower(arg0) > 0) + return iv->getChannelFieldValue(TYPE_DC, static_cast(arg0), FLD_PDC) / iv->getStringMaxPower(arg0) * 100.0f; } return 0.0; } diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index e4defc94c..c52514ad0 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -3,11 +3,12 @@ #include "Parser.h" #include #include +#include #define STATISTIC_PACKET_SIZE (4 * 16) // units -enum { +enum UnitId_t { UNIT_V = 0, UNIT_A, UNIT_W, @@ -22,7 +23,7 @@ enum { const char* const units[] = { "V", "A", "W", "Wh", "kWh", "Hz", "°C", "%", "var", "" }; // field types -enum { +enum FieldId_t { FLD_UDC = 0, FLD_IDC, FLD_PDC, @@ -54,7 +55,7 @@ enum { enum { CMD_CALC = 0xffff }; // CH0 is default channel (freq, ac, temp) -enum { +enum ChannelNum_t { CH0 = 0, CH1, CH2, @@ -62,10 +63,18 @@ enum { CH4 }; +enum ChannelType_t { + TYPE_AC = 0, + TYPE_DC, + TYPE_INV +}; +const char* const channelsTypes[] = { "AC", "DC", "INV" }; + typedef struct { - uint8_t ch; // channel 0 - 4 - uint8_t fieldId; // field id - uint8_t unitId; // uint id + ChannelType_t type; + ChannelNum_t ch; // channel 0 - 4 + FieldId_t fieldId; // field id + UnitId_t unitId; // uint id uint8_t start; // pos of first byte in buffer uint8_t num; // number of bytes in buffer uint16_t div; // divisor / calc command @@ -73,24 +82,38 @@ typedef struct { uint8_t digits; // number of valid digits after the decimal point } byteAssign_t; +typedef struct { + ChannelType_t type; + ChannelNum_t ch; // channel 0 - 4 + FieldId_t fieldId; // field id + float offset; // offset (positive/negative) to be applied on the fetched value +} fieldSettings_t; + class StatisticsParser : public Parser { public: void clearBuffer(); void appendFragment(uint8_t offset, uint8_t* payload, uint8_t len); - void setByteAssignment(const byteAssign_t* byteAssignment, const uint8_t count); + void setByteAssignment(const std::list* byteAssignment); + + const byteAssign_t* getAssignmentByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + fieldSettings_t* getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + + float getChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + bool hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + const char* getChannelFieldUnit(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + uint8_t getChannelFieldDigits(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); - uint8_t getAssignIdxByChannelField(uint8_t channel, uint8_t fieldId); - float getChannelFieldValue(uint8_t channel, uint8_t fieldId); - bool hasChannelFieldValue(uint8_t channel, uint8_t fieldId); - const char* getChannelFieldUnit(uint8_t channel, uint8_t fieldId); - const char* getChannelFieldName(uint8_t channel, uint8_t fieldId); - uint8_t getChannelFieldDigits(uint8_t channel, uint8_t fieldId); + float getChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + void setChannelFieldOffset(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, float offset); - uint8_t getChannelCount(); + std::list getChannelTypes(); + const char* getChannelTypeName(ChannelType_t type); + std::list getChannelsByType(ChannelType_t type); - uint16_t getChannelMaxPower(uint8_t channel); - void setChannelMaxPower(uint8_t channel, uint16_t power); + uint16_t getStringMaxPower(uint8_t channel); + void setStringMaxPower(uint8_t channel, uint16_t power); void resetRxFailureCount(); void incrementRxFailureCount(); @@ -99,10 +122,10 @@ class StatisticsParser : public Parser { private: uint8_t _payloadStatistic[STATISTIC_PACKET_SIZE] = {}; uint8_t _statisticLength = 0; - uint16_t _chanMaxPower[CH4]; + uint16_t _stringMaxPower[CH4]; - const byteAssign_t* _byteAssignment; - uint8_t _byteAssignmentCount; + const std::list* _byteAssignment; + std::list _fieldSettings; uint32_t _rxFailureCount = 0; }; \ No newline at end of file diff --git a/lib/ResetReason/src/ResetReason.cpp b/lib/ResetReason/src/ResetReason.cpp index dbd627124..b3ceea5d6 100644 --- a/lib/ResetReason/src/ResetReason.cpp +++ b/lib/ResetReason/src/ResetReason.cpp @@ -34,7 +34,7 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id) case 3: reason_str = F("Software reset digital core"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) case 4: reason_str = F("Legacy watch dog reset digital core"); break; @@ -42,7 +42,7 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id) case 5: reason_str = F("Deep Sleep reset digital core"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) case 6: reason_str = F("Reset by SLC module, reset digital core"); break; @@ -68,9 +68,9 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id) case 13: reason_str = F("RTC Watch dog Reset CPU"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) case 14: - reason_str = F("for APP CPU, reseted by PRO CPU"); + reason_str = F("for APP CPU, reset by PRO CPU"); break; #endif case 15: @@ -100,7 +100,7 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) case 3: reason_str = F("SW_RESET"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) case 4: reason_str = F("OWDT_RESET"); break; @@ -108,7 +108,7 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) case 5: reason_str = F("DEEPSLEEP_RESET"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) case 6: reason_str = F("SDIO_RESET"); break; @@ -134,7 +134,7 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) case 13: reason_str = F("RTCWDT_CPU_RESET"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) && !defined(CONFIG_IDF_TARGET_ESP32S2) case 14: reason_str = F("EXT_CPU_RESET"); break; @@ -152,4 +152,4 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) return reason_str; } -ResetReasonClass ResetReason; \ No newline at end of file +ResetReasonClass ResetReason; diff --git a/platformio.ini b/platformio.ini index 161e6f992..a084aac57 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,7 +15,7 @@ extra_configs = [env] framework = arduino -platform = espressif32@>=6.0.0 +platform = espressif32@>=6.0.1 build_flags = -DCOMPONENT_EMBED_FILES=webapp_dist/index.html.gz:webapp_dist/zones.json.gz:webapp_dist/favicon.ico:webapp_dist/js/app.js.gz @@ -23,10 +23,11 @@ build_flags = lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer - bblanchon/ArduinoJson @ ^6.20.0 + bblanchon/ArduinoJson @ ^6.20.1 https://github.com/bertmelis/espMqttClient.git#v1.3.3 nrf24/RF24 @ ^1.4.5 olikraus/U8g2 @ ^2.34.13 + buelowp/sunset @ ^1.1.7 emelianov/modbus-esp8266@^4.1.0 extra_scripts = @@ -146,4 +147,14 @@ build_flags = ${env.build_flags} -DHOYMILES_PIN_SCLK=18 -DHOYMILES_PIN_IRQ=16 -DHOYMILES_PIN_CE=17 - -DHOYMILES_PIN_CS=5 \ No newline at end of file + -DHOYMILES_PIN_CS=5 + +[env:lolin_s2_mini] +board = lolin_s2_mini +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=13 + -DHOYMILES_PIN_MOSI=11 + -DHOYMILES_PIN_SCLK=12 + -DHOYMILES_PIN_CS=10 + -DHOYMILES_PIN_IRQ=4 + -DHOYMILES_PIN_CE=5 \ No newline at end of file diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 47142e732..5568d3428 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -44,6 +44,8 @@ bool ConfigurationClass::write() ntp["server"] = config.Ntp_Server; ntp["timezone"] = config.Ntp_Timezone; ntp["timezone_descr"] = config.Ntp_TimezoneDescr; + ntp["latitude"] = config.Ntp_Latitude; + ntp["longitude"] = config.Ntp_Longitude; JsonObject mqtt = doc.createNestedObject("mqtt"); mqtt["enabled"] = config.Mqtt_Enabled; @@ -53,7 +55,7 @@ bool ConfigurationClass::write() mqtt["password"] = config.Mqtt_Password; mqtt["topic"] = config.Mqtt_Topic; mqtt["retain"] = config.Mqtt_Retain; - mqtt["publish_invterval"] = config.Mqtt_PublishInterval; + mqtt["publish_interval"] = config.Mqtt_PublishInterval; JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); mqtt_lwt["topic"] = config.Mqtt_LwtTopic; @@ -94,12 +96,17 @@ bool ConfigurationClass::write() JsonObject inv = inverters.createNestedObject(); inv["serial"] = config.Inverter[i].Serial; inv["name"] = config.Inverter[i].Name; + inv["poll_enable"] = config.Inverter[i].Poll_Enable; + inv["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night; + inv["command_enable"] = config.Inverter[i].Command_Enable; + inv["command_enable_night"] = config.Inverter[i].Command_Enable_Night; JsonArray channel = inv.createNestedArray("channel"); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { JsonObject chanData = channel.createNestedObject(); chanData["name"] = config.Inverter[i].channel[c].Name; chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; + chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset; } } @@ -174,6 +181,8 @@ bool ConfigurationClass::read() strlcpy(config.Ntp_Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp_Server)); strlcpy(config.Ntp_Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp_Timezone)); strlcpy(config.Ntp_TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr)); + config.Ntp_Latitude = ntp["latitude"] | NTP_LATITUDE; + config.Ntp_Longitude = ntp["longitude"] | NTP_LONGITUDE; JsonObject mqtt = doc["mqtt"]; config.Mqtt_Enabled = mqtt["enabled"] | MQTT_ENABLED; @@ -183,7 +192,7 @@ bool ConfigurationClass::read() strlcpy(config.Mqtt_Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt_Password)); strlcpy(config.Mqtt_Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt_Topic)); config.Mqtt_Retain = mqtt["retain"] | MQTT_RETAIN; - config.Mqtt_PublishInterval = mqtt["publish_invterval"] | MQTT_PUBLISH_INTERVAL; + config.Mqtt_PublishInterval = mqtt["publish_interval"] | MQTT_PUBLISH_INTERVAL; JsonObject mqtt_lwt = mqtt["lwt"]; strlcpy(config.Mqtt_LwtTopic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); @@ -225,9 +234,15 @@ bool ConfigurationClass::read() config.Inverter[i].Serial = inv["serial"] | 0ULL; strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name)); + config.Inverter[i].Poll_Enable = inv["poll_enable"] | true; + config.Inverter[i].Poll_Enable_Night = inv["poll_enable_night"] | true; + config.Inverter[i].Command_Enable = inv["command_enable"] | true; + config.Inverter[i].Command_Enable_Night = inv["command_enable_night"] | true; + JsonArray channel = inv["channel"]; for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { config.Inverter[i].channel[c].MaxChannelPower = channel[c]["max_power"] | 0; + config.Inverter[i].channel[c].YieldTotalOffset = channel[c]["yield_total_offset"] | 0.0f; strlcpy(config.Inverter[i].channel[c].Name, channel[c]["name"] | "", sizeof(config.Inverter[i].channel[c].Name)); } } @@ -238,21 +253,21 @@ bool ConfigurationClass::read() void ConfigurationClass::migrate() { - if (config.Cfg_Version < 0x00011700) { - File f = LittleFS.open(CONFIG_FILENAME, "r", false); - if (!f) { - MessageOutput.println(F("Failed to open file, cancel migration")); - return; - } + File f = LittleFS.open(CONFIG_FILENAME, "r", false); + if (!f) { + MessageOutput.println(F("Failed to open file, cancel migration")); + return; + } - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - // Deserialize the JSON document - DeserializationError error = deserializeJson(doc, f); - if (error) { - MessageOutput.println(F("Failed to read file, cancel migration")); - return; - } + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, f); + if (error) { + MessageOutput.printf("Failed to read file, cancel migration: %s\r\n", error.c_str()); + return; + } + if (config.Cfg_Version < 0x00011700) { JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { JsonObject inv = inverters[i].as(); @@ -264,6 +279,13 @@ void ConfigurationClass::migrate() } } + if (config.Cfg_Version < 0x00011800) { + JsonObject mqtt = doc["mqtt"]; + config.Mqtt_PublishInterval = mqtt["publish_invterval"]; + } + + f.close(); + config.Cfg_Version = CONFIG_VERSION; write(); read(); diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index aa192b924..23c826ea4 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -37,7 +37,8 @@ std::map }; DisplayGraphicClass::DisplayGraphicClass() -{} +{ +} DisplayGraphicClass::~DisplayGraphicClass() { @@ -88,7 +89,7 @@ void DisplayGraphicClass::printText(const char* text, uint8_t line) break; } - // get the font height, to calculate the textheight + // get the font height, to calculate the textheight _dispY += (_display->getMaxCharHeight()) + 1; // calculate the starting position of the text @@ -126,9 +127,11 @@ void DisplayGraphicClass::loop() isprod++; } - totalPower += inv->Statistics()->getChannelFieldValue(CH0, FLD_PAC); - totalYieldDay += inv->Statistics()->getChannelFieldValue(CH0, FLD_YD); - totalYieldTotal += inv->Statistics()->getChannelFieldValue(CH0, FLD_YT); + for (auto& c : inv->Statistics()->getChannelsByType(TYPE_AC)) { + totalPower += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC); + totalYieldDay += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YD); + totalYieldTotal += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YT); + } } _display->clearBuffer(); diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp new file mode 100644 index 000000000..ebb354bbf --- /dev/null +++ b/src/InverterSettings.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "InverterSettings.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "PinMapping.h" +#include "SunPosition.h" +#include + +InverterSettingsClass InverterSettings; + +void InverterSettingsClass::init() +{ + const CONFIG_T& config = Configuration.get(); + const PinMapping_t& pin = PinMapping.get(); + + // Initialize inverter communication + MessageOutput.print(F("Initialize Hoymiles interface... ")); + if (PinMapping.isValidNrf24Config()) { + SPIClass* spiClass = new SPIClass(HSPI); + spiClass->begin(pin.nrf24_clk, pin.nrf24_miso, pin.nrf24_mosi, pin.nrf24_cs); + Hoymiles.setMessageOutput(&MessageOutput); + Hoymiles.init(spiClass, pin.nrf24_en, pin.nrf24_irq); + + MessageOutput.println(F(" Setting radio PA level... ")); + Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); + + MessageOutput.println(F(" Setting DTU serial... ")); + Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial); + + MessageOutput.println(F(" Setting poll interval... ")); + Hoymiles.setPollInterval(config.Dtu_PollInterval); + + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + if (config.Inverter[i].Serial > 0) { + MessageOutput.print(F(" Adding inverter: ")); + MessageOutput.print(config.Inverter[i].Serial, HEX); + MessageOutput.print(F(" - ")); + MessageOutput.print(config.Inverter[i].Name); + auto inv = Hoymiles.addInverter( + config.Inverter[i].Name, + config.Inverter[i].Serial); + + if (inv != nullptr) { + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + inv->Statistics()->setStringMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); + inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, config.Inverter[i].channel[c].YieldTotalOffset); + } + } + MessageOutput.println(F(" done")); + } + } + MessageOutput.println(F("done")); + } else { + MessageOutput.println(F("Invalid pin config")); + } +} + +void InverterSettingsClass::loop() +{ + if (millis() - _lastUpdate > SUNPOS_UPDATE_INTERVAL) { + const CONFIG_T& config = Configuration.get(); + + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + auto const& inv_cfg = config.Inverter[i]; + if (inv_cfg.Serial == 0) { + continue; + } + auto inv = Hoymiles.getInverterBySerial(inv_cfg.Serial); + if (inv == nullptr) { + continue; + } + + inv->setEnablePolling(inv_cfg.Poll_Enable && (SunPosition.isDayPeriod() || inv_cfg.Poll_Enable_Night)); + inv->setEnableCommands(inv_cfg.Command_Enable && (SunPosition.isDayPeriod() || inv_cfg.Command_Enable_Night)); + } + } + + Hoymiles.loop(); +} \ No newline at end of file diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 0eaed40b7..612e19ff6 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -65,13 +65,15 @@ void MqttHandleHassClass::publishConfig() publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0"); // Loop all channels - for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { - for (uint8_t f = 0; f < DEVICE_CLS_ASSIGN_LIST_LEN; f++) { - bool clear = false; - if (c > 0 && !config.Mqtt_Hass_IndividualPanels) { - clear = true; + for (auto& t : inv->Statistics()->getChannelTypes()) { + for (auto& c : inv->Statistics()->getChannelsByType(t)) { + for (uint8_t f = 0; f < DEVICE_CLS_ASSIGN_LIST_LEN; f++) { + bool clear = false; + if (t == TYPE_DC && !config.Mqtt_Hass_IndividualPanels) { + clear = true; + } + publishField(inv, t, c, deviceFieldAssignment[f], clear); } - publishField(inv, c, deviceFieldAssignment[f], clear); } } @@ -79,45 +81,53 @@ void MqttHandleHassClass::publishConfig() } } -void MqttHandleHassClass::publishField(std::shared_ptr inv, uint8_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear) +void MqttHandleHassClass::publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, byteAssign_fieldDeviceClass_t fieldType, bool clear) { - if (!inv->Statistics()->hasChannelFieldValue(channel, fieldType.fieldId)) { + if (!inv->Statistics()->hasChannelFieldValue(type, channel, fieldType.fieldId)) { return; } String serial = inv->serialString(); String fieldName; - if (channel == CH0 && fieldType.fieldId == FLD_PDC) { + if (type == TYPE_AC && fieldType.fieldId == FLD_PDC) { fieldName = "PowerDC"; } else { - fieldName = inv->Statistics()->getChannelFieldName(channel, fieldType.fieldId); + fieldName = inv->Statistics()->getChannelFieldName(type, channel, fieldType.fieldId); + } + + String chanNum; + if (type == TYPE_DC) { + // TODO(tbnobody) + chanNum = static_cast(channel) + 1; + } else { + chanNum = channel; } String configTopic = "sensor/dtu_" + serial - + "/" + "ch" + String(channel) + "_" + fieldName + + "/" + "ch" + chanNum + "_" + fieldName + "/config"; if (!clear) { - String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, channel, fieldType.fieldId); + String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId); const char* devCls = deviceClasses[fieldType.deviceClsId]; const char* stateCls = stateClasses[fieldType.stateClsId]; String name; - if (channel == CH0) { + if (type != TYPE_DC) { name = String(inv->name()) + " " + fieldName; } else { - name = String(inv->name()) + " CH" + String(channel) + " " + fieldName; + name = String(inv->name()) + " CH" + chanNum + " " + fieldName; } DynamicJsonDocument root(1024); root[F("name")] = name; root[F("stat_t")] = stateTopic; - root[F("uniq_id")] = serial + "_ch" + String(channel) + "_" + fieldName; + root[F("uniq_id")] = serial + "_ch" + chanNum + "_" + fieldName; - String unit_of_meausure = inv->Statistics()->getChannelFieldUnit(channel, fieldType.fieldId); - if (unit_of_meausure != "") { - root[F("unit_of_meas")] = unit_of_meausure; + String unit_of_measure = inv->Statistics()->getChannelFieldUnit(type, channel, fieldType.fieldId); + if (unit_of_measure != "") { + root[F("unit_of_meas")] = unit_of_measure; } JsonObject deviceObj = root.createNestedObject("dev"); diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index 46cc0d2b3..3703630c7 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -96,15 +96,18 @@ void MqttHandleInverterClass::loop() _lastPublishStats[i] = lastUpdate; // Loop all channels - for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { - if (c > 0) { - INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); - if (inv_cfg != nullptr) { - MqttSettings.publish(inv->serialString() + "/" + String(c) + "/name", inv_cfg->channel[c - 1].Name); + for (auto& t : inv->Statistics()->getChannelTypes()) { + for (auto& c : inv->Statistics()->getChannelsByType(t)) { + if (t == TYPE_DC) { + INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg != nullptr) { + // TODO(tbnobody) + MqttSettings.publish(inv->serialString() + "/" + String(static_cast(c) + 1) + "/name", inv_cfg->channel[c].Name); + } + } + for (uint8_t f = 0; f < sizeof(_publishFields) / sizeof(FieldId_t); f++) { + publishField(inv, t, c, _publishFields[f]); } - } - for (uint8_t f = 0; f < sizeof(_publishFields); f++) { - publishField(inv, c, _publishFields[f]); } } } @@ -116,31 +119,39 @@ void MqttHandleInverterClass::loop() } } -void MqttHandleInverterClass::publishField(std::shared_ptr inv, uint8_t channel, uint8_t fieldId) +void MqttHandleInverterClass::publishField(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { - String topic = getTopic(inv, channel, fieldId); + String topic = getTopic(inv, type, channel, fieldId); if (topic == "") { return; } - MqttSettings.publish(topic, String(inv->Statistics()->getChannelFieldValue(channel, fieldId))); + MqttSettings.publish(topic, String(inv->Statistics()->getChannelFieldValue(type, channel, fieldId))); } -String MqttHandleInverterClass::getTopic(std::shared_ptr inv, uint8_t channel, uint8_t fieldId) +String MqttHandleInverterClass::getTopic(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { - if (!inv->Statistics()->hasChannelFieldValue(channel, fieldId)) { + if (!inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { return String(""); } String chanName; - if (channel == 0 && fieldId == FLD_PDC) { + if (type == TYPE_AC && fieldId == FLD_PDC) { chanName = "powerdc"; } else { - chanName = inv->Statistics()->getChannelFieldName(channel, fieldId); + chanName = inv->Statistics()->getChannelFieldName(type, channel, fieldId); chanName.toLowerCase(); } - return inv->serialString() + "/" + String(channel) + "/" + chanName; + String chanNum; + if (type == TYPE_DC) { + // TODO(tbnobody) + chanNum = static_cast(channel) + 1; + } else { + chanNum = channel; + } + + return inv->serialString() + "/" + chanNum + "/" + chanName; } void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index 4e93bf954..a37b1fbf1 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -121,6 +121,8 @@ void NetworkSettingsClass::setupMode() dnsServer->stop(); dnsServerStatus = false; if (_networkMode == network_mode::WiFi) { + WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); + WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); WiFi.mode(WIFI_STA); } else { WiFi.mode(WIFI_MODE_NULL); diff --git a/src/SunPosition.cpp b/src/SunPosition.cpp new file mode 100644 index 000000000..1485eaa44 --- /dev/null +++ b/src/SunPosition.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2023 Thomas Basler and others + */ +#include "SunPosition.h" +#include "Configuration.h" +#include "Utils.h" + +SunPositionClass SunPosition; + +SunPositionClass::SunPositionClass() +{ +} + +void SunPositionClass::init() +{ +} + +void SunPositionClass::loop() +{ + if (millis() - _lastUpdate > SUNPOS_UPDATE_INTERVAL) { + updateSunData(); + _lastUpdate = millis(); + } +} + +bool SunPositionClass::isDayPeriod() +{ + return _isDayPeriod; +} + +void SunPositionClass::updateSunData() +{ + CONFIG_T const& config = Configuration.get(); + int offset = Utils::getTimezoneOffset() / 3600; + _sun.setPosition(config.Ntp_Latitude, config.Ntp_Longitude, offset); + + struct tm timeinfo; + if (!getLocalTime(&timeinfo, 5)) { + _isDayPeriod = false; + _sunriseMinutes = 0; + _sunsetMinutes = 0; + _isValidInfo = false; + return; + } + + _sun.setCurrentDate(1900 + timeinfo.tm_year, timeinfo.tm_mon + 1, timeinfo.tm_mday); + _sunriseMinutes = static_cast(_sun.calcCustomSunrise(SunSet::SUNSET_NAUTICAL)); + _sunsetMinutes = static_cast(_sun.calcCustomSunset(SunSet::SUNSET_NAUTICAL)); + uint minutesPastMidnight = timeinfo.tm_hour * 60 + timeinfo.tm_min; + + _isDayPeriod = (minutesPastMidnight >= _sunriseMinutes) && (minutesPastMidnight < _sunsetMinutes); + _isValidInfo = true; +} + +bool SunPositionClass::sunsetTime(struct tm* info) +{ + // Get today's date + time_t aTime = time(NULL); + + // Set the time to midnight + struct tm tm; + localtime_r(&aTime, &tm); + tm.tm_sec = 0; + tm.tm_min = _sunsetMinutes; + tm.tm_hour = 0; + tm.tm_isdst = -1; + time_t midnight = mktime(&tm); + + localtime_r(&midnight, info); + return _isValidInfo; +} + +bool SunPositionClass::sunriseTime(struct tm* info) +{ + // Get today's date + time_t aTime = time(NULL); + + // Set the time to midnight + struct tm tm; + localtime_r(&aTime, &tm); + tm.tm_sec = 0; + tm.tm_min = _sunriseMinutes; + tm.tm_hour = 0; + tm.tm_isdst = -1; + time_t midnight = mktime(&tm); + + localtime_r(&midnight, info); + return _isValidInfo; +} \ No newline at end of file diff --git a/src/Utils.cpp b/src/Utils.cpp index 7fd090d25..db8363ad3 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 - 2023 Thomas Basler and others + */ #include "Utils.h" #include @@ -31,4 +35,21 @@ uint64_t Utils::generateDtuSerial() } return dtuId; +} + +int Utils::getTimezoneOffset() +{ + // see: https://stackoverflow.com/questions/13804095/get-the-time-zone-gmt-offset-in-c/44063597#44063597 + + time_t gmt, rawtime = time(NULL); + struct tm* ptm; + + struct tm gbuf; + ptm = gmtime_r(&rawtime, &gbuf); + + // Request that mktime() looksup dst in timezone database + ptm->tm_isdst = -1; + gmt = mktime(ptm); + + return static_cast(difftime(rawtime, gmt)); } \ No newline at end of file diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 349fb58c3..cd5340902 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -90,4 +90,11 @@ bool WebApiClass::checkCredentialsReadonly(AsyncWebServerRequest* request) } } +void WebApiClass::sendTooManyRequests(AsyncWebServerRequest* request) +{ + auto response = request->beginResponse(429, "text/plain", "Too Many Requests"); + response->addHeader("Retry-After", "60"); + request->send(response); +} + WebApiClass WebApi; \ No newline at end of file diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index cda8f338b..f133167dd 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -51,6 +51,10 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) ((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)), ((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF))); obj[F("serial")] = buffer; + obj[F("poll_enable")] = config.Inverter[i].Poll_Enable; + obj[F("poll_enable_night")] = config.Inverter[i].Poll_Enable_Night; + obj[F("command_enable")] = config.Inverter[i].Command_Enable; + obj[F("command_enable_night")] = config.Inverter[i].Command_Enable_Night; auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); uint8_t max_channels; @@ -59,7 +63,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) max_channels = INV_MAX_CHAN_COUNT; } else { obj[F("type")] = inv->typeName(); - max_channels = inv->Statistics()->getChannelCount(); + max_channels = inv->Statistics()->getChannelsByType(TYPE_DC).size(); } JsonArray channel = obj.createNestedArray("channel"); @@ -67,6 +71,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) JsonObject chanData = channel.createNestedObject(); chanData["name"] = config.Inverter[i].channel[c].Name; chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; + chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset; } } } @@ -167,7 +172,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (inv != nullptr) { for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - inv->Statistics()->setChannelMaxPower(c, inverter->channel[c].MaxChannelPower); + inv->Statistics()->setStringMaxPower(c, inverter->channel[c].MaxChannelPower); } } @@ -267,7 +272,13 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) uint8_t arrayCount = 0; for (JsonVariant channel : channelArray) { inverter.channel[arrayCount].MaxChannelPower = channel[F("max_power")].as(); + inverter.channel[arrayCount].YieldTotalOffset = channel[F("yield_total_offset")].as(); strncpy(inverter.channel[arrayCount].Name, channel[F("name")] | "", sizeof(inverter.channel[arrayCount].Name)); + inverter.Poll_Enable = root[F("poll_enable")] | true; + inverter.Poll_Enable_Night = root[F("poll_enable_night")] | true; + inverter.Command_Enable = root[F("command_enable")] | true; + inverter.Command_Enable_Night = root[F("command_enable_night")] | true; + arrayCount++; } @@ -295,8 +306,11 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } if (inv != nullptr) { + inv->setEnablePolling(inverter.Poll_Enable); + inv->setEnableCommands(inverter.Command_Enable); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - inv->Statistics()->setChannelMaxPower(c, inverter.channel[c].MaxChannelPower); + inv->Statistics()->setStringMaxPower(c, inverter.channel[c].MaxChannelPower); + inv->Statistics()->setChannelFieldOffset(TYPE_DC, static_cast(c), FLD_YT, inverter.channel[c].YieldTotalOffset); } } diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index 53a92e87a..836247a03 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -5,6 +5,7 @@ #include "WebApi_ntp.h" #include "Configuration.h" #include "NtpSettings.h" +#include "SunPosition.h" #include "WebApi.h" #include "WebApi_errors.h" #include "helper.h" @@ -51,6 +52,16 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo); root[F("ntp_localtime")] = timeStringBuff; + SunPosition.sunriseTime(&timeinfo); + strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo); + root[F("sun_risetime")] = timeStringBuff; + + SunPosition.sunsetTime(&timeinfo); + strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo); + root[F("sun_settime")] = timeStringBuff; + + root[F("sun_isDayPeriod")] = SunPosition.isDayPeriod(); + response->setLength(); request->send(response); } @@ -68,6 +79,8 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) root[F("ntp_server")] = config.Ntp_Server; root[F("ntp_timezone")] = config.Ntp_Timezone; root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr; + root[F("longitude")] = config.Ntp_Longitude; + root[F("latitude")] = config.Ntp_Latitude; response->setLength(); request->send(response); @@ -112,7 +125,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) return; } - if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone"))) { + if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone") && root.containsKey("longitude") && root.containsKey("latitude"))) { retMsg[F("message")] = F("Values are missing!"); retMsg[F("code")] = WebApiError::GenericValueMissing; response->setLength(); @@ -151,6 +164,8 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) strlcpy(config.Ntp_Server, root[F("ntp_server")].as().c_str(), sizeof(config.Ntp_Server)); strlcpy(config.Ntp_Timezone, root[F("ntp_timezone")].as().c_str(), sizeof(config.Ntp_Timezone)); strlcpy(config.Ntp_TimezoneDescr, root[F("ntp_timezone_descr")].as().c_str(), sizeof(config.Ntp_TimezoneDescr)); + config.Ntp_Latitude = root[F("latitude")].as(); + config.Ntp_Longitude = root[F("longitude")].as(); Configuration.write(); retMsg[F("type")] = F("success"); diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index 45f23ff6a..8b59f9b72 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -5,7 +5,9 @@ */ #include "WebApi_prometheus.h" #include "Configuration.h" +#include "MessageOutput.h" #include "NetworkSettings.h" +#include "WebApi.h" #include void WebApiPrometheusClass::init(AsyncWebServer* server) @@ -23,79 +25,97 @@ void WebApiPrometheusClass::loop() void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* request) { - auto stream = request->beginResponseStream("text/plain; charset=utf-8", 40960); - - stream->print(F("# HELP opendtu_build Build info\n")); - stream->print(F("# TYPE opendtu_build gauge\n")); - stream->printf("opendtu_build{name=\"%s\",id=\"%s\",version=\"%d.%d.%d\"} 1\n", - NetworkSettings.getHostname().c_str(), AUTO_GIT_HASH, CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); - - stream->print(F("# HELP opendtu_platform Platform info\n")); - stream->print(F("# TYPE opendtu_platform gauge\n")); - stream->printf("opendtu_platform{arch=\"%s\",mac=\"%s\"} 1\n", ESP.getChipModel(), NetworkSettings.macAddress().c_str()); - - stream->print(F("# HELP opendtu_uptime Uptime in seconds\n")); - stream->print(F("# TYPE opendtu_uptime counter\n")); - stream->printf("opendtu_uptime %lld\n", esp_timer_get_time() / 1000000); - - stream->print(F("# HELP opendtu_heap_size System memory size\n")); - stream->print(F("# TYPE opendtu_heap_size gauge\n")); - stream->printf("opendtu_heap_size %zu\n", ESP.getHeapSize()); - - stream->print(F("# HELP opendtu_free_heap_size System free memory\n")); - stream->print(F("# TYPE opendtu_free_heap_size gauge\n")); - stream->printf("opendtu_free_heap_size %zu\n", ESP.getFreeHeap()); - - stream->print(F("# HELP wifi_rssi WiFi RSSI\n")); - stream->print(F("# TYPE wifi_rssi gauge\n")); - stream->printf("wifi_rssi %d\n", WiFi.RSSI()); - - for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { - auto inv = Hoymiles.getInverterByPos(i); - - String serial = inv->serialString(); - const char* name = inv->name(); - if (i == 0) { - stream->print(F("# HELP opendtu_last_update last update from inverter in s\n")); - stream->print(F("# TYPE opendtu_last_update gauge\n")); - } - stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%d\",name=\"%s\"} %d\n", - serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000); - - // Loop all channels - for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { - addField(stream, serial, i, inv, c, FLD_PAC); - addField(stream, serial, i, inv, c, FLD_UAC); - addField(stream, serial, i, inv, c, FLD_IAC); - if (c == 0) { - addField(stream, serial, i, inv, c, FLD_PDC, "PowerDC"); - } else { - addField(stream, serial, i, inv, c, FLD_PDC); + try { + auto stream = request->beginResponseStream("text/plain; charset=utf-8", 40960); + + stream->print(F("# HELP opendtu_build Build info\n")); + stream->print(F("# TYPE opendtu_build gauge\n")); + stream->printf("opendtu_build{name=\"%s\",id=\"%s\",version=\"%d.%d.%d\"} 1\n", + NetworkSettings.getHostname().c_str(), AUTO_GIT_HASH, CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); + + stream->print(F("# HELP opendtu_platform Platform info\n")); + stream->print(F("# TYPE opendtu_platform gauge\n")); + stream->printf("opendtu_platform{arch=\"%s\",mac=\"%s\"} 1\n", ESP.getChipModel(), NetworkSettings.macAddress().c_str()); + + stream->print(F("# HELP opendtu_uptime Uptime in seconds\n")); + stream->print(F("# TYPE opendtu_uptime counter\n")); + stream->printf("opendtu_uptime %lld\n", esp_timer_get_time() / 1000000); + + stream->print(F("# HELP opendtu_heap_size System memory size\n")); + stream->print(F("# TYPE opendtu_heap_size gauge\n")); + stream->printf("opendtu_heap_size %zu\n", ESP.getHeapSize()); + + stream->print(F("# HELP opendtu_free_heap_size System free memory\n")); + stream->print(F("# TYPE opendtu_free_heap_size gauge\n")); + stream->printf("opendtu_free_heap_size %zu\n", ESP.getFreeHeap()); + + stream->print(F("# HELP wifi_rssi WiFi RSSI\n")); + stream->print(F("# TYPE wifi_rssi gauge\n")); + stream->printf("wifi_rssi %d\n", WiFi.RSSI()); + + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + + String serial = inv->serialString(); + const char* name = inv->name(); + if (i == 0) { + stream->print(F("# HELP opendtu_last_update last update from inverter in s\n")); + stream->print(F("# TYPE opendtu_last_update gauge\n")); + } + stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%d\",name=\"%s\"} %d\n", + serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000); + + // Loop all channels if Statistics have been updated at least once since DTU boot + if (inv->Statistics()->getLastUpdate() > 0) { + for (auto& t : inv->Statistics()->getChannelTypes()) { + for (auto& c : inv->Statistics()->getChannelsByType(t)) { + addField(stream, serial, i, inv, t, c, FLD_PAC); + addField(stream, serial, i, inv, t, c, FLD_UAC); + addField(stream, serial, i, inv, t, c, FLD_IAC); + if (t == TYPE_AC) { + addField(stream, serial, i, inv, t, c, FLD_PDC, "PowerDC"); + } else { + addField(stream, serial, i, inv, t, c, FLD_PDC); + } + addField(stream, serial, i, inv, t, c, FLD_UDC); + addField(stream, serial, i, inv, t, c, FLD_IDC); + addField(stream, serial, i, inv, t, c, FLD_YD); + addField(stream, serial, i, inv, t, c, FLD_YT); + addField(stream, serial, i, inv, t, c, FLD_F); + addField(stream, serial, i, inv, t, c, FLD_T); + addField(stream, serial, i, inv, t, c, FLD_PF); + addField(stream, serial, i, inv, t, c, FLD_PRA); + addField(stream, serial, i, inv, t, c, FLD_EFF); + addField(stream, serial, i, inv, t, c, FLD_IRR); + } + } } - addField(stream, serial, i, inv, c, FLD_UDC); - addField(stream, serial, i, inv, c, FLD_IDC); - addField(stream, serial, i, inv, c, FLD_YD); - addField(stream, serial, i, inv, c, FLD_YT); - addField(stream, serial, i, inv, c, FLD_F); - addField(stream, serial, i, inv, c, FLD_T); - addField(stream, serial, i, inv, c, FLD_PF); - addField(stream, serial, i, inv, c, FLD_PRA); - addField(stream, serial, i, inv, c, FLD_EFF); - addField(stream, serial, i, inv, c, FLD_IRR); } + stream->addHeader(F("Cache-Control"), F("no-cache")); + request->send(stream); + + } catch (std::bad_alloc& bad_alloc) { + MessageOutput.printf("Call to /api/prometheus/metrics temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); + + WebApi.sendTooManyRequests(request); } - stream->addHeader(F("Cache-Control"), F("no-cache")); - request->send(stream); } -void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, uint8_t channel, uint8_t fieldId, const char* channelName) +void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* channelName) { - if (inv->Statistics()->hasChannelFieldValue(channel, fieldId)) { - const char* chanName = (channelName == NULL) ? inv->Statistics()->getChannelFieldName(channel, fieldId) : channelName; - if (idx == 0 && channel == 0) { - stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(channel, fieldId)); - stream->printf("# TYPE opendtu_%s gauge\n", chanName); + if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { + const char* chanName = (channelName == NULL) ? inv->Statistics()->getChannelFieldName(type, channel, fieldId) : channelName; + if (idx == 0 && type == TYPE_AC && channel == 0) { + stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId)); + stream->printf("# TYPE opendtu_%s %s\n", chanName, _metricTypes[_fieldMetricAssignment[fieldId]]); } - stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %f\n", chanName, serial.c_str(), idx, inv->name(), channel, inv->Statistics()->getChannelFieldValue(channel, fieldId)); + stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %f\n", + chanName, + serial.c_str(), + idx, + inv->name(), + inv->Statistics()->getChannelTypeName(type), + channel, + inv->Statistics()->getChannelFieldValue(type, channel, fieldId)); } } \ No newline at end of file diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index ae1ed1881..1a69d46d1 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -60,21 +60,26 @@ void WebApiWsLiveClass::loop() // Update on every inverter change or at least after 10 seconds if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) { - DynamicJsonDocument root(40960); - JsonVariant var = root; - generateJsonResponse(var); - - String buffer; - if (buffer) { - serializeJson(root, buffer); - - if (Configuration.get().Security_AllowReadonly) { - _ws.setAuthentication("", ""); - } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + try { + DynamicJsonDocument root(40960); + JsonVariant var = root; + generateJsonResponse(var); + + String buffer; + if (buffer) { + serializeJson(root, buffer); + + if (Configuration.get().Security_AllowReadonly) { + _ws.setAuthentication("", ""); + } else { + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + } + + _ws.textAll(buffer); } - _ws.textAll(buffer); + } catch (std::bad_alloc& bad_alloc) { + MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); } _lastWsPublish = millis(); @@ -111,36 +116,39 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) } // Loop all channels - for (uint8_t c = 0; c <= inv->Statistics()->getChannelCount(); c++) { - if (c > 0) { - INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); - if (inv_cfg != nullptr) { - invObject[String(c)][F("name")]["u"] = inv_cfg->channel[c - 1].Name; + for (auto& t : inv->Statistics()->getChannelTypes()) { + JsonObject chanTypeObj = invObject.createNestedObject(inv->Statistics()->getChannelTypeName(t)); + for (auto& c : inv->Statistics()->getChannelsByType(t)) { + if (t == TYPE_DC) { + INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg != nullptr) { + chanTypeObj[String(static_cast(c))][F("name")]["u"] = inv_cfg->channel[c].Name; + } + } + addField(chanTypeObj, i, inv, t, c, FLD_PAC); + addField(chanTypeObj, i, inv, t, c, FLD_UAC); + addField(chanTypeObj, i, inv, t, c, FLD_IAC); + if (t == TYPE_AC) { + addField(chanTypeObj, i, inv, t, c, FLD_PDC, F("Power DC")); + } else { + addField(chanTypeObj, i, inv, t, c, FLD_PDC); + } + addField(chanTypeObj, i, inv, t, c, FLD_UDC); + addField(chanTypeObj, i, inv, t, c, FLD_IDC); + addField(chanTypeObj, i, inv, t, c, FLD_YD); + addField(chanTypeObj, i, inv, t, c, FLD_YT); + addField(chanTypeObj, i, inv, t, c, FLD_F); + addField(chanTypeObj, i, inv, t, c, FLD_T); + addField(chanTypeObj, i, inv, t, c, FLD_PF); + addField(chanTypeObj, i, inv, t, c, FLD_PRA); + addField(chanTypeObj, i, inv, t, c, FLD_EFF); + if (t == TYPE_DC && inv->Statistics()->getStringMaxPower(c) > 0) { + addField(chanTypeObj, i, inv, t, c, FLD_IRR); } - } - addField(invObject, i, inv, c, FLD_PAC); - addField(invObject, i, inv, c, FLD_UAC); - addField(invObject, i, inv, c, FLD_IAC); - if (c == 0) { - addField(invObject, i, inv, c, FLD_PDC, F("Power DC")); - } else { - addField(invObject, i, inv, c, FLD_PDC); - } - addField(invObject, i, inv, c, FLD_UDC); - addField(invObject, i, inv, c, FLD_IDC); - addField(invObject, i, inv, c, FLD_YD); - addField(invObject, i, inv, c, FLD_YT); - addField(invObject, i, inv, c, FLD_F); - addField(invObject, i, inv, c, FLD_T); - addField(invObject, i, inv, c, FLD_PF); - addField(invObject, i, inv, c, FLD_PRA); - addField(invObject, i, inv, c, FLD_EFF); - if (c > 0 && inv->Statistics()->getChannelMaxPower(c - 1) > 0) { - addField(invObject, i, inv, c, FLD_IRR); } } - if (inv->Statistics()->hasChannelFieldValue(CH0, FLD_EVT_LOG)) { + if (inv->Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { invObject[F("events")] = inv->EventLog()->getEntryCount(); } else { invObject[F("events")] = -1; @@ -150,9 +158,11 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) _newestInverterTimestamp = inv->Statistics()->getLastUpdate(); } - totalPower += inv->Statistics()->getChannelFieldValue(CH0, FLD_PAC); - totalYieldDay += inv->Statistics()->getChannelFieldValue(CH0, FLD_YD); - totalYieldTotal += inv->Statistics()->getChannelFieldValue(CH0, FLD_YT); + for (auto& c : inv->Statistics()->getChannelsByType(TYPE_AC)) { + totalPower += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC); + totalYieldDay += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YD); + totalYieldTotal += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YT); + } } JsonObject totalObj = root.createNestedObject("total"); @@ -172,18 +182,20 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) } } -void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, uint8_t channel, uint8_t fieldId, String topic) +void WebApiWsLiveClass::addField(JsonObject& root, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, String topic) { - if (inv->Statistics()->hasChannelFieldValue(channel, fieldId)) { + if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { String chanName; if (topic == "") { - chanName = inv->Statistics()->getChannelFieldName(channel, fieldId); + chanName = inv->Statistics()->getChannelFieldName(type, channel, fieldId); } else { chanName = topic; } - root[String(channel)][chanName]["v"] = inv->Statistics()->getChannelFieldValue(channel, fieldId); - root[String(channel)][chanName]["u"] = inv->Statistics()->getChannelFieldUnit(channel, fieldId); - root[String(channel)][chanName]["d"] = inv->Statistics()->getChannelFieldDigits(channel, fieldId); + String chanNum; + chanNum = channel; + root[chanNum][chanName]["v"] = inv->Statistics()->getChannelFieldValue(type, channel, fieldId); + root[chanNum][chanName]["u"] = inv->Statistics()->getChannelFieldUnit(type, channel, fieldId); + root[chanNum][chanName]["d"] = inv->Statistics()->getChannelFieldDigits(type, channel, fieldId); } } @@ -213,11 +225,18 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 40960U); - JsonVariant root = response->getRoot(); + try { + AsyncJsonResponse* response = new AsyncJsonResponse(false, 40960U); + JsonVariant root = response->getRoot(); + + generateJsonResponse(root); - generateJsonResponse(root); + response->setLength(); + request->send(response); - response->setLength(); - request->send(response); + } catch (std::bad_alloc& bad_alloc) { + MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); + + WebApi.sendTooManyRequests(request); + } } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 6c90bea75..6ff7b3f54 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include "ModbusDtu.h" #include "WatchDogDtu.h" #include "Display_Graphic.h" +#include "InverterSettings.h" #include "MessageOutput.h" #include "MqttHandleDtu.h" #include "MqttHandleHass.h" @@ -14,11 +15,11 @@ #include "NetworkSettings.h" #include "NtpSettings.h" #include "PinMapping.h" +#include "SunPosition.h" #include "Utils.h" #include "WebApi.h" #include "defaults.h" #include -#include #include void setup() @@ -82,6 +83,11 @@ void setup() NtpSettings.init(); MessageOutput.println(F("done")); + // Initialize SunPosition + MessageOutput.print(F("Initialize SunPosition... ")); + SunPosition.init(); + MessageOutput.println(F("done")); + // Initialize MqTT MessageOutput.print(F("Initialize MqTT... ")); MqttSettings.init(); @@ -161,19 +167,21 @@ void setup() } else { MessageOutput.println(F("Invalid pin config")); } + // Initialize Modbus MessageOutput.print(F("Initialize Modbus... ")); ModbusDtu.init(); MessageOutput.print(F("Initialize WatchDog... ")); WatchDogDtu.init(); MessageOutput.println(F("done")); + InverterSettings.init(); } void loop() { NetworkSettings.loop(); yield(); - Hoymiles.loop(); + InverterSettings.loop(); yield(); ModbusDtu.loop(); yield(); @@ -187,6 +195,8 @@ void loop() yield(); Display.loop(); yield(); + SunPosition.loop(); + yield(); MessageOutput.loop(); yield(); WatchDogDtu.loop(); diff --git a/webapp/package.json b/webapp/package.json index e82162bef..538e91a75 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -12,30 +12,32 @@ }, "dependencies": { "@popperjs/core": "^2.11.6", - "bootstrap": "^5.2.3", + "bootstrap": "^5.3.0-alpha1", "bootstrap-icons-vue": "^1.8.1", "mitt": "^3.0.0", "spark-md5": "^3.0.2", - "vue": "^3.2.45", + "vue": "^3.2.47", "vue-i18n": "^9.2.2", "vue-router": "^4.1.6" }, "devDependencies": { + "@intlify/unplugin-vue-i18n": "^0.8.2", "@rushstack/eslint-patch": "^1.2.0", "@types/bootstrap": "^5.2.6", - "@types/node": "^18.11.18", + "@types/node": "^18.14.6", "@types/spark-md5": "^3.0.2", "@vitejs/plugin-vue": "^4.0.0", "@vue/eslint-config-typescript": "^11.0.2", "@vue/tsconfig": "^0.1.3", - "eslint": "^8.33.0", + "eslint": "^8.35.0", "eslint-plugin-vue": "^9.9.0", "npm-run-all": "^4.1.5", - "sass": "^1.57.1", - "typescript": "^4.9.4", - "vite": "^4.0.4", + "sass": "^1.58.3", + "terser": "^5.16.5", + "typescript": "^4.9.5", + "vite": "^4.1.4", "vite-plugin-compression": "^0.5.1", - "vite-plugin-css-injected-by-js": "^2.4.0", - "vue-tsc": "^1.0.24" + "vite-plugin-css-injected-by-js": "^3.0.1", + "vue-tsc": "^1.2.0" } } diff --git a/webapp/src/components/DevInfo.vue b/webapp/src/components/DevInfo.vue index 7e4f5fb9c..df049b7a4 100644 --- a/webapp/src/components/DevInfo.vue +++ b/webapp/src/components/DevInfo.vue @@ -6,6 +6,18 @@ + + + + + + + + + + + + @@ -61,6 +73,16 @@ export default defineComponent({ const version_patch = Math.floor((value - version_major * 10000 - version_minor * 100)); return version_major + "." + version_minor + "." + version_patch; }; + }, + productionYear() { + return() => { + return ((parseInt(this.devInfoList.serial.toString(), 16) >> (7 * 4)) & 0xF) + 2014; + } + }, + productionWeek() { + return() => { + return ((parseInt(this.devInfoList.serial.toString(), 16) >> (5 * 4)) & 0xFF).toString(16); + } } } }); diff --git a/webapp/src/components/InputElement.vue b/webapp/src/components/InputElement.vue index c2ab2b43b..eff8e9f66 100644 --- a/webapp/src/components/InputElement.vue +++ b/webapp/src/components/InputElement.vue @@ -27,6 +27,7 @@ :maxlength="maxlength" :min="min" :max="max" + :step="step" :disabled="disabled" :aria-describedby="descriptionId" /> @@ -69,6 +70,7 @@ export default defineComponent({ 'maxlength': String, 'min': String, 'max': String, + 'step': String, 'rows': String, 'disabled': Boolean, 'postfix': String, diff --git a/webapp/src/components/InverterChannelInfo.vue b/webapp/src/components/InverterChannelInfo.vue index 0025a3b88..d169f5a25 100644 --- a/webapp/src/components/InverterChannelInfo.vue +++ b/webapp/src/components/InverterChannelInfo.vue @@ -1,11 +1,21 @@ @@ -41,6 +51,7 @@ import { defineComponent, type PropType } from 'vue'; export default defineComponent({ props: { channelData: { type: Object as PropType, required: true }, + channelType: { type: String, required: true }, channelNumber: { type: Number, required: true }, }, }); diff --git a/webapp/src/components/NavBar.vue b/webapp/src/components/NavBar.vue index e82853016..79328fd76 100644 --- a/webapp/src/components/NavBar.vue +++ b/webapp/src/components/NavBar.vue @@ -1,5 +1,5 @@
{{ $t('devinfo.Serial') }}{{ devInfoList.serial }}
{{ $t('devinfo.ProdYear') }}{{ productionYear() }}
{{ $t('devinfo.ProdWeek') }}{{ productionWeek() }}
{{ $t('devinfo.Model') }} {{ devInfoList.hw_model_name }}