diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml index 7f539871..1d536b5e 100644 --- a/.github/workflows/compile_development.yml +++ b/.github/workflows/compile_development.yml @@ -23,16 +23,19 @@ jobs: strategy: matrix: variant: + - opendtufusion + - opendtufusion-ethernet - esp8266 + - esp8266-all + - esp8266-minimal - esp8266-prometheus - esp8285 - esp32-wroom32 + - esp32-wroom32-minimal - esp32-wroom32-prometheus - esp32-wroom32-ethernet - esp32-s2-mini - esp32-c3-mini - - opendtufusion - - opendtufusion-ethernet steps: - uses: actions/checkout@v4 - uses: benjlevesque/short-sha@v3.0 @@ -84,7 +87,10 @@ jobs: strategy: matrix: variant: + - opendtufusion-de + - opendtufusion-ethernet-de - esp8266-de + - esp8266-all-de - esp8266-prometheus-de - esp8285-de - esp32-wroom32-de @@ -92,8 +98,6 @@ jobs: - esp32-wroom32-ethernet-de - esp32-s2-mini-de - esp32-c3-mini-de - - opendtufusion-de - - opendtufusion-ethernet-de steps: - uses: actions/checkout@v4 - uses: benjlevesque/short-sha@v3.0 diff --git a/scripts/convertHtml.py b/scripts/convertHtml.py index c39e95ac..ec16b5f3 100644 --- a/scripts/convertHtml.py +++ b/scripts/convertHtml.py @@ -7,8 +7,37 @@ import json from datetime import date from pathlib import Path import subprocess +import configparser Import("env") +import htmlPreprocessorDefines as prepro + + + +def get_build_flags(): + config = configparser.ConfigParser() + config.read('platformio.ini') + global build_flags + build_flags = config["env:" + env['PIOENV']]['build_flags'].split('\n') + + for i in range(len(build_flags)): + build_flags[i] = build_flags[i][2:] + + # translate board + board = config["env:" + env['PIOENV']]['board'] + if board == "esp12e" or board == "esp8285": + build_flags.append("ESP8266") + elif board == "lolin_d32": + build_flags.append("ESP32") + elif board == "lolin_s2_mini": + build_flags.append("ESP32") + build_flags.append("ESP32-S2") + elif board == "lolin_c3_mini": + build_flags.append("ESP32") + build_flags.append("ESP32-C3") + elif board == "esp32-s3-devkitc-1": + build_flags.append("ESP32") + build_flags.append("ESP32-S3") def get_git_sha(): try: @@ -50,38 +79,46 @@ def readVersionFull(path): return version def htmlParts(file, header, nav, footer, versionPath, lang): - p = ""; f = open(file, "r") lines = f.readlines() f.close(); f = open(header, "r") - h = f.read().strip() + h = f.readlines() f.close() f = open(nav, "r") - n = f.read().strip() + n = f.readlines() f.close() f = open(footer, "r") - fo = f.read().strip() + fo = f.readlines() f.close() + linesExt = [] for line in lines: - line = line.replace("{#HTML_HEADER}", h) - line = line.replace("{#HTML_NAV}", n) - line = line.replace("{#HTML_FOOTER}", fo) - p += line + if line.find("{#HTML_HEADER}") != -1: + linesExt.extend(h) + elif line.find("{#HTML_NAV}") != -1: + linesExt.extend(n) + elif line.find("{#HTML_FOOTER}") != -1: + linesExt.extend(fo) + else: + linesExt.append(line) + + linesMod = prepro.conv(linesExt, build_flags) #placeholders version = readVersion(versionPath); link = 'GIT SHA: ' + get_git_sha() + ' :: ' + version + '' + p = "" + for line in linesMod: + p += line + p = p.replace("{#VERSION}", version) p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath)) p = p.replace("{#VERSION_GIT}", link) - # remove if - endif ESP32 - p = checkIf(p) p = translate(file, p, lang) p = translate("general", p, lang) # menu / header / footer @@ -90,30 +127,6 @@ def htmlParts(file, header, nav, footer, versionPath, lang): f.close(); return p -def checkIf(data): - if (env['PIOENV'][0:5] == "esp32") or env['PIOENV'][0:4] == "open": - data = data.replace("", "") - data = data.replace("", "") - data = data.replace("/*IF_ESP32*/", "") - data = data.replace("/*ENDIF_ESP32*/", "") - else: - while 1: - start = data.find("") - end = data.find("")+18 - if -1 == start: - break - else: - data = data[0:start] + data[end:] - while 1: - start = data.find("/*IF_ESP32*/") - end = data.find("/*ENDIF_ESP32*/")+15 - if -1 == start: - break - else: - data = data[0:start] + data[end:] - - return data - def findLang(file): with open('../lang.json') as j: lang = json.load(j) @@ -189,33 +202,41 @@ def convert2Header(inFile, versionPath, lang): f.write("#endif /*__{}_{}_H__*/\n".format(define, define2)) f.close() -# delete all files in the 'h' dir -wd = 'web/html/h' - -if os.path.exists(wd): - for f in os.listdir(wd): - os.remove(os.path.join(wd, f)) -wd += "/tmp" -if os.path.exists(wd): - for f in os.listdir(wd): - os.remove(os.path.join(wd, f)) - -# grab all files with following extensions -os.chdir('./web/html') -types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types -files_grabbed = [] -for files in types: - files_grabbed.extend(glob.glob(files)) - -Path("h").mkdir(exist_ok=True) -Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements -shutil.copyfile("style.css", "tmp/style.css") - -# get language from environment -lang = "en" -if env['PIOENV'][-3:] == "-de": - lang = "de" - -# go throw the array -for val in files_grabbed: - convert2Header(val, "../../defines.h", lang) + +def main(): + get_build_flags() + + # delete all files in the 'h' dir + wd = 'web/html/h' + + if os.path.exists(wd): + for f in os.listdir(wd): + os.remove(os.path.join(wd, f)) + wd += "/tmp" + if os.path.exists(wd): + for f in os.listdir(wd): + os.remove(os.path.join(wd, f)) + + # grab all files with following extensions + os.chdir('./web/html') + types = ('*.html', '*.css', '*.js', '*.ico', '*.json') # the tuple of file types + files_grabbed = [] + for files in types: + files_grabbed.extend(glob.glob(files)) + + Path("h").mkdir(exist_ok=True) + Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements + shutil.copyfile("style.css", "tmp/style.css") + + # get language from environment + lang = "en" + if env['PIOENV'][-3:] == "-de": + lang = "de" + + + # go throw the array + for val in files_grabbed: + convert2Header(val, "../../defines.h", lang) + + +main() diff --git a/scripts/htmlPreprocessorDefines.py b/scripts/htmlPreprocessorDefines.py new file mode 100644 index 00000000..f5d7cc31 --- /dev/null +++ b/scripts/htmlPreprocessorDefines.py @@ -0,0 +1,40 @@ +import re +import os +import queue + +def error(msg): + print("ERROR: " + msg) + exit() + +def check(inp, lst, pattern): + q = queue.LifoQueue() + out = [] + keep = True + for line in inp: + x = re.findall(pattern, line) + if len(x) > 0: + if line.find("ENDIF_") != -1: + if not q.empty(): + e = q.get() + if e[0] == x[0]: + keep = e[1] + elif line.find("IF_") != -1: + q.put((x[0], keep)) + if keep is True: + keep = x[0] in lst + elif line.find("E") != -1: + if q.empty(): + error("(ELSE) missing open statement!") + e = q.get() + q.put(e) + if e[1] is True: + keep = not keep + else: + if keep is True: + out.append(line) + return out + +def conv(inp, lst): + #print(lst) + out = check(inp, lst, r'\/\*(?:IF_|ELS|ENDIF_)([A-Z0-9\-_]+)?\*\/') + return check(out, lst, r'\<\!\-\-(?:IF_|ELS|ENDIF_)([A-Z0-9\-_]+)?\-\-\>') diff --git a/src/CHANGES.md b/src/CHANGES.md index f5aa9cd9..412c57d8 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,22 @@ # Development Changes +## 0.8.93 - 2024-03-14 +* improved history graph in WebUI #1491 +* merge PR: 1491 + +## 0.8.92 - 2024-03-10 +* fix read back of limit value, now with one decimal place +* added grid profile for Mexico #1493 +* added language to display on compile time #1484, #1255, #1479 +* added new environment `esp8266-all` which replace the original `esp8266`. The original now only have `MqTT` support but `Display` and `History` plugins are not included any more #1451 + +## 0.8.91 - 2024-03-05 +* fix javascript issues #1480 + +## 0.8.90 - 2024-03-05 +* added preprocessor defines to HTML (from platform.ini) to reduce the HTML in size if modules aren't enabled +* auto build minimal English versions of ESP8266 and ESP32 + ## 0.8.89 - 2024-03-02 * merge PR: Collection of small fixes #1465 * fix: show esp type on `/history` #1463 diff --git a/src/app.h b/src/app.h index b16d7aeb..4069662f 100644 --- a/src/app.h +++ b/src/app.h @@ -314,6 +314,14 @@ class app : public IApp, public ah::Scheduler { #endif } + uint32_t getHistoryPeriod(uint8_t type) override { + #if defined(ENABLE_HISTORY) + return mHistory.getPeriod((HistoryStorageType)type); + #else + return 0; + #endif + } + uint16_t getHistoryMaxDay() override { #if defined(ENABLE_HISTORY) return mHistory.getMaximumDay(); @@ -322,6 +330,21 @@ class app : public IApp, public ah::Scheduler { #endif } + uint32_t getHistoryLastValueTs(uint8_t type) override { + #if defined(ENABLE_HISTORY) + return mHistory.getLastValueTs((HistoryStorageType)type); + #else + return 0; + #endif + } + #if defined(ENABLE_HISTORY_LOAD_DATA) + void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) override { + #if defined(ENABLE_HISTORY) + return mHistory.addValue((HistoryStorageType)historyType, valueType, value); + #endif + } + #endif + private: #define CHECK_AVAIL true #define SKIP_YIELD_DAY true diff --git a/src/appInterface.h b/src/appInterface.h index 8a378272..b465edf9 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -63,8 +63,12 @@ class IApp { virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0; virtual uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0; + virtual uint32_t getHistoryPeriod(uint8_t type) = 0; virtual uint16_t getHistoryMaxDay() = 0; - + virtual uint32_t getHistoryLastValueTs(uint8_t type) = 0; + #if defined(ENABLE_HISTORY_LOAD_DATA) + virtual void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) = 0; + #endif virtual void* getRadioObj(bool nrf) = 0; }; diff --git a/src/config/config.h b/src/config/config.h index 9e59f146..23e27a8c 100644 --- a/src/config/config.h +++ b/src/config/config.h @@ -77,6 +77,9 @@ #ifndef DEF_ETH_CS_PIN #define DEF_ETH_CS_PIN 15 #endif + #ifndef DEF_ETH_RST_PIN + #define DEF_ETH_RST_PIN 2 + #endif #else /* defined(ETHERNET) */ // time in seconds how long the station info (ssid + pwd) will be tried #define WIFI_TRY_CONNECT_TIME 30 diff --git a/src/defines.h b/src/defines.h index b7ee4406..8fcdc000 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 8 -#define VERSION_PATCH 89 +#define VERSION_PATCH 93 //------------------------------------- typedef struct { diff --git a/src/eth/ahoyeth.cpp b/src/eth/ahoyeth.cpp index 402abfc2..9dc0fb36 100644 --- a/src/eth/ahoyeth.cpp +++ b/src/eth/ahoyeth.cpp @@ -31,12 +31,12 @@ void ahoyeth::setup(settings_t *config, uint32_t *utcTimestamp, OnNetworkCB onNe WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void { this->onEthernetEvent(event, info); }); Serial.flush(); - #if defined(CONFIG_IDF_TARGET_ESP32S3) + //#if defined(CONFIG_IDF_TARGET_ESP32S3) mEthSpi.begin(DEF_ETH_MISO_PIN, DEF_ETH_MOSI_PIN, DEF_ETH_SCK_PIN, DEF_ETH_CS_PIN, DEF_ETH_IRQ_PIN, DEF_ETH_RST_PIN); - #else + //#else //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_TYPE, ETH_CLK_MODE); - ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, DEF_ETH_MOSI_PIN, ETH_PHY_TYPE, ETH_CLK_MODE); - #endif + //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, DEF_ETH_MOSI_PIN, ETH_PHY_TYPE, ETH_CLK_MODE); + //#endif if(mConfig->sys.ip.ip[0] != 0) { IPAddress ip(mConfig->sys.ip.ip); diff --git a/src/eth/ahoyeth.h b/src/eth/ahoyeth.h index b431ce33..557db5ec 100644 --- a/src/eth/ahoyeth.h +++ b/src/eth/ahoyeth.h @@ -46,9 +46,9 @@ class ahoyeth { void onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info); private: - #if defined(CONFIG_IDF_TARGET_ESP32S3) + //#if defined(CONFIG_IDF_TARGET_ESP32S3) EthSpi mEthSpi; - #endif + //#endif settings_t *mConfig = nullptr; uint32_t *mUtcTimestamp; diff --git a/src/eth/ethSpi.h b/src/eth/ethSpi.h index d0ef9487..1339c8ec 100644 --- a/src/eth/ethSpi.h +++ b/src/eth/ethSpi.h @@ -1,10 +1,8 @@ //----------------------------------------------------------------------------- -// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 +// 2024 Ahoy, https://www.mikrocontroller.net/topic/525778 // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ //----------------------------------------------------------------------------- - -#if defined(CONFIG_IDF_TARGET_ESP32S3) #if defined(ETHERNET) #ifndef __ETH_SPI_H__ #define __ETH_SPI_H__ @@ -138,4 +136,3 @@ class EthSpi { #endif /*__ETH_SPI_H__*/ #endif /*ETHERNET*/ -#endif /*CONFIG_IDF_TARGET_ESP32S3*/ diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h index 4252800d..1d7e6620 100644 --- a/src/hm/hmInverter.h +++ b/src/hm/hmInverter.h @@ -335,7 +335,7 @@ class Inverter { // eg. hw version ... } else if (rec->assign == SystemConfigParaAssignment) { DPRINTLN(DBG_DEBUG, "add config"); - if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ + if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos) { actPowerLimit = rec->record[pos]; DPRINT(DBG_DEBUG, F("Inverter actual power limit: ")); DPRINTLN(DBG_DEBUG, String(actPowerLimit, 1)); diff --git a/src/platformio.ini b/src/platformio.ini index d3b884b5..869f74c0 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -44,6 +44,29 @@ build_unflags = platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L +build_flags = ${env.build_flags} + -DEMC_MIN_FREE_MEMORY=4096 + -DENABLE_MQTT + ;-Wl,-Map,output.map +monitor_filters = + esp8266_exception_decoder + +[env:esp8266-de] +platform = espressif8266 +board = esp12e +board_build.f_cpu = 80000000L +build_flags = ${env.build_flags} + -DEMC_MIN_FREE_MEMORY=4096 + -DLANG_DE + -DENABLE_MQTT + ;-Wl,-Map,output.map +monitor_filters = + esp8266_exception_decoder + +[env:esp8266-all] +platform = espressif8266 +board = esp12e +board_build.f_cpu = 80000000L build_flags = ${env.build_flags} -DEMC_MIN_FREE_MEMORY=4096 -DENABLE_MQTT @@ -53,7 +76,7 @@ build_flags = ${env.build_flags} monitor_filters = esp8266_exception_decoder -[env:esp8266-de] +[env:esp8266-all-de] platform = espressif8266 board = esp12e board_build.f_cpu = 80000000L @@ -191,7 +214,7 @@ monitor_filters = [env:esp32-wroom32-ethernet] platform = espressif32 -board = esp32dev +board = lolin_d32 build_flags = ${env.build_flags} -D ETHERNET -DRELEASE @@ -199,12 +222,24 @@ build_flags = ${env.build_flags} -DENABLE_MQTT -DPLUGIN_DISPLAY -DENABLE_HISTORY + -DDEF_ETH_CS_PIN=15 + -DDEF_ETH_SCK_PIN=14 + -DDEF_ETH_MISO_PIN=12 + -DDEF_ETH_MOSI_PIN=13 + -DDEF_ETH_IRQ_PIN=4 + -DDEF_ETH_RST_PIN=2 + -DDEF_NRF_CS_PIN=5 + -DDEF_NRF_CE_PIN=17 + -DDEF_NRF_IRQ_PIN=16 + -DDEF_NRF_MISO_PIN=19 + -DDEF_NRF_MOSI_PIN=23 + -DDEF_NRF_SCLK_PIN=18 monitor_filters = esp32_exception_decoder [env:esp32-wroom32-ethernet-de] platform = espressif32 -board = esp32dev +board = lolin_d32 build_flags = ${env.build_flags} -D ETHERNET -DRELEASE @@ -213,6 +248,18 @@ build_flags = ${env.build_flags} -DENABLE_MQTT -DPLUGIN_DISPLAY -DENABLE_HISTORY + -DDEF_ETH_CS_PIN=15 + -DDEF_ETH_SCK_PIN=14 + -DDEF_ETH_MISO_PIN=12 + -DDEF_ETH_MOSI_PIN=13 + -DDEF_ETH_IRQ_PIN=4 + -DDEF_ETH_RST_PIN=2 + -DDEF_NRF_CS_PIN=5 + -DDEF_NRF_CE_PIN=17 + -DDEF_NRF_IRQ_PIN=16 + -DDEF_NRF_MISO_PIN=19 + -DDEF_NRF_MOSI_PIN=23 + -DDEF_NRF_SCLK_PIN=18 monitor_filters = esp32_exception_decoder diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index b2c88b05..e263d667 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -9,6 +9,7 @@ #include "../../hm/hmSystem.h" #include "../../hm/hmRadio.h" #include "../../utils/helper.h" +#include "../plugin_lang.h" #include "Display_Mono.h" #include "Display_Mono_128X32.h" #include "Display_Mono_128X64.h" diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h index 6262e3f6..def87507 100644 --- a/src/plugins/Display/Display_Mono_128X32.h +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -40,20 +40,20 @@ class DisplayMono128X32 : public DisplayMono { printText(mFmtText, 0); } else { - printText("offline", 0); + printText(STR_OFFLINE, 0); } - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", mDisplayData->totalYieldDay); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %4.0f Wh", STR_TODAY, mDisplayData->totalYieldDay); printText(mFmtText, 1); - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", mDisplayData->totalYieldTotal); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %.1f kWh", STR_TOTAL, mDisplayData->totalYieldTotal); printText(mFmtText, 2); IPAddress ip = WiFi.localIP(); if (!(mExtra % 10) && (ip)) printText(ip.toString().c_str(), 3); else if (!(mExtra % 5)) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", mDisplayData->nrProducing); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %d", STR_ACTIVE_INVERTERS, mDisplayData->nrProducing); printText(mFmtText, 3); } else if (0 != mDisplayData->utcTs) printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3); diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h index c63f0b22..c93c5c1a 100644 --- a/src/plugins/Display/Display_Mono_128X64.h +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -93,7 +93,7 @@ class DisplayMono128X64 : public DisplayMono { // print Date and time if (0 != mDisplayData->utcTs) - printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff); + printText(ah::getDateTimeStrShort_i18n(mDisplayData->utcTs).c_str(), l_Time, 0xff); if (showLine(l_Status)) { // alternatively: @@ -108,7 +108,7 @@ class DisplayMono128X64 : public DisplayMono { int8_t moon_pos = -1; setLineFont(l_Status); if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, STR_NO_INVERTER); else if (0 == mDisplayData->nrSleeping) { snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); sun_pos = 0; @@ -145,7 +145,7 @@ class DisplayMono128X64 : public DisplayMono { printText(mFmtText, l_TotalPower, 0xff); } else { - printText("offline", l_TotalPower, 0xff); + printText(STR_OFFLINE, l_TotalPower, 0xff); } } diff --git a/src/plugins/Display/Display_Mono_64X48.h b/src/plugins/Display/Display_Mono_64X48.h index 7f98cae5..799d787e 100644 --- a/src/plugins/Display/Display_Mono_64X48.h +++ b/src/plugins/Display/Display_Mono_64X48.h @@ -42,7 +42,7 @@ class DisplayMono64X48 : public DisplayMono { printText(mFmtText, 0); } else { - printText("offline", 0); + printText(STR_OFFLINE, 0); } snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", mDisplayData->totalYieldDay); @@ -55,7 +55,7 @@ class DisplayMono64X48 : public DisplayMono { if (!(mExtra % 10) && (ip)) printText(ip.toString().c_str(), 3); else if (!(mExtra % 5)) { - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "active Inv: %d", mDisplayData->nrProducing); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%s: %d", STR_ACTIVE_INVERTERS, mDisplayData->nrProducing); printText(mFmtText, 3); } else if (0 != mDisplayData->utcTs) printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3); diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h index b5daacd5..ccbc8083 100644 --- a/src/plugins/Display/Display_Mono_84X48.h +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -78,7 +78,7 @@ class DisplayMono84X48 : public DisplayMono { // print Date and time if (0 != mDisplayData->utcTs) - printText(ah::getDateTimeStrShort(mDisplayData->utcTs).c_str(), l_Time, 0xff); + printText(ah::getDateTimeStrShort_i18n(mDisplayData->utcTs).c_str(), l_Time, 0xff); if (showLine(l_Status)) { // alternatively: @@ -90,7 +90,7 @@ class DisplayMono84X48 : public DisplayMono { // print status of inverters else { if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) - snprintf(mFmtText, DISP_FMT_TEXT_LEN, "no inverter"); + snprintf(mFmtText, DISP_FMT_TEXT_LEN, STR_NO_INVERTER); else if (0 == mDisplayData->nrSleeping) snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol else if (0 == mDisplayData->nrProducing) @@ -110,9 +110,8 @@ class DisplayMono84X48 : public DisplayMono { snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); printText(mFmtText, l_TotalPower, 0xff); - } else { - printText("offline", l_TotalPower, 0xff); - } + } else + printText(STR_OFFLINE, l_TotalPower, 0xff); } if (showLine(l_YieldDay)) { diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp index 087d784b..6d9d929e 100644 --- a/src/plugins/Display/Display_ePaper.cpp +++ b/src/plugins/Display/Display_ePaper.cpp @@ -8,6 +8,7 @@ #include "../../utils/helper.h" #include "imagedata.h" #include "defines.h" +#include "../plugin_lang.h" #if defined(ESP32) @@ -120,7 +121,7 @@ void DisplayEPaper::headlineIP() { if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) { snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s", WiFi.localIP().toString().c_str()); } else { - snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "WiFi not connected"); + snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_NO_WIFI); } _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); uint16_t x = ((_display->width() - tbw) / 2) - tbx; @@ -162,7 +163,7 @@ void DisplayEPaper::versionFooter() { _display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding); _display->fillScreen(GxEPD_BLACK); do { - snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "Version: %s", _version); + snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s: %s", STR_VERSION, _version); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); uint16_t x = ((_display->width() - tbw) / 2) - tbx; @@ -183,7 +184,7 @@ void DisplayEPaper::offlineFooter() { _display->fillScreen(GxEPD_BLACK); do { if (NULL != mUtcTs) { - snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline"); + snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_OFFLINE); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); uint16_t x = ((_display->width() - tbw) / 2) - tbx; @@ -213,7 +214,7 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.0f W", totalPower); _changed = true; } else - snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline"); + snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_OFFLINE); if ((totalPower == 0) && (mEnPowerSave)) { _display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK); @@ -268,7 +269,7 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa // Inverter online _display->setFont(&FreeSans12pt7b); y = _display->height() - (mHeadFootPadding + 10); - snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d online", isprod); + snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, " %d %s", isprod, STR_ONLINE); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); _display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK); x = ((_display->width() - tbw - 20) / 2) - tbx; diff --git a/src/plugins/history.h b/src/plugins/history.h index 5076e295..7d7be57c 100644 --- a/src/plugins/history.h +++ b/src/plugins/history.h @@ -17,6 +17,7 @@ enum class HistoryStorageType : uint8_t { POWER, + POWER_DAY, YIELD }; @@ -27,12 +28,14 @@ class HistoryData { uint16_t refreshCycle = 0; uint16_t loopCnt = 0; uint16_t listIdx = 0; // index for next Element to write into WattArr - uint16_t dispIdx = 0; // index for 1st Element to display from WattArr - bool wrapped = false; // ring buffer for watt history std::array data; - storage_t() { data.fill(0); } + void reset() { + loopCnt = 0; + listIdx = 0; + data.fill(0); + } }; public: @@ -43,33 +46,56 @@ class HistoryData { mTs = ts; mCurPwr.refreshCycle = mConfig->inst.sendInterval; - //mYieldDay.refreshCycle = 60; + mCurPwrDay.refreshCycle = mConfig->inst.sendInterval; + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + mYieldDay.refreshCycle = 60; + #endif + mLastValueTs = 0; + mPgPeriod=0; + mMaximumDay = 0; } void tickerSecond() { - ; float curPwr = 0; - float maxPwr = 0; + //float maxPwr = 0; float yldDay = -0.1; + uint32_t ts = 0; + for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { Inverter<> *iv = mSys->getInverterByPos(i); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); if (iv == NULL) continue; curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec); - maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec); + //maxPwr += iv->getChannelFieldValue(CH0, FLD_MP, rec); yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec); + if (rec->ts > ts) + ts = rec->ts; } if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) { mCurPwr.loopCnt = 0; - if (curPwr > 0) + if (curPwr > 0) { + mLastValueTs = ts; addValue(&mCurPwr, roundf(curPwr)); - if (maxPwr > 0) - mMaximumDay = roundf(maxPwr); + if (curPwr > mMaximumDay) + mMaximumDay = roundf(curPwr); + } + //if (maxPwr > 0) + // mMaximumDay = roundf(maxPwr); + } + + if ((++mCurPwrDay.loopCnt % mCurPwrDay.refreshCycle) == 0) { + mCurPwrDay.loopCnt = 0; + if (curPwr > 0) { + mLastValueTs = ts; + addValueDay(&mCurPwrDay, roundf(curPwr)); + } } - /*if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) { + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) { + mYieldDay.loopCnt = 0; if (*mTs > mApp->getSunset()) { if ((!mDayStored) && (yldDay > 0)) { addValue(&mYieldDay, roundf(yldDay)); @@ -77,28 +103,172 @@ class HistoryData { } } else if (*mTs > mApp->getSunrise()) mDayStored = false; - }*/ + } + #endif } uint16_t valueAt(HistoryStorageType type, uint16_t i) { - //storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay; - storage_t *s = &mCurPwr; - uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH; - return s->data[idx]; + storage_t *s = nullptr; + uint16_t idx=i; + DPRINTLN(DBG_VERBOSE, F("valueAt ") + String((uint8_t)type) + " i=" + String(i)); + + switch (type) { + default: + [[fallthrough]]; + case HistoryStorageType::POWER: + s = &mCurPwr; + idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH; + break; + case HistoryStorageType::POWER_DAY: + s = &mCurPwrDay; + break; + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + case HistoryStorageType::YIELD: + s = &mYieldDay; + idx = (s->listIdx + i) % HISTORY_DATA_ARR_LENGTH; + break; + #endif + } + + return (nullptr == s) ? 0 : s->data[idx]; } uint16_t getMaximumDay() { return mMaximumDay; } + uint32_t getLastValueTs(HistoryStorageType type) { + DPRINTLN(DBG_VERBOSE, F("getLastValueTs ") + String((uint8_t)type)); + if (type == HistoryStorageType::POWER_DAY) + return mPgEndTime; + return mLastValueTs; + } + + uint32_t getPeriod(HistoryStorageType type) { + DPRINTLN(DBG_VERBOSE, F("getPeriode ") + String((uint8_t)type)); + switch (type) { + case HistoryStorageType::POWER: + return mCurPwr.refreshCycle; + break; + case HistoryStorageType::POWER_DAY: + return mPgPeriod / HISTORY_DATA_ARR_LENGTH; + break; + case HistoryStorageType::YIELD: + return (60 * 60 * 24); // 1 day + break; + } + return 0; + } + + bool isDataValid(void) { + return ((0 != mPgStartTime) && (0 != mPgEndTime)); + } + + #if defined(ENABLE_HISTORY_LOAD_DATA) + void addValue(HistoryStorageType historyType, uint8_t valueType, uint32_t value) { + if (valueType < 2) { + storage_t *s = NULL; + switch (historyType) { + default: + [[fallthrough]]; + case HistoryStorageType::POWER: + s = &mCurPwr; + break; + case HistoryStorageType::POWER_DAY: + s = &mCurPwrDay; + break; + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + case HistoryStorageType::YIELD: + s = &mYieldDay; + break; + #endif + } + if (s) { + if (0 == valueType) + addValue(s, value); + else { + if (historyType == HistoryStorageType::POWER) + s->refreshCycle = value; + if (historyType == HistoryStorageType::POWER_DAY) + mPgPeriod = value * HISTORY_DATA_ARR_LENGTH; + } + } + return; + } + if (2 == valueType) { + if (historyType == HistoryStorageType::POWER) + mLastValueTs = value; + if (historyType == HistoryStorageType::POWER_DAY) + mPgEndTime = value; + } + } + #endif + private: void addValue(storage_t *s, uint16_t value) { - if (s->wrapped) // after 1st time array wrap we have to increase the display index - s->dispIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); s->data[s->listIdx] = value; s->listIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); - if (s->listIdx == 0) - s->wrapped = true; + } + + void addValueDay(storage_t *s, uint16_t value) { + DPRINTLN(DBG_VERBOSE, F("addValueDay ") + String(value)); + bool storeStartEndTimes = false; + bool store_entry = false; + uint32_t pGraphStartTime = mApp->getSunrise(); + uint32_t pGraphEndTime = mApp->getSunset(); + uint32_t utcTs = mApp->getTimestamp(); + switch (mPgState) { + case PowerGraphState::NO_TIME_SYNC: + if ((pGraphStartTime > 0) + && (pGraphEndTime > 0) // wait until period data is available ... + && (utcTs >= pGraphStartTime) + && (utcTs < pGraphEndTime)) // and current time is in period + { + storeStartEndTimes = true; // period was received -> store + store_entry = true; + mPgState = PowerGraphState::IN_PERIOD; + } + break; + case PowerGraphState::IN_PERIOD: + if (utcTs > mPgEndTime) // check if end of day is reached ... + mPgState = PowerGraphState::WAIT_4_NEW_PERIOD; // then wait for new period setting + else + store_entry = true; + break; + case PowerGraphState::WAIT_4_NEW_PERIOD: + if ((mPgStartTime != pGraphStartTime) || (mPgEndTime != pGraphEndTime)) { // wait until new time period was received ... + storeStartEndTimes = true; // and store it for next period + mPgState = PowerGraphState::WAIT_4_RESTART; + } + break; + case PowerGraphState::WAIT_4_RESTART: + if ((utcTs >= mPgStartTime) && (utcTs < mPgEndTime)) { // wait until current time is in period again ... + mCurPwrDay.reset(); // then reset power graph data + store_entry = true; + mPgState = PowerGraphState::IN_PERIOD; + mCurPwr.reset(); // also reset "last values" graph + mMaximumDay = 0; // and the maximum of the (last) day + } + break; + } + + // store start and end times of current time period and calculate period length + if (storeStartEndTimes) { + mPgStartTime = pGraphStartTime; + mPgEndTime = pGraphEndTime; + mPgPeriod = pGraphEndTime - pGraphStartTime; // time period of power graph in sec for scaling of x-axis + } + + if (store_entry) { + DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry") + String(value)); + if (mPgPeriod) { + uint16_t pgPos = (utcTs - mPgStartTime) * (HISTORY_DATA_ARR_LENGTH - 1) / mPgPeriod; + s->listIdx = std::min(pgPos, (uint16_t)(HISTORY_DATA_ARR_LENGTH - 1)); + } else + s->listIdx = 0; + DPRINTLN(DBG_VERBOSE, F("addValueDay store_entry idx=") + String(s->listIdx)); + s->data[s->listIdx] = std::max(s->data[s->listIdx], value); // update current datapoint to maximum of all seen values + } } private: @@ -109,8 +279,23 @@ class HistoryData { uint32_t *mTs = nullptr; storage_t mCurPwr; + storage_t mCurPwrDay; + #if defined(ENABLE_HISTORY_YIELD_PER_DAY) + storage_t mYieldDay; + #endif bool mDayStored = false; uint16_t mMaximumDay = 0; + uint32_t mLastValueTs = 0; + enum class PowerGraphState { + NO_TIME_SYNC, + IN_PERIOD, + WAIT_4_NEW_PERIOD, + WAIT_4_RESTART + }; + PowerGraphState mPgState = PowerGraphState::NO_TIME_SYNC; + uint32_t mPgStartTime = 0; + uint32_t mPgEndTime = 0; + uint32_t mPgPeriod = 0; // seconds }; #endif /*ENABLE_HISTORY*/ diff --git a/src/plugins/plugin_lang.h b/src/plugins/plugin_lang.h new file mode 100644 index 00000000..8d7a987f --- /dev/null +++ b/src/plugins/plugin_lang.h @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +// 2024 Ahoy, https://ahoydtu.de +// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#ifndef __PLUGIN_LANG_H__ +#define __PLUGIN_LANG_H__ + +#ifdef LANG_DE + #define STR_MONTHNAME_3_CHAR_LIST "ErrJanFebMrzAprMaiJunJulAugSepOktNovDez" + #define STR_DAYNAME_3_CHAR_LIST "ErrSonMonDieMitDonFreSam" + #define STR_OFFLINE "aus" + #define STR_ONLINE "aktiv" + #define STR_NO_INVERTER "kein inverter" + #define STR_NO_WIFI "WLAN nicht verbunden" + #define STR_VERSION "Version" + #define STR_ACTIVE_INVERTERS "aktive WR" + #define STR_TODAY "heute" + #define STR_TOTAL "Gesamt" +#elif LANG_FR + #define STR_MONTHNAME_3_CHAR_LIST "ErrJanFevMarAvrMaiJunJulAouSepOctNovDec" + #define STR_DAYNAME_3_CHAR_LIST "ErrDimLunMarMerJeuVenSam" + #define STR_OFFLINE "eteint" + #define STR_ONLINE "online" + #define STR_NO_INVERTER "pas d'onduleur" + #define STR_NO_WIFI "WiFi not connected" + #define STR_VERSION "Version" + #define STR_ACTIVE_INVERTERS "active Inv" + #define STR_TODAY "today" + #define STR_TOTAL "total" +#else + #define STR_MONTHNAME_3_CHAR_LIST "ErrJanFebMarAprMayJunJulAugSepOctNovDec" + #define STR_DAYNAME_3_CHAR_LIST "ErrSunMonTueWedThuFriSat" + #define STR_OFFLINE "offline" + #define STR_ONLINE "online" + #define STR_NO_INVERTER "no inverter" + #define STR_NO_WIFI "WiFi not connected" + #define STR_VERSION "Version" + #define STR_ACTIVE_INVERTERS "active Inv" + #define STR_TODAY "today" + #define STR_TOTAL "total" +#endif + +#endif /*__PLUGIN_LANG_H__*/ diff --git a/src/utils/helper.cpp b/src/utils/helper.cpp index 24a4d9ee..edb9b9b9 100644 --- a/src/utils/helper.cpp +++ b/src/utils/helper.cpp @@ -5,6 +5,12 @@ #include "helper.h" #include "dbg.h" +#include "../plugins/plugin_lang.h" + +#define dt_SHORT_STR_LEN_i18n 3 // the length of short strings +static char buffer_i18n[dt_SHORT_STR_LEN_i18n + 1]; // must be big enough for longest string and the terminating null +const char monthShortNames_P[] PROGMEM = STR_MONTHNAME_3_CHAR_LIST; +const char dayShortNames_P[] PROGMEM = STR_DAYNAME_3_CHAR_LIST; namespace ah { void ip2Arr(uint8_t ip[], const char *ipStr) { @@ -28,6 +34,10 @@ namespace ah { snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); } + double round1(double value) { + return (int)(value * 10 + 0.5) / 10.0; + } + double round3(double value) { return (int)(value * 1000 + 0.5) / 1000.0; } @@ -82,6 +92,31 @@ namespace ah { return String(str); } + static char* monthShortStr_i18n(uint8_t month) { + for (int i=0; i < dt_SHORT_STR_LEN_i18n; i++) + buffer_i18n[i] = pgm_read_byte(&(monthShortNames_P[i + month * dt_SHORT_STR_LEN_i18n])); + buffer_i18n[dt_SHORT_STR_LEN_i18n] = 0; + return buffer_i18n; + } + + static char* dayShortStr_i18n(uint8_t day) { + for (int i=0; i < dt_SHORT_STR_LEN_i18n; i++) + buffer_i18n[i] = pgm_read_byte(&(dayShortNames_P[i + day * dt_SHORT_STR_LEN_i18n])); + buffer_i18n[dt_SHORT_STR_LEN_i18n] = 0; + return buffer_i18n; + } + + String getDateTimeStrShort_i18n(time_t t) { + char str[20]; + if(0 == t) + sprintf(str, "n/a"); + else { + sprintf(str, "%3s ", dayShortStr_i18n(dayOfWeek(t))); + sprintf(str+4, "%2d.%3s %02d:%02d", day(t), monthShortStr_i18n(month(t)), hour(t), minute(t)); + } + return String(str); + } + uint64_t Serial2u64(const char *val) { char tmp[3]; uint64_t ret = 0ULL; diff --git a/src/utils/helper.h b/src/utils/helper.h index 1dbba3d9..ff1a9aed 100644 --- a/src/utils/helper.h +++ b/src/utils/helper.h @@ -39,9 +39,11 @@ static Timezone gTimezone(CEST, CET); namespace ah { void ip2Arr(uint8_t ip[], const char *ipStr); void ip2Char(uint8_t ip[], char *str); + double round1(double value); double round3(double value); String getDateTimeStr(time_t t); String getDateTimeStrShort(time_t t); + String getDateTimeStrShort_i18n(time_t t); String getDateTimeStrFile(time_t t); String getTimeStr(time_t t); String getTimeStrMs(uint64_t t); diff --git a/src/web/RestApi.h b/src/web/RestApi.h index f2da1edd..1ab12c30 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -45,6 +45,11 @@ class RestApi { mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false); #endif mConfig = config; + #if defined(ENABLE_HISTORY_LOAD_DATA) + mSrv->on("/api/addYDHist", + HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1), + std::bind(&RestApi::onApiPostYDHist,this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + #endif mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody( std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5)); mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1)); @@ -99,6 +104,8 @@ class RestApi { #endif /* !defined(ETHERNET) */ else if(path == "live") getLive(request,root); else if (path == "powerHistory") getPowerHistory(request, root); + else if (path == "powerHistoryDay") getPowerHistoryDay(request, root); + else if (path == "yieldDayHistory") getYieldDayHistory(request, root); else { if(path.substring(0, 12) == "inverter/id/") getInverter(root, request->url().substring(17).toInt()); @@ -133,7 +140,94 @@ class RestApi { #endif } - void onApiPostBody(AsyncWebServerRequest *request, const uint8_t *data, size_t len, size_t index, size_t total) { + #if defined(ENABLE_HISTORY_LOAD_DATA) + // VArt67: For debugging history graph. Loading data into graph + void onApiPostYDHist(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, size_t final) { + uint32_t total = request->contentLength(); + DPRINTLN(DBG_DEBUG, "[onApiPostYieldDHistory ] " + filename + " index:" + index + " len:" + len + " total:" + total + " final:" + final); + + if (0 == index) { + if (NULL != mTmpBuf) + delete[] mTmpBuf; + mTmpBuf = new uint8_t[total + 1]; + mTmpSize = total; + } + if (mTmpSize >= (len + index)) + memcpy(&mTmpBuf[index], data, len); + + if (!final) + return; // not last frame - nothing to do + + mTmpSize = len + index; // correct the total size + mTmpBuf[mTmpSize] = 0; + + #ifndef ESP32 + DynamicJsonDocument json(ESP.getMaxFreeBlockSize() - 512); // need some memory on heap + #else + DynamicJsonDocument json(12000); // does this work? I have no ESP32 :-( + #endif + DeserializationError err = deserializeJson(json, (const char *)mTmpBuf, mTmpSize); + json.shrinkToFit(); + JsonObject obj = json.as(); + + // Debugging + // mTmpBuf[mTmpSize] = 0; + // DPRINTLN(DBG_DEBUG, (const char *)mTmpBuf); + + if (!err && obj) { + // insert data into yieldDayHistory object + HistoryStorageType dataType; + if (obj["maxDay"] > 0) // this is power history data + { + dataType = HistoryStorageType::POWER; + if (obj["refresh"] > 60) + dataType = HistoryStorageType::POWER_DAY; + + } + else + dataType = HistoryStorageType::YIELD; + + size_t cnt = obj[F("value")].size(); + DPRINTLN(DBG_DEBUG, "ArraySize: " + String(cnt)); + + for (uint16_t i = 0; i < cnt; i++) { + uint16_t val = obj[F("value")][i]; + mApp->addValueToHistory((uint8_t)dataType, 0, val); + // DPRINT(DBG_VERBOSE, "value " + String(i) + ": " + String(val) + ", "); + } + uint32_t refresh = obj[F("refresh")]; + mApp->addValueToHistory((uint8_t)dataType, 1, refresh); + if (dataType != HistoryStorageType::YIELD) { + uint32_t ts = obj[F("lastValueTs")]; + mApp->addValueToHistory((uint8_t)dataType, 2, ts); + } + + } else { + switch (err.code()) { + case DeserializationError::Ok: + break; + case DeserializationError::IncompleteInput: + DPRINTLN(DBG_DEBUG, F("Incomplete input")); + break; + case DeserializationError::InvalidInput: + DPRINTLN(DBG_DEBUG, F("Invalid input")); + break; + case DeserializationError::NoMemory: + DPRINTLN(DBG_DEBUG, F("Not enough memory ") + String(json.capacity()) + " bytes"); + break; + default: + DPRINTLN(DBG_DEBUG, F("Deserialization failed")); + break; + } + } + + request->send(204); // Success with no page load + delete[] mTmpBuf; + mTmpBuf = NULL; + } + #endif + + void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { DPRINTLN(DBG_VERBOSE, "onApiPostBody"); if(0 == index) { @@ -203,6 +297,8 @@ class RestApi { ep[F("live")] = url + F("live"); #if defined(ENABLE_HISTORY) ep[F("powerHistory")] = url + F("powerHistory"); + ep[F("powerHistoryDay")] = url + F("powerHistoryDay"); + ep[F("yieldDayHistory")] = url + F("yieldDayHistory"); #endif } @@ -297,6 +393,7 @@ class RestApi { obj[F("heap_free")] = mHeapFree; obj[F("sketch_total")] = ESP.getFreeSketchSpace(); obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb + obj[F("wifi_channel")] = WiFi.channel(); getGeneric(request, obj); getRadioNrf(obj.createNestedObject(F("radioNrf"))); @@ -495,7 +592,7 @@ class RestApi { obj[F("name")] = String(iv->config->name); obj[F("serial")] = String(iv->config->serial.u64, HEX); obj[F("version")] = String(iv->getFwVersion()); - obj[F("power_limit_read")] = iv->actPowerLimit; + obj[F("power_limit_read")] = ah::round1(iv->getChannelFieldValue(CH0, FLD_ACT_ACTIVE_PWR_LIMIT, iv->getRecordStruct(SystemConfigPara))); obj[F("power_limit_ack")] = iv->powerLimitAck; obj[F("max_pwr")] = iv->getMaxPower(); obj[F("ts_last_success")] = rec->ts; @@ -811,7 +908,7 @@ class RestApi { void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) { getGeneric(request, obj.createNestedObject(F("generic"))); #if defined(ENABLE_HISTORY) - obj[F("refresh")] = mConfig->inst.sendInterval; + obj[F("refresh")] = mApp->getHistoryPeriod((uint8_t)HistoryStorageType::POWER); uint16_t max = 0; for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld); @@ -820,9 +917,39 @@ class RestApi { max = value; } obj[F("max")] = max; - obj[F("maxDay")] = mApp->getHistoryMaxDay(); - #else - obj[F("refresh")] = 86400; // 1 day; + obj[F("lastValueTs")] = mApp->getHistoryLastValueTs((uint8_t)HistoryStorageType::POWER); + #endif /*ENABLE_HISTORY*/ + } + + void getPowerHistoryDay(AsyncWebServerRequest *request, JsonObject obj){ + //getGeneric(request, obj.createNestedObject(F("generic"))); + #if defined(ENABLE_HISTORY) + obj[F("refresh")] = mApp->getHistoryPeriod((uint8_t)HistoryStorageType::POWER_DAY); + uint16_t max = 0; + for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { + uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER_DAY, fld); + obj[F("value")][fld] = value; + if (value > max) + max = value; + } + obj[F("max")] = max; + obj[F("lastValueTs")] = mApp->getHistoryLastValueTs((uint8_t)HistoryStorageType::POWER_DAY); + #endif /*ENABLE_HISTORY*/ + } + + + void getYieldDayHistory(AsyncWebServerRequest *request, JsonObject obj) { + //getGeneric(request, obj.createNestedObject(F("generic"))); + #if defined(ENABLE_HISTORY) && defined(ENABLE_HISTORY_YIELD_PER_DAY) + obj[F("refresh")] = mApp->getHistoryPeriod((uint8_t)HistoryStorageType::YIELD); + uint16_t max = 0; + for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { + uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::YIELD, fld); + obj[F("value")][fld] = value; + if (value > max) + max = value; + } + obj[F("max")] = max; #endif /*ENABLE_HISTORY*/ } diff --git a/src/web/html/api.js b/src/web/html/api.js index 5cce4206..6c2ccec2 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -61,6 +61,23 @@ function ml(tagName, ...args) { return nester(el, args[1]) } +function mlNs(tagName, ...args) { + var el = document.createElementNS("http://www.w3.org/2000/svg", tagName); + if(args[0]) { + for(var name in args[0]) { + if(name.indexOf("on") === 0) { + el.addEventListener(name.substr(2).toLowerCase(), args[0][name], false) + } else { + el.setAttribute(name, args[0][name]); + } + } + } + if (!args[1]) { + return el; + } + return nester(el, args[1]) +} + function nester(el, n) { if (typeof n === "string") { el.innerHTML = n; @@ -84,10 +101,12 @@ function topnav() { } function parseNav(obj) { - for(i = 0; i < 13; i++) { + for(i = 0; i < 14; i++) { if(i == 2) continue; var l = document.getElementById("nav"+i); + if(null == l) + continue if(12 == i) { if(obj.cst_lnk.length > 0) { l.href = obj.cst_lnk diff --git a/src/web/html/colorBright.css b/src/web/html/colorBright.css index 2e676029..aedd05d4 100644 --- a/src/web/html/colorBright.css +++ b/src/web/html/colorBright.css @@ -30,4 +30,8 @@ --ch-head-bg: #006ec0; --ts-head: #333; --ts-bg: #555; + + --chart-cont: #fbfbfb; + --chart-bg: #f9f9f9; + --chart-text: #000000; } diff --git a/src/web/html/colorDark.css b/src/web/html/colorDark.css index 40bd4cf3..b5b1a72b 100644 --- a/src/web/html/colorDark.css +++ b/src/web/html/colorDark.css @@ -30,4 +30,8 @@ --ch-head-bg: #236; --ts-head: #333; --ts-bg: #555; + + --chart-cont: #0b0b0b; + --chart-bg: #090909; + --chart-text: #FFFFFF; } diff --git a/src/web/html/grid_info.json b/src/web/html/grid_info.json index 12760ccf..bff1ae80 100644 --- a/src/web/html/grid_info.json +++ b/src/web/html/grid_info.json @@ -14,6 +14,7 @@ {"0x0d04": "NF_EN_50549-1:2019"}, {"0x1000": "ES_RD1699"}, {"0x1200": "EU_EN50438"}, + {"0x1300": "MEX_NOM_220V"}, {"0x2600": "BE_C10_26"}, {"0x2900": "NL_NEN-EN50549-1_2019"}, {"0x2a00": "PL_PN-EN 50549-1:2019"}, @@ -35,6 +36,44 @@ {"0xb0": "Watt Power Factor"} ], "group": [ + { + "0x0000": [ + { + "name": "Nominal Voltage", + "div": 10, + "def": 220, + "unit": "V" + }, + { + "name": "Low Voltage 1", + "div": 10, + "min": 170, + "max": 195.5, + "def": 184, + "unit": "V" + }, + { + "name": "LV1 Maximum Trip Time", + "div": 10, + "def": 0.1, + "unit": "s" + }, + { + "name": "High Voltage 1", + "div": 10, + "min": 253, + "max": 275, + "def": 270, + "unit": "V" + }, + { + "name": "HV1 Maximum Trip Time", + "div": 10, + "def": 0.1, + "unit": "s" + } + ] + }, { "0x0003": [ { diff --git a/src/web/html/history.html b/src/web/html/history.html index 6372ab82..2d1509d4 100644 --- a/src/web/html/history.html +++ b/src/web/html/history.html @@ -12,84 +12,175 @@ {#HTML_NAV}
-

{#TOTAL_POWER}

-
-
-

- {#MAX_DAY}: W. {#LAST_VALUE}: W.
- {#MAXIMUM}: W. {#UPDATED} {#SECONDS} -

-
+

Total Power

+
+

Total Power Today

+
+ +

Total Yield per day

+
+ + +

Insert data into Yield per day history

+
+ Insert data (*.json) i.e. from a saved "/api/yieldDayHistory" call + +
+ + +
+
+
{#HTML_FOOTER} diff --git a/src/web/html/includes/nav.html b/src/web/html/includes/nav.html index bab64829..4718e257 100644 --- a/src/web/html/includes/nav.html +++ b/src/web/html/includes/nav.html @@ -7,7 +7,9 @@
{#NAV_LIVE} + {#NAV_HISTORY} + {#NAV_WEBSERIAL} {#NAV_SETTINGS} @@ -15,7 +17,8 @@ System REST API - {#NAV_DOCUMENTATION} + {#NAV_DOCUMENTATION} + Website {#NAV_ABOUT} Custom Link diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 57dc6a8c..b6b8929e 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -272,7 +272,7 @@
- +
@@ -301,6 +301,7 @@
+
{#BTN_REBOOT_SUCCESSFUL_SAVE}
@@ -341,6 +342,7 @@ var maxInv = 0; var ts = 0; + /*IF_ESP8266*/ var esp8266pins = [ [255, "{#PIN_OFF}"], [0, "D3 (GPIO0)"], @@ -361,6 +363,7 @@ [15, "D8 (GPIO15)"], [16, "D0 (GPIO16 - {#PIN_NO_IRQ})"] ]; + /*ENDIF_ESP8266*/ /*IF_ESP32*/ var esp32pins = [ @@ -392,6 +395,7 @@ [36, "VP (GPIO36, {#PIN_INPUT_ONLY})"], [39, "VN (GPIO39, {#PIN_INPUT_ONLY})"] ]; + /*IF_ESP32-S2*/ var esp32sXpins = [ [255, "off / default"], [0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"], @@ -440,6 +444,58 @@ [47, "GPIO47"], [48, "GPIO48"], ]; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + var esp32sXpins = [ + [255, "off / default"], + [0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"], + [1, "GPIO1"], + [2, "GPIO2"], + [3, "GPIO3"], + [4, "GPIO4"], + [5, "GPIO5"], + [6, "GPIO6"], + [7, "GPIO7"], + [8, "GPIO8"], + [9, "GPIO9"], + [10, "GPIO10"], + [11, "GPIO11"], + [12, "GPIO12"], + [13, "GPIO13"], + [14, "GPIO14"], + [15, "GPIO15"], + [16, "GPIO16"], + [17, "GPIO17"], + [18, "GPIO18"], + [19, "GPIO19 ({#PIN_DONT_USE} - USB-)"], + [20, "GPIO20 ({#PIN_DONT_USE} - USB+)"], + [21, "GPIO21"], + [26, "GPIO26 (PSRAM - {#PIN_NOT_AVAIL})"], + [27, "GPIO27 (FLASH - {#PIN_NOT_AVAIL})"], + [28, "GPIO28 (FLASH - {#PIN_NOT_AVAIL})"], + [29, "GPIO29 (FLASH - {#PIN_NOT_AVAIL})"], + [30, "GPIO30 (FLASH - {#PIN_NOT_AVAIL})"], + [31, "GPIO31 (FLASH - {#PIN_NOT_AVAIL})"], + [32, "GPIO32 (FLASH - {#PIN_NOT_AVAIL})"], + [33, "GPIO33 (not exposed on S3-WROOM modules)"], + [34, "GPIO34 (not exposed on S3-WROOM modules)"], + [35, "GPIO35"], + [36, "GPIO36"], + [37, "GPIO37"], + [38, "GPIO38"], + [39, "GPIO39"], + [40, "GPIO40"], + [41, "GPIO41"], + [42, "GPIO42"], + [43, "GPIO43"], + [44, "GPIO44"], + [45, "GPIO45 ({#PIN_DONT_USE} - STRAPPING PIN)"], + [46, "GPIO46 ({#PIN_DONT_USE} - STRAPPING PIN)"], + [47, "GPIO47"], + [48, "GPIO48"], + ]; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ var esp32c3pins = [ [255, "off / default"], [0, "GPIO0"], @@ -465,6 +521,7 @@ [20, "GPIO20 (RX)"], [21, "GPIO21 (TX)"], ]; + /*ENDIF_ESP32-C3*/ /*ENDIF_ESP32*/ var nrfPa = [ [0, "MIN ({#PIN_RECOMMENDED})"], @@ -890,11 +947,19 @@ function parsePinout(obj, type, system) { var e = document.getElementById("pinout"); - var pinList = esp8266pins; /*IF_ESP32*/ var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; - else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*IF_ESP32-S2*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ + pinList = esp32c3pins; + /*ENDIF_ESP32-C3*/ + /*ELSE*/ + var pinList = esp8266pins; /*ENDIF_ESP32*/ pins = [['led0', 'pinLed0', '{#LED_AT_LEAST_ONE_PRODUCING}'], ['led1', 'pinLed1', '{#LED_MQTT_CONNECTED}'], ['led2', 'pinLed2', '{#LED_NIGHT_TIME}']]; for(p of pins) { @@ -926,11 +991,19 @@ var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox"); en.checked = obj["en"]; - var pinList = esp8266pins; /*IF_ESP32*/ var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; - else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*IF_ESP32-S2*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ + pinList = esp32c3pins; + /*ENDIF_ESP32-C3*/ + /*ELSE*/ + var pinList = esp8266pins; /*ENDIF_ESP32*/ e.replaceChildren ( @@ -962,8 +1035,15 @@ var e = document.getElementById("cmt"); var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox"); var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; - else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*IF_ESP32-S2*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ + pinList = esp32c3pins; + /*ENDIF_ESP32-C3*/ en.checked = obj["en"]; @@ -1008,12 +1088,21 @@ } } + /*IF_PLUGIN_DISPLAY*/ function parseDisplay(obj, type, system) { - var pinList = esp8266pins; /*IF_ESP32*/ var pinList = esp32pins; - if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; - else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; + /*IF_ESP32-S2*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S2*/ + /*IF_ESP32-S3*/ + pinList = esp32sXpins; + /*ENDIF_ESP32-S3*/ + /*IF_ESP32-C3*/ + pinList = esp32c3pins; + /*ENDIF_ESP32-C3*/ + /*ELSE*/ + var pinList = esp8266pins; /*ENDIF_ESP32*/ for(var i of ["disp_pwr"]) @@ -1149,6 +1238,7 @@ setHide("screenSaver", !optionsMap.get(dispType)[2]); setHide("pirPin", !(optionsMap.get(dispType)[2] && (screenSaver==2))); // show pir pin only for motion screensaver } + /*ENDIF_PLUGIN_DISPLAY*/ function tick() { document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000))); @@ -1168,7 +1258,9 @@ parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]); /*ENDIF_ESP32*/ parseSerial(root["serial"]); + /*IF_PLUGIN_DISPLAY*/ parseDisplay(root["display"], root["system"]["esp_type"], root["system"]); + /*ENDIF_PLUGIN_DISPLAY*/ getAjax("/api/inverter/list", parseIv); } } diff --git a/src/web/html/style.css b/src/web/html/style.css index 74cf4e8e..60805e19 100644 --- a/src/web/html/style.css +++ b/src/web/html/style.css @@ -33,13 +33,17 @@ textarea { color: var(--fg2); } -svg rect {fill: #00A;} -svg.chart { - background: #f2f2f2; - border: 2px solid gray; - padding: 1px; +svg polyline { + fill-opacity: .5; + stroke-width: 1; +} + +svg text { + font-size: x-small; + fill: var(--chart-text); } + div.chartDivContainer { padding: 1px; margin: 1px; diff --git a/src/web/html/system.html b/src/web/html/system.html index a646e8b8..4b2ea1f7 100644 --- a/src/web/html/system.html +++ b/src/web/html/system.html @@ -21,8 +21,8 @@ } function parseSysInfo(obj) { - const data = ["sdk", "cpu_freq", "chip_revision", - "chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime", + const data = ["sdk", "cpu_freq", "chip_revision", "device_name", + "chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "wifi_channel", "ts_uptime", "flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag", "max_free_blk", "version", "modules", "env", "core_version", "reboot_reason"]; @@ -49,7 +49,7 @@ case 0: return badge(false, "{#UNKNOWN}", "warning"); break; case 1: return badge(true, "{#TRUE}"); break; default: return badge(false, "{#FALSE}"); break; - } + } } function parseRadio(obj) { diff --git a/src/web/html/visualization.html b/src/web/html/visualization.html index 81962add..aec6b0d8 100644 --- a/src/web/html/visualization.html +++ b/src/web/html/visualization.html @@ -116,7 +116,7 @@ if(65535 != obj.power_limit_read) { pwrLimit = obj.power_limit_read + " %"; if(0 != obj.max_pwr) - pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + " W"; + pwrLimit += ", " + (obj.max_pwr * obj.power_limit_read / 100) + " W"; } var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000)); diff --git a/src/web/lang.json b/src/web/lang.json index 67b08b75..6416a61a 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -1528,6 +1528,21 @@ "en": "Total Power", "de": "Gesamtleistung" }, + { + "token": "LAST", + "en": "Last", + "de": "Die letzten" + }, + { + "token": "VALUES", + "en": "values", + "de": "Werte" + }, + { + "token": "TOTAL_POWER_DAY", + "en": "Total Power Today", + "de": "Gesamtleistung heute" + }, { "token": "TOTAL_YIELD_PER_DAY", "en": "Total Yield per day", @@ -1535,23 +1550,23 @@ }, { "token": "MAX_DAY", - "en": "maximum day", + "en": "Maximum day", "de": "Tagesmaximum" }, { "token": "LAST_VALUE", - "en": "last value", - "de": "letzter Wert" + "en": "Last value", + "de": "Letzter Wert" }, { "token": "MAXIMUM", - "en": "maximum value", + "en": "Maximum value", "de": "Maximalwert" }, { "token": "UPDATED", "en": "Updated every", - "de": "aktualisiert alle" + "de": "Aktualisiert alle" }, { "token": "SECONDS",