Browse Source

Merge branch 'development03' into ethW5500

Ethernet is working, /system seems to be slow
pull/1512/head
lumapu 11 months ago
parent
commit
3d2ed81adf
  1. 12
      .github/workflows/compile_development.yml
  2. 89
      scripts/convertHtml.py
  3. 40
      scripts/htmlPreprocessorDefines.py
  4. 17
      src/CHANGES.md
  5. 23
      src/app.h
  6. 6
      src/appInterface.h
  7. 3
      src/config/config.h
  8. 2
      src/defines.h
  9. 8
      src/eth/ahoyeth.cpp
  10. 4
      src/eth/ahoyeth.h
  11. 5
      src/eth/ethSpi.h
  12. 53
      src/platformio.ini
  13. 1
      src/plugins/Display/Display.h
  14. 8
      src/plugins/Display/Display_Mono_128X32.h
  15. 6
      src/plugins/Display/Display_Mono_128X64.h
  16. 4
      src/plugins/Display/Display_Mono_64X48.h
  17. 9
      src/plugins/Display/Display_Mono_84X48.h
  18. 11
      src/plugins/Display/Display_ePaper.cpp
  19. 225
      src/plugins/history.h
  20. 44
      src/plugins/plugin_lang.h
  21. 35
      src/utils/helper.cpp
  22. 2
      src/utils/helper.h
  23. 139
      src/web/RestApi.h
  24. 21
      src/web/html/api.js
  25. 4
      src/web/html/colorBright.css
  26. 4
      src/web/html/colorDark.css
  27. 39
      src/web/html/grid_info.json
  28. 211
      src/web/html/history.html
  29. 5
      src/web/html/includes/nav.html
  30. 116
      src/web/html/setup.html
  31. 14
      src/web/html/style.css
  32. 4
      src/web/html/system.html
  33. 2
      src/web/html/visualization.html
  34. 25
      src/web/lang.json

12
.github/workflows/compile_development.yml

@ -23,16 +23,19 @@ jobs:
strategy: strategy:
matrix: matrix:
variant: variant:
- opendtufusion
- opendtufusion-ethernet
- esp8266 - esp8266
- esp8266-all
- esp8266-minimal
- esp8266-prometheus - esp8266-prometheus
- esp8285 - esp8285
- esp32-wroom32 - esp32-wroom32
- esp32-wroom32-minimal
- esp32-wroom32-prometheus - esp32-wroom32-prometheus
- esp32-wroom32-ethernet - esp32-wroom32-ethernet
- esp32-s2-mini - esp32-s2-mini
- esp32-c3-mini - esp32-c3-mini
- opendtufusion
- opendtufusion-ethernet
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0 - uses: benjlevesque/short-sha@v3.0
@ -84,7 +87,10 @@ jobs:
strategy: strategy:
matrix: matrix:
variant: variant:
- opendtufusion-de
- opendtufusion-ethernet-de
- esp8266-de - esp8266-de
- esp8266-all-de
- esp8266-prometheus-de - esp8266-prometheus-de
- esp8285-de - esp8285-de
- esp32-wroom32-de - esp32-wroom32-de
@ -92,8 +98,6 @@ jobs:
- esp32-wroom32-ethernet-de - esp32-wroom32-ethernet-de
- esp32-s2-mini-de - esp32-s2-mini-de
- esp32-c3-mini-de - esp32-c3-mini-de
- opendtufusion-de
- opendtufusion-ethernet-de
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0 - uses: benjlevesque/short-sha@v3.0

89
scripts/convertHtml.py

@ -7,8 +7,37 @@ import json
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
import subprocess import subprocess
import configparser
Import("env") 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(): def get_git_sha():
try: try:
@ -50,38 +79,46 @@ def readVersionFull(path):
return version return version
def htmlParts(file, header, nav, footer, versionPath, lang): def htmlParts(file, header, nav, footer, versionPath, lang):
p = "";
f = open(file, "r") f = open(file, "r")
lines = f.readlines() lines = f.readlines()
f.close(); f.close();
f = open(header, "r") f = open(header, "r")
h = f.read().strip() h = f.readlines()
f.close() f.close()
f = open(nav, "r") f = open(nav, "r")
n = f.read().strip() n = f.readlines()
f.close() f.close()
f = open(footer, "r") f = open(footer, "r")
fo = f.read().strip() fo = f.readlines()
f.close() f.close()
linesExt = []
for line in lines: for line in lines:
line = line.replace("{#HTML_HEADER}", h) if line.find("{#HTML_HEADER}") != -1:
line = line.replace("{#HTML_NAV}", n) linesExt.extend(h)
line = line.replace("{#HTML_FOOTER}", fo) elif line.find("{#HTML_NAV}") != -1:
p += line linesExt.extend(n)
elif line.find("{#HTML_FOOTER}") != -1:
linesExt.extend(fo)
else:
linesExt.append(line)
linesMod = prepro.conv(linesExt, build_flags)
#placeholders #placeholders
version = readVersion(versionPath); version = readVersion(versionPath);
link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>' link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>'
p = ""
for line in linesMod:
p += line
p = p.replace("{#VERSION}", version) p = p.replace("{#VERSION}", version)
p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath)) p = p.replace("{#VERSION_FULL}", readVersionFull(versionPath))
p = p.replace("{#VERSION_GIT}", link) p = p.replace("{#VERSION_GIT}", link)
# remove if - endif ESP32
p = checkIf(p)
p = translate(file, p, lang) p = translate(file, p, lang)
p = translate("general", p, lang) # menu / header / footer p = translate("general", p, lang) # menu / header / footer
@ -90,30 +127,6 @@ def htmlParts(file, header, nav, footer, versionPath, lang):
f.close(); f.close();
return p return p
def checkIf(data):
if (env['PIOENV'][0:5] == "esp32") or env['PIOENV'][0:4] == "open":
data = data.replace("<!--IF_ESP32-->", "")
data = data.replace("<!--ENDIF_ESP32-->", "")
data = data.replace("/*IF_ESP32*/", "")
data = data.replace("/*ENDIF_ESP32*/", "")
else:
while 1:
start = data.find("<!--IF_ESP32-->")
end = data.find("<!--ENDIF_ESP32-->")+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): def findLang(file):
with open('../lang.json') as j: with open('../lang.json') as j:
lang = json.load(j) lang = json.load(j)
@ -189,6 +202,10 @@ def convert2Header(inFile, versionPath, lang):
f.write("#endif /*__{}_{}_H__*/\n".format(define, define2)) f.write("#endif /*__{}_{}_H__*/\n".format(define, define2))
f.close() f.close()
def main():
get_build_flags()
# delete all files in the 'h' dir # delete all files in the 'h' dir
wd = 'web/html/h' wd = 'web/html/h'
@ -216,6 +233,10 @@ lang = "en"
if env['PIOENV'][-3:] == "-de": if env['PIOENV'][-3:] == "-de":
lang = "de" lang = "de"
# go throw the array # go throw the array
for val in files_grabbed: for val in files_grabbed:
convert2Header(val, "../../defines.h", lang) convert2Header(val, "../../defines.h", lang)
main()

40
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\-_]+)?\-\-\>')

17
src/CHANGES.md

@ -1,5 +1,22 @@
# Development Changes # 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 ## 0.8.89 - 2024-03-02
* merge PR: Collection of small fixes #1465 * merge PR: Collection of small fixes #1465
* fix: show esp type on `/history` #1463 * fix: show esp type on `/history` #1463

23
src/app.h

@ -314,6 +314,14 @@ class app : public IApp, public ah::Scheduler {
#endif #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 { uint16_t getHistoryMaxDay() override {
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
return mHistory.getMaximumDay(); return mHistory.getMaximumDay();
@ -322,6 +330,21 @@ class app : public IApp, public ah::Scheduler {
#endif #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: private:
#define CHECK_AVAIL true #define CHECK_AVAIL true
#define SKIP_YIELD_DAY true #define SKIP_YIELD_DAY true

6
src/appInterface.h

@ -63,8 +63,12 @@ class IApp {
virtual bool isProtected(const char *clientIp, const char *token, bool askedFromWeb) const = 0; 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 uint16_t getHistoryValue(uint8_t type, uint16_t i) = 0;
virtual uint32_t getHistoryPeriod(uint8_t type) = 0;
virtual uint16_t getHistoryMaxDay() = 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; virtual void* getRadioObj(bool nrf) = 0;
}; };

3
src/config/config.h

@ -77,6 +77,9 @@
#ifndef DEF_ETH_CS_PIN #ifndef DEF_ETH_CS_PIN
#define DEF_ETH_CS_PIN 15 #define DEF_ETH_CS_PIN 15
#endif #endif
#ifndef DEF_ETH_RST_PIN
#define DEF_ETH_RST_PIN 2
#endif
#else /* defined(ETHERNET) */ #else /* defined(ETHERNET) */
// time in seconds how long the station info (ssid + pwd) will be tried // time in seconds how long the station info (ssid + pwd) will be tried
#define WIFI_TRY_CONNECT_TIME 30 #define WIFI_TRY_CONNECT_TIME 30

2
src/defines.h

@ -13,7 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 8 #define VERSION_MINOR 8
#define VERSION_PATCH 89 #define VERSION_PATCH 93
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {

8
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); }); WiFi.onEvent([this](WiFiEvent_t event, arduino_event_info_t info) -> void { this->onEthernetEvent(event, info); });
Serial.flush(); 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); 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, 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); //ETH.begin(ETH_PHY_ADDR, ETH_PHY_POWER, ETH_PHY_MDC, DEF_ETH_MOSI_PIN, ETH_PHY_TYPE, ETH_CLK_MODE);
#endif //#endif
if(mConfig->sys.ip.ip[0] != 0) { if(mConfig->sys.ip.ip[0] != 0) {
IPAddress ip(mConfig->sys.ip.ip); IPAddress ip(mConfig->sys.ip.ip);

4
src/eth/ahoyeth.h

@ -46,9 +46,9 @@ class ahoyeth {
void onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info); void onEthernetEvent(WiFiEvent_t event, arduino_event_info_t info);
private: private:
#if defined(CONFIG_IDF_TARGET_ESP32S3) //#if defined(CONFIG_IDF_TARGET_ESP32S3)
EthSpi mEthSpi; EthSpi mEthSpi;
#endif //#endif
settings_t *mConfig = nullptr; settings_t *mConfig = nullptr;
uint32_t *mUtcTimestamp; uint32_t *mUtcTimestamp;

5
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/ // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#if defined(CONFIG_IDF_TARGET_ESP32S3)
#if defined(ETHERNET) #if defined(ETHERNET)
#ifndef __ETH_SPI_H__ #ifndef __ETH_SPI_H__
#define __ETH_SPI_H__ #define __ETH_SPI_H__
@ -138,4 +136,3 @@ class EthSpi {
#endif /*__ETH_SPI_H__*/ #endif /*__ETH_SPI_H__*/
#endif /*ETHERNET*/ #endif /*ETHERNET*/
#endif /*CONFIG_IDF_TARGET_ESP32S3*/

53
src/platformio.ini

@ -44,6 +44,29 @@ build_unflags =
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L 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} build_flags = ${env.build_flags}
-DEMC_MIN_FREE_MEMORY=4096 -DEMC_MIN_FREE_MEMORY=4096
-DENABLE_MQTT -DENABLE_MQTT
@ -53,7 +76,7 @@ build_flags = ${env.build_flags}
monitor_filters = monitor_filters =
esp8266_exception_decoder esp8266_exception_decoder
[env:esp8266-de] [env:esp8266-all-de]
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
@ -191,7 +214,7 @@ monitor_filters =
[env:esp32-wroom32-ethernet] [env:esp32-wroom32-ethernet]
platform = espressif32 platform = espressif32
board = esp32dev board = lolin_d32
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-D ETHERNET -D ETHERNET
-DRELEASE -DRELEASE
@ -199,12 +222,24 @@ build_flags = ${env.build_flags}
-DENABLE_MQTT -DENABLE_MQTT
-DPLUGIN_DISPLAY -DPLUGIN_DISPLAY
-DENABLE_HISTORY -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 = monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-wroom32-ethernet-de] [env:esp32-wroom32-ethernet-de]
platform = espressif32 platform = espressif32
board = esp32dev board = lolin_d32
build_flags = ${env.build_flags} build_flags = ${env.build_flags}
-D ETHERNET -D ETHERNET
-DRELEASE -DRELEASE
@ -213,6 +248,18 @@ build_flags = ${env.build_flags}
-DENABLE_MQTT -DENABLE_MQTT
-DPLUGIN_DISPLAY -DPLUGIN_DISPLAY
-DENABLE_HISTORY -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 = monitor_filters =
esp32_exception_decoder esp32_exception_decoder

1
src/plugins/Display/Display.h

@ -9,6 +9,7 @@
#include "../../hm/hmSystem.h" #include "../../hm/hmSystem.h"
#include "../../hm/hmRadio.h" #include "../../hm/hmRadio.h"
#include "../../utils/helper.h" #include "../../utils/helper.h"
#include "../plugin_lang.h"
#include "Display_Mono.h" #include "Display_Mono.h"
#include "Display_Mono_128X32.h" #include "Display_Mono_128X32.h"
#include "Display_Mono_128X64.h" #include "Display_Mono_128X64.h"

8
src/plugins/Display/Display_Mono_128X32.h

@ -40,20 +40,20 @@ class DisplayMono128X32 : public DisplayMono {
printText(mFmtText, 0); printText(mFmtText, 0);
} else { } 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); 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); printText(mFmtText, 2);
IPAddress ip = WiFi.localIP(); IPAddress ip = WiFi.localIP();
if (!(mExtra % 10) && (ip)) if (!(mExtra % 10) && (ip))
printText(ip.toString().c_str(), 3); printText(ip.toString().c_str(), 3);
else if (!(mExtra % 5)) { 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); printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs) } else if (0 != mDisplayData->utcTs)
printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3); printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);

6
src/plugins/Display/Display_Mono_128X64.h

@ -93,7 +93,7 @@ class DisplayMono128X64 : public DisplayMono {
// print Date and time // print Date and time
if (0 != mDisplayData->utcTs) 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)) { if (showLine(l_Status)) {
// alternatively: // alternatively:
@ -108,7 +108,7 @@ class DisplayMono128X64 : public DisplayMono {
int8_t moon_pos = -1; int8_t moon_pos = -1;
setLineFont(l_Status); setLineFont(l_Status);
if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) 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) { else if (0 == mDisplayData->nrSleeping) {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, " "); snprintf(mFmtText, DISP_FMT_TEXT_LEN, " ");
sun_pos = 0; sun_pos = 0;
@ -145,7 +145,7 @@ class DisplayMono128X64 : public DisplayMono {
printText(mFmtText, l_TotalPower, 0xff); printText(mFmtText, l_TotalPower, 0xff);
} else { } else {
printText("offline", l_TotalPower, 0xff); printText(STR_OFFLINE, l_TotalPower, 0xff);
} }
} }

4
src/plugins/Display/Display_Mono_64X48.h

@ -42,7 +42,7 @@ class DisplayMono64X48 : public DisplayMono {
printText(mFmtText, 0); printText(mFmtText, 0);
} else { } else {
printText("offline", 0); printText(STR_OFFLINE, 0);
} }
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", mDisplayData->totalYieldDay); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "D: %4.0f Wh", mDisplayData->totalYieldDay);
@ -55,7 +55,7 @@ class DisplayMono64X48 : public DisplayMono {
if (!(mExtra % 10) && (ip)) if (!(mExtra % 10) && (ip))
printText(ip.toString().c_str(), 3); printText(ip.toString().c_str(), 3);
else if (!(mExtra % 5)) { 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); printText(mFmtText, 3);
} else if (0 != mDisplayData->utcTs) } else if (0 != mDisplayData->utcTs)
printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3); printText(ah::getTimeStr(mDisplayData->utcTs).c_str(), 3);

9
src/plugins/Display/Display_Mono_84X48.h

@ -78,7 +78,7 @@ class DisplayMono84X48 : public DisplayMono {
// print Date and time // print Date and time
if (0 != mDisplayData->utcTs) 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)) { if (showLine(l_Status)) {
// alternatively: // alternatively:
@ -90,7 +90,7 @@ class DisplayMono84X48 : public DisplayMono {
// print status of inverters // print status of inverters
else { else {
if (0 == mDisplayData->nrSleeping + mDisplayData->nrProducing) 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) else if (0 == mDisplayData->nrSleeping)
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol snprintf(mFmtText, DISP_FMT_TEXT_LEN, "\x86"); // sun symbol
else if (0 == mDisplayData->nrProducing) else if (0 == mDisplayData->nrProducing)
@ -110,9 +110,8 @@ class DisplayMono84X48 : public DisplayMono {
snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower); snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%.0f W", mDisplayData->totalPower);
printText(mFmtText, l_TotalPower, 0xff); printText(mFmtText, l_TotalPower, 0xff);
} else { } else
printText("offline", l_TotalPower, 0xff); printText(STR_OFFLINE, l_TotalPower, 0xff);
}
} }
if (showLine(l_YieldDay)) { if (showLine(l_YieldDay)) {

11
src/plugins/Display/Display_ePaper.cpp

@ -8,6 +8,7 @@
#include "../../utils/helper.h" #include "../../utils/helper.h"
#include "imagedata.h" #include "imagedata.h"
#include "defines.h" #include "defines.h"
#include "../plugin_lang.h"
#if defined(ESP32) #if defined(ESP32)
@ -120,7 +121,7 @@ void DisplayEPaper::headlineIP() {
if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) { if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) {
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s", WiFi.localIP().toString().c_str()); snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%s", WiFi.localIP().toString().c_str());
} else { } 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); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx; 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->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding);
_display->fillScreen(GxEPD_BLACK); _display->fillScreen(GxEPD_BLACK);
do { 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); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx; uint16_t x = ((_display->width() - tbw) / 2) - tbx;
@ -183,7 +184,7 @@ void DisplayEPaper::offlineFooter() {
_display->fillScreen(GxEPD_BLACK); _display->fillScreen(GxEPD_BLACK);
do { do {
if (NULL != mUtcTs) { 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); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx; 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); snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "%.0f W", totalPower);
_changed = true; _changed = true;
} else } else
snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, "offline"); snprintf(_fmtText, EPAPER_MAX_TEXT_LEN, STR_OFFLINE);
if ((totalPower == 0) && (mEnPowerSave)) { if ((totalPower == 0) && (mEnPowerSave)) {
_display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK); _display->fillRect(0, mHeadFootPadding, 200, 200, GxEPD_BLACK);
@ -268,7 +269,7 @@ void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, floa
// Inverter online // Inverter online
_display->setFont(&FreeSans12pt7b); _display->setFont(&FreeSans12pt7b);
y = _display->height() - (mHeadFootPadding + 10); 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->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
_display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK); _display->drawInvertedBitmap(10, y - tbh, myWR, 20, 20, GxEPD_BLACK);
x = ((_display->width() - tbw - 20) / 2) - tbx; x = ((_display->width() - tbw - 20) / 2) - tbx;

225
src/plugins/history.h

@ -17,6 +17,7 @@
enum class HistoryStorageType : uint8_t { enum class HistoryStorageType : uint8_t {
POWER, POWER,
POWER_DAY,
YIELD YIELD
}; };
@ -27,12 +28,14 @@ class HistoryData {
uint16_t refreshCycle = 0; uint16_t refreshCycle = 0;
uint16_t loopCnt = 0; uint16_t loopCnt = 0;
uint16_t listIdx = 0; // index for next Element to write into WattArr 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 // ring buffer for watt history
std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data; std::array<uint16_t, (HISTORY_DATA_ARR_LENGTH + 1)> data;
storage_t() { data.fill(0); } void reset() {
loopCnt = 0;
listIdx = 0;
data.fill(0);
}
}; };
public: public:
@ -43,33 +46,56 @@ class HistoryData {
mTs = ts; mTs = ts;
mCurPwr.refreshCycle = mConfig->inst.sendInterval; 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() { void tickerSecond() {
;
float curPwr = 0; float curPwr = 0;
float maxPwr = 0; //float maxPwr = 0;
float yldDay = -0.1; float yldDay = -0.1;
uint32_t ts = 0;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) { for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
Inverter<> *iv = mSys->getInverterByPos(i); Inverter<> *iv = mSys->getInverterByPos(i);
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv == NULL) if (iv == NULL)
continue; continue;
curPwr += iv->getChannelFieldValue(CH0, FLD_PAC, rec); 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); yldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
if (rec->ts > ts)
ts = rec->ts;
} }
if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) { if ((++mCurPwr.loopCnt % mCurPwr.refreshCycle) == 0) {
mCurPwr.loopCnt = 0; mCurPwr.loopCnt = 0;
if (curPwr > 0) if (curPwr > 0) {
mLastValueTs = ts;
addValue(&mCurPwr, roundf(curPwr)); addValue(&mCurPwr, roundf(curPwr));
if (maxPwr > 0) if (curPwr > mMaximumDay)
mMaximumDay = roundf(maxPwr); mMaximumDay = roundf(curPwr);
}
//if (maxPwr > 0)
// mMaximumDay = roundf(maxPwr);
} }
/*if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) { if ((++mCurPwrDay.loopCnt % mCurPwrDay.refreshCycle) == 0) {
mCurPwrDay.loopCnt = 0;
if (curPwr > 0) {
mLastValueTs = ts;
addValueDay(&mCurPwrDay, roundf(curPwr));
}
}
#if defined(ENABLE_HISTORY_YIELD_PER_DAY)
if((++mYieldDay.loopCnt % mYieldDay.refreshCycle) == 0) {
mYieldDay.loopCnt = 0;
if (*mTs > mApp->getSunset()) { if (*mTs > mApp->getSunset()) {
if ((!mDayStored) && (yldDay > 0)) { if ((!mDayStored) && (yldDay > 0)) {
addValue(&mYieldDay, roundf(yldDay)); addValue(&mYieldDay, roundf(yldDay));
@ -77,28 +103,172 @@ class HistoryData {
} }
} else if (*mTs > mApp->getSunrise()) } else if (*mTs > mApp->getSunrise())
mDayStored = false; mDayStored = false;
}*/ }
#endif
} }
uint16_t valueAt(HistoryStorageType type, uint16_t i) { uint16_t valueAt(HistoryStorageType type, uint16_t i) {
//storage_t *s = (HistoryStorageType::POWER == type) ? &mCurPwr : &mYieldDay; storage_t *s = nullptr;
storage_t *s = &mCurPwr; uint16_t idx=i;
uint16_t idx = (s->dispIdx + i) % HISTORY_DATA_ARR_LENGTH; DPRINTLN(DBG_VERBOSE, F("valueAt ") + String((uint8_t)type) + " i=" + String(i));
return s->data[idx];
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() { uint16_t getMaximumDay() {
return mMaximumDay; 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: private:
void addValue(storage_t *s, uint16_t value) { 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->data[s->listIdx] = value;
s->listIdx = (s->listIdx + 1) % (HISTORY_DATA_ARR_LENGTH); 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: private:
@ -109,8 +279,23 @@ class HistoryData {
uint32_t *mTs = nullptr; uint32_t *mTs = nullptr;
storage_t mCurPwr; storage_t mCurPwr;
storage_t mCurPwrDay;
#if defined(ENABLE_HISTORY_YIELD_PER_DAY)
storage_t mYieldDay;
#endif
bool mDayStored = false; bool mDayStored = false;
uint16_t mMaximumDay = 0; 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*/ #endif /*ENABLE_HISTORY*/

44
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__*/

35
src/utils/helper.cpp

@ -5,6 +5,12 @@
#include "helper.h" #include "helper.h"
#include "dbg.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 { namespace ah {
void ip2Arr(uint8_t ip[], const char *ipStr) { 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]); 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) { double round3(double value) {
return (int)(value * 1000 + 0.5) / 1000.0; return (int)(value * 1000 + 0.5) / 1000.0;
} }
@ -82,6 +92,31 @@ namespace ah {
return String(str); 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) { uint64_t Serial2u64(const char *val) {
char tmp[3]; char tmp[3];
uint64_t ret = 0ULL; uint64_t ret = 0ULL;

2
src/utils/helper.h

@ -39,9 +39,11 @@ static Timezone gTimezone(CEST, CET);
namespace ah { namespace ah {
void ip2Arr(uint8_t ip[], const char *ipStr); void ip2Arr(uint8_t ip[], const char *ipStr);
void ip2Char(uint8_t ip[], char *str); void ip2Char(uint8_t ip[], char *str);
double round1(double value);
double round3(double value); double round3(double value);
String getDateTimeStr(time_t t); String getDateTimeStr(time_t t);
String getDateTimeStrShort(time_t t); String getDateTimeStrShort(time_t t);
String getDateTimeStrShort_i18n(time_t t);
String getDateTimeStrFile(time_t t); String getDateTimeStrFile(time_t t);
String getTimeStr(time_t t); String getTimeStr(time_t t);
String getTimeStrMs(uint64_t t); String getTimeStrMs(uint64_t t);

139
src/web/RestApi.h

@ -45,6 +45,11 @@ class RestApi {
mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false); mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false);
#endif #endif
mConfig = config; 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( 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)); 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)); mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1));
@ -99,6 +104,8 @@ class RestApi {
#endif /* !defined(ETHERNET) */ #endif /* !defined(ETHERNET) */
else if(path == "live") getLive(request,root); else if(path == "live") getLive(request,root);
else if (path == "powerHistory") getPowerHistory(request, root); else if (path == "powerHistory") getPowerHistory(request, root);
else if (path == "powerHistoryDay") getPowerHistoryDay(request, root);
else if (path == "yieldDayHistory") getYieldDayHistory(request, root);
else { else {
if(path.substring(0, 12) == "inverter/id/") if(path.substring(0, 12) == "inverter/id/")
getInverter(root, request->url().substring(17).toInt()); getInverter(root, request->url().substring(17).toInt());
@ -133,7 +140,94 @@ class RestApi {
#endif #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<JsonObject>();
// 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"); DPRINTLN(DBG_VERBOSE, "onApiPostBody");
if(0 == index) { if(0 == index) {
@ -203,6 +297,8 @@ class RestApi {
ep[F("live")] = url + F("live"); ep[F("live")] = url + F("live");
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
ep[F("powerHistory")] = url + F("powerHistory"); ep[F("powerHistory")] = url + F("powerHistory");
ep[F("powerHistoryDay")] = url + F("powerHistoryDay");
ep[F("yieldDayHistory")] = url + F("yieldDayHistory");
#endif #endif
} }
@ -297,6 +393,7 @@ class RestApi {
obj[F("heap_free")] = mHeapFree; obj[F("heap_free")] = mHeapFree;
obj[F("sketch_total")] = ESP.getFreeSketchSpace(); obj[F("sketch_total")] = ESP.getFreeSketchSpace();
obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb
obj[F("wifi_channel")] = WiFi.channel();
getGeneric(request, obj); getGeneric(request, obj);
getRadioNrf(obj.createNestedObject(F("radioNrf"))); getRadioNrf(obj.createNestedObject(F("radioNrf")));
@ -495,7 +592,7 @@ class RestApi {
obj[F("name")] = String(iv->config->name); obj[F("name")] = String(iv->config->name);
obj[F("serial")] = String(iv->config->serial.u64, HEX); obj[F("serial")] = String(iv->config->serial.u64, HEX);
obj[F("version")] = String(iv->getFwVersion()); 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("power_limit_ack")] = iv->powerLimitAck;
obj[F("max_pwr")] = iv->getMaxPower(); obj[F("max_pwr")] = iv->getMaxPower();
obj[F("ts_last_success")] = rec->ts; obj[F("ts_last_success")] = rec->ts;
@ -811,7 +908,7 @@ class RestApi {
void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) { void getPowerHistory(AsyncWebServerRequest *request, JsonObject obj) {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
#if defined(ENABLE_HISTORY) #if defined(ENABLE_HISTORY)
obj[F("refresh")] = mConfig->inst.sendInterval; obj[F("refresh")] = mApp->getHistoryPeriod((uint8_t)HistoryStorageType::POWER);
uint16_t max = 0; uint16_t max = 0;
for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) { for (uint16_t fld = 0; fld < HISTORY_DATA_ARR_LENGTH; fld++) {
uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld); uint16_t value = mApp->getHistoryValue((uint8_t)HistoryStorageType::POWER, fld);
@ -820,9 +917,39 @@ class RestApi {
max = value; max = value;
} }
obj[F("max")] = max; obj[F("max")] = max;
obj[F("maxDay")] = mApp->getHistoryMaxDay(); obj[F("lastValueTs")] = mApp->getHistoryLastValueTs((uint8_t)HistoryStorageType::POWER);
#else #endif /*ENABLE_HISTORY*/
obj[F("refresh")] = 86400; // 1 day; }
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*/ #endif /*ENABLE_HISTORY*/
} }

21
src/web/html/api.js

@ -61,6 +61,23 @@ function ml(tagName, ...args) {
return nester(el, args[1]) 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) { function nester(el, n) {
if (typeof n === "string") { if (typeof n === "string") {
el.innerHTML = n; el.innerHTML = n;
@ -84,10 +101,12 @@ function topnav() {
} }
function parseNav(obj) { function parseNav(obj) {
for(i = 0; i < 13; i++) { for(i = 0; i < 14; i++) {
if(i == 2) if(i == 2)
continue; continue;
var l = document.getElementById("nav"+i); var l = document.getElementById("nav"+i);
if(null == l)
continue
if(12 == i) { if(12 == i) {
if(obj.cst_lnk.length > 0) { if(obj.cst_lnk.length > 0) {
l.href = obj.cst_lnk l.href = obj.cst_lnk

4
src/web/html/colorBright.css

@ -30,4 +30,8 @@
--ch-head-bg: #006ec0; --ch-head-bg: #006ec0;
--ts-head: #333; --ts-head: #333;
--ts-bg: #555; --ts-bg: #555;
--chart-cont: #fbfbfb;
--chart-bg: #f9f9f9;
--chart-text: #000000;
} }

4
src/web/html/colorDark.css

@ -30,4 +30,8 @@
--ch-head-bg: #236; --ch-head-bg: #236;
--ts-head: #333; --ts-head: #333;
--ts-bg: #555; --ts-bg: #555;
--chart-cont: #0b0b0b;
--chart-bg: #090909;
--chart-text: #FFFFFF;
} }

39
src/web/html/grid_info.json

@ -14,6 +14,7 @@
{"0x0d04": "NF_EN_50549-1:2019"}, {"0x0d04": "NF_EN_50549-1:2019"},
{"0x1000": "ES_RD1699"}, {"0x1000": "ES_RD1699"},
{"0x1200": "EU_EN50438"}, {"0x1200": "EU_EN50438"},
{"0x1300": "MEX_NOM_220V"},
{"0x2600": "BE_C10_26"}, {"0x2600": "BE_C10_26"},
{"0x2900": "NL_NEN-EN50549-1_2019"}, {"0x2900": "NL_NEN-EN50549-1_2019"},
{"0x2a00": "PL_PN-EN 50549-1:2019"}, {"0x2a00": "PL_PN-EN 50549-1:2019"},
@ -35,6 +36,44 @@
{"0xb0": "Watt Power Factor"} {"0xb0": "Watt Power Factor"}
], ],
"group": [ "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": [ "0x0003": [
{ {

211
src/web/html/history.html

@ -12,84 +12,175 @@
{#HTML_NAV} {#HTML_NAV}
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<h3>{#TOTAL_POWER}</h3> <h3>Total Power</h3>
<div>
<div class="chartDiv" id="pwrChart"></div> <div class="chartDiv" id="pwrChart"></div>
<p> <h3>Total Power Today</h3>
{#MAX_DAY}: <span id="pwrMaxDay"></span> W. {#LAST_VALUE}: <span id="pwrLast"></span> W.<br /> <div class="chartDiv" id="pwrDayChart"></div>
{#MAXIMUM}: <span id="pwrMax"></span> W. {#UPDATED} <span id="pwrRefresh"></span> {#SECONDS} <!--IF_ENABLE_HISTORY_YIELD_PER_DAY-->
</p> <h3>Total Yield per day</h3>
</div> <div class="chartDiv" id="ydChart"></div>
<!--ENDIF_ENABLE_HISTORY_YIELD_PER_DAY-->
<!--IF_ENABLE_HISTORY_LOAD_DATA-->
<h4 style="margin-bottom:0px;">Insert data into Yield per day history</h4>
<fieldset style="padding: 1px;">
<legend class="des" style="margin-top: 0px;">Insert data (*.json) i.e. from a saved "/api/yieldDayHistory" call
</legend>
<form id="form" method="POST" action="/api/addYDHist" enctype="multipart/form-data"
accept-charset="utf-8">
<input type="button" class="btn my-4" style="padding: 3px;margin: 3px;" value="Insert" onclick="submit()">
<input type="file" name="insert" style="width: 80%;">
</form>
</fieldset>
<!--ENDIF_ENABLE_HISTORY_LOAD_DATA-->
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}
<script type="text/javascript"> <script type="text/javascript">
const svgns = "http://www.w3.org/2000/svg"; const height = 250
var pwrExeOnce = true; var once = true
var ydExeOnce = true;
// make a simple rectangle
var mRefresh = 60;
var mLastValue = 0;
const mChartHeight = 250;
function parseHistory(obj, namePrefix, execOnce) { function calcScale(obj) {
mRefresh = obj.refresh let s = {}
var data = Object.assign({}, obj.value) s.x_mul = 60
numDataPts = Object.keys(data).length s.ts_start = obj.lastValueTs - (obj.refresh * obj.value.length)
s.ts_dur = obj.lastValueTs - s.ts_start
s.ts_pad = (s.ts_dur < 1800) ? s.ts_start % 300 : s.ts_start % 1800
s.ts_dur -= s.ts_pad
while(s.x_mul * 10 <= s.ts_dur)
s.x_mul += (s.x_mul == 60) ? 240 : ((s.x_mul < 1800) ? 300 : 1800)
s.x_step = Math.ceil(s.ts_dur / s.x_mul)
s.x_max = s.x_mul * s.x_step
if (true == execOnce) { s.y_mul = 10
let s = document.createElementNS(svgns, "svg"); while(s.y_mul * 10 <= obj.max)
s.setAttribute("class", "chart"); s.y_mul += (s.y_mul < 100) ? 10 : 100
s.setAttribute("width", (numDataPts + 2) * 2); s.y_step = Math.ceil(obj.max / s.y_mul)
s.setAttribute("height", mChartHeight); s.y_max = s.y_mul * s.y_step
s.setAttribute("role", "img"); return s
}
let g = document.createElementNS(svgns, "g"); function setupSvg(id, obj) {
s.appendChild(g); let scale = calcScale(obj)
for (var i = 0; i < numDataPts; i++) { let n = obj.value.length
val = data[i]; return mlNs("svg", {class: "container", id: id+"_svg", viewBox: "0 0 "+String(n*2+50)+" "+String(height+20), width: "100%", height: "100%"}, [
let rect = document.createElementNS(svgns, "rect"); mlNs("defs", {}, [
rect.setAttribute("id", namePrefix+"Rect" + i); mlNs("linearGradient", {id: "gLine", x1: 0, y1: 0, x2: 0, y2: "100%"}, [
rect.setAttribute("x", i * 2); mlNs("stop", {offset: 0, "stop-color": "#006ec0"}),
rect.setAttribute("width", 2); mlNs("stop", {offset: "80%", "stop-color": "#5050ff"}),
g.appendChild(rect); mlNs("stop", {offset: "100%", "stop-color": "gray"})
} ]),
document.getElementById(namePrefix+"Chart").appendChild(s); mlNs("linearGradient", {id: "gFill", x1: 0, y1: 0, x2: 0, y2: "100%"}, [
mlNs("stop", {offset: 0, "stop-color": "#006ec0"}),
mlNs("stop", {offset: "50%", "stop-color": "#0034c0"}),
mlNs("stop", {offset: "100%", "stop-color": "#e0e0e0"})
])
]),
...gridText(n*2, scale),
mlNs("g", {transform: "translate(30, 5)"}, [
...grid(n*2, scale),
...poly(obj, scale)
])
])
} }
// normalize data to chart function gridText(x2, scale) {
let divider = obj.max / mChartHeight; let g = []
if (divider == 0) let div = height / scale.y_max
divider = 1; for(let i = 0; i <= scale.y_max; i += scale.y_mul) {
for (var i = 0; i < numDataPts; i++) { g.push(mlNs("text", {x: 0, y: height-(i*div)+9}, String(i)))
val = data[i]; }
if (val > 0) div = x2 / scale.x_max
mLastValue = val for(let i = 0; i < scale.x_max; i++) {
val = val / divider if((i + scale.ts_pad) % scale.x_mul == 0) {
rect = document.getElementById(namePrefix+"Rect" + i); let d = new Date((scale.ts_start + i) * 1000)
rect.setAttribute("height", val); g.push(mlNs("text", {x: (i*div)+17, y: height+20}, ("0"+d.getHours()).slice(-2) + ":" + ("0"+d.getMinutes()).slice(-2)))
rect.setAttribute("y", mChartHeight - val); }
} }
document.getElementById(namePrefix + "Max").innerHTML = obj.max; return g
if (mRefresh < 5)
mRefresh = 5;
document.getElementById(namePrefix + "Refresh").innerHTML = mRefresh;
} }
function grid(x2, scale) {
let g = []
let div = height / scale.y_max
for(let i = 0; i <= scale.y_max; i += scale.y_mul) {
g.push(mlNs("line", {x1: 0, x2: x2, y1: height-i*div, y2: height-i*div, "stroke-width": 1, "stroke-dasharray": "1,3", stroke: "#aaa"}))
}
div = x2 / scale.x_max
for(let i = 0; i <= scale.x_max; i++) {
if((i + scale.ts_pad) % scale.x_mul == 0) {
g.push(mlNs("line", {x1: (i*div), x2: (i*div), y1: 0, y2: height, "stroke-width": 1, "stroke-dasharray": "1,3", stroke: "#aaa"}))
}
}
return g
}
function poly(obj, scale) {
let pts = ""
let i = 0, first = -1, last = -1, lastVal = 0
let div = scale.y_max / height
if(div == 0)
div = 1
for (val of obj.value) {
if(val > 0) {
lastVal = val
pts += " " + String(i) + "," + String(height - val / div)
if(first < 0)
first = i
last = i
}
i += 2
}
let pts2 = pts + " " + String(last) + "," + String(height)
pts2 += " " + String(first) + "," + String(height)
return [
mlNs("polyline", {stroke: "url(#gLine)", fill: "none", points: pts}),
mlNs("polyline", {stroke: "none", fill: "url(#gFill)", points: pts2}),
mlNs("text", {x: i*.8, y: 10}, "Maximum: " + String(obj.max) + "W"),
mlNs("text", {x: i*.8, y: 25}, "Last: " + String(lastVal) + "W")
]
}
function parsePowerHistory(obj){ function parsePowerHistory(obj){
if (null != obj) { if(once) {
once = false
parseNav(obj.generic); parseNav(obj.generic);
parseESP(obj.generic); window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", obj.refresh * 1000)
parseHistory(obj,"pwr", pwrExeOnce) setTimeout(() => {
document.getElementById("pwrLast").innerHTML = mLastValue window.setInterval("getAjax('/api/powerHistoryDay', parsePowerHistoryDay)", refresh * 1000)
document.getElementById("pwrMaxDay").innerHTML = obj.maxDay }, 200)
/*IF_ENABLE_HISTORY_YIELD_PER_DAY*/
setTimeout(() => {
window.setInterval("getAjax('/api/yieldDayHistory', parseYieldDayHistory)", refresh * 1000)
}, 400)
/*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/
}
if (null != obj) {
let svg = setupSvg("ph", obj);
document.getElementById("pwrChart").replaceChildren(svg);
setTimeout(() => { getAjax("/api/powerHistoryDay", parsePowerHistoryDay) }, 50);
}
} }
if (pwrExeOnce) {
pwrExeOnce = false; function parsePowerHistoryDay(obj) {
window.setInterval("getAjax('/api/powerHistory', parsePowerHistory)", mRefresh * 1000); if (null != obj) {
let svg = setupSvg("phDay", obj);
document.getElementById("pwrDayChart").replaceChildren(svg);
/*IF_ENABLE_HISTORY_YIELD_PER_DAY*/
setTimeout(() => { getAjax("/api/yieldDayHistory", parseYieldDayHistory) }, 50);
/*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/
}
}
/*IF_ENABLE_HISTORY_YIELD_PER_DAY*/
function parseYieldDayHistory(obj) {
if (null != obj) {
let svg = setupSvg("phDay", obj);
document.getElementById("pwrDayChart").replaceChildren(svg);
} }
} }
/*ENDIF_ENABLE_HISTORY_YIELD_PER_DAY*/
getAjax("/api/powerHistory", parsePowerHistory); getAjax("/api/powerHistory", parsePowerHistory);
</script> </script>

5
src/web/html/includes/nav.html

@ -7,7 +7,9 @@
</a> </a>
<div id="topnav" class="mobile"> <div id="topnav" class="mobile">
<a id="nav3" class="hide" href="/live?v={#VERSION}">{#NAV_LIVE}</a> <a id="nav3" class="hide" href="/live?v={#VERSION}">{#NAV_LIVE}</a>
<!--IF_ENABLE_HISTORY-->
<a id="nav11" class="acitve" href="/history?v={#VERSION}">{#NAV_HISTORY}</a> <a id="nav11" class="acitve" href="/history?v={#VERSION}">{#NAV_HISTORY}</a>
<!--ENDIF_ENABLE_HISTORY-->
<a id="nav4" class="hide" href="/serial?v={#VERSION}">{#NAV_WEBSERIAL}</a> <a id="nav4" class="hide" href="/serial?v={#VERSION}">{#NAV_WEBSERIAL}</a>
<a id="nav5" class="hide" href="/setup?v={#VERSION}">{#NAV_SETTINGS}</a> <a id="nav5" class="hide" href="/setup?v={#VERSION}">{#NAV_SETTINGS}</a>
<span class="separator"></span> <span class="separator"></span>
@ -15,7 +17,8 @@
<a id="nav7" class="hide" href="/system?v={#VERSION}">System</a> <a id="nav7" class="hide" href="/system?v={#VERSION}">System</a>
<span class="separator"></span> <span class="separator"></span>
<a id="nav8" href="/api" target="_blank">REST API</a> <a id="nav8" href="/api" target="_blank">REST API</a>
<a id="nav9" href="https://ahoydtu.de" target="_blank">{#NAV_DOCUMENTATION}</a> <a id="nav9" href="https://docs.ahoydtu.de" target="_blank">{#NAV_DOCUMENTATION}</a>
<a id="nav13" href="https://ahoydtu.de" target="_blank">Website</a>
<a id="nav10" href="/about?v={#VERSION}">{#NAV_ABOUT}</a> <a id="nav10" href="/about?v={#VERSION}">{#NAV_ABOUT}</a>
<a id="nav12" href="#" class="hide" target="_blank">Custom Link</a> <a id="nav12" href="#" class="hide" target="_blank">Custom Link</a>
<span class="separator"></span> <span class="separator"></span>

116
src/web/html/setup.html

@ -272,7 +272,7 @@
<!--ENDIF_ESP32--> <!--ENDIF_ESP32-->
</fieldset> </fieldset>
</div> </div>
<!--IF_PLUGIN_DISPLAY-->
<button type="button" class="s_collapsible">{#DISPLAY_CONFIG}</button> <button type="button" class="s_collapsible">{#DISPLAY_CONFIG}</button>
<div class="s_content"> <div class="s_content">
<fieldset class="mb-4"> <fieldset class="mb-4">
@ -301,6 +301,7 @@
</div> </div>
</fieldset> </fieldset>
</div> </div>
<!--ENDIF_PLUGIN_DISPLAY-->
<div class="row mb-4 mt-4"> <div class="row mb-4 mt-4">
<div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div> <div class="col-8 col-sm-3">{#BTN_REBOOT_SUCCESSFUL_SAVE}</div>
@ -341,6 +342,7 @@
var maxInv = 0; var maxInv = 0;
var ts = 0; var ts = 0;
/*IF_ESP8266*/
var esp8266pins = [ var esp8266pins = [
[255, "{#PIN_OFF}"], [255, "{#PIN_OFF}"],
[0, "D3 (GPIO0)"], [0, "D3 (GPIO0)"],
@ -361,6 +363,7 @@
[15, "D8 (GPIO15)"], [15, "D8 (GPIO15)"],
[16, "D0 (GPIO16 - {#PIN_NO_IRQ})"] [16, "D0 (GPIO16 - {#PIN_NO_IRQ})"]
]; ];
/*ENDIF_ESP8266*/
/*IF_ESP32*/ /*IF_ESP32*/
var esp32pins = [ var esp32pins = [
@ -392,6 +395,7 @@
[36, "VP (GPIO36, {#PIN_INPUT_ONLY})"], [36, "VP (GPIO36, {#PIN_INPUT_ONLY})"],
[39, "VN (GPIO39, {#PIN_INPUT_ONLY})"] [39, "VN (GPIO39, {#PIN_INPUT_ONLY})"]
]; ];
/*IF_ESP32-S2*/
var esp32sXpins = [ var esp32sXpins = [
[255, "off / default"], [255, "off / default"],
[0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"], [0, "GPIO0 ({#PIN_DONT_USE} - BOOT)"],
@ -440,6 +444,58 @@
[47, "GPIO47"], [47, "GPIO47"],
[48, "GPIO48"], [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 = [ var esp32c3pins = [
[255, "off / default"], [255, "off / default"],
[0, "GPIO0"], [0, "GPIO0"],
@ -465,6 +521,7 @@
[20, "GPIO20 (RX)"], [20, "GPIO20 (RX)"],
[21, "GPIO21 (TX)"], [21, "GPIO21 (TX)"],
]; ];
/*ENDIF_ESP32-C3*/
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
var nrfPa = [ var nrfPa = [
[0, "MIN ({#PIN_RECOMMENDED})"], [0, "MIN ({#PIN_RECOMMENDED})"],
@ -890,11 +947,19 @@
function parsePinout(obj, type, system) { function parsePinout(obj, type, system) {
var e = document.getElementById("pinout"); var e = document.getElementById("pinout");
var pinList = esp8266pins;
/*IF_ESP32*/ /*IF_ESP32*/
var pinList = esp32pins; var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; /*IF_ESP32-S2*/
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; 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*/ /*ENDIF_ESP32*/
pins = [['led0', 'pinLed0', '{#LED_AT_LEAST_ONE_PRODUCING}'], ['led1', 'pinLed1', '{#LED_MQTT_CONNECTED}'], ['led2', 'pinLed2', '{#LED_NIGHT_TIME}']]; pins = [['led0', 'pinLed0', '{#LED_AT_LEAST_ONE_PRODUCING}'], ['led1', 'pinLed1', '{#LED_MQTT_CONNECTED}'], ['led2', 'pinLed2', '{#LED_NIGHT_TIME}']];
for(p of pins) { for(p of pins) {
@ -926,11 +991,19 @@
var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox"); var en = inp("nrfEnable", null, null, ["cb"], "nrfEnable", "checkbox");
en.checked = obj["en"]; en.checked = obj["en"];
var pinList = esp8266pins;
/*IF_ESP32*/ /*IF_ESP32*/
var pinList = esp32pins; var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; /*IF_ESP32-S2*/
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; 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*/ /*ENDIF_ESP32*/
e.replaceChildren ( e.replaceChildren (
@ -962,8 +1035,15 @@
var e = document.getElementById("cmt"); var e = document.getElementById("cmt");
var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox"); var en = inp("cmtEnable", null, null, ["cb"], "cmtEnable", "checkbox");
var pinList = esp32pins; var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; /*IF_ESP32-S2*/
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; 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"]; en.checked = obj["en"];
@ -1008,12 +1088,21 @@
} }
} }
/*IF_PLUGIN_DISPLAY*/
function parseDisplay(obj, type, system) { function parseDisplay(obj, type, system) {
var pinList = esp8266pins;
/*IF_ESP32*/ /*IF_ESP32*/
var pinList = esp32pins; var pinList = esp32pins;
if ("ESP32-S3" == system.chip_model || "ESP32-S2" == system.chip_model) pinList = esp32sXpins; /*IF_ESP32-S2*/
else if("ESP32-C3" == system["chip_model"]) pinList = esp32c3pins; 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*/ /*ENDIF_ESP32*/
for(var i of ["disp_pwr"]) for(var i of ["disp_pwr"])
@ -1149,6 +1238,7 @@
setHide("screenSaver", !optionsMap.get(dispType)[2]); setHide("screenSaver", !optionsMap.get(dispType)[2]);
setHide("pirPin", !(optionsMap.get(dispType)[2] && (screenSaver==2))); // show pir pin only for motion screensaver setHide("pirPin", !(optionsMap.get(dispType)[2] && (screenSaver==2))); // show pir pin only for motion screensaver
} }
/*ENDIF_PLUGIN_DISPLAY*/
function tick() { function tick() {
document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000))); document.getElementById("date").innerHTML = toIsoDateStr((new Date((++ts) * 1000)));
@ -1168,7 +1258,9 @@
parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]); parseCmtRadio(root["radioCmt"], root["system"]["esp_type"], root["system"]);
/*ENDIF_ESP32*/ /*ENDIF_ESP32*/
parseSerial(root["serial"]); parseSerial(root["serial"]);
/*IF_PLUGIN_DISPLAY*/
parseDisplay(root["display"], root["system"]["esp_type"], root["system"]); parseDisplay(root["display"], root["system"]["esp_type"], root["system"]);
/*ENDIF_PLUGIN_DISPLAY*/
getAjax("/api/inverter/list", parseIv); getAjax("/api/inverter/list", parseIv);
} }
} }

14
src/web/html/style.css

@ -33,13 +33,17 @@ textarea {
color: var(--fg2); color: var(--fg2);
} }
svg rect {fill: #00A;} svg polyline {
svg.chart { fill-opacity: .5;
background: #f2f2f2; stroke-width: 1;
border: 2px solid gray; }
padding: 1px;
svg text {
font-size: x-small;
fill: var(--chart-text);
} }
div.chartDivContainer { div.chartDivContainer {
padding: 1px; padding: 1px;
margin: 1px; margin: 1px;

4
src/web/html/system.html

@ -21,8 +21,8 @@
} }
function parseSysInfo(obj) { function parseSysInfo(obj) {
const data = ["sdk", "cpu_freq", "chip_revision", const data = ["sdk", "cpu_freq", "chip_revision", "device_name",
"chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "ts_uptime", "chip_model", "chip_cores", "esp_type", "mac", "wifi_rssi", "wifi_channel", "ts_uptime",
"flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag", "flash_size", "sketch_used", "heap_total", "heap_free", "heap_frag",
"max_free_blk", "version", "modules", "env", "core_version", "reboot_reason"]; "max_free_blk", "version", "modules", "env", "core_version", "reboot_reason"];

2
src/web/html/visualization.html

@ -116,7 +116,7 @@
if(65535 != obj.power_limit_read) { if(65535 != obj.power_limit_read) {
pwrLimit = obj.power_limit_read + "&nbsp;%"; pwrLimit = obj.power_limit_read + "&nbsp;%";
if(0 != obj.max_pwr) if(0 != obj.max_pwr)
pwrLimit += ", " + Math.round(obj.max_pwr * obj.power_limit_read / 100) + "&nbsp;W"; pwrLimit += ", " + (obj.max_pwr * obj.power_limit_read / 100) + "&nbsp;W";
} }
var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000)); var maxAcPwr = toIsoDateStr(new Date(obj.ts_max_ac_pwr * 1000));

25
src/web/lang.json

@ -1528,6 +1528,21 @@
"en": "Total Power", "en": "Total Power",
"de": "Gesamtleistung" "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", "token": "TOTAL_YIELD_PER_DAY",
"en": "Total Yield per day", "en": "Total Yield per day",
@ -1535,23 +1550,23 @@
}, },
{ {
"token": "MAX_DAY", "token": "MAX_DAY",
"en": "maximum day", "en": "Maximum day",
"de": "Tagesmaximum" "de": "Tagesmaximum"
}, },
{ {
"token": "LAST_VALUE", "token": "LAST_VALUE",
"en": "last value", "en": "Last value",
"de": "letzter Wert" "de": "Letzter Wert"
}, },
{ {
"token": "MAXIMUM", "token": "MAXIMUM",
"en": "maximum value", "en": "Maximum value",
"de": "Maximalwert" "de": "Maximalwert"
}, },
{ {
"token": "UPDATED", "token": "UPDATED",
"en": "Updated every", "en": "Updated every",
"de": "aktualisiert alle" "de": "Aktualisiert alle"
}, },
{ {
"token": "SECONDS", "token": "SECONDS",

Loading…
Cancel
Save