Browse Source

Merge branch 'development03' into hms

pull/935/head
lumapu 2 years ago
parent
commit
7ce78ab56e
  1. 1
      .gitignore
  2. 3
      User_Manual.md
  3. 22
      src/CHANGES.md
  4. 7
      src/LICENSE
  5. 23
      src/app.cpp
  6. 29
      src/app.h
  7. 69
      src/config/settings.h
  8. 2
      src/defines.h
  9. 4
      src/hm/hmDefines.h
  10. 4
      src/hm/hmPayload.h
  11. 57
      src/hm/miPayload.h
  12. 8
      src/platformio.ini
  13. 113
      src/plugins/Display/Display.h
  14. 149
      src/plugins/Display/Display_Mono.cpp
  15. 36
      src/plugins/Display/Display_Mono.h
  16. 197
      src/plugins/Display/Display_ePaper.cpp
  17. 52
      src/plugins/Display/Display_ePaper.h
  18. 329
      src/plugins/Display/imagedata.h
  19. 217
      src/plugins/MonochromeDisplay/MonochromeDisplay.h
  20. 33
      src/publisher/pubMqtt.h
  21. 162
      src/web/RestApi.h
  22. 86
      src/web/html/api.js
  23. 27
      src/web/html/colorBright.css
  24. 27
      src/web/html/colorDark.css
  25. 89
      src/web/html/convert.py
  26. 16
      src/web/html/includes/footer.html
  27. 5
      src/web/html/includes/header.html
  28. 25
      src/web/html/includes/nav.html
  29. 81
      src/web/html/index.html
  30. 35
      src/web/html/login.html
  31. 92
      src/web/html/serial.html
  32. 526
      src/web/html/setup.html
  33. 485
      src/web/html/style.css
  34. 35
      src/web/html/system.html
  35. 46
      src/web/html/update.html
  36. 285
      src/web/html/visualization.html
  37. 141
      src/web/web.h
  38. BIN
      tools/cases/EKD_ESPNRF_Case/EKDESPNRFCase.jpg
  39. BIN
      tools/cases/EKD_ESPNRF_Case/EKD_ESPNRF_Case_Body.3mf
  40. BIN
      tools/cases/EKD_ESPNRF_Case/EKD_ESPNRF_Case_Lid.3mf
  41. 30
      tools/cases/EKD_ESPNRF_Case/Readme.md
  42. 73
      tools/rpi/README.md
  43. 6
      tools/rpi/ahoy.service
  44. 13
      tools/rpi/ahoy.yml.example
  45. 39
      tools/rpi/hoymiles/__init__.py
  46. 58
      tools/rpi/hoymiles/__main__.py
  47. 50
      tools/rpi/hoymiles/decoders/__init__.py
  48. 72
      tools/rpi/hoymiles/outputs.py

1
.gitignore

@ -6,6 +6,7 @@
.vscode/extensions.json .vscode/extensions.json
src/config/config_override.h src/config/config_override.h
src/web/html/h/* src/web/html/h/*
src/web/html/tmp/*
/**/Debug /**/Debug
/**/v16/* /**/v16/*
*.db *.db

3
User_Manual.md

@ -306,7 +306,8 @@ To get the information open the URL `/api/record/info` on your AhoyDTU. The info
| chehrlic | HM-600 | | 1.0.10 | 2021 | 11-01 | 104 | | | | chehrlic | HM-600 | | 1.0.10 | 2021 | 11-01 | 104 | | |
| chehrlic | TSOL-M800de | | 1.0.10 | 2021 | 11-01 | 104 | | | | chehrlic | TSOL-M800de | | 1.0.10 | 2021 | 11-01 | 104 | | |
| B5r1oJ0A9G | HM-800 | | 1.0.10 | 2021 | | 104 | | | | B5r1oJ0A9G | HM-800 | | 1.0.10 | 2021 | | 104 | | |
| | | | | | | | | | | B5r1oJ0A9G | HM-800 | | 1.0.10 | 2021 | | 104 | | |
| tomquist | TSOL-M1600 | | 1.0.12 | 2020 | 06-24 | 100 | | |
| | | | | | | | | | | | | | | | | | | |
## Developer Information about Command Queue ## Developer Information about Command Queue

22
src/CHANGES.md

@ -2,6 +2,28 @@
(starting from release version `0.5.66`) (starting from release version `0.5.66`)
## 0.5.94
* added ePaper (for ESP32 only!), thx @dAjaY85 #735
* improved `/live` margins #732
* renamed `var` to `VAr` #732
## 0.5.93
* improved web API for `live`
* added dark mode option
* converted all forms to reponsive design
* repaired menu with password protection #720, #716, #709
* merged MI series fixes #729
## 0.5.92
* fix mobile menu
* fix inverters in select `serial.html` #709
## 0.5.91
* improved html and navi, navi is visible even when API dies #660
* reduced maximum allowed JSON size for API to 6000Bytes #660
* small fix: output command at `prepareDevInformCmd` #692
* improved inverter handling #671
## 0.5.90 ## 0.5.90
* merged PR #684, #698, #705 * merged PR #684, #698, #705
* webserial minor overflow fix #660 * webserial minor overflow fix #660

7
src/LICENSE

@ -0,0 +1,7 @@
License
CC-CY-NC-SA 3.0
https://creativecommons.org/licenses/by-nc-sa/3.0/de
This project is for non-commercial use only!

23
src/app.cpp

@ -21,12 +21,6 @@ void app::setup() {
resetSystem(); resetSystem();
/*DBGPRINTLN("--- start");
DBGPRINTLN(String(ESP.getFreeHeap()));
DBGPRINTLN(String(ESP.getHeapFragmentation()));
DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));*/
mSettings.setup(); mSettings.setup();
mSettings.getPtr(mConfig); mSettings.getPtr(mConfig);
DPRINT(DBG_INFO, F("Settings valid: ")); DPRINT(DBG_INFO, F("Settings valid: "));
@ -81,6 +75,8 @@ void app::setup() {
DBGPRINTLN(String(ESP.getHeapFragmentation())); DBGPRINTLN(String(ESP.getHeapFragmentation()));
DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));*/ DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));*/
//if (!mSys.Radio.isChipConnected())
// DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
// when WiFi is in client mode, then enable mqtt broker // when WiFi is in client mode, then enable mqtt broker
#if !defined(AP_ONLY) #if !defined(AP_ONLY)
@ -100,17 +96,17 @@ void app::setup() {
// Plugins // Plugins
if (mConfig->plugin.display.type != 0) if (mConfig->plugin.display.type != 0)
mMonoDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, 0xff, mVersion); mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, 0xff, mVersion);
mPubSerial.setup(mConfig, &mSys, &mTimestamp); mPubSerial.setup(mConfig, &mSys, &mTimestamp);
regularTickers(); regularTickers();
/*DBGPRINTLN("--- end setup"); // DBGPRINTLN("--- end setup");
DBGPRINTLN(String(ESP.getFreeHeap())); // DBGPRINTLN(String(ESP.getFreeHeap()));
DBGPRINTLN(String(ESP.getHeapFragmentation())); // DBGPRINTLN(String(ESP.getHeapFragmentation()));
DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));*/ // DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -209,8 +205,7 @@ void app::onWifi(bool gotIp) {
mMqttEnabled = false; mMqttEnabled = false;
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
} }
} } else {
else {
mInnerLoopCb = std::bind(&app::loopWifi, this); mInnerLoopCb = std::bind(&app::loopWifi, this);
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL"); everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
} }
@ -222,7 +217,7 @@ void app::regularTickers(void) {
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc"); everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
// Plugins // Plugins
if (mConfig->plugin.display.type != 0) if (mConfig->plugin.display.type != 0)
everySec(std::bind(&MonoDisplayType::tickerSecond, &mMonoDisplay), "disp"); everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart");
} }

29
src/app.h

@ -6,31 +6,29 @@
#ifndef __APP_H__ #ifndef __APP_H__
#define __APP_H__ #define __APP_H__
#include "utils/dbg.h"
#include <Arduino.h> #include <Arduino.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <RF24.h>
#include <RF24_config.h>
#include "appInterface.h" #include "appInterface.h"
#include "config/settings.h" #include "config/settings.h"
#include "defines.h" #include "defines.h"
#include "utils/crc.h" #include "hm/hmPayload.h"
#include "utils/scheduler.h"
#include "hm/hmSystem.h" #include "hm/hmSystem.h"
#include "hm/hmRadio.h" #include "hm/hmRadio.h"
#include "hms/hmsRadio.h" #include "hms/hmsRadio.h"
#include "hms/hmsPayload.h" #include "hms/hmsPayload.h"
#include "hm/hmPayload.h" #include "hm/hmPayload.h"
#include "hm/miPayload.h" #include "hm/miPayload.h"
#include "wifi/ahoywifi.h"
#include "web/web.h"
#include "web/RestApi.h"
#include "publisher/pubMqtt.h" #include "publisher/pubMqtt.h"
#include "publisher/pubSerial.h" #include "publisher/pubSerial.h"
#include "utils/crc.h"
#include "utils/dbg.h"
#include "utils/scheduler.h"
#include "web/RestApi.h"
#include "web/web.h"
#include "wifi/ahoywifi.h"
// convert degrees and radians for sun calculation // convert degrees and radians for sun calculation
#define SIN(x) (sin(radians(x))) #define SIN(x) (sin(radians(x)))
@ -49,9 +47,8 @@ typedef PubMqtt<HmSystemType> PubMqttType;
typedef PubSerial<HmSystemType> PubSerialType; typedef PubSerial<HmSystemType> PubSerialType;
// PLUGINS // PLUGINS
#include "plugins/MonochromeDisplay/MonochromeDisplay.h" #include "plugins/Display/Display.h"
typedef MonochromeDisplay<HmSystemType> MonoDisplayType; typedef Display<HmSystemType> DisplayType;
class app : public IApp, public ah::Scheduler { class app : public IApp, public ah::Scheduler {
public: public:
@ -225,7 +222,7 @@ class app : public IApp, public ah::Scheduler {
mMqtt.payloadEventListener(cmd); mMqtt.payloadEventListener(cmd);
#endif #endif
if(mConfig->plugin.display.type != 0) if(mConfig->plugin.display.type != 0)
mMonoDisplay.payloadEventListener(cmd); mDisplay.payloadEventListener(cmd);
} }
void mqttSubRxCb(JsonObject obj); void mqttSubRxCb(JsonObject obj);
@ -301,7 +298,7 @@ class app : public IApp, public ah::Scheduler {
uint32_t mSunrise, mSunset; uint32_t mSunrise, mSunset;
// plugins // plugins
MonoDisplayType mMonoDisplay; DisplayType mDisplay;
}; };
#endif /*__APP_H__*/ #endif /*__APP_H__*/

69
src/config/settings.h

@ -7,11 +7,12 @@
#define __SETTINGS_H__ #define __SETTINGS_H__
#include <Arduino.h> #include <Arduino.h>
#include <LittleFS.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <LittleFS.h>
#include "../defines.h"
#include "../utils/dbg.h" #include "../utils/dbg.h"
#include "../utils/helper.h" #include "../utils/helper.h"
#include "../defines.h"
/** /**
* More info: * More info:
@ -51,6 +52,7 @@ typedef struct {
char deviceName[DEVNAME_LEN]; char deviceName[DEVNAME_LEN];
char adminPwd[PWD_LEN]; char adminPwd[PWD_LEN];
uint16_t protectionMask; uint16_t protectionMask;
bool darkMode;
// wifi // wifi
char stationSsid[SSID_LEN]; char stationSsid[SSID_LEN];
@ -129,14 +131,17 @@ typedef struct {
typedef struct { typedef struct {
uint8_t type; uint8_t type;
bool pwrSaveAtIvOffline; bool pwrSaveAtIvOffline;
bool logoEn;
bool pxShift; bool pxShift;
bool rot180; uint8_t rot;
uint16_t wakeUp; //uint16_t wakeUp;
uint16_t sleepAt; //uint16_t sleepAt;
uint8_t contrast; uint8_t contrast;
uint8_t pin0; uint8_t disp_data;
uint8_t pin1; uint8_t disp_clk;
uint8_t disp_cs;
uint8_t disp_reset;
uint8_t disp_busy;
uint8_t disp_dc;
} display_t; } display_t;
typedef struct { typedef struct {
@ -228,7 +233,7 @@ class settings {
else { else {
//DPRINTLN(DBG_INFO, fp.readString()); //DPRINTLN(DBG_INFO, fp.readString());
//fp.seek(0, SeekSet); //fp.seek(0, SeekSet);
DynamicJsonDocument root(4500); DynamicJsonDocument root(5500);
DeserializationError err = deserializeJson(root, fp); DeserializationError err = deserializeJson(root, fp);
if(!err && (root.size() > 0)) { if(!err && (root.size() > 0)) {
mCfg.valid = true; mCfg.valid = true;
@ -262,7 +267,7 @@ class settings {
return false; return false;
} }
DynamicJsonDocument json(4500); DynamicJsonDocument json(6500);
JsonObject root = json.to<JsonObject>(); JsonObject root = json.to<JsonObject>();
jsonWifi(root.createNestedObject(F("wifi")), true); jsonWifi(root.createNestedObject(F("wifi")), true);
jsonNrf(root.createNestedObject(F("nrf")), true); jsonNrf(root.createNestedObject(F("nrf")), true);
@ -307,6 +312,7 @@ class settings {
memset(&mCfg, 0, sizeof(settings_t)); memset(&mCfg, 0, sizeof(settings_t));
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
| DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT; | DEF_PROT_UPDATE | DEF_PROT_SYSTEM | DEF_PROT_API | DEF_PROT_MQTT;
mCfg.sys.darkMode = false;
// restore temp settings // restore temp settings
if(keepWifi) if(keepWifi)
memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t)); memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t));
@ -360,11 +366,14 @@ class settings {
mCfg.plugin.display.pwrSaveAtIvOffline = false; mCfg.plugin.display.pwrSaveAtIvOffline = false;
mCfg.plugin.display.contrast = 60; mCfg.plugin.display.contrast = 60;
mCfg.plugin.display.logoEn = true;
mCfg.plugin.display.pxShift = true; mCfg.plugin.display.pxShift = true;
mCfg.plugin.display.rot180 = false; mCfg.plugin.display.rot = 0;
mCfg.plugin.display.pin0 = DEF_PIN_OFF; // SCL mCfg.plugin.display.disp_data = DEF_PIN_OFF; // SDA
mCfg.plugin.display.pin1 = DEF_PIN_OFF; // SDA mCfg.plugin.display.disp_clk = DEF_PIN_OFF; // SCL
mCfg.plugin.display.disp_cs = DEF_PIN_OFF;
mCfg.plugin.display.disp_reset = DEF_PIN_OFF;
mCfg.plugin.display.disp_busy = DEF_PIN_OFF;
mCfg.plugin.display.disp_dc = DEF_PIN_OFF;
} }
void jsonWifi(JsonObject obj, bool set = false) { void jsonWifi(JsonObject obj, bool set = false) {
@ -375,6 +384,7 @@ class settings {
obj[F("dev")] = mCfg.sys.deviceName; obj[F("dev")] = mCfg.sys.deviceName;
obj[F("adm")] = mCfg.sys.adminPwd; obj[F("adm")] = mCfg.sys.adminPwd;
obj[F("prot_mask")] = mCfg.sys.protectionMask; obj[F("prot_mask")] = mCfg.sys.protectionMask;
obj[F("dark")] = mCfg.sys.darkMode;
ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf); ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf);
ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf); ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf);
ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf); ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
@ -386,6 +396,7 @@ class settings {
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as<const char*>()); snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as<const char*>());
snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as<const char*>()); snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as<const char*>());
mCfg.sys.protectionMask = obj[F("prot_mask")]; mCfg.sys.protectionMask = obj[F("prot_mask")];
mCfg.sys.darkMode = obj[F("dark")];
ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>()); ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>());
ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>()); ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>());
ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>()); ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>());
@ -502,26 +513,32 @@ class settings {
JsonObject disp = obj.createNestedObject("disp"); JsonObject disp = obj.createNestedObject("disp");
disp[F("type")] = mCfg.plugin.display.type; disp[F("type")] = mCfg.plugin.display.type;
disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline; disp[F("pwrSafe")] = (bool)mCfg.plugin.display.pwrSaveAtIvOffline;
disp[F("logo")] = (bool)mCfg.plugin.display.logoEn;
disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift; disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift;
disp[F("rot180")] = (bool)mCfg.plugin.display.rot180; disp[F("rotation")] = mCfg.plugin.display.rot;
disp[F("wake")] = mCfg.plugin.display.wakeUp; //disp[F("wake")] = mCfg.plugin.display.wakeUp;
disp[F("sleep")] = mCfg.plugin.display.sleepAt; //disp[F("sleep")] = mCfg.plugin.display.sleepAt;
disp[F("contrast")] = mCfg.plugin.display.contrast; disp[F("contrast")] = mCfg.plugin.display.contrast;
disp[F("pin0")] = mCfg.plugin.display.pin0; disp[F("data")] = mCfg.plugin.display.disp_data;
disp[F("pin1")] = mCfg.plugin.display.pin1; disp[F("clock")] = mCfg.plugin.display.disp_clk;
disp[F("cs")] = mCfg.plugin.display.disp_cs;
disp[F("reset")] = mCfg.plugin.display.disp_reset;
disp[F("busy")] = mCfg.plugin.display.disp_busy;
disp[F("dc")] = mCfg.plugin.display.disp_dc;
} else { } else {
JsonObject disp = obj["disp"]; JsonObject disp = obj["disp"];
mCfg.plugin.display.type = disp[F("type")]; mCfg.plugin.display.type = disp[F("type")];
mCfg.plugin.display.pwrSaveAtIvOffline = (bool)disp[F("pwrSafe")]; mCfg.plugin.display.pwrSaveAtIvOffline = (bool)disp[F("pwrSafe")];
mCfg.plugin.display.logoEn = (bool) disp[F("logo")];
mCfg.plugin.display.pxShift = (bool)disp[F("pxShift")]; mCfg.plugin.display.pxShift = (bool)disp[F("pxShift")];
mCfg.plugin.display.rot180 = (bool) disp[F("rot180")]; mCfg.plugin.display.rot = disp[F("rotation")];
mCfg.plugin.display.wakeUp = disp[F("wake")]; //mCfg.plugin.display.wakeUp = disp[F("wake")];
mCfg.plugin.display.sleepAt = disp[F("sleep")]; //mCfg.plugin.display.sleepAt = disp[F("sleep")];
mCfg.plugin.display.contrast = disp[F("contrast")]; mCfg.plugin.display.contrast = disp[F("contrast")];
mCfg.plugin.display.pin0 = disp[F("pin0")]; mCfg.plugin.display.disp_data = disp[F("data")];
mCfg.plugin.display.pin1 = disp[F("pin1")]; mCfg.plugin.display.disp_clk = disp[F("clock")];
mCfg.plugin.display.disp_cs = disp[F("cs")];
mCfg.plugin.display.disp_reset = disp[F("reset")];
mCfg.plugin.display.disp_busy = disp[F("busy")];
mCfg.plugin.display.disp_dc = disp[F("dc")];
} }
} }

2
src/defines.h

@ -13,7 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 5 #define VERSION_MINOR 5
#define VERSION_PATCH 90 #define VERSION_PATCH 94
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {

4
src/hm/hmDefines.h

@ -29,6 +29,10 @@ const char* const fields[] = {"U_DC", "I_DC", "P_DC", "YieldDay", "YieldWeek", "
"active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode"}; "active_PowerLimit", /*"reactivePowerLimit","Powerfactor",*/ "LastAlarmCode"};
const char* const notAvail = "n/a"; const char* const notAvail = "n/a";
const uint8_t fieldUnits[] = {UNIT_V, UNIT_A, UNIT_W, UNIT_WH, UNIT_KWH, UNIT_KWH,
UNIT_V, UNIT_A, UNIT_W, UNIT_HZ, UNIT_C, UNIT_NONE, UNIT_PCT, UNIT_PCT, UNIT_VAR,
UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_NONE, UNIT_PCT, UNIT_NONE};
// mqtt discovery device classes // mqtt discovery device classes
enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP}; enum {DEVICE_CLS_NONE = 0, DEVICE_CLS_CURRENT, DEVICE_CLS_ENERGY, DEVICE_CLS_PWR, DEVICE_CLS_VOLTAGE, DEVICE_CLS_FREQ, DEVICE_CLS_TEMP};
const char* const deviceClasses[] = {0, "current", "energy", "power", "voltage", "frequency", "temperature"}; const char* const deviceClasses[] = {0, "current", "energy", "power", "voltage", "frequency", "temperature"};

4
src/hm/hmPayload.h

@ -9,6 +9,7 @@
#include "../utils/dbg.h" #include "../utils/dbg.h"
#include "../utils/crc.h" #include "../utils/crc.h"
#include "../config/config.h" #include "../config/config.h"
#include "hmRadio.h"
#include <Arduino.h> #include <Arduino.h>
typedef struct { typedef struct {
@ -158,7 +159,8 @@ class HmPayload {
uint8_t cmd = iv->getQueuedCmd(); uint8_t cmd = iv->getQueuedCmd();
DPRINT(DBG_INFO, F("(#")); DPRINT(DBG_INFO, F("(#"));
DBGPRINT(String(iv->id)); DBGPRINT(String(iv->id));
DBGPRINT(F(") prepareDevInformCmd")); // + String(cmd, HEX)); DBGPRINT(F(") prepareDevInformCmd 0x"));
DBGPRINTLN(String(cmd, HEX));
mRadio->prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); mRadio->prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false);
mPayload[iv->id].txCmd = cmd; mPayload[iv->id].txCmd = cmd;
} }

57
src/hm/miPayload.h

@ -92,14 +92,14 @@ class MiPayload {
} }
void add(Inverter<> *iv, packet_t *p) { void add(Inverter<> *iv, packet_t *p) {
DPRINTLN(DBG_INFO, F("MI got data [0]=") + String(p->packet[0], HEX)); //DPRINTLN(DBG_INFO, F("MI got data [0]=") + String(p->packet[0], HEX));
if (p->packet[0] == (0x08 + ALL_FRAMES)) { // 0x88; MI status response to 0x09 if (p->packet[0] == (0x08 + ALL_FRAMES)) { // 0x88; MI status response to 0x09
mPayload[iv->id].stsa = true; mPayload[iv->id].stsa = true;
miStsDecode(iv, p); miStsDecode(iv, p);
} else if (p->packet[0] == (0x11 + SINGLE_FRAME)) { // 0x92; MI status response to 0x11 } else if (p->packet[0] == (0x11 + SINGLE_FRAME)) { // 0x92; MI status response to 0x11
mPayload[iv->id].stsb = true; mPayload[iv->id].stsb = true;
miStsDecode(iv, p, 2); miStsDecode(iv, p, CH2);
} else if (p->packet[0] == (0x09 + ALL_FRAMES)) { // MI data response to 0x09 } else if (p->packet[0] == (0x09 + ALL_FRAMES)) { // MI data response to 0x09
mPayload[iv->id].txId = p->packet[0]; mPayload[iv->id].txId = p->packet[0];
miDataDecode(iv,p); miDataDecode(iv,p);
@ -359,12 +359,11 @@ class MiPayload {
(mCbMiPayload)(val); (mCbMiPayload)(val);
} }
void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t chan = 1) { void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t chan = CH1) {
DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(": status msg 0x") + String(p->packet[0], HEX));
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure
rec->ts = mPayload[iv->id].ts; rec->ts = mPayload[iv->id].ts;
int8_t offset = -2;
//iv->setValue(iv->getPosByChFld(chan, FLD_YD, rec), rec, (int)((p->packet[11+6] << 8) + p->packet[12+6])); // was 11/12, might be wrong! //iv->setValue(iv->getPosByChFld(chan, FLD_YD, rec), rec, (int)((p->packet[11+6] << 8) + p->packet[12+6])); // was 11/12, might be wrong!
//if (INV_TYPE_1CH == iv->type) //if (INV_TYPE_1CH == iv->type)
@ -372,16 +371,21 @@ class MiPayload {
//iv->setValue(iv->getPosByChFld(chan, FLD_EVT, rec), rec, (int)((p->packet[13] << 8) + p->packet[14])); //iv->setValue(iv->getPosByChFld(chan, FLD_EVT, rec), rec, (int)((p->packet[13] << 8) + p->packet[14]));
iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, (int)((p->packet[11+offset] << 8) + p->packet[12+offset])); iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, (int)((p->packet[11] << 8) + p->packet[12]));
//iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, (int)((p->packet[14] << 8) + p->packet[16]));
if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){ if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){
iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; // seems there's no status per channel in 3rd gen. models?!?
DPRINTLN(DBG_INFO, "alarm ID incremented to " + String(iv->alarmMesIndex)); DPRINTLN(DBG_INFO, "alarm ID incremented to " + String(iv->alarmMesIndex));
iv->enqueCommand<InfoCommand>(AlarmData); iv->enqueCommand<InfoCommand>(AlarmData);
} }
/* Unclear how in HM inverters Info and alarm data is handled...
*/
/* int8_t offset = -2;
/* for decoding see for decoding see
void MI600StsMsg (NRF24_packet_t *p){ void MI600StsMsg (NRF24_packet_t *p){
STAT = (int)((p->packet[11] << 8) + p->packet[12]); STAT = (int)((p->packet[11] << 8) + p->packet[12]);
FCNT = (int)((p->packet[13] << 8) + p->packet[14]); FCNT = (int)((p->packet[13] << 8) + p->packet[14]);
@ -393,23 +397,22 @@ class MiPayload {
#endif #endif
} }
*/ */
DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(": status msg ") + p->packet[0]);
} }
void miDataDecode(Inverter<> *iv, packet_t *p) { void miDataDecode(Inverter<> *iv, packet_t *p) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the parser record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the parser
rec->ts = mPayload[iv->id].ts; rec->ts = mPayload[iv->id].ts;
uint8_t chan = ( p->packet[2] == 0x89 || p->packet[2] == (0x36 + ALL_FRAMES) ) ? 1 : uint8_t datachan = ( p->packet[0] == 0x89 || p->packet[0] == (0x36 + ALL_FRAMES) ) ? CH1 :
( p->packet[2] == 0x91 || p->packet[2] == (0x37 + ALL_FRAMES) ) ? 2 : ( p->packet[0] == 0x91 || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 :
p->packet[2] == (0x38 + ALL_FRAMES) ? 3 : p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 :
4; CH4;
int8_t offset = -2; int8_t offset = -2;
// U_DC = (float) ((p->packet[11] << 8) + p->packet[12])/10; // U_DC = (float) ((p->packet[11] << 8) + p->packet[12])/10;
iv->setValue(iv->getPosByChFld(chan, FLD_UDC, rec), rec, (float)((p->packet[11+offset] << 8) + p->packet[12+offset])/10); iv->setValue(iv->getPosByChFld(datachan, FLD_UDC, rec), rec, (float)((p->packet[11+offset] << 8) + p->packet[12+offset])/10);
yield(); yield();
// I_DC = (float) ((p->packet[13] << 8) + p->packet[14])/10; // I_DC = (float) ((p->packet[13] << 8) + p->packet[14])/10;
iv->setValue(iv->getPosByChFld(chan, FLD_IDC, rec), rec, (float)((p->packet[13+offset] << 8) + p->packet[14+offset])/10); iv->setValue(iv->getPosByChFld(datachan, FLD_IDC, rec), rec, (float)((p->packet[13+offset] << 8) + p->packet[14+offset])/10);
yield(); yield();
// U_AC = (float) ((p->packet[15] << 8) + p->packet[16])/10; // U_AC = (float) ((p->packet[15] << 8) + p->packet[16])/10;
iv->setValue(iv->getPosByChFld(0, FLD_UAC, rec), rec, (float)((p->packet[15+offset] << 8) + p->packet[16+offset])/10); iv->setValue(iv->getPosByChFld(0, FLD_UAC, rec), rec, (float)((p->packet[15+offset] << 8) + p->packet[16+offset])/10);
@ -418,23 +421,23 @@ class MiPayload {
//iv->setValue(iv->getPosByChFld(0, FLD_IAC, rec), rec, (float)((p->packet[17+offset] << 8) + p->packet[18+offset])/100); //iv->setValue(iv->getPosByChFld(0, FLD_IAC, rec), rec, (float)((p->packet[17+offset] << 8) + p->packet[18+offset])/100);
//yield(); //yield();
// P_DC = (float)((p->packet[19] << 8) + p->packet[20])/10; // P_DC = (float)((p->packet[19] << 8) + p->packet[20])/10;
iv->setValue(iv->getPosByChFld(chan, FLD_PDC, rec), rec, (float)((p->packet[19+offset] << 8) + p->packet[20+offset])/10); iv->setValue(iv->getPosByChFld(datachan, FLD_PDC, rec), rec, (float)((p->packet[19+offset] << 8) + p->packet[20+offset])/10);
yield(); yield();
// Q_DC = (float)((p->packet[21] << 8) + p->packet[22])/1; // Q_DC = (float)((p->packet[21] << 8) + p->packet[22])/1;
iv->setValue(iv->getPosByChFld(chan, FLD_Q, rec), rec, (float)((p->packet[21+offset] << 8) + p->packet[22+offset])/1); iv->setValue(iv->getPosByChFld(datachan, FLD_YD, rec), rec, (float)((p->packet[21+offset] << 8) + p->packet[22+offset])/1);
yield(); yield();
iv->setValue(iv->getPosByChFld(0, FLD_T, rec), rec, (float) ((int16_t)(p->packet[23+offset] << 8) + p->packet[24+offset])/10); iv->setValue(iv->getPosByChFld(0, FLD_T, rec), rec, (float) ((int16_t)(p->packet[23+offset] << 8) + p->packet[24+offset])/10); //23 is freq or IAC?
iv->setValue(iv->getPosByChFld(0, FLD_F, rec), rec, (float) ((p->packet[17+offset] << 8) + p->packet[18+offset])/100); //23 is freq or IAC? iv->setValue(iv->getPosByChFld(0, FLD_F, rec), rec, (float) ((p->packet[17+offset] << 8) + p->packet[18+offset])/100);
iv->setValue(iv->getPosByChFld(0, FLD_IRR, rec), rec, (float) (calcIrradiation(iv, datachan)));
yield(); yield();
//FLD_YD //FLD_YD
if (p->packet[2] >= (0x36 + ALL_FRAMES) ) { if (p->packet[0] >= (0x36 + ALL_FRAMES) ) {
/*status message analysis most liklely needs to be changed, see MiStsMst*/
/*STAT = (uint8_t)(p->packet[25] ); /*STAT = (uint8_t)(p->packet[25] );
FCNT = (uint8_t)(p->packet[26]); FCNT = (uint8_t)(p->packet[26]);
FCODE = (uint8_t)(p->packet[27]); // MI300: (int)((p->packet[15] << 8) + p->packet[16]); */ FCODE = (uint8_t)(p->packet[27]); // MI300/Mi600 stsMsg:: (int)((p->packet[15] << 8) + p->packet[16]); */
//iv->setValue(iv->getPosByChFld(chan, FLD_YD, rec), rec, (uint8_t)(p->packet[25])); iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, (uint8_t)(p->packet[25+offset]));
//iv->setValue(iv->getPosByChFld(chan, FLD_EVT, rec), rec, (uint8_t)(p->packet[27]));
iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, (uint8_t)(p->packet[21+offset]));
yield(); yield();
if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){ if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){
iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)];
@ -443,8 +446,8 @@ class MiPayload {
iv->enqueCommand<InfoCommand>(AlarmData); iv->enqueCommand<InfoCommand>(AlarmData);
} }
} }
iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, CALC_YD_CH0); // (getValue(iv->getPosByChFld(1, FLD_YD, rec), rec) + getValue(iv->getPosByChFld(2, FLD_YD, rec), rec))); //iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, CALC_YD_CH0); // (getValue(iv->getPosByChFld(1, FLD_YD, rec), rec) + getValue(iv->getPosByChFld(2, FLD_YD, rec), rec)));
iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); //datachan));
iv->doCalculations(); iv->doCalculations();
notify(mPayload[iv->id].txCmd); notify(mPayload[iv->id].txCmd);
/* /*
@ -502,7 +505,7 @@ class MiPayload {
FCODE = (uint8_t)(p->packet[27]); FCODE = (uint8_t)(p->packet[27]);
} }
*/ */
DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(": data msg ") + p->packet[0]); DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(": data msg 0x") + String(p->packet[0], HEX) + F(" channel ") + datachan);
} }
bool build(uint8_t id, bool *complete) { bool build(uint8_t id, bool *complete) {

8
src/platformio.ini

@ -15,6 +15,7 @@ include_dir = .
[env] [env]
framework = arduino framework = arduino
board_build.filesystem = littlefs board_build.filesystem = littlefs
upload_speed = 921600
;build_flags = ;build_flags =
; ;;;;; Possible Debug options ;;;;;; ; ;;;;; Possible Debug options ;;;;;;
@ -40,6 +41,7 @@ lib_deps =
bblanchon/ArduinoJson bblanchon/ArduinoJson
https://github.com/JChristensen/Timezone https://github.com/JChristensen/Timezone
olikraus/U8g2 olikraus/U8g2
zinggjm/GxEPD2@^1.5.0
;esp8266/DNSServer ;esp8266/DNSServer
;esp8266/EEPROM ;esp8266/EEPROM
;esp8266/ESP8266WiFi ;esp8266/ESP8266WiFi
@ -54,8 +56,9 @@ board_build.f_cpu = 80000000L
build_flags = -D RELEASE build_flags = -D RELEASE
monitor_filters = monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line ;time ; Add timestamp with milliseconds for each new line
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
esp8266_exception_decoder
[env:esp8266-debug] [env:esp8266-debug]
platform = espressif8266 platform = espressif8266
@ -98,8 +101,9 @@ build_flags = -D RELEASE -std=gnu++14
build_unflags = -std=gnu++11 build_unflags = -std=gnu++11
monitor_filters = monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line ;time ; Add timestamp with milliseconds for each new line
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
esp32_exception_decoder
[env:esp32-wroom32-debug] [env:esp32-wroom32-debug]
platform = espressif32 platform = espressif32

113
src/plugins/Display/Display.h

@ -0,0 +1,113 @@
#ifndef __DISPLAY__
#define __DISPLAY__
#include <Timezone.h>
#include <U8g2lib.h>
#include "../../hm/hmSystem.h"
#include "../../utils/helper.h"
#include "Display_Mono.h"
#include "Display_ePaper.h"
template <class HMSYSTEM>
class Display {
public:
Display() {}
void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, uint8_t disp_reset, const char *version) {
mCfg = cfg;
mSys = sys;
mUtcTs = utcTs;
mNewPayload = false;
mLoopCnt = 0;
mVersion = version;
if (mCfg->type == 0)
return;
if ((1 < mCfg->type) && (mCfg->type < 10)) {
mMono.config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast);
mMono.init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion);
} else if (mCfg->type >= 10) {
#if defined(ESP32)
mRefreshCycle = 0;
mEpaper.config(mCfg->rot);
mEpaper.init(mCfg->type, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_busy, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion);
#endif
}
}
void payloadEventListener(uint8_t cmd) {
mNewPayload = true;
}
void tickerSecond() {
if (mNewPayload || ((++mLoopCnt % 10) == 0)) {
mNewPayload = false;
mLoopCnt = 0;
DataScreen();
}
}
private:
void DataScreen() {
if (mCfg->type == 0)
return;
if (*mUtcTs == 0)
return;
float totalPower = 0;
float totalYieldDay = 0;
float totalYieldTotal = 0;
uint8_t isprod = 0;
Inverter<> *iv;
record_t<> *rec;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
iv = mSys->getInverterByPos(i);
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv == NULL)
continue;
if (iv->isProducing(*mUtcTs))
isprod++;
totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
}
if ((1 < mCfg->type) && (mCfg->type < 10)) {
mMono.loop(totalPower, totalYieldDay, totalYieldTotal, isprod);
} else if (mCfg->type >= 10) {
#if defined(ESP32)
mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod);
mRefreshCycle++;
#endif
}
#if defined(ESP32)
if (mRefreshCycle > 480) {
mEpaper.fullRefresh();
mRefreshCycle = 0;
}
#endif
}
// private member variables
bool mNewPayload;
uint8_t mLoopCnt;
uint32_t *mUtcTs;
const char *mVersion;
display_t *mCfg;
HMSYSTEM *mSys;
uint16_t mRefreshCycle;
#if defined(ESP32)
DisplayEPaper mEpaper;
#endif
DisplayMono mMono;
};
#endif /*__DISPLAY__*/

149
src/plugins/Display/Display_Mono.cpp

@ -0,0 +1,149 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Display_Mono.h"
#ifdef ESP8266
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#include "../../utils/helper.h"
//#ifdef U8X8_HAVE_HW_SPI
//#include <SPI.h>
//#endif
//#ifdef U8X8_HAVE_HW_I2C
//#include <Wire.h>
//#endif
DisplayMono::DisplayMono() {
mEnPowerSafe = true;
mEnScreenSaver = true;
mLuminance = 60;
_dispY = 0;
mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds)
mUtcTs = NULL;
}
void DisplayMono::init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version) {
if ((0 < type) && (type < 4)) {
u8g2_cb_t *rot = (u8g2_cb_t *)((rot != 0x00) ? U8G2_R2 : U8G2_R0);
switch(type) {
case 1:
mDisplay = new U8G2_PCD8544_84X48_F_4W_HW_SPI(rot, cs, dc, reset);
break;
case 2:
mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data);
break;
default:
case 3:
mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data);
break;
}
mUtcTs = utcTs;
mDisplay->begin();
mIsLarge = (mDisplay->getWidth() > 120);
calcLineHeights();
mDisplay->clearBuffer();
mDisplay->setContrast(mLuminance);
printText("AHOY!", 0, 35);
printText("ahoydtu.de", 2, 20);
printText(version, 3, 46);
mDisplay->sendBuffer();
}
}
void DisplayMono::config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) {
mEnPowerSafe = enPowerSafe;
mEnScreenSaver = enScreenSaver;
mLuminance = lum;
}
void DisplayMono::loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) {
if (mEnPowerSafe)
if(mTimeout != 0)
mTimeout--;
mDisplay->clearBuffer();
// set Contrast of the Display to raise the lifetime
mDisplay->setContrast(mLuminance);
if ((totalPower > 0) && (isprod > 0)) {
mTimeout = DISP_DEFAULT_TIMEOUT;
mDisplay->setPowerSave(false);
if (totalPower > 999) {
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000));
} else {
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower);
}
printText(_fmtText, 0);
} else {
printText("offline", 0, 25);
// check if it's time to enter power saving mode
if (mTimeout == 0)
mDisplay->setPowerSave(mEnPowerSafe);
}
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay);
printText(_fmtText, 1);
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal);
printText(_fmtText, 2);
IPAddress ip = WiFi.localIP();
if (!(_mExtra % 10) && (ip)) {
printText(ip.toString().c_str(), 3);
} else if (!(_mExtra % 5)) {
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "#%d Inverter online", isprod);
printText(_fmtText, 3);
} else {
if(mIsLarge && (NULL != mUtcTs))
printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3);
else
printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3);
}
mDisplay->sendBuffer();
_dispY = 0;
_mExtra++;
}
void DisplayMono::calcLineHeights() {
uint8_t yOff = 0;
for (uint8_t i = 0; i < 4; i++) {
setFont(i);
yOff += (mDisplay->getMaxCharHeight());
mLineOffsets[i] = yOff;
}
}
inline void DisplayMono::setFont(uint8_t line) {
switch (line) {
case 0:
mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB14_tr : u8g2_font_logisoso16_tr);
break;
case 3:
mDisplay->setFont(u8g2_font_5x8_tr);
break;
default:
mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr);
break;
}
}
void DisplayMono::printText(const char* text, uint8_t line, uint8_t dispX) {
if (!mIsLarge) {
dispX = (line == 0) ? 10 : 5;
}
setFont(line);
dispX += (mEnPowerSafe) ? (_mExtra % 7) : 0;
mDisplay->drawStr(dispX, mLineOffsets[line], text);
}

36
src/plugins/Display/Display_Mono.h

@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <U8g2lib.h>
#define DISP_DEFAULT_TIMEOUT 60 // in seconds
#define DISP_FMT_TEXT_LEN 32
class DisplayMono {
public:
DisplayMono();
void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version);
void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum);
void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod);
private:
void calcLineHeights();
void setFont(uint8_t line);
void printText(const char* text, uint8_t line, uint8_t dispX = 5);
U8G2* mDisplay;
bool mEnPowerSafe, mEnScreenSaver;
uint8_t mLuminance;
bool mIsLarge = false;
uint8_t mLoopCnt;
uint32_t* mUtcTs;
uint8_t mLineOffsets[5];
uint16_t _dispY;
uint8_t _mExtra;
uint16_t mTimeout;
char _fmtText[DISP_FMT_TEXT_LEN];
};

197
src/plugins/Display/Display_ePaper.cpp

@ -0,0 +1,197 @@
#include "Display_ePaper.h"
#ifdef ESP8266
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#include "../../utils/helper.h"
#include "imagedata.h"
#if defined(ESP32)
static const uint32_t spiClk = 4000000; // 4 MHz
#if defined(ESP32) && defined(USE_HSPI_FOR_EPD)
SPIClass hspi(HSPI);
#endif
DisplayEPaper::DisplayEPaper() {
mDisplayRotation = 2;
mHeadFootPadding = 16;
}
//***************************************************************************
void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char *version) {
mUtcTs = utcTs;
if (type > 9) {
Serial.begin(115200);
_display = new GxEPD2_BW<GxEPD2_150_BN, GxEPD2_150_BN::HEIGHT>(GxEPD2_150_BN(_CS, _DC, _RST, _BUSY));
hspi.begin(_SCK, _BUSY, _MOSI, _CS);
#if defined(ESP32) && defined(USE_HSPI_FOR_EPD)
_display->epd2.selectSPI(hspi, SPISettings(spiClk, MSBFIRST, SPI_MODE0));
#endif
_display->init(115200, true, 2, false);
_display->setRotation(mDisplayRotation);
_display->setFullWindow();
// Logo
_display->fillScreen(GxEPD_BLACK);
_display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
while (_display->nextPage())
;
// clean the screen
delay(2000);
_display->fillScreen(GxEPD_WHITE);
while (_display->nextPage())
;
headlineIP();
// call the PowerPage to change the PV Power Values
actualPowerPaged(0, 0, 0, 0);
}
}
void DisplayEPaper::config(uint8_t rotation) {
mDisplayRotation = rotation;
}
//***************************************************************************
void DisplayEPaper::fullRefresh() {
// screen complete black
_display->fillScreen(GxEPD_BLACK);
while (_display->nextPage())
;
delay(2000);
// screen complete white
_display->fillScreen(GxEPD_WHITE);
while (_display->nextPage())
;
}
//***************************************************************************
void DisplayEPaper::headlineIP() {
int16_t tbx, tby;
uint16_t tbw, tbh;
_display->setFont(&FreeSans9pt7b);
_display->setTextColor(GxEPD_WHITE);
_display->setPartialWindow(0, 0, _display->width(), mHeadFootPadding);
_display->fillScreen(GxEPD_BLACK);
do {
if ((WiFi.isConnected() == true) && (WiFi.localIP() > 0)) {
snprintf(_fmtText, sizeof(_fmtText), "%s", WiFi.localIP().toString().c_str());
} else {
snprintf(_fmtText, sizeof(_fmtText), "WiFi not connected");
}
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
_display->setCursor(x, (mHeadFootPadding - 2));
_display->println(_fmtText);
} while (_display->nextPage());
}
//***************************************************************************
void DisplayEPaper::lastUpdatePaged() {
int16_t tbx, tby;
uint16_t tbw, tbh;
_display->setFont(&FreeSans9pt7b);
_display->setTextColor(GxEPD_WHITE);
_display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding);
_display->fillScreen(GxEPD_BLACK);
do {
if (NULL != mUtcTs) {
snprintf(_fmtText, sizeof(_fmtText), ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str());
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((_display->width() - tbw) / 2) - tbx;
_display->setCursor(x, (_display->height() - 3));
_display->println(_fmtText);
}
} while (_display->nextPage());
}
//***************************************************************************
void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod) {
int16_t tbx, tby;
uint16_t tbw, tbh, x, y;
_display->setFont(&FreeSans24pt7b);
_display->setTextColor(GxEPD_BLACK);
_display->setPartialWindow(0, mHeadFootPadding, _display->width(), _display->height() - (mHeadFootPadding * 2));
_display->fillScreen(GxEPD_WHITE);
do {
if (_totalPower > 9999) {
snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (_totalPower / 10000));
_changed = true;
} else if ((_totalPower > 0) && (_totalPower <= 9999)) {
snprintf(_fmtText, sizeof(_fmtText), "%.0f W", _totalPower);
_changed = true;
} else {
snprintf(_fmtText, sizeof(_fmtText), "offline");
}
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
x = ((_display->width() - tbw) / 2) - tbx;
_display->setCursor(x, mHeadFootPadding + tbh + 10);
_display->print(_fmtText);
_display->setFont(&FreeSans12pt7b);
y = _display->height() / 2;
_display->setCursor(0, y);
_display->print("today:");
snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
x = ((_display->width() - tbw) / 2) - tbx;
_display->setCursor(x, y);
_display->print(_fmtText);
_display->setCursor(_display->width() - 33, y);
_display->println("Wh");
y = y + tbh + 7;
_display->setCursor(0, y);
_display->print("total:");
snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
x = ((_display->width() - tbw) / 2) - tbx;
_display->setCursor(x, y);
_display->print(_fmtText);
_display->setCursor(_display->width() - 45, y);
_display->println("kWh");
_display->setCursor(0, _display->height() - (mHeadFootPadding + 10));
snprintf(_fmtText, sizeof(_fmtText), "#%d Inverter online", _isprod);
_display->println(_fmtText);
} while (_display->nextPage());
}
//***************************************************************************
void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) {
// check if the IP has changed
if (_settedIP != WiFi.localIP().toString().c_str()) {
// save the new IP and call the Headline Funktion to adapt the Headline
_settedIP = WiFi.localIP().toString().c_str();
headlineIP();
}
// call the PowerPage to change the PV Power Values
actualPowerPaged(totalPower, totalYieldDay, totalYieldTotal, isprod);
// if there was an change and the Inverter is producing set a new Timestam in the footline
if ((isprod > 0) && (_changed)) {
_changed = false;
lastUpdatePaged();
}
_display->powerOff();
}
//***************************************************************************
#endif // ESP32

52
src/plugins/Display/Display_ePaper.h

@ -0,0 +1,52 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#if defined(ESP32)
// uncomment next line to use HSPI for EPD (and VSPI for SD), e.g. with Waveshare ESP32 Driver Board
#define USE_HSPI_FOR_EPD
/// uncomment next line to use class GFX of library GFX_Root instead of Adafruit_GFX, to use less code and ram
// #include <GFX.h>
// base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code
// enable GxEPD2_GFX base class
#define ENABLE_GxEPD2_GFX 1
#include <GxEPD2_3C.h>
#include <GxEPD2_BW.h>
#include <SPI.h>
#include <map>
// FreeFonts from Adafruit_GFX
#include <Fonts/FreeSans12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
#include <Fonts/FreeSans24pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
// GDEW027C44 2.7 " b/w/r 176x264, IL91874
// GDEH0154D67 1.54" b/w 200x200
class DisplayEPaper {
public:
DisplayEPaper();
void fullRefresh();
void init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char* version);
void config(uint8_t rotation);
void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod);
private:
void headlineIP();
void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod);
void lastUpdatePaged();
uint8_t mDisplayRotation;
bool _changed = false;
char _fmtText[35];
const char* _settedIP;
uint8_t mHeadFootPadding;
GxEPD2_GFX* _display;
uint32_t *mUtcTs;
};
#endif // ESP32

329
src/plugins/Display/imagedata.h

@ -0,0 +1,329 @@
// GxEPD2_ESP32_ESP8266_WifiData_V1_und_V2
#ifndef __IMAGEDATA_H__
#define __IMAGEDATA_H__
#if defined(__AVR__) || defined(ARDUINO_ARCH_SAMD)
#include <avr/pgmspace.h>
#elif defined(ESP8266) || defined(ESP32)
#include <pgmspace.h>
#endif
// 'Logo', 200x200px
const unsigned char logo[] PROGMEM = {
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00,
0x0b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x06,
0x0f, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x7e, 0x0f, 0xff, 0xff, 0xfc, 0x03, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
0x03, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x19, 0xfe, 0x07, 0xff, 0xff, 0xff, 0xfe,
0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xe0, 0x70, 0x7f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe0, 0x3f, 0x07, 0xff, 0xff,
0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xfc, 0x0f, 0xe0, 0x3f, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xe0, 0x1f, 0x83,
0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xe0, 0x1f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xe0,
0x0f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xe0, 0x0f, 0x83, 0xff, 0xff, 0xff, 0xff, 0xfe,
0x07, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
0xff, 0xc1, 0x07, 0x80, 0x07, 0xfe, 0xff, 0xff, 0xfc, 0x07, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xe1, 0x07, 0xc0, 0x01, 0xe0, 0x0f,
0xff, 0xfc, 0x0f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xfc, 0xff, 0xe1, 0x83, 0xc0, 0x01, 0xc0, 0x07, 0xff, 0xf8, 0x0f, 0xfc, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe1, 0x83, 0xc0, 0x00,
0xc0, 0x07, 0x8f, 0xf8, 0x1f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xfe, 0x7f, 0xe0, 0x01, 0xc0, 0x00, 0x81, 0x83, 0x07, 0xf0, 0x3f, 0xf9, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xe0, 0x01,
0xe0, 0xe0, 0x87, 0xe3, 0x0f, 0xf0, 0x3f, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xe0, 0x00, 0xe0, 0xe0, 0x87, 0xe1, 0x0c, 0x60, 0x7f,
0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f,
0xe0, 0x00, 0xe1, 0xf0, 0x87, 0xe1, 0x08, 0x60, 0x7f, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xe0, 0xe0, 0xe0, 0xe0, 0x87, 0xc2, 0x00,
0x40, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x8f, 0xc0, 0xe0, 0x60, 0xe0, 0xc0, 0x82, 0x00, 0xc0, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xc0, 0xe0, 0x60, 0xe0, 0xc0,
0x06, 0x01, 0x81, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xcf, 0xe0, 0xe0, 0x20, 0xe0, 0xe0, 0x0c, 0x03, 0x81, 0xff, 0x1f, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xc0, 0xf0, 0x30,
0xe1, 0xf8, 0x18, 0x07, 0xe1, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xc0, 0xf0, 0x7f, 0xff, 0xff, 0xf0, 0x1f, 0xf3, 0xfe, 0x01,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xc0,
0xfb, 0xff, 0xff, 0xff, 0xe0, 0x3e, 0x1f, 0xfc, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xfc, 0x0f,
0xf8, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
0x33, 0xef, 0xff, 0xff, 0xff, 0xff, 0x81, 0xfc, 0x0f, 0xf1, 0xff, 0x07, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xf1, 0xff, 0xff, 0xa0, 0x00, 0x7f, 0xe3,
0xfc, 0x0f, 0xf3, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xf1, 0xf9, 0xff, 0xf0, 0x00, 0x00, 0x00, 0xff, 0xfc, 0x0f, 0xe7, 0xff, 0xe0, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xf9, 0xff, 0x80, 0x3f, 0xff,
0xe0, 0x0f, 0xfe, 0x1f, 0xc7, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xcf, 0xf8, 0xf0, 0x07, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0x8f, 0xff, 0xfc,
0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x70, 0x3f,
0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0x1f, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xfc, 0x63, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0x3f,
0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfe,
0x23, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7e, 0x3f, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x23, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
0x0c, 0x7f, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,
0x7f, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0xff, 0xff, 0xff, 0xe1, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xf1, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xfc, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xf8,
0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x87, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xfc, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0x00, 0x3f, 0xff, 0xf8, 0x00, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xfc, 0x00, 0x00, 0x01, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x55, 0x00, 0x3f, 0xf8, 0x00,
0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff,
0xff, 0xff, 0x01, 0xff, 0xff, 0xf8, 0x0f, 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x9f, 0xff, 0xf8, 0x03, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xff, 0x03,
0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xe3, 0xf1, 0xff,
0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xe0, 0xfe, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xe7, 0xf9, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff,
0xff, 0xf8, 0x7e, 0x06, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xcf,
0xf8, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0x03, 0x3f, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xcf, 0xfc, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff,
0xff, 0xff, 0xff, 0xff, 0x1f, 0x23, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f,
0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xf3, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xf1, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xe3, 0xf8,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xfe, 0x7f, 0xff, 0x9f, 0xff, 0x0f, 0xff, 0x8f, 0xf1, 0xff, 0xff, 0xff, 0xfe, 0xf5, 0x90, 0x07,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x9f, 0xff, 0x03, 0xff,
0x1f, 0xe3, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xfe, 0x7f, 0xff, 0x3f, 0xfe, 0x31, 0xfe, 0x7f, 0xe7, 0xff, 0x80, 0x00, 0x40, 0x00,
0x07, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0x3f, 0x3c,
0xf9, 0xfc, 0xff, 0xe7, 0xfe, 0x3f, 0xc9, 0xff, 0xf1, 0x1f, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x3f, 0x3c, 0xf9, 0xf9, 0xff, 0xc7, 0xfc, 0xff, 0x90,
0x7f, 0xf3, 0x03, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff,
0x3f, 0x39, 0xfd, 0xf3, 0xff, 0xcf, 0xfc, 0xff, 0x90, 0x3f, 0xf3, 0x83, 0xf8, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x3f, 0x39, 0xf9, 0xc7, 0xff, 0xcf, 0xfc,
0xff, 0x32, 0x7f, 0xe4, 0x77, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc,
0xff, 0xff, 0x7f, 0x33, 0xf9, 0x8f, 0xff, 0xcf, 0xf9, 0xff, 0x00, 0x7f, 0xe0, 0x67, 0xfc, 0x7f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xb3, 0xf3, 0xbf, 0xff,
0xcf, 0xf9, 0xff, 0x00, 0xff, 0xfe, 0x47, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xf9, 0xff, 0xff, 0x7f, 0xf7, 0xf3, 0xff, 0xff, 0xcf, 0xf9, 0xff, 0xe0, 0xff, 0xfc, 0x0f,
0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x7f, 0xe7, 0xe7,
0xff, 0xff, 0xcf, 0xf9, 0xff, 0xe1, 0xff, 0xfc, 0x1f, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, 0xef, 0xe7, 0xef, 0xff, 0xc7, 0xf9, 0xff, 0xc3, 0xff,
0xfc, 0x3f, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f,
0xef, 0xef, 0xc0, 0xff, 0xe7, 0xf9, 0xff, 0xc3, 0xff, 0xf8, 0x3f, 0xff, 0x3f, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x3f, 0xef, 0xcf, 0xf0, 0x01, 0xe7, 0xf1, 0xff,
0x87, 0xff, 0xf8, 0x7f, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff,
0xff, 0xbf, 0xcf, 0xe7, 0xff, 0xc1, 0xe3, 0xe1, 0xff, 0x8f, 0xff, 0xf0, 0xff, 0xff, 0x9f, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0x9f, 0xef, 0xe7, 0xff, 0xff, 0xf3,
0xc1, 0xff, 0x96, 0xaf, 0xf9, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xf9, 0xff, 0xff, 0x9f, 0xe7, 0xe3, 0xff, 0xff, 0xf1, 0xc1, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff,
0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xcf, 0xe7, 0xf3, 0xff,
0xff, 0xf8, 0xc0, 0x00, 0x4a, 0x90, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xf9, 0xff, 0xff, 0xef, 0xf3, 0xf3, 0x9f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xe7, 0xf1,
0xe7, 0xc7, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf3, 0xf0, 0x07, 0xe3, 0xff, 0xff, 0x81, 0xff, 0xff,
0xff, 0xff, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff,
0xf8, 0x07, 0x1f, 0xf1, 0xff, 0xff, 0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xfc, 0x1f, 0x9f, 0xf8, 0xff, 0xff, 0xc3,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9,
0xff, 0xff, 0xf8, 0xff, 0x9f, 0xfe, 0x7f, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x8f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf9, 0xff, 0x9f, 0xfe, 0x3f,
0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xfd, 0xff, 0xff, 0xf1, 0xff, 0x9f, 0xff, 0x9f, 0xff, 0xf3, 0xff, 0x3f, 0x3f, 0xff, 0xff,
0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xe1, 0xff, 0xcf,
0xff, 0xc7, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xe1, 0xff, 0x8f, 0xff, 0xe7, 0xff, 0xf3, 0xff, 0x3f, 0x9f,
0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xc1,
0xff, 0xcf, 0xff, 0xf3, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0x81, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xf3, 0xff,
0x3f, 0x9f, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff,
0xff, 0x91, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0x3f, 0x9f, 0xff, 0xff, 0xfe, 0x3f, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0x11, 0xff, 0x9f, 0xff, 0xff, 0xff,
0xf3, 0xff, 0x1f, 0x9f, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xfe, 0x7f, 0xff, 0x21, 0xff, 0x9f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xbf, 0x9f, 0xff, 0xff, 0xfe,
0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x20, 0xff, 0x9f, 0xff,
0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xfe, 0x7f, 0xfe, 0x60, 0x7f, 0x9f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff,
0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0x64, 0x3f,
0x1f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0xe7, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff,
0xff, 0x3f, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xfc,
0xe7, 0x80, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xf1, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x9f, 0xf8, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3,
0xff, 0xff, 0xfe, 0x7f, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x9f, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xe7, 0xff, 0xfe, 0x7f, 0xff, 0xc3, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff,
0xff, 0xf3, 0xf3, 0xff, 0xfc, 0x7f, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xcf, 0xf9, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xff, 0xf8, 0xff, 0xff,
0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xf9, 0xe7, 0xff, 0xff,
0xff, 0xff, 0xff, 0xf3, 0xf9, 0xff, 0xe1, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xe7, 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0x3f, 0x07,
0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xf3, 0xe7,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x00, 0x1f, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff,
0xe0, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1,
0xf3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe3, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff,
0xf7, 0xff, 0xff, 0xff, 0xff, 0xe3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xf8, 0x83, 0xe7, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xc7, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x13, 0xe7, 0xff, 0xfc, 0x03,
0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xfc, 0x31, 0xe7, 0xff, 0xfc, 0x00, 0x7f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xfe,
0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x39, 0xe3, 0xff,
0xfc, 0x00, 0x1f, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x31, 0xf3, 0xff, 0xfc, 0x00, 0x1f, 0xff, 0xc7, 0xff, 0xff,
0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x19,
0xf3, 0xff, 0xfc, 0x00, 0x07, 0xff, 0x87, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xf3, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x07,
0xff, 0xff, 0xff, 0xe1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x83, 0xf3, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc3, 0xf3, 0xff, 0xff, 0xff, 0xff,
0xf8, 0x07, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xe3, 0xf1, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0xff, 0xff, 0xfe, 0x1f, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf1, 0xe1, 0xff, 0xfe,
0x01, 0xff, 0xfe, 0x07, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xe1, 0xff, 0xf0, 0x00, 0x3f, 0x80, 0x07, 0xff, 0xff, 0xf0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x4c,
0xff, 0xf0, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x0c, 0xff, 0xf0, 0x00, 0x00, 0x0b, 0x87, 0xff,
0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x0e, 0x7f, 0xf8, 0x00, 0x3f, 0xff, 0xc7, 0xff, 0xfe, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x86, 0x7f, 0xfe, 0x00, 0xff, 0xff,
0xc3, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x80, 0x7f, 0xff, 0x87, 0xff, 0xff, 0xf3, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff,
0xff, 0xff, 0xf3, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfe, 0x07, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03,
0xff, 0xff, 0xff, 0xff, 0xf3, 0xf0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xf3, 0xc0, 0x7f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xf8, 0x1f, 0xff, 0xff, 0xff, 0xe3, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xff, 0xe0,
0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xff, 0xe0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x03, 0xff,
0xfe, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xf0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xa0, 0x17, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
};
#endif /*__IMAGEDATA_H__*/

217
src/plugins/MonochromeDisplay/MonochromeDisplay.h

@ -1,217 +0,0 @@
#ifndef __MONOCHROME_DISPLAY__
#define __MONOCHROME_DISPLAY__
#include <U8g2lib.h>
#include <Timezone.h>
#include "../../utils/helper.h"
#include "../../hm/hmSystem.h"
#define DISP_DEFAULT_TIMEOUT 60 // in seconds
static uint8_t bmp_logo[] PROGMEM = {
B00000000, B00000000, // ................
B11101100, B00110111, // ..##.######.##..
B11101100, B00110111, // ..##.######.##..
B11100000, B00000111, // .....######.....
B11010000, B00001011, // ....#.####.#....
B10011000, B00011001, // ...##..##..##...
B10000000, B00000001, // .......##.......
B00000000, B00000000, // ................
B01111000, B00011110, // ...####..####...
B11111100, B00111111, // ..############..
B01111100, B00111110, // ..#####..#####..
B00000000, B00000000, // ................
B11111100, B00111111, // ..############..
B11111110, B01111111, // .##############.
B01111110, B01111110, // .######..######.
B00000000, B00000000 // ................
};
static uint8_t bmp_arrow[] PROGMEM = {
B00000000, B00011100, B00011100, B00001110, B00001110, B11111110, B01111111,
B01110000, B01110000, B00110000, B00111000, B00011000, B01111111, B00111111,
B00011110, B00001110, B00000110, B00000000, B00000000, B00000000, B00000000
};
template<class HMSYSTEM>
class MonochromeDisplay {
public:
MonochromeDisplay() {}
void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, uint8_t disp_reset, const char *version) {
mCfg = cfg;
mSys = sys;
mUtcTs = utcTs;
mNewPayload = false;
mLoopCnt = 0;
mTimeout = DISP_DEFAULT_TIMEOUT; // power off timeout (after inverters go offline)
u8g2_cb_t *rot = (u8g2_cb_t *)((mCfg->rot180) ? U8G2_R2 : U8G2_R0);
if(mCfg->type) {
switch(mCfg->type) {
case 1:
mDisplay = new U8G2_PCD8544_84X48_F_4W_HW_SPI(rot, mCfg->pin0, mCfg->pin1, disp_reset);
break;
case 2:
mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, disp_reset, mCfg->pin0, mCfg->pin1);
break;
case 3:
mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, disp_reset, mCfg->pin0, mCfg->pin1);
break;
}
mDisplay->begin();
mIsLarge = ((mDisplay->getWidth() > 120) && (mDisplay->getHeight() > 60));
calcLineHeights();
mDisplay->clearBuffer();
mDisplay->setContrast(mCfg->contrast);
printText("Ahoy!", 0, 35);
printText("ahoydtu.de", 2, 20);
printText(version, 3, 46);
mDisplay->sendBuffer();
}
}
void payloadEventListener(uint8_t cmd) {
mNewPayload = true;
}
void tickerSecond() {
if(mCfg->pwrSaveAtIvOffline) {
if(mTimeout != 0)
mTimeout--;
}
if(mNewPayload || ((++mLoopCnt % 10) == 0)) {
mNewPayload = false;
mLoopCnt = 0;
DataScreen();
}
}
private:
void DataScreen() {
if (mCfg->type == 0)
return;
if(*mUtcTs == 0)
return;
float totalPower = 0;
float totalYieldDay = 0;
float totalYieldTotal = 0;
bool isprod = false;
Inverter<> *iv;
record_t<> *rec;
for (uint8_t i = 0; i < mSys->getNumInverters(); i++) {
iv = mSys->getInverterByPos(i);
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (iv == NULL)
continue;
if (iv->isProducing(*mUtcTs))
isprod = true;
totalPower += iv->getChannelFieldValue(CH0, FLD_PAC, rec);
totalYieldDay += iv->getChannelFieldValue(CH0, FLD_YD, rec);
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
}
mDisplay->clearBuffer();
// Logos
// pxMovement +x (0 - 6 px)
uint8_t ex = (_mExtra % 7);
if (isprod) {
mDisplay->drawXBMP(5 + ex, 1, 8, 17, bmp_arrow);
if (mCfg->logoEn)
mDisplay->drawXBMP(mDisplay->getWidth() - 24 + ex, 2, 16, 16, bmp_logo);
}
if ((totalPower > 0) && isprod) {
mTimeout = DISP_DEFAULT_TIMEOUT;
mDisplay->setPowerSave(false);
mDisplay->setContrast(mCfg->contrast);
if (totalPower > 999)
snprintf(_fmtText, sizeof(_fmtText), "%2.1f kW", (totalPower / 1000));
else
snprintf(_fmtText, sizeof(_fmtText), "%3.0f W", totalPower);
printText(_fmtText, 0, 20);
} else {
printText("offline", 0, 25);
if(mCfg->pwrSaveAtIvOffline) {
if(mTimeout == 0)
mDisplay->setPowerSave(true);
}
}
snprintf(_fmtText, sizeof(_fmtText), "today: %4.0f Wh", totalYieldDay);
printText(_fmtText, 1);
snprintf(_fmtText, sizeof(_fmtText), "total: %.1f kWh", totalYieldTotal);
printText(_fmtText, 2);
IPAddress ip = WiFi.localIP();
if (!(_mExtra % 10) && (ip)) {
printText(ip.toString().c_str(), 3);
} else {
// Get current time
if(mIsLarge)
printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3);
else
printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3);
}
mDisplay->sendBuffer();
_mExtra++;
}
void calcLineHeights() {
uint8_t yOff = 0;
for(uint8_t i = 0; i < 4; i++) {
setFont(i);
yOff += (mDisplay->getMaxCharHeight() + 1);
mLineOffsets[i] = yOff;
}
}
inline void setFont(uint8_t line) {
switch (line) {
case 0: mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB14_tr : u8g2_font_lubBI14_tr); break;
case 3: mDisplay->setFont(u8g2_font_5x8_tr); break;
default: mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr); break;
}
}
void printText(const char* text, uint8_t line, uint8_t dispX = 5) {
if(!mIsLarge)
dispX = (line == 0) ? 10 : 5;
setFont(line);
if(mCfg->pxShift)
dispX += (_mExtra % 7); // add pixel movement
mDisplay->drawStr(dispX, mLineOffsets[line], text);
}
// private member variables
U8G2* mDisplay;
uint8_t _mExtra;
uint16_t mTimeout; // interval at which to power save (milliseconds)
char _fmtText[32];
bool mNewPayload;
bool mIsLarge;
uint8_t mLoopCnt;
uint32_t *mUtcTs;
uint8_t mLineOffsets[5];
display_t *mCfg;
HMSYSTEM *mSys;
};
#endif /*__MONOCHROME_DISPLAY__*/

33
src/publisher/pubMqtt.h

@ -419,17 +419,19 @@ class PubMqtt {
iv = mSys->getInverterByPos(id); iv = mSys->getInverterByPos(id);
if (NULL == iv) if (NULL == iv)
continue; // skip to next inverter continue; // skip to next inverter
if (!iv->config->enabled)
continue; // skip to next inverter
rec = iv->getRecordStruct(RealTimeRunData_Debug); rec = iv->getRecordStruct(RealTimeRunData_Debug);
// inverter status // inverter status
uint8_t status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; uint8_t status = MQTT_STATUS_NOT_AVAIL_NOT_PROD;
if (iv->config->enabled) { if (iv->isAvailable(*mUtcTimestamp)) {
if (iv->isAvailable(*mUtcTimestamp)) anyAvail = true;
status = (iv->isProducing(*mUtcTimestamp)) ? MQTT_STATUS_AVAIL_PROD : MQTT_STATUS_AVAIL_NOT_PROD; status = (iv->isProducing(*mUtcTimestamp)) ? MQTT_STATUS_AVAIL_PROD : MQTT_STATUS_AVAIL_NOT_PROD;
}
else // inverter is enabled but not available else // inverter is enabled but not available
allAvail = false; allAvail = false;
}
if(mLastIvState[id] != status) { if(mLastIvState[id] != status) {
// if status changed from producing to not producing send last data immediately // if status changed from producing to not producing send last data immediately
@ -439,11 +441,11 @@ class PubMqtt {
mLastIvState[id] = status; mLastIvState[id] = status;
changed = true; changed = true;
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/%s", iv->config->name, mqttStr[MQTT_STR_AVAILABLE]); snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
snprintf(val, 40, "%d", status); snprintf(val, 40, "%d", status);
publish(topic, val, true); publish(topic, val, true);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/%s", iv->config->name, mqttStr[MQTT_STR_LAST_SUCCESS]); snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name);
snprintf(val, 40, "%d", iv->getLastTs(rec)); snprintf(val, 40, "%d", iv->getLastTs(rec));
publish(topic, val, true); publish(topic, val, true);
} }
@ -451,7 +453,7 @@ class PubMqtt {
if(changed) { if(changed) {
snprintf(val, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE))); snprintf(val, 32, "%d", ((allAvail) ? MQTT_STATUS_ONLINE : ((anyAvail) ? MQTT_STATUS_PARTIAL : MQTT_STATUS_OFFLINE)));
publish(subtopics[MQTT_STATUS], val, true); publish("status", val, true);
} }
return anyAvail; return anyAvail;
@ -474,6 +476,7 @@ class PubMqtt {
char topic[7 + MQTT_TOPIC_LEN], val[40]; char topic[7 + MQTT_TOPIC_LEN], val[40];
record_t<> *rec = iv->getRecordStruct(curInfoCmd); record_t<> *rec = iv->getRecordStruct(curInfoCmd);
if (iv->getLastTs(rec) > 0) {
for (uint8_t i = 0; i < rec->length; i++) { for (uint8_t i = 0; i < rec->length; i++) {
bool retained = false; bool retained = false;
if (curInfoCmd == RealTimeRunData_Debug) { if (curInfoCmd == RealTimeRunData_Debug) {
@ -494,6 +497,7 @@ class PubMqtt {
yield(); yield();
} }
} }
}
void sendIvData() { void sendIvData() {
bool anyAvail = processIvStatus(); bool anyAvail = processIvStatus();
@ -512,19 +516,25 @@ class PubMqtt {
uint8_t curInfoCmd = mSendList.front(); uint8_t curInfoCmd = mSendList.front();
if ((curInfoCmd != RealTimeRunData_Debug) || !RTRDataHasBeenSent) { // send RTR Data only once if ((curInfoCmd != RealTimeRunData_Debug) || !RTRDataHasBeenSent) { // send RTR Data only once
bool sendTotals = (curInfoCmd == RealTimeRunData_Debug);
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id); Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL == iv) if (NULL == iv)
continue; // skip to next inverter continue; // skip to next inverter
if (!iv->config->enabled)
continue; // skip to next inverter
// send RTR Data only if status is available // send RTR Data only if status is available
if ((curInfoCmd != RealTimeRunData_Debug) || (MQTT_STATUS_AVAIL_PROD == mLastIvState[id])) if ((curInfoCmd != RealTimeRunData_Debug) || (MQTT_STATUS_NOT_AVAIL_NOT_PROD != mLastIvState[id]))
sendData(iv, curInfoCmd); sendData(iv, curInfoCmd);
// calculate total values for RealTimeRunData_Debug // calculate total values for RealTimeRunData_Debug
if (curInfoCmd == RealTimeRunData_Debug) { if (sendTotals) {
record_t<> *rec = iv->getRecordStruct(curInfoCmd); record_t<> *rec = iv->getRecordStruct(curInfoCmd);
sendTotals &= (iv->getLastTs(rec) > 0);
if (sendTotals) {
for (uint8_t i = 0; i < rec->length; i++) { for (uint8_t i = 0; i < rec->length; i++) {
if (CH0 == rec->assign[i].ch) { if (CH0 == rec->assign[i].ch) {
switch (rec->assign[i].fieldId) { switch (rec->assign[i].fieldId) {
@ -543,11 +553,12 @@ class PubMqtt {
} }
} }
} }
yield();
} }
} }
yield();
}
if (curInfoCmd == RealTimeRunData_Debug) { if (sendTotals) {
uint8_t fieldId; uint8_t fieldId;
for (uint8_t i = 0; i < 4; i++) { for (uint8_t i = 0; i < 4; i++) {
switch (i) { switch (i) {
@ -565,7 +576,7 @@ class PubMqtt {
fieldId = FLD_PDC; fieldId = FLD_PDC;
break; break;
} }
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/%s", mqttStr[MQTT_STR_TOTAL], fields[fieldId]); snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]);
snprintf(val, 40, "%g", ah::round3(total[i])); snprintf(val, 40, "%g", ah::round3(total[i]));
publish(topic, val, true); publish(topic, val, true);
} }

162
src/web/RestApi.h

@ -12,18 +12,20 @@
#else #else
#include "ESPAsyncTCP.h" #include "ESPAsyncTCP.h"
#endif #endif
#include "ESPAsyncWebServer.h" #include "../appInterface.h"
#include "AsyncJson.h"
#include "../hm/hmSystem.h" #include "../hm/hmSystem.h"
#include "../utils/helper.h" #include "../utils/helper.h"
#include "AsyncJson.h"
#include "../appInterface.h" #include "ESPAsyncWebServer.h"
#if defined(F) && defined(ESP32) #if defined(F) && defined(ESP32)
#undef F #undef F
#define F(sl) (sl) #define F(sl) (sl)
#endif #endif
const uint8_t acList[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q};
const uint8_t dcList[] = {FLD_UDC, FLD_IDC, FLD_PDC, FLD_YD, FLD_YT, FLD_IRR};
template<class HMSYSTEM, class HMRADIO> template<class HMSYSTEM, class HMRADIO>
class RestApi { class RestApi {
public: public:
@ -72,7 +74,7 @@ class RestApi {
mHeapFrag = ESP.getHeapFragmentation(); mHeapFrag = ESP.getHeapFragmentation();
#endif #endif
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
String path = request->url().substring(5); String path = request->url().substring(5);
@ -84,7 +86,6 @@ class RestApi {
else if(path == "reboot") getReboot(root); else if(path == "reboot") getReboot(root);
else if(path == "statistics") getStatistics(root); else if(path == "statistics") getStatistics(root);
else if(path == "inverter/list") getInverterList(root); else if(path == "inverter/list") getInverterList(root);
else if(path == "menu") getMenu(root);
else if(path == "index") getIndex(root); else if(path == "index") getIndex(root);
else if(path == "setup") getSetup(root); else if(path == "setup") getSetup(root);
else if(path == "setup/networks") getNetworks(root); else if(path == "setup/networks") getNetworks(root);
@ -93,8 +94,12 @@ class RestApi {
else if(path == "record/alarm") getRecord(root, AlarmData); else if(path == "record/alarm") getRecord(root, AlarmData);
else if(path == "record/config") getRecord(root, SystemConfigPara); else if(path == "record/config") getRecord(root, SystemConfigPara);
else if(path == "record/live") getRecord(root, RealTimeRunData_Debug); else if(path == "record/live") getRecord(root, RealTimeRunData_Debug);
else {
if(path.substring(0, 12) == "inverter/id/")
getInverter(root, request->url().substring(17).toInt());
else else
getNotFound(root, F("http://") + request->host() + F("/api/")); getNotFound(root, F("http://") + request->host() + F("/api/"));
}
//DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage())); //DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage()));
response->addHeader("Access-Control-Allow-Origin", "*"); response->addHeader("Access-Control-Allow-Origin", "*");
@ -154,13 +159,14 @@ class RestApi {
ep[F("record/live")] = url + F("record/live"); ep[F("record/live")] = url + F("record/live");
} }
void onDwnldSetup(AsyncWebServerRequest *request) { void onDwnldSetup(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response; AsyncWebServerResponse *response;
File fp = LittleFS.open("/settings.json", "r"); File fp = LittleFS.open("/settings.json", "r");
if(!fp) { if(!fp) {
DPRINTLN(DBG_ERROR, F("failed to load settings")); DPRINTLN(DBG_ERROR, F("failed to load settings"));
response = request->beginResponse(200, F("application/json"), "{}"); response = request->beginResponse(200, F("application/json; charset=utf-8"), "{}");
} }
else { else {
String tmp = fp.readString(); String tmp = fp.readString();
@ -173,7 +179,7 @@ class RestApi {
tmp.remove(i, tmp.indexOf("\"", i)-i); tmp.remove(i, tmp.indexOf("\"", i)-i);
} }
} }
response = request->beginResponse(200, F("application/json"), tmp); response = request->beginResponse(200, F("application/json; charset=utf-8"), tmp);
} }
response->addHeader("Content-Type", "application/octet-stream"); response->addHeader("Content-Type", "application/octet-stream");
@ -184,10 +190,11 @@ class RestApi {
} }
void getGeneric(JsonObject obj) { void getGeneric(JsonObject obj) {
obj[F("version")] = String(mApp->getVersion());
obj[F("build")] = String(AUTO_GIT_HASH);
obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI(); obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI();
obj[F("ts_uptime")] = mApp->getUptime(); obj[F("ts_uptime")] = mApp->getUptime();
obj[F("menu_prot")] = mApp->getProtection();
obj[F("menu_mask")] = (uint16_t)(mConfig->sys.protectionMask );
obj[F("menu_protEn")] = (bool) (strlen(mConfig->sys.adminPwd) > 0);
#if defined(ESP32) #if defined(ESP32)
obj[F("esp_type")] = F("ESP32"); obj[F("esp_type")] = F("ESP32");
@ -199,6 +206,7 @@ class RestApi {
void getSysInfo(JsonObject obj) { void getSysInfo(JsonObject obj) {
obj[F("ssid")] = mConfig->sys.stationSsid; obj[F("ssid")] = mConfig->sys.stationSsid;
obj[F("device_name")] = mConfig->sys.deviceName; obj[F("device_name")] = mConfig->sys.deviceName;
obj[F("dark_mode")] = (bool)mConfig->sys.darkMode;
obj[F("mac")] = WiFi.macAddress(); obj[F("mac")] = WiFi.macAddress();
obj[F("hostname")] = mConfig->sys.deviceName; obj[F("hostname")] = mConfig->sys.deviceName;
@ -245,15 +253,12 @@ class RestApi {
} }
void getHtmlSystem(JsonObject obj) { void getHtmlSystem(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getSysInfo(obj.createNestedObject(F("system"))); getSysInfo(obj.createNestedObject(F("system")));
getGeneric(obj.createNestedObject(F("generic"))); getGeneric(obj.createNestedObject(F("generic")));
obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">Factory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>"); obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">Factory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>");
} }
void getHtmlLogout(JsonObject obj) { void getHtmlLogout(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic"))); getGeneric(obj.createNestedObject(F("generic")));
obj[F("refresh")] = 3; obj[F("refresh")] = 3;
obj[F("refresh_url")] = "/"; obj[F("refresh_url")] = "/";
@ -261,7 +266,6 @@ class RestApi {
} }
void getHtmlSave(JsonObject obj) { void getHtmlSave(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic"))); getGeneric(obj.createNestedObject(F("generic")));
obj[F("refresh")] = 2; obj[F("refresh")] = 2;
obj[F("refresh_url")] = "/setup"; obj[F("refresh_url")] = "/setup";
@ -269,7 +273,6 @@ class RestApi {
} }
void getReboot(JsonObject obj) { void getReboot(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic"))); getGeneric(obj.createNestedObject(F("generic")));
obj[F("refresh")] = 10; obj[F("refresh")] = 10;
obj[F("refresh_url")] = "/"; obj[F("refresh_url")] = "/";
@ -316,6 +319,41 @@ class RestApi {
obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop; obj[F("rstComStop")] = (bool)mConfig->inst.rstValsCommStop;
} }
void getInverter(JsonObject obj, uint8_t id) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
obj[F("id")] = id;
obj[F("enabled")] = (bool)iv->config->enabled;
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")] = ah::round3(iv->actPowerLimit);
obj[F("ts_last_success")] = rec->ts;
JsonArray ch = obj.createNestedArray("ch");
// AC
uint8_t pos;
obj[F("ch_name")][0] = "AC";
JsonArray ch0 = ch.createNestedArray();
for (uint8_t fld = 0; fld < sizeof(acList); fld++) {
pos = (iv->getPosByChFld(CH0, acList[fld], rec));
ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
}
// DC
for(uint8_t j = 0; j < iv->channels; j ++) {
obj[F("ch_name")][j+1] = iv->config->chName[j];
JsonArray cur = ch.createNestedArray();
for (uint8_t fld = 0; fld < sizeof(dcList); fld++) {
pos = (iv->getPosByChFld((j+1), dcList[fld], rec));
cur[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
}
}
}
}
void getMqtt(JsonObject obj) { void getMqtt(JsonObject obj) {
obj[F("broker")] = String(mConfig->mqtt.broker); obj[F("broker")] = String(mConfig->mqtt.broker);
obj[F("port")] = String(mConfig->mqtt.port); obj[F("port")] = String(mConfig->mqtt.port);
@ -376,64 +414,21 @@ class RestApi {
} }
void getDisplay(JsonObject obj) { void getDisplay(JsonObject obj) {
obj[F("disp_type")] = (uint8_t)mConfig->plugin.display.type; obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type;
obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline; obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline;
obj[F("logo_en")] = (bool)mConfig->plugin.display.logoEn; obj[F("disp_pxshift")] = (bool)mConfig->plugin.display.pxShift;
obj[F("px_shift")] = (bool)mConfig->plugin.display.pxShift; obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot;
obj[F("rot180")] = (bool)mConfig->plugin.display.rot180; obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast;
obj[F("contrast")] = (uint8_t)mConfig->plugin.display.contrast; obj[F("disp_clk")] = mConfig->plugin.display.disp_clk;
obj[F("pinDisp0")] = mConfig->plugin.display.pin0; obj[F("disp_data")] = mConfig->plugin.display.disp_data;
obj[F("pinDisp1")] = mConfig->plugin.display.pin1; obj[F("disp_cs")] = mConfig->plugin.display.disp_cs;
} obj[F("disp_dc")] = mConfig->plugin.display.disp_dc;
obj[F("disp_rst")] = mConfig->plugin.display.disp_reset;
void getMenu(JsonObject obj) { obj[F("disp_bsy")] = mConfig->plugin.display.disp_busy;
uint8_t i = 0;
uint16_t mask = (mApp->getProtection()) ? mConfig->sys.protectionMask : 0;
if(!CHECK_MASK(mask, PROT_MASK_LIVE)) {
obj[F("name")][i] = "Live";
obj[F("link")][i++] = "/live";
}
if(!CHECK_MASK(mask, PROT_MASK_SERIAL)) {
obj[F("name")][i] = "Serial / Control";
obj[F("link")][i++] = "/serial";
}
if(!CHECK_MASK(mask, PROT_MASK_SETUP)) {
obj[F("name")][i] = "Settings";
obj[F("link")][i++] = "/setup";
}
obj[F("name")][i++] = "-";
obj[F("name")][i] = "REST API";
obj[F("link")][i] = "/api";
obj[F("trgt")][i++] = "_blank";
obj[F("name")][i++] = "-";
if(!CHECK_MASK(mask, PROT_MASK_UPDATE)) {
obj[F("name")][i] = "Update";
obj[F("link")][i++] = "/update";
}
if(!CHECK_MASK(mask, PROT_MASK_SYSTEM)) {
obj[F("name")][i] = "System";
obj[F("link")][i++] = "/system";
}
obj[F("name")][i++] = "-";
obj[F("name")][i] = "Documentation";
obj[F("link")][i] = "https://ahoydtu.de";
obj[F("trgt")][i++] = "_blank";
if(strlen(mConfig->sys.adminPwd) > 0) {
obj[F("name")][i++] = "-";
if(mApp->getProtection()) {
obj[F("name")][i] = "Login";
obj[F("link")][i++] = "/login";
} else {
obj[F("name")][i] = "Logout";
obj[F("link")][i++] = "/logout";
}
}
} }
void getIndex(JsonObject obj) { void getIndex(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic"))); getGeneric(obj.createNestedObject(F("generic")));
obj[F("ts_now")] = mApp->getTimestamp(); obj[F("ts_now")] = mApp->getTimestamp();
obj[F("ts_sunrise")] = mApp->getSunrise(); obj[F("ts_sunrise")] = mApp->getSunrise();
obj[F("ts_sunset")] = mApp->getSunset(); obj[F("ts_sunset")] = mApp->getSunset();
@ -482,10 +477,9 @@ class RestApi {
} }
void getSetup(JsonObject obj) { void getSetup(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic"))); getGeneric(obj.createNestedObject(F("generic")));
getSysInfo(obj.createNestedObject(F("system"))); getSysInfo(obj.createNestedObject(F("system")));
getInverterList(obj.createNestedObject(F("inverter"))); //getInverterList(obj.createNestedObject(F("inverter")));
getMqtt(obj.createNestedObject(F("mqtt"))); getMqtt(obj.createNestedObject(F("mqtt")));
getNtp(obj.createNestedObject(F("ntp"))); getNtp(obj.createNestedObject(F("ntp")));
getSun(obj.createNestedObject(F("sun"))); getSun(obj.createNestedObject(F("sun")));
@ -502,14 +496,29 @@ class RestApi {
} }
void getLive(JsonObject obj) { void getLive(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic"))); getGeneric(obj.createNestedObject(F("generic")));
JsonArray invArr = obj.createNestedArray(F("inverter")); //JsonArray invArr = obj.createNestedArray(F("inverter"));
obj["refresh_interval"] = mConfig->nrf.sendInterval; obj[F("refresh")] = mConfig->nrf.sendInterval;
uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q}; for (uint8_t fld = 0; fld < sizeof(acList); fld++) {
obj[F("ch0_fld_units")][fld] = String(units[fieldUnits[acList[fld]]]);
obj[F("ch0_fld_names")][fld] = String(fields[acList[fld]]);
}
for (uint8_t fld = 0; fld < sizeof(dcList); fld++) {
obj[F("fld_units")][fld] = String(units[fieldUnits[dcList[fld]]]);
obj[F("fld_names")][fld] = String(fields[dcList[fld]]);
}
Inverter<> *iv; Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i);
bool parse = false;
if(NULL != iv)
parse = iv->config->enabled;
obj[F("iv")][i] = parse;
}
/*Inverter<> *iv;
uint8_t pos; uint8_t pos;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i); iv = mSys->getInverterByPos(i);
@ -553,7 +562,7 @@ class RestApi {
} }
} }
} }
} }*/
} }
void getRecord(JsonObject obj, uint8_t recType) { void getRecord(JsonObject obj, uint8_t recType) {
@ -632,8 +641,7 @@ class RestApi {
mTimezoneOffset = jsonIn[F("val")]; mTimezoneOffset = jsonIn[F("val")];
else if(F("discovery_cfg") == jsonIn[F("cmd")]) { else if(F("discovery_cfg") == jsonIn[F("cmd")]) {
mApp->setMqttDiscoveryFlag(); // for homeassistant mApp->setMqttDiscoveryFlag(); // for homeassistant
} } else {
else {
jsonOut[F("error")] = F("unknown cmd"); jsonOut[F("error")] = F("unknown cmd");
return false; return false;
} }

86
src/web/html/api.js

@ -33,23 +33,66 @@ iconSuccess = [
/** /**
* GENERIC FUNCTIONS * GENERIC FUNCTIONS
*/ */
function ml(tagName, ...args) {
var el = document.createElement(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") {
var t = document.createTextNode(n);
el.appendChild(t);
} else if (n instanceof Array) {
for(var i = 0; i < n.length; i++) {
if (typeof n[i] === "string") {
var t = document.createTextNode(n[i]);
el.appendChild(t);
} else if (n[i] instanceof Node){
el.appendChild(n[i]);
}
}
} else if (n instanceof Node){
el.appendChild(n)
}
return el;
}
function topnav() { function topnav() {
toggle("topnav"); toggle("topnav", "mobile");
} }
function parseMenu(obj) { function parseNav(obj) {
var e = document.getElementById("topnav"); for(i = 0; i < 10; i++) {
e.innerHTML = ""; if(i == 2)
for(var i = 0; i < obj["name"].length; i ++) { continue;
if(obj["name"][i] == "-") var l = document.getElementById("nav"+i);
e.appendChild(span("", ["seperator"])); if(window.location.pathname == "/" + l.href.split('/').pop())
else {
var l = link(obj["link"][i], obj["name"][i], obj["trgt"][i]);
if(obj["link"][i] == window.location.pathname)
l.classList.add("active"); l.classList.add("active");
e.appendChild(l);
if(obj["menu_protEn"]) {
if(obj["menu_prot"]) {
if(0 == i)
l.classList.remove("hide");
else if(i > 2) {
if(((obj["menu_mask"] >> (i-2)) & 0x01) == 0x00)
l.classList.remove("hide");
} }
} else if(0 != i)
l.classList.remove("hide");
} else if(i > 1)
l.classList.remove("hide");
} }
} }
@ -60,7 +103,9 @@ function parseVersion(obj) {
} }
function parseESP(obj) { function parseESP(obj) {
document.getElementById("esp_type").innerHTML="Board: " + obj["esp_type"]; document.getElementById("esp_type").append(
document.createTextNode("Board: " + obj["esp_type"])
);
} }
function parseRssi(obj) { function parseRssi(obj) {
@ -69,7 +114,7 @@ function parseRssi(obj) {
icon = iconWifi1; icon = iconWifi1;
else if(obj["wifi_rssi"] <= -70) else if(obj["wifi_rssi"] <= -70)
icon = iconWifi2; icon = iconWifi2;
document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "#fff", null, obj["wifi_rssi"])); document.getElementById("wifiicon").replaceChildren(svg(icon, 32, 32, "wifi", obj["wifi_rssi"]));
} }
function setHide(id, hide) { function setHide(id, hide) {
@ -82,12 +127,12 @@ function setHide(id, hide) {
elm.classList.remove('hide'); elm.classList.remove('hide');
} }
function toggle(id) { function toggle(id, cl="hide") {
var e = document.getElementById(id); var e = document.getElementById(id);
if(!e.classList.contains("hide")) if(!e.classList.contains(cl))
e.classList.add("hide"); e.classList.add(cl);
else else
e.classList.remove('hide'); e.classList.remove(cl);
} }
function getAjax(url, ptr, method="GET", json=null) { function getAjax(url, ptr, method="GET", json=null) {
@ -198,11 +243,10 @@ function link(dst, text, target=null) {
return a; return a;
} }
function svg(data=null, w=24, h=24, color="#000", cl=null, tooltip=null) { function svg(data=null, w=24, h=24, cl=null, tooltip=null) {
var s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); var s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
s.setAttribute('width', w); s.setAttribute('width', w);
s.setAttribute('height', h); s.setAttribute('height', h);
s.setAttribute('fill', color);
s.setAttribute('viewBox', '0 0 16 16'); s.setAttribute('viewBox', '0 0 16 16');
if(null != cl) s.setAttribute('class', cl); if(null != cl) s.setAttribute('class', cl);
if(null != data) { if(null != data) {

27
src/web/html/colorBright.css

@ -0,0 +1,27 @@
:root {
--bg: #fff;
--fg: #000;
--fg2: #fff;
--info: #0000dd;
--warn: #ff7700;
--success: #009900;
--input-bg: #eee;
--nav-bg: #333;
--primary: #006ec0;
--primary-hover: #044e86;
--secondary: #0072c8;
--nav-active: #555;
--footer-bg: #282828;
--total-head-title: #8e5903;
--total-bg: #b06e04;
--iv-head-title: #1c6800;
--iv-head-bg: #32b004;
--ch-head-title: #003c80;
--ch-head-bg: #006ec0;
--ts-head: #333;
--ts-bg: #555;
}

27
src/web/html/colorDark.css

@ -0,0 +1,27 @@
:root {
--bg: #222;
--fg: #ccc;
--fg2: #fff;
--info: #0072c8;
--warn: #ffaa00;
--success: #00bb00;
--input-bg: #333;
--nav-bg: #333;
--primary: #004d87;
--primary-hover: #023155;
--secondary: #0072c8;
--nav-active: #555;
--footer-bg: #282828;
--total-head-title: #555511;
--total-bg: #666622;
--iv-head-title: #115511;
--iv-head-bg: #226622;
--ch-head-title: #112255;
--ch-head-bg: #223366;
--ts-head: #333;
--ts-bg: #555;
}

89
src/web/html/convert.py

@ -2,10 +2,67 @@ import re
import os import os
import gzip import gzip
import glob import glob
import shutil
from datetime import date
from pathlib import Path from pathlib import Path
import subprocess
def get_git_sha():
return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip()
def readVersion(path):
f = open(path, "r")
lines = f.readlines()
f.close()
today = date.today()
search = ["_MAJOR", "_MINOR", "_PATCH"]
version = today.strftime("%y%m%d") + "_ahoy_"
ver = ""
for line in lines:
if(line.find("VERSION_") != -1):
for s in search:
p = line.find(s)
if(p != -1):
version += line[p+13:].rstrip() + "."
ver += line[p+13:].rstrip() + "."
return ver[:-1]
def htmlParts(file, header, nav, footer, version):
p = "";
f = open(file, "r")
lines = f.readlines()
f.close();
f = open(header, "r")
h = f.read().strip()
f.close()
f = open(nav, "r")
n = f.read().strip()
f.close()
def convert2Header(inFile): f = open(footer, "r")
fo = f.read().strip()
f.close()
for line in lines:
line = line.replace("{#HTML_HEADER}", h)
line = line.replace("{#HTML_NAV}", n)
line = line.replace("{#HTML_FOOTER}", fo)
p += line
#placeholders
link = '<a target="_blank" href="https://github.com/lumapu/ahoy/commits/' + get_git_sha() + '">GIT SHA: ' + get_git_sha() + ' :: ' + version + '</a>'
p = p.replace("{#VERSION}", version)
p = p.replace("{#VERSION_GIT}", link)
f = open("tmp/" + file, "w")
f.write(p);
f.close();
return p
def convert2Header(inFile, version):
fileType = inFile.split(".")[1] fileType = inFile.split(".")[1]
define = inFile.split(".")[0].upper() define = inFile.split(".")[0].upper()
define2 = inFile.split(".")[1].upper() define2 = inFile.split(".")[1].upper()
@ -17,10 +74,15 @@ def convert2Header(inFile):
Path("html/h").mkdir(exist_ok=True) Path("html/h").mkdir(exist_ok=True)
else: else:
outName = "h/" + inFileVarName + ".h" outName = "h/" + inFileVarName + ".h"
Path("h").mkdir(exist_ok=True)
data = ""
if fileType == "ico": if fileType == "ico":
f = open(inFile, "rb") f = open(inFile, "rb")
data = f.read()
f.close()
else:
if fileType == "html":
data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", version)
else: else:
f = open(inFile, "r") f = open(inFile, "r")
data = f.read() data = f.read()
@ -53,13 +115,17 @@ def convert2Header(inFile):
f.close() f.close()
# delete all files in the 'h' dir # delete all files in the 'h' dir
dir = 'h' wd = 'h'
if os.getcwd()[-4:] != "html": if os.getcwd()[-4:] != "html":
dir = "web/html/" + dir wd = "web/html/" + wd
if os.path.exists(dir): if os.path.exists(wd):
for f in os.listdir(dir): for f in os.listdir(wd):
os.remove(os.path.join(dir, f)) 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 # grab all files with following extensions
if os.getcwd()[-4:] != "html": if os.getcwd()[-4:] != "html":
@ -69,6 +135,11 @@ files_grabbed = []
for files in types: for files in types:
files_grabbed.extend(glob.glob(files)) 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")
version = readVersion("../../defines.h")
# go throw the array # go throw the array
for val in files_grabbed: for val in files_grabbed:
convert2Header(val) convert2Header(val, version)

16
src/web/html/includes/footer.html

@ -0,0 +1,16 @@
<div id="footer">
<div class="left">
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2023</a>
<ul>
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>
</ul>
</div>
<div class="right">
<ul>
<li>{#VERSION_GIT}</li>
<li id="esp_type"></li>
<li><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a></li>
</ul>
</div>
</div>

5
src/web/html/includes/header.html

@ -0,0 +1,5 @@
<link rel="stylesheet" type="text/css" href="colors.css"/>
<link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<script type="text/javascript" src="api.js"></script>

25
src/web/html/includes/nav.html

@ -0,0 +1,25 @@
<div class="topnav">
<a href="/" class="title">AhoyDTU</a>
<a href="javascript:void(0);" class="icon" onclick="topnav()">
<span></span>
<span></span>
<span></span>
</a>
<div id="topnav" class="mobile">
<a id="nav3" class="hide" href="/live">Live</a>
<a id="nav4" class="hide" href="/serial">Serial / Control</a>
<a id="nav5" class="hide" href="/setup">Settings</a>
<span class="seperator"></span>
<a id="nav6" class="hide" href="/update">Update</a>
<a id="nav7" class="hide" href="/system">System</a>
<span class="seperator"></span>
<a id="nav8" href="/api" target="_blank">REST API</a>
<a id="nav9" href="https://ahoydtu.de" target="_blank">Documentation</a>
<span class="seperator"></span>
<a id="nav0" class="hide" href="/login">Login</a>
<a id="nav1" class="hide" href="/logout">Logout</a>
</div>
<div id="wifiicon" class="info"></div>
</div>

81
src/web/html/index.html

@ -2,36 +2,12 @@
<html> <html>
<head> <head>
<title>Index</title> <title>Index</title>
<link rel="stylesheet" type="text/css" href="style.css"/> {#HTML_HEADER}
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
</head> </head>
<body> <body>
<div class="topnav"> {#HTML_NAV}
<a href="/" class="title">AhoyDTU</a>
<a href="javascript:void(0);" class="icon" onclick="topnav()">
<span></span>
<span></span>
<span></span>
</a>
<div id="topnav" class="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<script>
function promptFunction() {
var Text = prompt("This project was started from https://www.mikrocontroller.net/topic/525778 this discussion.\n\n" +
"The Hoymiles protocol was decrypted through the voluntary efforts of many participants. ahoy, among others, was developed based on this work.\n" +
"The software was developed to the best of our knowledge and belief. Nevertheless, no liability can be accepted for a malfunction or guarantee loss of the inverter.\n\n" +
"Ahoy is freely available. If you paid money for the software, you probably got ripped off.\n\nPlease type in 'YeS', you are accept our Disclaim. You should then save your config.", "");
if (Text != "YeS")
promptFunction();
else
return true;
}
</script>
<p> <p>
<span class="des">Uptime: </span><span id="uptime"></span><br/> <span class="des">Uptime: </span><span id="uptime"></span><br/>
<span class="des">ESP-Time: </span><span id="date"></span> <span class="des">ESP-Time: </span><span id="date"></span>
@ -60,22 +36,7 @@
</div> </div>
</div> </div>
</div> </div>
<div id="footer"> {#HTML_FOOTER}
<div class="left">
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2023</a>
<ul>
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>
</ul>
</div>
<div class="right">
<ul>
<li><span id="version"></span></li>
<li><span id="esp_type"></span></li>
<li><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a></li>
</ul>
</div>
</div>
<script type="text/javascript"> <script type="text/javascript">
var exeOnce = true; var exeOnce = true;
var tickCnt = 0; var tickCnt = 0;
@ -106,12 +67,8 @@
} }
function parseGeneric(obj) { function parseGeneric(obj) {
// Disclaimer if(exeOnce)
//if(obj["disclaimer"] == false) sessionStorage.setItem("gDisclaimer", promptFunction());
if(exeOnce){
parseVersion(obj);
parseESP(obj); parseESP(obj);
}
parseRssi(obj); parseRssi(obj);
} }
@ -163,14 +120,14 @@
var p = div(["none"]); var p = div(["none"]);
for(var i of obj) { for(var i of obj) {
var icon = iconWarn; var icon = iconWarn;
var color = "#F70"; var cl = "icon-warn";
avail = ""; avail = "";
if(false == i["enabled"]) { if(false == i["enabled"]) {
avail = "disabled"; avail = "disabled";
} }
else if(false == i["is_avail"]) { else if(false == i["is_avail"]) {
icon = iconInfo; icon = iconInfo;
color = "#00d"; cl = "icon-info";
avail = "not yet available"; avail = "not yet available";
} }
else if(0 == i["ts_last_success"]) { else if(0 == i["ts_last_success"]) {
@ -183,12 +140,12 @@
if(false == i["is_producing"]) if(false == i["is_producing"])
avail += "not "; avail += "not ";
else else
color = "#090"; cl = "icon-success";
avail += "producing"; avail += "producing";
} }
p.append( p.append(
svg(icon, 20, 20, color, "icon"), svg(icon, 30, 30, "icon " + cl),
span("Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is " + avail), span("Inverter #" + i["id"] + ": " + i["name"] + " (v" + i["version"] + ") is " + avail),
br() br()
); );
@ -203,25 +160,25 @@
document.getElementById("iv").replaceChildren(p); document.getElementById("iv").replaceChildren(p);
} }
function parseWarnInfo(warn, success, version) { function parseWarnInfo(warn, success) {
var p = div(["none"]); var p = div(["none"]);
for(var w of warn) { for(var w of warn) {
p.append(svg(iconWarn, 20, 20, "#F70", "icon"), span(w), br()); p.append(svg(iconWarn, 30, 30, "icon icon-warn"), span(w), br());
} }
for(var i of success) { for(var i of success) {
p.append(svg(iconSuccess, 20, 20, "#090", "icon"), span(i), br()); p.append(svg(iconSuccess, 30, 30, "icon icon-success"), span(i), br());
} }
if(commInfo.length > 0) if(commInfo.length > 0)
p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span(commInfo), br()); p.append(svg(iconInfo, 30, 30, "icon icon-info"), span(commInfo), br());
if(null != release) { if(null != release) {
if(getVerInt(version) < getVerInt(release)) if(getVerInt("{#VERSION}") < getVerInt(release))
p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span("Update available, current released version: " + release), br()); p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("Update available, current released version: " + release), br());
else if(getVerInt(version) > getVerInt(release)) else if(getVerInt("{#VERSION}") > getVerInt(release))
p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span("You are using development version " + version +". In case of issues you may want to try the current stable release: " + release), br()); p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using development version {#VERSION}. In case of issues you may want to try the current stable release: " + release), br());
else else
p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span("You are using the current stable release: " + release), br()); p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("You are using the current stable release: " + release), br());
} }
document.getElementById("warn_info").replaceChildren(p); document.getElementById("warn_info").replaceChildren(p);
@ -239,11 +196,11 @@
function parse(obj) { function parse(obj) {
if(null != obj) { if(null != obj) {
if(exeOnce) if(exeOnce)
parseMenu(obj["menu"]); parseNav(obj["generic"]);
parseGeneric(obj["generic"]); parseGeneric(obj["generic"]);
parseSys(obj); parseSys(obj);
parseIv(obj["inverter"]); parseIv(obj["inverter"]);
parseWarnInfo(obj["warnings"], obj["infos"], obj["generic"]["version"]); parseWarnInfo(obj["warnings"], obj["infos"]);
if(exeOnce) { if(exeOnce) {
window.setInterval("tick()", 1000); window.setInterval("tick()", 1000);
exeOnce = false; exeOnce = false;

35
src/web/html/login.html

@ -2,41 +2,22 @@
<html> <html>
<head> <head>
<title>Login</title> <title>Login</title>
<link rel="stylesheet" type="text/css" href="style.css"/> {#HTML_HEADER}
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
</head> </head>
<body> <body>
<div id="wrapper"> <div id="wrapper">
<div id="login"> <div id="login">
<div class="pad"> <div class="p-4">
<form action="/login" method="post"> <form action="/login" method="post">
<h2>AhoyDTU</h2> <div class="row"><h2>AhoyDTU</h2></div>
<input type="password" name="pwd" value="" autofocus> <div class="row">
<input type="submit" name="login" value="login" class="btn"> <div class="col-8"><input type="password" name="pwd" autofocus></div>
</form> <div class="col-4"><input type="submit" name="login" value="login" class="btn"></div>
</div>
</div> </div>
</form>
</div> </div>
<div id="footer">
<div class="left">
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2023</a>
<ul>
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>
</ul>
</div>
<div class="right">
<span id="version"></span><br/><br/>
<a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a>
</div> </div>
</div> </div>
<script type="text/javascript"> {#HTML_FOOTER}
function parse(obj) {
parseVersion(obj["general"]);
}
getAjax("/api/generic", parse);
</script>
</body> </body>
</html> </html>

92
src/web/html/serial.html

@ -2,37 +2,34 @@
<html> <html>
<head> <head>
<title>Serial Console</title> <title>Serial Console</title>
<link rel="stylesheet" type="text/css" href="style.css"/> {#HTML_HEADER}
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
</head> </head>
<body> <body>
<div class="topnav"> {#HTML_NAV}
<a href="/" class="title">AhoyDTU</a>
<a href="javascript:void(0);" class="icon" onclick="topnav()">
<span></span>
<span></span>
<span></span>
</a>
<div id="topnav" class="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<div class="serial"> <div class="row">
<textarea id="serial" cols="80" rows="20" readonly></textarea><br/> <textarea id="serial" class="mt-3" cols="80" rows="20" readonly></textarea>
connected: <span class="dot" id="connected"></span> </div>
Uptime: <span id="uptime"></span> <div class="row my-3">
<div class="col-3">connected: <span class="dot" id="connected"></span></div>
<div class="col-3 col-sm-4 my-3">Uptime: <span id="uptime"></span></div>
<div class="col-6 col-sm-4">
<input type="button" value="clear" class="btn" id="clear"/> <input type="button" value="clear" class="btn" id="clear"/>
<input type="button" value="autoscroll" class="btn" id="scroll"/> <input type="button" value="autoscroll" class="btn" id="scroll"/>
<div class="hr mt-3 mb-3"></div> </div>
</div>
<div class="hr my-3"></div>
<div class="row mb-3">
<h3>Commands</h3> <h3>Commands</h3>
<label for="iv">Select Inverter:</label> </div>
<select name="iv" id="InvID"> <div class="row mb-3">
</select> <div class="col-12 col-sm-3 my-2">Select Inverter</div>
<label for="pwrlimval">Power Limit Value</label> <div class="col-12 col-sm-9"><select name="iv" id="InvID"></select></div>
<input type="number" class="text" name="pwrlimval" maxlength="4"/> </div>
<label for="pwrlimctrl">Power Limit Command</label> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Power Limit Command</div>
<div class="col-12 col-sm-9">
<select name="pwrlimctrl"> <select name="pwrlimctrl">
<option value="" selected disabled hidden>select the unit and persistence</option> <option value="" selected disabled hidden>select the unit and persistence</option>
<option value="limit_nonpersistent_absolute">absolute non persistent [W]</option> <option value="limit_nonpersistent_absolute">absolute non persistent [W]</option>
@ -40,35 +37,31 @@
<option value="limit_persistent_absolute">absolute persistent [W]</option> <option value="limit_persistent_absolute">absolute persistent [W]</option>
<option value="limit_persistent_relative">relative persistent [%]</option> <option value="limit_persistent_relative">relative persistent [%]</option>
</select> </select>
<br/> </div>
<input type="button" value="Send Power Limit" class="btn" id="sendpwrlim"/> </div>
<div class="hr mt-3 mb-3"></div> <div class="row mb-3">
<div id="power" class="mt-3"> <div class="col-12 col-sm-3 my-2">Power Limit Value</div>
<div class="col-12 col-sm-9"><input type="number" name="pwrlimval" maxlength="4"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3"></div>
<div class="col-12 col-sm-9"><input type="button" value="Send Power Limit" class="btn" id="sendpwrlim"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Control Inverter</div>
<div class="col-12 col-sm-9" id="power">
<input type="button" value="Restart" class="btn" id="restart"/> <input type="button" value="Restart" class="btn" id="restart"/>
<input type="button" value="Turn Off" class="btn" id="power_off"/> <input type="button" value="Turn Off" class="btn" id="power_off"/>
<input type="button" value="Turn On" class="btn" id="power_on"/> <input type="button" value="Turn On" class="btn" id="power_on"/>
</div> </div>
<br/>
<p>Ctrl result: <span id="result">n/a</span></p>
</div>
</div>
</div> </div>
<div id="footer"> <div class="row mb-5">
<div class="left"> <div class="col-3 my-2">Ctrl result</div>
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2023</a> <div class="col-9 my-2"><span id="result">n/a</span></div>
<ul>
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>
</ul>
</div> </div>
<div class="right">
<ul>
<li><span id="version"></span></li>
<li><span id="esp_type"></span></li>
<li><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a></li>
</ul>
</div> </div>
</div> </div>
{#HTML_FOOTER}
<script type="text/javascript"> <script type="text/javascript">
var mAutoScroll = true; var mAutoScroll = true;
var con = document.getElementById("serial"); var con = document.getElementById("serial");
@ -87,22 +80,21 @@
parseRssi(obj); parseRssi(obj);
if(true == exeOnce) { if(true == exeOnce) {
parseVersion(obj); parseNav(obj);
parseESP(obj); parseESP(obj);
window.setInterval("getAjax('/api/generic', parseGeneric)", 10000); window.setInterval("getAjax('/api/generic', parseGeneric)", 10000);
exeOnce = false; exeOnce = false;
getAjax("/api/setup", parse); getAjax("/api/inverter/list", parse);
} }
} }
function parse(root) { function parse(root) {
parseMenu(root["menu"]);
select = document.getElementById('InvID'); select = document.getElementById('InvID');
if(null == root) return; if(null == root) return;
root = root.inverter; root = root.inverter;
for(var i = 0; i < root.inverter.length; i++) { for(var i = 0; i < root.length; i++) {
inv = root.inverter[i]; inv = root[i];
var opt = document.createElement('option'); var opt = document.createElement('option');
opt.value = inv.id; opt.value = inv.id;
opt.innerHTML = inv.name; opt.innerHTML = inv.name;

526
src/web/html/setup.html

@ -2,9 +2,7 @@
<html> <html>
<head> <head>
<title>Setup</title> <title>Setup</title>
<link rel="stylesheet" type="text/css" href="style.css"/> {#HTML_HEADER}
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
<script type="text/javascript"> <script type="text/javascript">
function load() { function load() {
for(it of document.getElementsByClassName("s_collapsible")) { for(it of document.getElementsByClassName("s_collapsible")) {
@ -18,67 +16,116 @@
</script> </script>
</head> </head>
<body onload="load()"> <body onload="load()">
<div class="topnav"> {#HTML_NAV}
<a href="/" class="title">AhoyDTU</a>
<a href="javascript:void(0);" class="icon" onclick="topnav()">
<span></span>
<span></span>
<span></span>
</a>
<div id="topnav" class="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<form method="post" action="/save"> <form method="post" action="/save">
<fieldset> <fieldset>
<button type="button" class="s_collapsible mt-4">System Config</button>
<div class="s_content">
<fieldset class="mb-2">
<legend class="des">Device Host Name</legend> <legend class="des">Device Host Name</legend>
<label for="device">Device Name</label> <div class="row mb-3">
<input type="text" name="device" class="text"/> <div class="col-12 col-sm-3">Device Name</div>
<div class="col-12 col-sm-9"><input type="text" name="device"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Dark Mode</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="darkMode"/></div>
</div>
</fieldset>
<fieldset class="mb-4">
<legend class="des">System Config</legend>
<p class="des">Pinout</p>
<div id="pinout"></div>
<p class="des">Radio (NRF24L01+)</p>
<div id="rf24"></div>
<p class="des">Radio (CMT2300A)</p>
<div id="cmt"></div>
<p class="des">Serial Console</p>
<div class="row mb-3">
<div class="col-8 col-sm-3">print inverter data</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="serEn"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Serial Debug</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="serDbg"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Interval [s]</div>
<div class="col-12 col-sm-9"><input type="text" name="serIntvl" pattern="[0-9]+" title="Invalid input"/></div>
</div>
</fieldset> </fieldset>
</div>
<button type="button" class="s_collapsible">Network</button> <button type="button" class="s_collapsible">Network</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-2">
<legend class="des">WiFi</legend> <legend class="des">WiFi</legend>
<p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p> <p>Enter the credentials to your prefered WiFi station. After rebooting the device tries to connect with this information.</p>
<label for="scanbtn">Search Networks</label>
<input type="button" name="scanbtn" id="scanbtn" class="btn" value="scan" onclick="scan()"/><br/> <div class="row mb-3">
<label for="networks">Avail Networks</label> <div class="col-12 col-sm-3 my-2">Search Networks</div>
<div class="col-12 col-sm-9"><input type="button" name="scanbtn" id="scanbtn" class="btn" value="scan" onclick="scan()"/></div>
</div>
<div class="row mb-2 mb-sm-3">
<div class="col-12 col-sm-3 my-2">Avail Networks</div>
<div class="col-12 col-sm-9">
<select name="networks" id="networks" onChange="selNet()"> <select name="networks" id="networks" onChange="selNet()">
<option value="-1" selected disabled hidden>not scanned</option> <option value="-1" selected disabled hidden>not scanned</option>
</select> </select>
<label for="ssid">SSID</label> </div>
<input type="text" name="ssid" class="text"/> </div>
<label for="pwd">Password</label> <div class="row mb-2 mb-sm-3">
<input type="password" class="text" name="pwd" value="{PWD}"/> <div class="col-12 col-sm-3 my-2">SSID</div>
<div class="col-12 col-sm-9"><input type="text" name="ssid"/></div>
</div>
<div class="row mb-2 mb-sm-3">
<div class="col-12 col-sm-3 my-2">Password</div>
<div class="col-12 col-sm-9"><input type="password" name="pwd" value="{PWD}"/></div>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset class="mb-4">
<legend class="des">Static IP (optional)</legend> <legend class="des">Static IP (optional)</legend>
<p> <p>
Leave fields blank for DHCP<br/> Leave fields blank for DHCP<br/>
The following fields are parsed in this format: 192.168.1.1 The following fields are parsed in this format: 192.168.4.1
</p> </p>
<label for="ipAddr">IP Address</label> <div class="row mb-3">
<input type="text" name="ipAddr" class="text" maxlength="15" /> <div class="col-12 col-sm-3 my-2">IP Address</div>
<label for="ipMask">Submask</label> <div class="col-12 col-sm-9"><input type="text" name="ipAddr" maxlength="15" /></div>
<input type="text" name="ipMask" class="text" maxlength="15" /> </div>
<label for="ipDns1">DNS 1</label> <div class="row mb-3">
<input type="text" name="ipDns1" class="text" maxlength="15" /> <div class="col-12 col-sm-3 my-2">Submask</div>
<label for="ipDns2">DNS 2</label> <div class="col-12 col-sm-9"><input type="text" name="ipMask" maxlength="15" /></div>
<input type="text" name="ipDns2" class="text" maxlength="15" /> </div>
<label for="ipGateway">Gateway</label> <div class="row mb-3">
<input type="text" name="ipGateway" class="text" maxlength="15" /> <div class="col-12 col-sm-3 my-2">DNS 1</div>
<div class="col-12 col-sm-9"><input type="text" name="ipDns1" maxlength="15" /></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">DNS 2</div>
<div class="col-12 col-sm-9"><input type="text" name="ipDns2" maxlength="15" /></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Gateway</div>
<div class="col-12 col-sm-9"><input type="text" name="ipGateway" maxlength="15" /></div>
</div>
</fieldset> </fieldset>
</div> </div>
<button type="button" class="s_collapsible">Protection</button> <button type="button" class="s_collapsible">Protection</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">Protection</legend> <legend class="des mx-2">Protection</legend>
<label for="adminpwd">Admin Password</label> <div class="row mb-3">
<input type="password" name="adminpwd" class="text" value="{PWD}"/> <div class="col-12 col-sm-3 mb-2 mt-2">Admin Password</div>
<input type="hidden" name="disclaimer" value="false" id="disclaimer"> <div class="col-12 col-sm-9"><input type="password" name="adminpwd" value="{PWD}"/></div>
</div>
<p>Select pages which should be protected by password</p> <p>Select pages which should be protected by password</p>
<div id="prot_mask"></div> <div id="prot_mask"></div>
</fieldset> </fieldset>
@ -86,163 +133,174 @@
<button type="button" class="s_collapsible">Inverter</button> <button type="button" class="s_collapsible">Inverter</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">Inverter</legend> <legend class="des">Inverter</legend>
<div id="inverter"></div><br/> <div id="inverter"></div>
<input type="button" id="btnAdd" class="btn" value="Add Inverter"/> <div class="row mb-2">
<p class="subdes">General</p> <div class="col-12 col-sm-3"></div>
<label for="invInterval">Interval [s]</label> <div class="col-12 col-sm-9"><input type="button" id="btnAdd" class="btn" value="Add Inverter"/></div>
<input type="text" class="text" name="invInterval" pattern="[0-9]+" title="Invalid input"/> </div>
<label for="invRetry">Max retries per Payload</label> <div class="row mb-2">
<input type="text" class="text" name="invRetry"/> <div class="col-12 col-sm-3"><p class="subdes">General</p></div>
<div class="col-12 col-sm-9"></div>
</div>
<label for="invRstMid">Reset values and YieldDay at midnight</label> <div class="row mb-3">
<input type="checkbox" class="cb" name="invRstMid"/><br/> <div class="col-12 col-sm-3 my-2">Interval [s]</div>
<label for="invRstComStop">Reset values when inverter polling stops at sunset</label> <div class="col-12 col-sm-9"><input type="text" name="invInterval" pattern="[0-9]+" title="Invalid input"/></div>
<input type="checkbox" class="cb" name="invRstComStop"/><br/> </div>
<label for="invRstNotAvail">Reset values when inverter status is 'not available'</label> <div class="row mb-3">
<input type="checkbox" class="cb" name="invRstNotAvail"/><br/> <div class="col-12 col-sm-3 my-2">Max retries per Payload</div>
<div class="col-12 col-sm-9"><input type="text" name="invRetry"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3 mb-2">Reset values and YieldDay at midnight</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstMid"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3 mb-2">Reset values when inverter polling stops at sunset</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstComStop"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Reset values when inverter status is 'not available'</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstNotAvail"/></div>
</div>
</fieldset> </fieldset>
</div> </div>
<button type="button" class="s_collapsible">NTP Server</button> <button type="button" class="s_collapsible">NTP Server</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">NTP Server</legend> <legend class="des">NTP Server</legend>
<label for="ntpAddr">NTP Server / IP</label> <div class="row mb-3">
<input type="text" class="text" name="ntpAddr"/> <div class="col-12 col-sm-3 my-2">NTP Server / IP</div>
<label for="ntpPort">NTP Port</label> <div class="col-12 col-sm-9"><input type="text" name="ntpAddr"/></div>
<input type="text" class="text" name="ntpPort"/> </div>
<label for="ntpBtn">set system time</label> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">NTP Port</div>
<div class="col-12 col-sm-9"><input type="text" name="ntpPort"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">set system time</div>
<div class="col-12 col-sm-9">
<input type="button" name="ntpBtn" id="ntpBtn" class="btn" value="from browser" onclick="setTime()"/> <input type="button" name="ntpBtn" id="ntpBtn" class="btn" value="from browser" onclick="setTime()"/>
<input type="button" name="ntpSync" id="ntpSync" class="btn" value="sync NTP" onclick="syncTime()"/> <input type="button" name="ntpSync" id="ntpSync" class="btn" value="sync NTP" onclick="syncTime()"/>
<span id="apiResultNtp"></span> <span id="apiResultNtp"></span>
</div>
</div>
</fieldset> </fieldset>
</div> </div>
<button type="button" class="s_collapsible">Sunrise & Sunset</button> <button type="button" class="s_collapsible">Sunrise & Sunset</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">Sunrise & Sunset</legend> <legend class="des">Sunrise & Sunset</legend>
<p> <p>Use a decimal separator: '.' (dot) for Latitude and Longitude</p>
Use a decimal separator: '.' (dot) for Latitude and Longitude
</p> <div class="row mb-3">
<label for="sunLat">Latitude (decimal)</label> <div class="col-12 col-sm-3 my-2">Latitude (decimal)</div>
<input type="text" class="text" name="sunLat"/> <div class="col-12 col-sm-9"><input type="text" name="sunLat"/></div>
<label for="sunLon">Longitude (decimal)</label> </div>
<input type="text" class="text" name="sunLon"/> <div class="row mb-3">
<label for="sunOffs">Offset (pre sunrise, post sunset)</label> <div class="col-12 col-sm-3 my-2">Longitude (decimal)</div>
<select name="sunOffs"></select> <div class="col-12 col-sm-9"><input type="text" name="sunLon"/></div>
<br> </div>
<label for="sunDisNightCom">Stop polling inverters during night</label> <div class="row mb-3">
<input type="checkbox" class="cb" name="sunDisNightCom"/><br/> <div class="col-12 col-sm-3 my-2">Offset (pre sunrise, post sunset)</div>
<div class="col-12 col-sm-9"><select name="sunOffs"></select></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Stop polling inverters during night</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="sunDisNightCom"/></div>
</div>
</fieldset> </fieldset>
</div> </div>
<button type="button" class="s_collapsible">MQTT</button> <button type="button" class="s_collapsible">MQTT</button>
<div class="s_content"> <div class="s_content">
<fieldset> <fieldset class="mb-4">
<legend class="des">MQTT</legend> <legend class="des">MQTT</legend>
<label for="mqttAddr">Broker / Server IP</label> <div class="row mb-3">
<input type="text" class="text" name="mqttAddr"/> <div class="col-12 col-sm-3 my-2">Broker / Server IP</div>
<label for="mqttPort">Port</label> <div class="col-12 col-sm-9"><input type="text" name="mqttAddr"/></div>
<input type="text" class="text" name="mqttPort"/> </div>
<label for="mqttUser">Username (optional)</label> <div class="row mb-3">
<input type="text" class="text" name="mqttUser"/> <div class="col-12 col-sm-3 my-2">Port</div>
<label for="mqttPwd">Password (optional)</label> <div class="col-12 col-sm-9"><input type="text" name="mqttPort"/></div>
<input type="password" class="text" name="mqttPwd"/> </div>
<label for="mqttTopic">Topic</label> <div class="row mb-3">
<input type="text" class="text" name="mqttTopic" pattern="[A-Za-z0-9./#$%&=+_-]+" title="Invalid input" /> <div class="col-12 col-sm-3 my-2">Username (optional)</div>
<div class="col-12 col-sm-9"><input type="text" name="mqttUser"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Password (optional)</div>
<div class="col-12 col-sm-9"><input type="password" name="mqttPwd"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Topic</div>
<div class="col-12 col-sm-9"><input type="text" name="mqttTopic" pattern="[A-Za-z0-9./#$%&=+_-]+" title="Invalid input" /></div>
</div>
<p class="des">Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)</p> <p class="des">Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)</p>
<label for="mqttIntvl">Interval [s]</label> <div class="row mb-3">
<input type="text" class="text" name="mqttInterval" pattern="[0-9]+" title="Invalid input" /> <div class="col-12 col-sm-3 my-2">Interval [s]</div>
<label for="mqttBtn">Discovery Config (homeassistant)</label> <div class="col-12 col-sm-9"><input type="text" name="mqttInterval" pattern="[0-9]+" title="Invalid input" /></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Discovery Config (homeassistant)</div>
<div class="col-12 col-sm-9">
<input type="button" name="mqttDiscovery" id="mqttDiscovery" class="btn" value="send" onclick="sendDiscoveryConfig()"/> <input type="button" name="mqttDiscovery" id="mqttDiscovery" class="btn" value="send" onclick="sendDiscoveryConfig()"/>
<span id="apiResultMqtt"></span> <span id="apiResultMqtt"></span>
</fieldset>
</div> </div>
</div>
<button type="button" class="s_collapsible">System Config</button>
<div class="s_content">
<fieldset>
<legend class="des">System Config</legend>
<p class="des">Pinout</p>
<div id="pinout"></div>
<p class="des">Radio (NRF24L01+)</p>
<div id="rf24"></div>
<p class="des">Radio (CMT2300A)</p>
<div id="cmt"></div>
<p class="des">Serial Console</p>
<label for="serEn">print inverter data</label>
<input type="checkbox" class="cb" name="serEn"/><br/>
<label for="serDbg">Serial Debug</label>
<input type="checkbox" class="cb" name="serDbg"/><br/>
<label for="serIntvl">Interval [s]</label>
<input type="text" class="text" name="serIntvl" pattern="[0-9]+" title="Invalid input"/>
</fieldset> </fieldset>
</div> </div>
<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> <fieldset class="mb-4">
<legend class="des">Display Config</legend> <legend class="des">Display Config</legend>
<div id="dispType"></div> <div id="dispType"></div>
<label for="logoEn">Show Logo</label> <div id="dispRot"></div>
<input type="checkbox" class="cb" name="logoEn"/><br/> <div class="row mb-3">
<label for="dispPwr">Turn off while inverters are offline</label> <div class="col-8 col-sm-3">Turn off while inverters are offline</div>
<input type="checkbox" class="cb" name="dispPwr"/><br/> <div class="col-4 col-sm-9"><input type="checkbox" name="disp_pwr"/></div>
<label for="dispPxSh">Enable pixel shifting</label> </div>
<input type="checkbox" class="cb" name="dispPxSh"/><br/> <div class="row mb-3">
<label for="disp180">Rotate 180 degree</label> <div class="col-8 col-sm-3">Enable Screensaver (pixel shifting)</div>
<input type="checkbox" class="cb" name="disp180"/><br/> <div class="col-4 col-sm-9"><input type="checkbox" name="disp_pxshift"/></div>
</div>
<label for="dispCont">Contrast</label> <div class="row mb-3">
<select name="dispCont" id="contrast"></select> <div class="col-12 col-sm-3 my-2">Contrast</div>
<div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="1" max="100"></select></div>
</div>
<p class="des">Pinout</p> <p class="des">Pinout</p>
<div id="dispPins"></div> <div id="dispPins"></div>
</fieldset> </fieldset>
</div> </div>
<div class="mt-3"> <div class="row mb-4 mt-4">
<label for="reboot">Reboot device after successful save</label> <div class="col-8 col-sm-3">Reboot device after successful save</div>
<input type="checkbox" class="cb" name="reboot" checked /> <div class="col-2 col-md-6">
<input type="checkbox" name="reboot" checked />
<input type="submit" value="save" class="btn right"/> <input type="submit" value="save" class="btn right"/>
</div> </div>
</div>
<div class="hr mb-3 mt-3"></div> <div class="hr mb-3 mt-3"></div>
<div class="mb-4"> <div class="mb-4 mt-4">
<a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a> <a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a>
<fieldset> <fieldset class="mb-4">
<legend class="des">Upload / Store JSON Settings</legend> <legend class="des">Upload / Store JSON Settings</legend>
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8"> <form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="upload"> <input type="file" name="upload">
<input type="button" class="btn" value="Upload" onclick="hide()"> <input type="button" class="btn" value="Upload" onclick="hide()">
</form> </form>
</fieldset> </fieldset>
<a class="btn" href="/get_setup" target="_blank">Download settings (JSON file)</a> (only saved values, passwords will be removed!) <a class="btn" href="/get_setup" target="_blank">Download settings (JSON file)</a><span> (only saved values, passwords will be removed!)</span>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<div id="footer"> {#HTML_FOOTER}
<div class="left">
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2023</a>
<ul>
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>
</ul>
</div>
<div class="right">
<ul>
<li><span id="version"></span></li>
<li><span id="esp_type"></span></li>
<li><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a></li>
</ul>
</div>
</div>
<script type="text/javascript"> <script type="text/javascript">
var highestId = 0; var highestId = 0;
var maxInv = 0; var maxInv = 0;
@ -372,22 +430,38 @@
document.getElementsByName(id + "Name")[0].value = ""; document.getElementsByName(id + "Name")[0].value = "";
} }
function mlCb(id, des, chk=false) {
var cb = ml("input", {type: "checkbox", id: id, name: id}, "");
if(chk)
cb.checked = true;
return ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-8 col-sm-3"}, des),
ml("div", {class: "col-4 col-sm-9"}, cb)
]);
}
function mlE(des, e) {
return ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, des),
ml("div", {class: "col-12 col-sm-9"}, e)
]);
}
function ivHtml(obj, id) { function ivHtml(obj, id) {
highestId = id + 1; highestId = id + 1;
if(highestId == maxInv) if(highestId == maxInv)
setHide("btnAdd", true); setHide("btnAdd", true);
iv = document.getElementById("inverter");
var iv = document.getElementById("inverter");
iv.appendChild(des("Inverter " + id)); iv.appendChild(des("Inverter " + id));
id = "inv" + id; id = "inv" + id;
iv.appendChild(lbl(id + "Enable", "Communication Enable"));
var en = inp(id + "Enable", null, null, ["cb"], id + "Enable", "checkbox");
en.checked = obj["enabled"];
iv.append(en, br());
iv.appendChild(lbl(id + "Addr", "Serial Number (12 digits)*"));
var addr = inp(id + "Addr", obj["serial"], 12, ["text"], null, "text", "[0-9]+", "Invalid input"); var addr = inp(id + "Addr", obj["serial"], 12, ["text"], null, "text", "[0-9]+", "Invalid input");
iv.appendChild(addr); iv.append(
mlCb(id + "Enable", "Communication Enable", obj["enabled"]),
mlE("Serial Number (12 digits)*", addr)
);
['keyup', 'change'].forEach(function(evt) { ['keyup', 'change'].forEach(function(evt) {
addr.addEventListener(evt, (e) => { addr.addEventListener(evt, (e) => {
var serial = addr.value.substring(0,4); var serial = addr.value.substring(0,4);
@ -397,9 +471,9 @@
setHide(id+"ModName"+i, true); setHide(id+"ModName"+i, true);
setHide(id+"YieldCor"+i, true); setHide(id+"YieldCor"+i, true);
} }
setHide("lbl"+id+"ModPwr", true); setHide("row"+id+"ModPwr", true);
setHide("lbl"+id+"ModName", true); setHide("row"+id+"ModName", true);
setHide("lbl"+id+"YieldCor", true); setHide("row"+id+"YieldCor", true);
if(serial.charAt(0) == 1) { if(serial.charAt(0) == 1) {
if((serial.charAt(1) == 0) || (serial.charAt(1) == 1)) { if((serial.charAt(1) == 0) || (serial.charAt(1) == 1)) {
@ -423,39 +497,44 @@
setHide(id+"ModName"+i, false); setHide(id+"ModName"+i, false);
setHide(id+"YieldCor"+i, false); setHide(id+"YieldCor"+i, false);
} }
setHide("lbl"+id+"ModPwr", false); setHide("row"+id+"ModPwr", false);
setHide("lbl"+id+"ModName", false); setHide("row"+id+"ModName", false);
setHide("lbl"+id+"YieldCor", false); setHide("row"+id+"YieldCor", false);
} }
}) })
}); });
iv.append( iv.append(mlE("Name*", inp(id + "Name", obj["name"], 16, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input")));
lbl(id + "Name", "Name*"),
inp(id + "Name", obj["name"], 32, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input")
);
for(var j of [ for(var j of [
["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4, "[0-9]+"], ["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4, "[0-9]+"],
["ModName", "ch_name", "Module Name", 16, null], ["ModName", "ch_name", "Module Name", 15, null],
["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 16, "[0-9-]+"]]) { ["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 8, "[0-9-]+"]]) {
var cl = (re.test(obj["serial"])) ? null : ["hide"];
iv.appendChild(lbl(null, j[2], cl, "lbl" + id + j[0])); var cl = (re.test(obj["serial"])) ? "" : " hide";
d = div([j[0]]);
i = 0; i = 0;
cl = (re.test(obj["serial"])) ? ["text", "sh"] : ["text", "sh", "hide"]; arrIn = [];
for(it of obj[j[1]]) { for(it of obj[j[1]]) {
d.appendChild(inp(id + j[0] + i, it, j[3], cl, id + j[0] + i, "text", j[4], "Invalid input")); arrIn.push(ml("div", {class: "col-3 "},
inp(id + j[0] + i, it, j[3], [], id + j[0] + i, "text", j[4], "Invalid input")
));
i++; i++;
} }
iv.appendChild(d);
iv.append(
ml("div", {class: "row mb-2 mb-sm-3" + cl, id: "row" + id + j[0]}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, j[2]),
ml("div", {class: "col-12 col-sm-9"},
ml("div", {class: "row"}, arrIn)
)
])
);
} }
var del = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button"); var del = inp(id+"del", "X", 0, ["btn", "btnDel"], id+"del", "button");
del.addEventListener("click", delIv); del.addEventListener("click", delIv);
iv.append( iv.append(mlE("Delete", del));
lbl(id + "lbldel", "Delete"),
del
);
} }
function ivGlob(obj) { function ivGlob(obj) {
@ -468,24 +547,22 @@
function parseSys(obj) { function parseSys(obj) {
for(var i of [["device", "device_name"], ["ssid", "ssid"]]) for(var i of [["device", "device_name"], ["ssid", "ssid"]])
document.getElementsByName(i[0])[0].value = obj[i[1]]; document.getElementsByName(i[0])[0].value = obj[i[1]];
var e = document.getElementsByName("adminpwd")[0]; document.getElementsByName("darkMode")[0].checked = obj["dark_mode"];
e = document.getElementsByName("adminpwd")[0];
if(!obj["pwd_set"]) if(!obj["pwd_set"])
e.value = ""; e.value = "";
var d = document.getElementById("prot_mask"); var d = document.getElementById("prot_mask");
var a = ["Index", "Live", "Serial / Console", "Settings", "Update", "System"] var a = ["Index", "Live", "Serial / Console", "Settings", "Update", "System"];
var el = [];
for(var i = 0; i < 6; i++) { for(var i = 0; i < 6; i++) {
var chkd = ((obj["prot_mask"] & (1 << i)) == (1 << i)); var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i));
var sp = lbl("protMask" + i, a[i]); el.push(mlCb("protMask" + i, a[i], chk))
var cb = inp("protMask" + i, null, null, ["cb"], "protMask" + i, "checkbox", null, null, chkd);
if(0 == i)
d.replaceChildren(sp, cb, br());
else
d.append(sp, cb, br());
} }
d.append(...el);
} }
function parseGeneric(obj) { function parseGeneric(obj) {
parseVersion(obj); parseNav(obj);
parseESP(obj); parseESP(obj);
parseRssi(obj); parseRssi(obj);
} }
@ -528,35 +605,30 @@
pins = [['led0', 'pinLed0'], ['led1', 'pinLed1']]; pins = [['led0', 'pinLed0'], ['led1', 'pinLed1']];
for(p of pins) { for(p of pins) {
e.append( e.append(
lbl(p[1], p[0].toUpperCase()), ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[0]]) sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[0]])
)
])
); );
} }
} }
function parseNrfRadio(obj, type) { function parseNrfRadio(obj, type) {
var e = document.getElementById("rf24"); var e = document.getElementById("rf24").append(
var en = inp("rf24Enable", null, null, ["cb"], "rf24Enable", "checkbox"); ml("div", {class: "row mb-3"}, [
en.checked = obj["en"]; ml("div", {class: "col-12 col-sm-3 my-2"}, "Power Level"),
ml("div", {class: "col-12 col-sm-9"},
e.append(
lbl("rf24Enable", "NRF24 Enable"),
en, br(),
lbl("rf24Power", "Amplifier Power Level"),
sel("rf24Power", [ sel("rf24Power", [
[0, "MIN"], [0, "MIN"],
[1, "LOW"], [1, "LOW"],
[2, "HIGH"], [2, "HIGH"],
[3, "MAX"] [3, "MAX"]
], obj["power_level"]) ], obj["power_level"])
)
])
); );
pins = [['cs', 'pinCs'], ['ce', 'pinCe'], ['irq', 'pinIrq']];
for(p of pins) {
e.append(
lbl(p[1], p[0].toUpperCase()),
sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[0]])
);
}
} }
function parseCmtRadio(obj, type) { function parseCmtRadio(obj, type) {
@ -584,35 +656,56 @@
} }
function parseDisplay(obj, type) { function parseDisplay(obj, type) {
for(var i of [["logoEn", "logo_en"], ["dispPwr", "disp_pwr"], ["dispPxSh", "px_shift"], ["disp180", "rot180"]]) for(var i of ["disp_pwr", "disp_pxshift"])
document.getElementsByName(i[0])[0].checked = obj[i[1]]; document.getElementsByName(i)[0].checked = obj[i];
var e = document.getElementById("dispPins"); var e = document.getElementById("dispPins");
pins = [['SCL / CS', 'pinDisp0'], ['SDA / DC', 'pinDisp1']]; pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst'], ['busy', 'disp_bsy']];
for(p of pins) { for(p of pins) {
e.appendChild(lbl(p[1], p[0].toUpperCase())); if(("ESP8266" == type) && p[0] == "cs")
e.appendChild(sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]])); break;
e.append(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]])
)
])
);
} }
var opts = [[0, "None"], [1, "Nokia5110"], [2, "SSD1306 0.96\""], [3, "SH1106 1.3\""]]; var opts = [[0, "None"], [2, "SSD1306 0.96\""], [3, "SH1106 1.3\""]];
if("ESP32" == type) {
opts.push([1, "Nokia5110"]);
opts.push([10, "ePaper"]);
}
document.getElementById("dispType").append( document.getElementById("dispType").append(
lbl("dispType", "Type"), ml("div", {class: "row mb-3"}, [
sel("dispType", opts, obj["disp_type"]) ml("div", {class: "col-12 col-sm-3 my-2"}, "Type"),
ml("div", {class: "col-12 col-sm-9"}, sel("disp_typ", opts, obj["disp_typ"]))
])
); );
e = document.getElementById("contrast"); opts = [[0, "0&deg;"], [2, "180&deg;"]];
for(var i = 30; i < 101; i += 2) { if("ESP32" == type) {
e.appendChild(opt(i, i, (i == obj["contrast"]))); opts.push([1, "90&deg;"]);
opts.push([3, "270&deg;"]);
} }
document.getElementById("dispRot").append(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, "Rotation"),
ml("div", {class: "col-12 col-sm-9"}, sel("disp_rot", opts, obj["disp_rot"]))
])
);
document.getElementsByName("disp_cont")[0].value = obj["disp_cont"];
} }
function parse(root) { function parse(root) {
if(null != root) { if(null != root) {
parseMenu(root["menu"]);
parseSys(root["system"]); parseSys(root["system"]);
parseGeneric(root["generic"]); parseGeneric(root["generic"]);
parseStaticIp(root["static_ip"]); parseStaticIp(root["static_ip"]);
parseIv(root["inverter"]);
parseMqtt(root["mqtt"]); parseMqtt(root["mqtt"]);
parseNtp(root["ntp"]); parseNtp(root["ntp"]);
parseSun(root["sun"]); parseSun(root["sun"]);
@ -622,6 +715,7 @@
parseCmtRadio(root["radioCmt"], root["system"]["esp_type"]); parseCmtRadio(root["radioCmt"], root["system"]["esp_type"]);
parseSerial(root["serial"]); parseSerial(root["serial"]);
parseDisplay(root["display"], root["system"]["esp_type"]); parseDisplay(root["display"], root["system"]["esp_type"]);
getAjax("/api/inverter/list", parseIv);
} }
} }
@ -644,11 +738,7 @@
e.value = s.value; e.value = s.value;
} }
hiddenInput = document.getElementById("disclaimer")
hiddenInput.value = sessionStorage.getItem("gDisclaimer");
getAjax("/api/setup", parse); getAjax("/api/setup", parse);
</script> </script>
</body> </body>
</html> </html>

485
src/web/html/style.css

@ -4,26 +4,39 @@ html, body {
padding: 0; padding: 0;
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;
background-color: var(--bg);
color: var(--fg);
} }
h2 { h2 {
padding-left: 10px; padding-left: 10px;
} }
span, li, h3, label, fieldset {
color: var(--fg);
}
fieldset, input[type=submit], .btn {
border-radius: 4px;
}
#live span {
color: var(--fg2);
}
.topnav { .topnav {
background-color: #333; background-color: var(--nav-bg);
position: fixed; position: fixed;
top: 0; top: 0;
width: 100%; width: 100%;
} }
.topnav a { .topnav a {
color: #fff; color: var(--fg2);
padding: 14px 14px; padding: 14px 14px;
text-decoration: none; text-decoration: none;
font-size: 17px; font-size: 17px;
display: block; display: block;
height: 20px;
} }
#topnav a { #topnav a {
@ -33,23 +46,26 @@ h2 {
.topnav a.icon { .topnav a.icon {
top: 0; top: 0;
left: 0; left: 0;
background: #333; background: var(--nav-bg);
display: block; display: block;
position: absolute; position: absolute;
} }
.topnav a:hover { .topnav a:hover {
background-color: #044e86 !important; background-color: var(--primary-hover) !important;
color: #000;
} }
.topnav .info { .topnav .info {
color: #fff; color: var(--fg2);
position: absolute; position: absolute;
right: 24px; right: 24px;
top: 5px; top: 5px;
} }
.topnav .mobile {
display: none;
}
svg.icon { svg.icon {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
@ -57,8 +73,24 @@ svg.icon {
padding: 5px 7px 5px 0px; padding: 5px 7px 5px 0px;
} }
.icon-info {
fill: var(--info);
}
.icon-warn {
fill: var(--warn);
}
.icon-success {
fill: var(--success);
}
.wifi {
fill: var(--fg2);
}
.title { .title {
background-color: #006ec0; background-color: var(--primary);
color: #fff !important; color: #fff !important;
padding-left: 80px !important padding-left: 80px !important
} }
@ -74,7 +106,7 @@ svg.icon {
} }
.topnav .active { .topnav .active {
background-color: #555; background-color: var(--nav-active);
} }
span.seperator { span.seperator {
@ -85,6 +117,197 @@ span.seperator {
display: block; display: block;
} }
#content {
max-width: 1140px;
}
.total-h {
background-color: var(--total-head-title);
color: var(--fg2);
}
.total-bg {
background-color: var(--total-bg);
color: var(--fg2);
}
.iv-h {
background-color: var(--iv-head-title);
color: var(--fg2);
}
.iv-bg {
background-color: var(--iv-head-bg);
color: var(--fg2);
}
.ch-h {
background-color: var(--ch-head-title);
color: var(--fg2);
}
.ch-bg {
background-color: var(--ch-head-bg);
color: var(--fg2);
}
.ts-h {
background-color: var(--ts-head);
color: var(--fg2);
}
.ts-bg {
background-color: var(--ts-bg);
color: var(--fg2);
}
.hr {
border-top: 1px solid var(--iv-head-title);
margin: 1rem 0 1rem;
}
p {
text-align: justify;
font-size: 13pt;
color: var(--fg);
}
#footer {
background-color: var(--footer-bg);
}
.row { display: flex; max-width: 100%; flex-wrap: wrap; }
.col { flex: 1 0 0%; }
.col-1, .col-2, .col-3, .col-4,
.col-5, .col-6, .col-7, .col-8,
.col-9, .col-10, .col-11, .col-12 { flex: 0 0 auto; }
.col-1 { width: 8.333333333%; }
.col-2 { width: 16.66666667%; }
.col-3 { width: 25%; }
.col-4 { width: 33.33333333%; }
.col-5 { width: 41.66666667%; }
.col-6 { width: 50%; }
.col-7 { width: 58.33333333%; }
.col-8 { width: 66.66666667%; }
.col-9 { width: 75%; }
.col-10 { width: 83.33333333%; }
.col-11 { width: 91.66666667%; }
.col-12 { width: 100%; }
.p-1 { padding: 0.25rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 1rem; }
.p-4 { padding: 1.5rem; }
.p-5 { padding: 3rem; }
.px-1 { padding: 0 0.25rem 0 0.25rem; }
.px-2 { padding: 0 0.5rem 0 0.5rem; }
.px-3 { padding: 0 1rem 0 1rem; }
.px-4 { padding: 0 1.5rem 0 1.5rem; }
.px-5 { padding: 0 3rem 0 3rem; }
.py-1 { padding: 0.25rem 0 0.25rem; }
.py-2 { padding: 0.5rem 0 0.5rem; }
.py-3 { padding: 1rem 0 1rem; }
.py-4 { padding: 1.5rem 0 1.5rem; }
.py-5 { padding: 3rem 0 3rem; }
.mx-1 { margin: 0 0.25rem 0 0.25rem; }
.mx-2 { margin: 0 0.5rem 0 0.5rem; }
.mx-3 { margin: 0 1rem 0 1rem; }
.mx-4 { margin: 0 1.5rem 0 1.5rem; }
.mx-5 { margin: 0 3rem 0 3rem; }
.my-1 { margin: 0.25rem 0 0.25rem; }
.my-2 { margin: 0.5rem 0 0.5rem; }
.my-3 { margin: 1rem 0 1rem; }
.my-4 { margin: 1.5rem 0 1.5rem; }
.my-5 { margin: 3rem 0 3rem; }
.mt-1 { margin-top: 0.25rem }
.mt-2 { margin-top: 0.5rem }
.mt-3 { margin-top: 1rem }
.mt-4 { margin-top: 1.5rem }
.mt-5 { margin-top: 3rem }
.mb-1 { margin-bottom: 0.25rem }
.mb-2 { margin-bottom: 0.5rem }
.mb-3 { margin-bottom: 1rem }
.mb-4 { margin-bottom: 1.5rem }
.mb-5 { margin-bottom: 3rem }
.fs-1 { font-size: 3.5rem; }
.fs-2 { font-size: 3rem; }
.fs-3 { font-size: 2.5rem; }
.fs-4 { font-size: 2rem; }
.fs-5 { font-size: 1.75rem; }
.fs-6 { font-size: 1.5rem; }
.fs-7 { font-size: 1.25rem; }
.fs-8 { font-size: 1rem; }
.fs-9 { font-size: 0.75rem; }
.fs-10 { font-size: 0.5rem; }
.a-r { text-align: right; }
.a-c { text-align: center; }
.row > * {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
*, ::after, ::before {
box-sizing: border-box;
}
/* sm */
@media(min-width: 768px) {
.col-sm-1 { width: 8.333333333%; }
.col-sm-2 { width: 16.66666667%; }
.col-sm-3 { width: 25%; }
.col-sm-4 { width: 33.33333333%; }
.col-sm-5 { width: 41.66666667%; }
.col-sm-6 { width: 50%; }
.col-sm-7 { width: 58.33333333%; }
.col-sm-8 { width: 66.66666667%; }
.col-sm-9 { width: 75%; }
.col-sm-10 { width: 83.33333333%; }
.col-sm-11 { width: 91.66666667%; }
.col-sm-12 { width: 100%; }
.mb-sm-1 { margin-bottom: 0.25rem }
.mb-sm-2 { margin-bottom: 0.5rem }
.mb-sm-3 { margin-bottom: 1rem }
.mb-sm-4 { margin-bottom: 1.5rem }
.mb-sm-5 { margin-bottom: 3rem }
.fs-sm-1 { font-size: 3.5rem; }
.fs-sm-2 { font-size: 3rem; }
.fs-sm-3 { font-size: 2.5rem; }
.fs-sm-4 { font-size: 2rem; }
.fs-sm-5 { font-size: 1.75rem; }
.fs-sm-6 { font-size: 1.5rem; }
.fs-sm-7 { font-size: 1.25rem; }
.fs-sm-8 { font-size: 1rem; }
}
/* md */
@media(min-width: 992px) {
.col-md-1 { width: 8.333333333%; }
.col-md-2 { width: 16.66666667%; }
.col-md-3 { width: 25%; }
.col-md-4 { width: 33.33333333%; }
.col-md-5 { width: 41.66666667%; }
.col-md-6 { width: 50%; }
.col-md-7 { width: 58.33333333%; }
.col-md-8 { width: 66.66666667%; }
.col-md-9 { width: 75%; }
.col-md-10 { width: 83.33333333%; }
.col-md-11 { width: 91.66666667%; }
.col-md-12 { width: 100%; }
}
#wrapper { #wrapper {
min-height: 100%; min-height: 100%;
} }
@ -97,7 +320,6 @@ span.seperator {
#footer { #footer {
height: 121px; height: 121px;
margin-top: -121px; margin-top: -121px;
background-color: #555;
width: 100%; width: 100%;
font-size: 13px; font-size: 13px;
} }
@ -131,7 +353,7 @@ span.seperator {
} }
.hide { .hide {
display: none; display: none !important;
} }
@media only screen and (min-width: 992px) { @media only screen and (min-width: 992px) {
@ -152,7 +374,7 @@ span.seperator {
padding-left: 24px !important; padding-left: 24px !important;
} }
.topnav .hide { .topnav .mobile {
display: block; display: block;
} }
@ -172,13 +394,6 @@ span.seperator {
} }
} }
/** old CSS below **/
p {
text-align: justify;
font-size: 13pt;
}
p.lic, p.lic a { p.lic, p.lic a {
font-size: 8pt; font-size: 8pt;
color: #999; color: #999;
@ -187,11 +402,11 @@ p.lic, p.lic a {
.des { .des {
margin-top: 20px; margin-top: 20px;
font-size: 13pt; font-size: 13pt;
color: #006ec0; color: var(--secondary);
} }
.s_active, .s_collapsible:hover { .s_active, .s_collapsible:hover {
background-color: #044e86; background-color: var(--primary-hover);
color: #fff; color: #fff;
} }
@ -201,34 +416,34 @@ p.lic, p.lic a {
} }
.s_collapsible { .s_collapsible {
background-color: #006ec0; background-color: var(--primary);
color: white; color: white;
cursor: pointer; cursor: pointer;
padding: 18px; padding: 12px;
width: 100%; width: 100%;
border: none; border: none;
text-align: left; text-align: left;
outline: none; outline: none;
font-size: 15px; font-size: 15px;
margin-bottom: 4px; margin-bottom: 5px;
} }
.subdes { .subdes {
font-size: 12pt; font-size: 12pt;
color: #006ec0; color: var(--secondary);
margin-left: 7px; margin-left: 7px;
} }
.subsubdes { .subsubdes {
font-size:12pt; font-size:12pt;
color:#006ec0; color:var(--secondary);
margin: 0 0 7px 12px; margin: 0 0 7px 12px;
} }
a:link, a:visited { a:link, a:visited {
text-decoration: none; text-decoration: none;
font-size: 13pt; font-size: 13pt;
color: #006ec0; color: var(--secondary);
} }
a:hover, a:focus { a:hover, a:focus {
@ -236,14 +451,14 @@ a:hover, a:focus {
} }
a.btn { a.btn {
background-color: #006ec0; background-color: var(--primary);
color: #fff; color: #fff;
padding: 7px 15px 7px 15px; padding: 7px 15px 7px 15px;
display: inline-block; display: inline-block;
} }
a.btn:hover { a.btn:hover {
background-color: #044e86 !important; background-color: var(--primary-hover) !important;
} }
input, select { input, select {
@ -251,11 +466,13 @@ input, select {
font-size: 13pt; font-size: 13pt;
} }
input.text, select { input[type=text], input[type=password], select, input[type=number] {
width: 70%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 10px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 4px;
background-color: var(--input-bg);
color: var(--fg);
} }
input.sh { input.sh {
@ -268,7 +485,7 @@ input.btnDel {
} }
input.btn { input.btn {
background-color: #006ec0; background-color: var(--primary);
color: #fff; color: #fff;
border: 0px; border: 0px;
padding: 7px 20px 7px 20px; padding: 7px 20px 7px 20px;
@ -299,10 +516,6 @@ pre {
white-space: pre-wrap; white-space: pre-wrap;
} }
fieldset {
margin-bottom: 15px;
}
.left { .left {
float: left; float: left;
} }
@ -311,88 +524,11 @@ fieldset {
float: right; float: right;
} }
div.ch-iv {
width: 100%;
background-color: #32b004;
display: inline-block;
margin-bottom: 15px;
padding-bottom: 20px;
overflow: auto;
}
div.ch {
width: 220px;
min-height: 350px;
background-color: #006ec0;
display: inline-block;
margin: 0 20px 10px 0px;
overflow: auto;
padding-bottom: 20px;
}
div.ch-all {
width: 100%;
background-color: #b06e04;
display: inline-block;
margin-bottom: 15px;
padding-bottom: 20px;
overflow: auto;
}
div.ch .value, div.ch .info, div.ch .head, div.ch-iv .value, div.ch-iv .info, div.ch-iv .head, div.ch-all .value, div.ch-all .info, div.ch-all .head {
color: #fff;
display: block;
width: 100%;
text-align: center;
}
.subgrp { .subgrp {
float: left; float: left;
width: 220px; width: 220px;
} }
div.ch .unit, div.ch-iv .unit, div.ch-all .unit {
font-size: 19px;
margin-left: 10px;
}
div.ch .value, div.ch-iv .value, div.ch-all .value {
margin-top: 20px;
font-size: 24px;
}
div.ch .info, div.ch-iv .info, div.ch-all .info {
margin-top: 3px;
font-size: 10px;
}
div.ch .head {
background-color: #003c80;
padding: 10px 0 10px 0;
}
div.ch-all .head {
background-color: #8e5903;
padding: 10px 0 10px 0;
}
div.ch-iv .head {
background-color: #1c6800;
padding: 10px 0 10px 0;
}
div.iv {
max-width: 960px;
margin-bottom: 40px;
}
div.ts {
font-size: 13px;
background-color: #ddd;
border-top: 7px solid #999;
padding: 7px;
}
div.ModPwr, div.ModName, div.YieldCor { div.ModPwr, div.ModName, div.YieldCor {
width:70%; width:70%;
display: inline-block; display: inline-block;
@ -443,104 +579,55 @@ div.hr {
} }
#login { #login {
width: 300px; width: 450px;
height: 200px; height: 200px;
border: 1px solid #ccc; border: 1px solid #ccc;
background-color: #eee; background-color: var(--input-bg);
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin-top: -160px; margin-top: -160px;
margin-left: -150px; margin-left: -225px;
}
#login .pad {
padding: 20px;
}
#login .pad input {
width: 100%;
padding: 7px 0 7px 0;
border: 0px;
margin-bottom: 10px;
} }
.head { .head {
background-color: #006ec0; background-color: var(--primary);
color: #fff; color: #fff;
} }
.row { display: flex; max-width: 100%; flex-wrap: wrap; }
.col { flex: 1 0 0%; }
.col-1, .col-2, .col-3, .col-4, .css-tooltip{
.col-5, .col-6, .col-7, .col-8, position: relative;
.col-9, .col-10, .col-11, .col-12 { flex: 0 0 auto; }
.col-1 { width: 8.333333333%; }
.col-2 { width: 16.66666667%; }
.col-3 { width: 25%; }
.col-4 { width: 33.33333333%; }
.col-5 { width: 41.66666667%; }
.col-6 { width: 50%; }
.col-7 { width: 58.33333333%; }
.col-8 { width: 66.66666667%; }
.col-9 { width: 75%; }
.col-10 { width: 83.33333333%; }
.col-11 { width: 91.66666667%; }
.col-12 { width: 100%; }
.p-1 { padding: 0.25rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 1rem; }
.p-4 { padding: 1.5rem; }
.p-5 { padding: 3rem; }
.mt-1 { margin-top: 0.25rem }
.mt-2 { margin-top: 0.5rem }
.mt-3 { margin-top: 1rem }
.mt-4 { margin-top: 1.5rem }
.mt-5 { margin-top: 3rem }
.mb-1 { margin-bottom: 0.25rem }
.mb-2 { margin-bottom: 0.5rem }
.mb-3 { margin-bottom: 1rem }
.mb-4 { margin-bottom: 1.5rem }
.mb-5 { margin-bottom: 3rem }
.a-r { text-align: right; }
.a-c { text-align: center; }
/* sm */
@media(min-width: 768px) {
.col-sm-1 { width: 8.333333333%; }
.col-sm-2 { width: 16.66666667%; }
.col-sm-3 { width: 25%; }
.col-sm-4 { width: 33.33333333%; }
.col-sm-5 { width: 41.66666667%; }
.col-sm-6 { width: 50%; }
.col-sm-7 { width: 58.33333333%; }
.col-sm-8 { width: 66.66666667%; }
.col-sm-9 { width: 75%; }
.col-sm-10 { width: 83.33333333%; }
.col-sm-11 { width: 91.66666667%; }
.col-sm-12 { width: 100%; }
} }
.css-tooltip:hover:after{
/* md */ content:attr(data-tooltip);
@media(min-width: 992px) { background:#000;
.col-md-1 { width: 8.333333333%; } padding:5px;
.col-md-2 { width: 16.66666667%; } border-radius:3px;
.col-md-3 { width: 25%; } display: inline-block;
.col-md-4 { width: 33.33333333%; } position: absolute;
.col-md-5 { width: 41.66666667%; } transform: translate(-50%,-100%);
.col-md-6 { width: 50%; } margin:0 auto;
.col-md-7 { width: 58.33333333%; } color:#FFF;
.col-md-8 { width: 66.66666667%; } min-width:100px;
.col-md-9 { width: 75%; } min-width:150px;
.col-md-10 { width: 83.33333333%; } top:-5px;
.col-md-11 { width: 91.66666667%; } left: 50%;
.col-md-12 { width: 100%; } text-align:center;
}
.css-tooltip:hover:before {
top:-5px;
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(0, 0, 0, 0);
border-top-color: #000;
border-width: 5px;
margin-left: -5px;
transform: translate(0,0px);
} }

35
src/web/html/system.html

@ -2,21 +2,10 @@
<html> <html>
<head> <head>
<title>System</title> <title>System</title>
<link rel="stylesheet" type="text/css" href="style.css"/> {#HTML_HEADER}
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
</head> </head>
<body> <body>
<div class="topnav"> {#HTML_NAV}
<a href="/" class="title">AhoyDTU</a>
<a href="javascript:void(0);" class="icon" onclick="topnav()">
<span></span>
<span></span>
<span></span>
</a>
<div id="topnav" class="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<pre id="stat"></pre> <pre id="stat"></pre>
@ -26,25 +15,10 @@
<div id="html" class="mt-3 mb-3"></div> <div id="html" class="mt-3 mb-3"></div>
</div> </div>
</div> </div>
<div id="footer"> {#HTML_FOOTER}
<div class="left">
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2023</a>
<ul>
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>
</ul>
</div>
<div class="right">
<ul>
<li><span id="version"></span></li>
<li><span id="esp_type"></span></li>
<li><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a></li>
</ul>
</div>
</div>
<script type="text/javascript"> <script type="text/javascript">
function parseGeneric(obj) { function parseGeneric(obj) {
parseVersion(obj); parseNav(obj);
parseESP(obj); parseESP(obj);
parseRssi(obj); parseRssi(obj);
} }
@ -123,7 +97,6 @@
function parse(obj) { function parse(obj) {
if(null != obj) { if(null != obj) {
parseMenu(obj["menu"]);
parseGeneric(obj["generic"]); parseGeneric(obj["generic"]);
if(null != obj["refresh"]) { if(null != obj["refresh"]) {

46
src/web/html/update.html

@ -2,66 +2,36 @@
<html> <html>
<head> <head>
<title>Update</title> <title>Update</title>
<link rel="stylesheet" type="text/css" href="style.css"/> {#HTML_HEADER}
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
</head> </head>
<body> <body>
<div class="topnav"> {#HTML_NAV}
<a href="/" class="title">AhoyDTU</a>
<a href="javascript:void(0);" class="icon" onclick="topnav()">
<span></span>
<span></span>
<span></span>
</a>
<div id="topnav" class="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<fieldset>
<legend class="des">Select firmware file (*.bin)</legend>
<form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8"> <form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="update"> <input type="file" name="update">
<input type="button" class="btn" value="Update" onclick="hide()"> <input type="button" class="btn" value="Update" onclick="hide()">
</form> </form>
</fieldset>
</div> </div>
</div> </div>
<div id="footer"> {#HTML_FOOTER}
<div class="left">
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2023</a>
<ul>
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>
</ul>
</div>
<div class="right">
<ul>
<li><span id="version"></span></li>
<li><span id="esp_type"></span></li>
<li><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a></li>
</ul>
</div>
</div>
<script type="text/javascript"> <script type="text/javascript">
function parseGeneric(obj) { function parseGeneric(obj) {
parseVersion(obj); parseNav(obj);
parseESP(obj); parseESP(obj);
parseRssi(obj); parseRssi(obj);
} }
function parse(obj) {
if(null != obj) {
parseMenu(obj["menu"]);
parseGeneric(obj["generic"]);
}
}
function hide() { function hide() {
document.getElementById("form").submit(); document.getElementById("form").submit();
var e = document.getElementById("content"); var e = document.getElementById("content");
e.replaceChildren(span("update started")); e.replaceChildren(span("update started"));
} }
getAjax("/api/index", parse); getAjax("/api/generic", parseGeneric);
</script> </script>
</body> </body>
</html> </html>

285
src/web/html/visualization.html

@ -2,144 +2,227 @@
<html> <html>
<head> <head>
<title>Live</title> <title>Live</title>
<link rel="stylesheet" type="text/css" href="style.css"/> {#HTML_HEADER}
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<script type="text/javascript" src="api.js"></script>
</head> </head>
<body> <body>
<div class="topnav"> {#HTML_NAV}
<a href="/" class="title">AhoyDTU</a>
<a href="javascript:void(0);" class="icon" onclick="topnav()">
<span></span>
<span></span>
<span></span>
</a>
<div id="topnav" class="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<div id="live"></div> <div id="live"></div>
<p>Every <span id="refresh"></span> seconds the values are updated</p> <p>Every <span id="refresh"></span> seconds the values are updated</p>
</div> </div>
</div> </div>
<div id="footer"> {#HTML_FOOTER}
<div class="left">
<a href="https://ahoydtu.de" target="_blank">AhoyDTU &copy 2023</a>
<ul>
<li><a href="https://discord.gg/WzhxEY62mB" target="_blank">Discord</a></li>
<li><a href="https://github.com/lumapu/ahoy" target="_blank">Github</a></li>
</ul>
</div>
<div class="right">
<ul>
<li><span id="version"></span></li>
<li><span id="esp_type"></span></li>
<li><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a></li>
</ul>
</div>
</div>
<script type="text/javascript"> <script type="text/javascript">
var exeOnce = true; var exeOnce = true;
var units, ivEn;
var mIvHtml = [];
var mNum = 0;
var names = ["Voltage", "Current", "Power", "Yield Day", "Yield Total", "Irradiation"];
var total = Array(5).fill(0);
function parseGeneric(obj) { function parseGeneric(obj) {
if(true == exeOnce){ if(true == exeOnce){
parseVersion(obj); parseNav(obj);
parseESP(obj); parseESP(obj);
} }
parseRssi(obj); parseRssi(obj);
} }
function parseIv(obj, root) { function numBig(val, unit, des) {
var ivHtml = []; return ml("div", {class: "col-6 col-sm-4 a-c"}, [
ml("div", {class: "row"},
var tDiv = div(["ch-all", "iv"]); ml("div", {class: "col"}, [
tDiv.appendChild(span("Total", ["head"])); ml("span", {class: "fs-5 fs-md-4"}, String(val)),
var total = new Array(root.ch0_fld_names.length).fill(0); ml("span", {class: "fs-6 fs-md-7 mx-1"}, unit)
if(obj.length > 1) ])),
ivHtml.push(tDiv); ml("div", {class: "row"},
ml("div", {class: "col"},
for(var iv of obj) { ml("span", {class: "fs-9 px-1"}, des)
if(iv["enabled"]) { )
main = div(["iv"]); )
var ch0 = div(["ch-iv"]); ]);
var limit = iv["power_limit_read"] + "%"; }
if(limit == "65535%")
limit = "n/a"; function numMid(val, unit, des) {
ch0.appendChild(span(iv["name"] + " Limit " + limit, ["head"])); return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [
ml("div", {class: "row"},
for(var j = 0; j < root.ch0_fld_names.length; j++) { ml("div", {class: "col"}, [
var val = Math.round(iv["ch"][0][j] * 100) / 100; ml("span", {class: "fs-6"}, String(val)),
var sub = div(["subgrp"]); ml("span", {class: "fs-8 mx-1"}, unit)
sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"])); ])),
sub.appendChild(span(root["ch0_fld_names"][j], ["info"])); ml("div", {class: "row"},
ch0.appendChild(sub); ml("div", {class: "col"},
ml("span", {class: "fs-9"}, des)
switch(j) { )
case 2: total[j] += val; break; // P_AC )
case 6: total[j] += val; break; // YieldTotal ]);
case 7: total[j] += val; break; // YieldDay }
case 8: total[j] += val; break; // P_DC
case 10: total[j] += val; break; // Q_AC function totals() {
} for(var i = 0; i < 5; i++) {
} total[i] = Math.round(total[i] * 100) / 100;
main.appendChild(ch0); }
return ml("div", {class: "row mt-3 mb-5"},
for(var i = 1; i < (iv["channels"] + 1); i++) { ml("div", {class: "col"}, [
var ch = div(["ch"]); ml("div", {class: "p-2 total-h"},
ch.appendChild(span(("" == iv["ch_names"][i]) ? ("CHANNEL " + i) : iv["ch_names"][i], ["head"])); ml("div", {class: "row"},
ml("div", {class: "col mx-2 mx-md-1"}, "TOTAL")
for(var j = 0; j < root.fld_names.length; j++) { ),
var val = Math.round(iv["ch"][i][j] * 100) / 100; ),
ch.appendChild(span(val + " " + span(root["fld_units"][j], ["unit"]).innerHTML, ["value"])); ml("div", {class: "p-2 total-bg"}, [
ch.appendChild(span(root["fld_names"][j], ["info"])); ml("div", {class: "row"}, [
} numBig(total[0], "W", "AC Power"),
main.appendChild(ch); numBig(total[1], "Wh", "Yield Day"),
} numBig(total[2], "kWh", "Yield Total")
]),
var ts = div(["ts"]); ml("div", {class: "hr"}),
ml("div", {class: "row"}, [
numMid(total[3], "W", "DC Power"),
numMid(total[4], "var", "Reactive Power")
])
])
])
);
}
function ivHead(obj) {
total[0] += obj.ch[0][2]; // P_AC
total[1] += obj.ch[0][7]; // YieldDay
total[2] += obj.ch[0][6]; // YieldTotal
total[3] += obj.ch[0][8]; // P_DC
total[4] += obj.ch[0][10]; // Q_AC
var t = span(" &deg; C");
return ml("div", {class: "row"},
ml("div", {class: "col"}, [
ml("div", {class: "p-2 iv-h"},
ml("div", {class: "row"}, [
ml("div", {class: "col mx-2 mx-md-1"}, obj.name),
ml("div", {class: "col a-c"}, "Power limit " + ((obj.power_limit_read == 65535) ? "n/a" : (obj.power_limit_read + " %"))),
ml("div", {class: "col a-r mx-2 mx-md-1"}, String(obj.ch[0][5]) + t.innerHTML)
])
),
ml("div", {class: "p-2 iv-bg"}, [
ml("div", {class: "row"},[
numBig(obj.ch[0][2], "W", "AC Power"),
numBig(obj.ch[0][7], "Wh", "Yield Day"),
numBig(obj.ch[0][6], "kWh", "Yield Total")
]),
ml("div", {class: "hr"}),
ml("div", {class: "row mt-2"},[
numMid(obj.ch[0][8], "W", "DC Power"),
numMid(obj.ch[0][0], "V", "Voltage"),
numMid(obj.ch[0][1], "A", "Current"),
numMid(obj.ch[0][3], "Hz", "Frequency"),
numMid(obj.ch[0][9], "%", "Efficiency"),
numMid(obj.ch[0][10], "VAr", "Reactive Power"),
numMid(obj.ch[0][4], "", "Power Factor")
])
])
])
);
}
function numCh(val, unit, des) {
return ml("div", {class: "col-12 col-sm-6 col-md-12 mb-2"}, [
ml("div", {class: "row"},
ml("div", {class: "col"}, [
ml("span", {class: "fs-6 fs-md-7"}, String(val)),
ml("span", {class: "fs-8 mx-2"}, unit)
])),
ml("div", {class: "row"},
ml("div", {class: "col"},
ml("span", {class: "fs-9"}, des)
)
)
]);
}
function ch(name, vals) {
return ml("div", {class: "col-6 col-md-3 mt-2"}, [
ml("div", {class: "ch-h p-2 a-c"}, name),
ml("div", {class: "p-2 ch-bg"}, [
ml("div", {class: "row"}, [
numCh(vals[2], units[2], "Power"),
numCh(vals[5], units[5], "Irradiation"),
numCh(vals[3], units[3], "Yield Day"),
numCh(vals[4], units[4], "Yield Total"),
numCh(vals[0], units[0], "Voltage"),
numCh(vals[1], units[1], "Current")
])
])
]);
}
function tsInfo(ts) {
var ageInfo = "Last received data requested at: "; var ageInfo = "Last received data requested at: ";
if(iv["ts_last_success"] > 0) { if(ts > 0) {
var date = new Date(iv["ts_last_success"] * 1000); var date = new Date(ts * 1000);
ageInfo += date.toLocaleString('de-DE'); ageInfo += date.toLocaleString('de-DE');
} }
else else
ageInfo += "nothing received"; ageInfo += "nothing received";
ts.innerHTML = ageInfo; return ml("div", {class: "mb-5"}, [
ml("div", {class: "row p-1 ts-h mx-2"},
main.appendChild(ts); ml("div", {class: "col"}, "")
ivHtml.push(main); ),
ml("div", {class: "row p-2 ts-bg mx-2"},
ml("div", {class: "col mx-2"}, ageInfo)
)
]);
} }
function parseIv(obj) {
mNum++;
var chn = [];
for(var i = 1; i < obj.ch.length; i++) {
var name = obj.ch_name[i];
if(name.length == 0)
name = "CHANNEL " + i;
chn.push(ch(name, obj.ch[i]));
} }
mIvHtml.push(
ml("div", {}, [
ivHead(obj),
ml("div", {class: "row mb-2"}, chn),
tsInfo(obj.ts_last_success)
])
);
// total var last = true;
if(obj.length > 1) { for(var i = obj.id + 1; i < ivEn.length; i++) {
for(var j = 0; j < root.ch0_fld_names.length; j++) { if((i != ivEn.length) && ivEn[i]) {
var val = Math.round(total[j] * 100) / 100; last = false;
if((j == 2) || (j == 6) || (j == 7) || (j == 8) || (j == 10)) { getAjax("/api/inverter/id/" + i, parseIv);
var sub = div(["subgrp"]); break;
sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"]));
sub.appendChild(span(root["ch0_fld_names"][j], ["info"]));
tDiv.appendChild(sub);
} }
} }
if(last) {
if(mNum > 1)
mIvHtml.unshift(totals());
document.getElementById("live").replaceChildren(...mIvHtml);
} }
document.getElementById("live").replaceChildren(...ivHtml);
} }
function parse(obj) { function parse(obj) {
if(null != obj) { if(null != obj) {
if(true == exeOnce)
parseMenu(obj["menu"]);
parseGeneric(obj["generic"]); parseGeneric(obj["generic"]);
parseIv(obj["inverter"], obj); units = Object.assign({}, obj["fld_units"]);
document.getElementById("refresh").innerHTML = obj["refresh_interval"]; ivEn = Object.values(Object.assign({}, obj["iv"]));
mIvHtml = [];
mNum = 0;
total.fill(0);
for(var i = 0; i < obj.iv.length; i++) {
if(obj.iv[i])
getAjax("/api/inverter/id/" + i, parseIv);
break;
}
document.getElementById("refresh").innerHTML = obj["refresh"];
if(true == exeOnce) { if(true == exeOnce) {
window.setInterval("getAjax('/api/live', parse)", obj["refresh_interval"] * 1000); window.setInterval("getAjax('/api/live', parse)", obj["refresh"] * 1000);
exeOnce = false; exeOnce = false;
} }
} }

141
src/web/web.h

@ -13,23 +13,22 @@
#else #else
#include "ESPAsyncTCP.h" #include "ESPAsyncTCP.h"
#endif #endif
#include "ESPAsyncWebServer.h"
#include "../appInterface.h" #include "../appInterface.h"
#include "../hm/hmSystem.h" #include "../hm/hmSystem.h"
#include "../utils/helper.h" #include "../utils/helper.h"
#include "ESPAsyncWebServer.h"
#include "html/h/index_html.h"
#include "html/h/login_html.h"
#include "html/h/style_css.h"
#include "html/h/api_js.h" #include "html/h/api_js.h"
#include "html/h/colorBright_css.h"
#include "html/h/colorDark_css.h"
#include "html/h/favicon_ico.h" #include "html/h/favicon_ico.h"
#include "html/h/setup_html.h" #include "html/h/index_html.h"
#include "html/h/visualization_html.h" #include "html/h/login_html.h"
#include "html/h/update_html.h"
#include "html/h/serial_html.h" #include "html/h/serial_html.h"
#include "html/h/setup_html.h"
#include "html/h/style_css.h"
#include "html/h/system_html.h" #include "html/h/system_html.h"
#include "html/h/update_html.h"
#include "html/h/visualization_html.h"
#define WEB_SERIAL_BUF_SIZE 2048 #define WEB_SERIAL_BUF_SIZE 2048
@ -57,6 +56,7 @@ class Web {
mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1)); mWeb.on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1));
mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1)); mWeb.on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1));
mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1)); mWeb.on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1));
mWeb.on("/colors.css", HTTP_GET, std::bind(&Web::onColor, this, std::placeholders::_1));
mWeb.on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1)); mWeb.on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1));
mWeb.on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1)); mWeb.on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1));
mWeb.on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1)); mWeb.on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1));
@ -176,8 +176,7 @@ class Web {
if (!mApp->readSettings("tmp.json")) { if (!mApp->readSettings("tmp.json")) {
mUploadFail = true; mUploadFail = true;
DPRINTLN(DBG_ERROR, F("upload JSON error!")); DPRINTLN(DBG_ERROR, F("upload JSON error!"));
} } else
else
mApp->saveSettings(); mApp->saveSettings();
} }
DPRINTLN(DBG_INFO, F("upload finished!")); DPRINTLN(DBG_INFO, F("upload finished!"));
@ -195,8 +194,7 @@ class Web {
strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9); strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9);
mSerialBufFill += 9; mSerialBufFill += 9;
} }
} } else {
else {
mSerialBufFill = 0; mSerialBufFill = 0;
mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis()); mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis());
return; return;
@ -211,25 +209,37 @@ class Web {
if ((length + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) { if ((length + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length); strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length);
mSerialBufFill += length; mSerialBufFill += length;
} } else {
else {
mSerialBufFill = 0; mSerialBufFill = 0;
mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis()); mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis());
} }
} }
private: private:
void checkRedirect(AsyncWebServerRequest *request) {
if ((mConfig->sys.protectionMask & PROT_MASK_INDEX) != PROT_MASK_INDEX)
request->redirect(F("/index"));
else if ((mConfig->sys.protectionMask & PROT_MASK_LIVE) != PROT_MASK_LIVE)
request->redirect(F("/live"));
else if ((mConfig->sys.protectionMask & PROT_MASK_SERIAL) != PROT_MASK_SERIAL)
request->redirect(F("/serial"));
else if ((mConfig->sys.protectionMask & PROT_MASK_SYSTEM) != PROT_MASK_SYSTEM)
request->redirect(F("/system"));
else
request->redirect(F("/login"));
}
void onUpdate(AsyncWebServerRequest *request) { void onUpdate(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onUpdate")); DPRINTLN(DBG_VERBOSE, F("onUpdate"));
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) { if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) {
if (mProtected) { if (mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), update_html, update_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), update_html, update_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -244,10 +254,9 @@ class Web {
html += "failed"; html += "failed";
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>"); html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html); AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html);
response->addHeader("Connection", "close"); response->addHeader("Connection", "close");
request->send(response); request->send(response);
//if(reboot)
mApp->setRebootFlag(); mApp->setRebootFlag();
} }
@ -261,10 +270,9 @@ class Web {
html += "failed"; html += "failed";
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>"); html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html); AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), html);
response->addHeader("Connection", "close"); response->addHeader("Connection", "close");
request->send(response); request->send(response);
//if(reboot)
mApp->setRebootFlag(); mApp->setRebootFlag();
} }
@ -284,12 +292,12 @@ class Web {
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) { if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) {
if (mProtected) { if (mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), index_html, index_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), index_html, index_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -304,7 +312,7 @@ class Web {
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), login_html, login_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), login_html, login_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -313,13 +321,24 @@ class Web {
DPRINTLN(DBG_VERBOSE, F("onLogout")); DPRINTLN(DBG_VERBOSE, F("onLogout"));
if (mProtected) { if (mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
mProtected = true; mProtected = true;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void onColor(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onColor"));
AsyncWebServerResponse *response;
if (mConfig->sys.darkMode)
response = request->beginResponse_P(200, F("text/css"), colorDark_css, colorDark_css_len);
else
response = request->beginResponse_P(200, F("text/css"), colorBright_css, colorBright_css_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -349,21 +368,21 @@ class Web {
void showNotFound(AsyncWebServerRequest *request) { void showNotFound(AsyncWebServerRequest *request) {
if (mProtected) if (mProtected)
request->redirect("/login"); checkRedirect(request);
else else
request->redirect("/setup"); request->redirect("/setup");
} }
void onReboot(AsyncWebServerRequest *request) { void onReboot(AsyncWebServerRequest *request) {
mApp->setRebootFlag(); mApp->setRebootFlag();
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
void showErase(AsyncWebServerRequest *request) { void showErase(AsyncWebServerRequest *request) {
if (mProtected) { if (mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
@ -374,7 +393,7 @@ class Web {
void showFactoryRst(AsyncWebServerRequest *request) { void showFactoryRst(AsyncWebServerRequest *request) {
if (mProtected) { if (mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
@ -388,18 +407,16 @@ class Web {
content = F("factory reset: success\n\nrebooting ... "); content = F("factory reset: success\n\nrebooting ... ");
else else
content = F("factory reset: failed\n\nrebooting ... "); content = F("factory reset: failed\n\nrebooting ... ");
} } else {
else {
content = F("factory reset: aborted"); content = F("factory reset: aborted");
refresh = 3; refresh = 3;
} }
} } else {
else {
content = F("<h1>Factory Reset</h1>" content = F("<h1>Factory Reset</h1>"
"<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>"); "<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>");
refresh = 120; refresh = 120;
} }
request->send(200, F("text/html"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>")); request->send(200, F("text/html; charset=UTF-8"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>"));
if (refresh == 10) { if (refresh == 10) {
delay(1000); delay(1000);
ESP.restart(); ESP.restart();
@ -411,12 +428,12 @@ class Web {
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) { if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) {
if (mProtected) { if (mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), setup_html, setup_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), setup_html, setup_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -425,7 +442,7 @@ class Web {
DPRINTLN(DBG_VERBOSE, F("showSave")); DPRINTLN(DBG_VERBOSE, F("showSave"));
if (mProtected) { if (mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
@ -441,6 +458,7 @@ class Web {
request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN); request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN);
if (request->arg("device") != "") if (request->arg("device") != "")
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN); request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
mConfig->sys.darkMode = (request->arg("darkMode") == "on");
// protection // protection
if (request->arg("adminpwd") != "{PWD}") { if (request->arg("adminpwd") != "{PWD}") {
@ -465,7 +483,6 @@ class Web {
request->arg("ipGateway").toCharArray(buf, 20); request->arg("ipGateway").toCharArray(buf, 20);
ah::ip2Arr(mConfig->sys.ip.gateway, buf); ah::ip2Arr(mConfig->sys.ip.gateway, buf);
// inverter // inverter
Inverter<> *iv; Inverter<> *iv;
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
@ -551,8 +568,7 @@ class Web {
String addr = request->arg("mqttAddr"); String addr = request->arg("mqttAddr");
addr.trim(); addr.trim();
addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN);
} } else
else
mConfig->mqtt.broker[0] = '\0'; mConfig->mqtt.broker[0] = '\0';
request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN); request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN);
if (request->arg("mqttPwd") != "{PWD}") if (request->arg("mqttPwd") != "{PWD}")
@ -572,22 +588,24 @@ class Web {
} }
// display // display
mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("dispPwr") == "on"); mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on");
mConfig->plugin.display.logoEn = (request->arg("logoEn") == "on"); mConfig->plugin.display.pxShift = (request->arg("disp_pxshift") == "on");
mConfig->plugin.display.pxShift = (request->arg("dispPxSh") == "on"); mConfig->plugin.display.rot = request->arg("disp_rot").toInt();
mConfig->plugin.display.rot180 = (request->arg("disp180") == "on"); mConfig->plugin.display.type = request->arg("disp_typ").toInt();
mConfig->plugin.display.type = request->arg("dispType").toInt(); mConfig->plugin.display.contrast = request->arg("disp_cont").toInt();
mConfig->plugin.display.contrast = request->arg("dispCont").toInt(); mConfig->plugin.display.disp_data = request->arg("disp_data").toInt();
mConfig->plugin.display.pin0 = request->arg("pinDisp0").toInt(); mConfig->plugin.display.disp_clk = request->arg("disp_clk").toInt();
mConfig->plugin.display.pin1 = request->arg("pinDisp1").toInt(); mConfig->plugin.display.disp_cs = request->arg("disp_cs").toInt();
mConfig->plugin.display.disp_reset = request->arg("disp_rst").toInt();
mConfig->plugin.display.disp_busy = request->arg("disp_bsy").toInt();
mConfig->plugin.display.disp_dc = request->arg("disp_dc").toInt();
mApp->saveSettings(); mApp->saveSettings();
if (request->arg("reboot") == "on") if (request->arg("reboot") == "on")
onReboot(request); onReboot(request);
else { else {
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -598,13 +616,15 @@ class Web {
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) { if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) {
if (mProtected) { if (mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), visualization_html, visualization_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), visualization_html, visualization_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
response->addHeader(F("content-type"), "text/html; charset=UTF-8");
request->send(response); request->send(response);
} }
@ -675,7 +695,7 @@ class Web {
void onDebug(AsyncWebServerRequest *request) { void onDebug(AsyncWebServerRequest *request) {
mApp->getSchedulerNames(); mApp->getSchedulerNames();
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), "ok"); AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok");
request->send(response); request->send(response);
} }
@ -684,12 +704,12 @@ class Web {
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) { if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) {
if (mProtected) { if (mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), serial_html, serial_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), serial_html, serial_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -699,12 +719,12 @@ class Web {
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) { if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) {
if (mProtected) { if (mProtected) {
request->redirect("/login"); checkRedirect(request);
return; return;
} }
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
@ -853,8 +873,7 @@ class Web {
// TODO: find the right one channel with the alarm id // TODO: find the right one channel with the alarm id
alarmChannelId = 0; alarmChannelId = 0;
// printf("AlarmData Length %d\n",rec->length); // printf("AlarmData Length %d\n",rec->length);
if (alarmChannelId < rec->length) if (alarmChannelId < rec->length) {
{
//uint8_t channel = rec->assign[alarmChannelId].ch; //uint8_t channel = rec->assign[alarmChannelId].ch;
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec));
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str()); snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str());

BIN
tools/cases/EKD_ESPNRF_Case/EKDESPNRFCase.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
tools/cases/EKD_ESPNRF_Case/EKD_ESPNRF_Case_Body.3mf

Binary file not shown.

BIN
tools/cases/EKD_ESPNRF_Case/EKD_ESPNRF_Case_Lid.3mf

Binary file not shown.

30
tools/cases/EKD_ESPNRF_Case/Readme.md

@ -0,0 +1,30 @@
# EKD ESPNRF Case
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/10756851/221722400-eefc8790-6283-4c00-a82b-e2699cae72d6.jpg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/10756851/221722400-eefc8790-6283-4c00-a82b-e2699cae72d6.jpg">
<img alt="EKD ESPNRF Case" src="https://user-images.githubusercontent.com/10756851/221722400-eefc8790-6283-4c00-a82b-e2699cae72d6.jpg">
</picture>
### Print Details:
- Print with 0.2 mm Layers
- use 100% infill
- no supports needed
### Things needed:
- 3D Printer
- Wemos D1 Mini (format style)
- NRF24L01+ Board
- ~ 15cm wire
- Soldering Iron + Solder
- Suction pump to free the NRF Board from the pins.
(Solder wick works too but i do not recommend =)
- If you want to go for a wall mounted device, add some screws.
Unsolder the Pins from the NRF Board and use short wires instead. I went this way to keep the design as flat as possible.
<picture>
<img alt="EKD ESPNRF Case" src="https://user-images.githubusercontent.com/10756851/221722732-1ae9162c-ef77-492e-babf-075045b81f69.png">
</picture>
If you got questions or need help feel free to ask on discord.
or find me on github.com/subdancer
Cheers.

73
tools/rpi/README.md

@ -80,12 +80,79 @@ python3 getting_started.py # to test and see whether RF24 class can be loaded as
If there are no error messages on the last step, then the NRF24 Wrapper has been installed successfully. If there are no error messages on the last step, then the NRF24 Wrapper has been installed successfully.
Building RF24 Wrapper for Debian 11 (bullseye) 64 bit operating system
----------------------------------------------------------------------
The description above does not work on Debian 11 (bullseye) 64 bit operating system.
Please check first, if you have Debian 11 (bullseye) 64 bit operating system installed:
- `uname -a` search for aarch64
- `lsb_release -d`
- `cat /etc/debian_version`
There are 2 possible solutions to install the RF24 wrapper:
**__1. Solution:__**
```code
sudo apt install cmake git python3-dev libboost-python-dev python3-pip python3-rpi.gpio
sudo ln -s $(ls /usr/lib/$(ls /usr/lib/gcc | \
head -1)/libboost_python3*.so | \
tail -1) /usr/lib/$(ls /usr/lib/gcc | \
head -1)/libboost_python3.so
git clone https://github.com/nRF24/RF24.git
cd RF24
rm -rf build Makefile.inc
./configure --driver=SPIDEV
```
> _edit `Makefile.inc` with your prefered editor e.g. nano or vi_
>
> old:
>```code
> CPUFLAGS=-marm -march=armv6zk -mtune=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard
> CFLAGS=-marm -march=armv6zk -mtune=arm1176jzf-s -mfpu=vfp -mfloat-abi=hard -Ofast -Wall -pthread
>```
> new:
>```code
> CPUFLAGS=
> CFLAGS=-Ofast -Wall -pthread
>```
_continue now_
```code
make
sudo make install
cd pyRF24
rm -r ./build/ ./dist/ ./RF24.egg-info/ ./__pycache__/ #just to make sure there is no old stuff
python3 -m pip install --upgrade pip
python3 -m pip install .
python3 -m pip list #watch for RF24 module - if its there its installed
```
**__2. Solution:__**
```code
sudo apt install git python3-dev libboost-python-dev python3-pip python3-rpi.gpio
git clone --recurse-submodules https://github.com/nRF24/pyRF24.git
cd pyRF24
python3 -m pip install . -v # this step takes about 5 minutes on my RPI-4 !
```
If you have problems with your radio module from ahoi, e.g.: cannot interpret received data,
please try to reduce the speed of your radio module!
Add the following parameter to your ahoy.yml configuration file in "nrf" section:
`spispeed: 600000` (0.6 MHz)
Required python modules Required python modules
----------------------- -----------------------
Some modules are not installed by default on a RaspberryPi, therefore add them manually: Some modules are not installed by default on a RaspberryPi, therefore add them manually:
``` ```code
pip install crcmod pyyaml paho-mqtt SunTimes pip install crcmod pyyaml paho-mqtt SunTimes
``` ```
@ -112,7 +179,7 @@ Python parameters
The application describes itself The application describes itself
``` ```code
python3 -m hoymiles --help python3 -m hoymiles --help
usage: hoymiles [-h] -c [CONFIG_FILE] [--log-transactions] [--verbose] usage: hoymiles [-h] -c [CONFIG_FILE] [--log-transactions] [--verbose]
@ -180,7 +247,7 @@ Todo
- Ability to talk to multiple inverters - Ability to talk to multiple inverters
- MQTT gateway - MQTT gateway
- understand channel hopping - understand channel hopping
- configurable polling interval - ~~configurable polling interval~~ done: interval ist configurable in ahoy.yml
- commands - commands
- picture of setup! - picture of setup!
- python module - python module

6
tools/rpi/ahoy.service

@ -6,11 +6,9 @@
# WorkingDirectory (absolute path to your private ahoy dir) # WorkingDirectory (absolute path to your private ahoy dir)
# To change other config parameter, please consult systemd documentation # To change other config parameter, please consult systemd documentation
# #
# To activate this service, create a link, enable and start the ahoy.service # To activate this service, enable and start ahoy.service
# $ mkdir -p $HOME/.config/systemd/user # $ systemctl --user enable $(pwd)/ahoy/tools/rpi/ahoy.service
# $ ln -sf $(pwd)/ahoy/tools/rpi/ahoy.service -t $HOME/.config/systemd/user
# $ systemctl --user status ahoy # $ systemctl --user status ahoy
# $ systemctl --user enable ahoy
# $ systemctl --user start ahoy # $ systemctl --user start ahoy
# $ systemctl --user status ahoy # $ systemctl --user status ahoy
# #

13
tools/rpi/ahoy.yml.example

@ -31,7 +31,7 @@ ahoy:
QoS: 0 QoS: 0
Retain: True Retain: True
last_will: last_will:
topic: Appelweg_PV/114181807700 # defaults to 'hoymiles/{serial}' topic: my_DTU_name # Name of DTU - default: hoymiles/{DTU-serial}
payload: "LAST-WILL-MESSAGE: Please check my HOST and Process!" payload: "LAST-WILL-MESSAGE: Please check my HOST and Process!"
# Influx2 output # Influx2 output
@ -96,6 +96,7 @@ ahoy:
dtu: dtu:
serial: 99978563001 serial: 99978563001
name: my_DTU_name
inverters: inverters:
- name: 'balkon' - name: 'balkon'
@ -103,14 +104,14 @@ ahoy:
txpower: 'low' # txpower per inverter (min,low,high,max) txpower: 'low' # txpower per inverter (min,low,high,max)
mqtt: mqtt:
send_raw_enabled: false # allow inject debug data via mqtt send_raw_enabled: false # allow inject debug data via mqtt
topic: 'hoymiles/114172221234' # defaults to '{inverter-name}/{serial}' topic: 'hoymiles/114172220003' # defaults to '{inverter-name}/{serial}'
strings: # list all available strings strings: # list all available strings
- s_name: 'String 1 left' # String 1 name - s_name: 'String 1 left' # String 1 name
s_maxpower: 395 # String 1 max power in Wp s_maxpower: 395 # String 1 max power in inverter
- s_name: 'String 2 right' # String 2 name - s_name: 'String 2 right' # String 2 name
s_maxpower: 400 # String 2 max power in Wp s_maxpower: 400 # String 2 max power in inverter
- s_name: 'String 3 up' # String 3 name - s_name: 'String 3 up' # String 3 name
s_maxpower: 405 # String 3 max power in Wp s_maxpower: 405 # String 3 max power in inverter
- s_name: 'String 4 down' # String 4 name - s_name: 'String 4 down' # String 4 name
s_maxpower: 410 # String 4 max power in Wp s_maxpower: 410 # String 4 max power in inverter

39
tools/rpi/hoymiles/__init__.py

@ -11,8 +11,28 @@ import re
from datetime import datetime from datetime import datetime
import logging import logging
import crcmod import crcmod
from RF24 import RF24, RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
from .decoders import * from .decoders import *
from os import environ
try:
# OSI Layer 2 driver for nRF24L01 on Arduino & Raspberry Pi/Linux Devices
# https://github.com/nRF24/RF24.git
from RF24 import RF24, RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
if environ.get('TERM') is not None:
print('Using python Module: RF24')
except ModuleNotFoundError as e:
if environ.get('TERM') is not None:
print(f'{e} - try to use module: RF24')
try:
# Repo for pyRF24 package
# https://github.com/nRF24/pyRF24.git
from pyrf24 import RF24, RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH, RF24_PA_MAX, RF24_250KBPS, RF24_CRC_DISABLED, RF24_CRC_8, RF24_CRC_16
if environ.get('TERM') is not None:
print(f'{e} - Using python Module: pyrf24')
except ModuleNotFoundError as e:
if environ.get('TERM') is not None:
print(f'{e} - exit')
exit()
f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus') f_crc_m = crcmod.predefined.mkPredefinedCrcFun('modbus')
f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0) f_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
@ -158,14 +178,25 @@ class ResponseDecoder(ResponseDecoderFactory):
model = self.inverter_model model = self.inverter_model
command = self.request_command command = self.request_command
c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f") if HOYMILES_DEBUG_LOGGING:
logging.info(f'{c_datetime} model_decoder: {model}Decode{command.upper()}') if command.upper() == '01':
model_desc = "Firmware version / date"
elif command.upper() == '02':
model_desc = "Inverter generic events log"
elif command.upper() == '0B':
model_desc = "mirco-inverters status data"
elif command.upper() == '0C':
model_desc = "mirco-inverters status data"
elif command.upper() == '11':
model_desc = "Inverter generic events log"
elif command.upper() == '12':
model_desc = "Inverter major events log"
logging.info(f'model_decoder: {model}Decode{command.upper()} - {model_desc}')
model_decoders = __import__('hoymiles.decoders') model_decoders = __import__('hoymiles.decoders')
if hasattr(model_decoders, f'{model}Decode{command.upper()}'): if hasattr(model_decoders, f'{model}Decode{command.upper()}'):
device = getattr(model_decoders, f'{model}Decode{command.upper()}') device = getattr(model_decoders, f'{model}Decode{command.upper()}')
else: else:
if HOYMILES_DEBUG_LOGGING:
device = getattr(model_decoders, 'DebugDecodeAny') device = getattr(model_decoders, 'DebugDecodeAny')
return device(self.response, return device(self.response,

58
tools/rpi/hoymiles/__main__.py

@ -33,6 +33,12 @@ def signal_handler(sig_num, frame):
if mqtt_client: if mqtt_client:
mqtt_client.disco() mqtt_client.disco()
if influx_client:
influx_client.disco()
if volkszaehler_client:
volkszaehler_client.disco()
sys.exit(0) sys.exit(0)
signal(SIGINT, signal_handler) # Interrupt from keyboard (CTRL + C) signal(SIGINT, signal_handler) # Interrupt from keyboard (CTRL + C)
@ -75,7 +81,6 @@ class SunsetHandler:
else: else:
logging.info('Sunset disabled.') logging.info('Sunset disabled.')
def checkWaitForSunrise(self): def checkWaitForSunrise(self):
if not self.suntimes: if not self.suntimes:
return return
@ -94,6 +99,23 @@ class SunsetHandler:
time.sleep(time_to_sleep) time.sleep(time_to_sleep)
logging.info (f'Woke up...') logging.info (f'Woke up...')
def sun_status2mqtt(self, dtu_ser, dtu_name):
if not mqtt_client:
return
local_sunrise = self.suntimes.riselocal(datetime.now()).strftime("%d.%m.%YT%H:%M")
local_sunset = self.suntimes.setlocal(datetime.now()).strftime("%d.%m.%YT%H:%M")
local_zone = self.suntimes.setlocal(datetime.now()).tzinfo._key
if self.suntimes:
mqtt_client.info2mqtt({'topic' : f'{dtu_name}/{dtu_ser}'}, \
{'dis_night_comm' : 'True', \
'local_sunrise' : local_sunrise, \
'local_sunset' : local_sunset,
'local_zone' : local_zone})
else:
mqtt_client.sun_info2mqtt({'sun_topic': f'{dtu_name}/{dtu_ser}'}, \
{'dis_night_comm': 'False'})
def main_loop(ahoy_config): def main_loop(ahoy_config):
"""Main loop""" """Main loop"""
inverters = [ inverters = [
@ -101,7 +123,9 @@ def main_loop(ahoy_config):
if not inverter.get('disabled', False)] if not inverter.get('disabled', False)]
sunset = SunsetHandler(ahoy_config.get('sunset')) sunset = SunsetHandler(ahoy_config.get('sunset'))
dtu_ser = ahoy_config.get('dtu', {}).get('serial') dtu_ser = ahoy_config.get('dtu', {}).get('serial', None)
dtu_name = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu')
sunset.sun_status2mqtt(dtu_ser, dtu_name)
loop_interval = ahoy_config.get('interval', 1) loop_interval = ahoy_config.get('interval', 1)
try: try:
@ -112,6 +136,11 @@ def main_loop(ahoy_config):
t_loop_start = time.time() t_loop_start = time.time()
for inverter in inverters: for inverter in inverters:
if not 'name' in inverter:
inverter['name'] = 'hoymiles'
if not 'serial' in inverter:
logging.error("No inverter serial number found in ahoy.yml - exit")
sys.exit(999)
if hoymiles.HOYMILES_DEBUG_LOGGING: if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}') logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}')
poll_inverter(inverter, dtu_ser, do_init, 3) poll_inverter(inverter, dtu_ser, do_init, 3)
@ -122,8 +151,6 @@ def main_loop(ahoy_config):
if time_to_sleep > 0: if time_to_sleep > 0:
time.sleep(time_to_sleep) time.sleep(time_to_sleep)
except KeyboardInterrupt:
sys.exit()
except Exception as e: except Exception as e:
logging.fatal('Exception catched: %s' % e) logging.fatal('Exception catched: %s' % e)
logging.fatal(traceback.print_exc()) logging.fatal(traceback.print_exc())
@ -174,13 +201,14 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
response = com.get_payload() response = com.get_payload()
payload_ttl = 0 payload_ttl = 0
except Exception as e_all: except Exception as e_all:
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
logging.error(f'Error while retrieving data: {e_all}') logging.error(f'Error while retrieving data: {e_all}')
pass pass
# Handle the response data if any # Handle the response data if any
if response: if response:
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
c_datetime = datetime.now() c_datetime = datetime.now()
if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response)) logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
# prepare decoder object # prepare decoder object
@ -195,7 +223,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
# get decoder object # get decoder object
result = decoder.decode() result = decoder.decode()
if hoymiles.HOYMILES_DEBUG_LOGGING: if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.info(f'{c_datetime} Decoded: {result.__dict__()}') logging.info(f'Decoded: {result.__dict__()}')
# check decoder object for output # check decoder object for output
if isinstance(result, hoymiles.decoders.StatusResponse): if isinstance(result, hoymiles.decoders.StatusResponse):
@ -281,7 +309,13 @@ def init_logging(ahoy_config):
lvl = logging.WARNING lvl = logging.WARNING
elif level == 'ERROR': elif level == 'ERROR':
lvl = logging.ERROR lvl = logging.ERROR
elif level == 'FATAL':
lvl = logging.FATAL
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
lvl = logging.DEBUG
logging.basicConfig(filename=fn, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl) logging.basicConfig(filename=fn, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl)
dtu_name = ahoy_config.get('dtu',{}).get('name','hoymiles-dtu')
logging.info(f'start logging for {dtu_name} with level: {logging.root.level}')
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles") parser = argparse.ArgumentParser(description='Ahoy - Hoymiles solar inverter gateway', prog="hoymiles")
@ -308,29 +342,29 @@ if __name__ == '__main__':
logging.error(f'Failed to load config file {global_config.config_file}: {e_yaml}') logging.error(f'Failed to load config file {global_config.config_file}: {e_yaml}')
sys.exit(1) sys.exit(1)
# read AHOY configuration file and prepare logging
ahoy_config = dict(cfg.get('ahoy', {}))
init_logging(ahoy_config)
if global_config.log_transactions: if global_config.log_transactions:
hoymiles.HOYMILES_TRANSACTION_LOGGING=True hoymiles.HOYMILES_TRANSACTION_LOGGING=True
if global_config.verbose: if global_config.verbose:
hoymiles.HOYMILES_DEBUG_LOGGING=True hoymiles.HOYMILES_DEBUG_LOGGING=True
# read AHOY configuration file and prepare logging
ahoy_config = dict(cfg.get('ahoy', {}))
init_logging(ahoy_config)
# Prepare for multiple transceivers, makes them configurable # Prepare for multiple transceivers, makes them configurable
for radio_config in ahoy_config.get('nrf', [{}]): for radio_config in ahoy_config.get('nrf', [{}]):
hmradio = hoymiles.HoymilesNRF(**radio_config) hmradio = hoymiles.HoymilesNRF(**radio_config)
# create MQTT - client object # create MQTT - client object
mqtt_client = None mqtt_client = None
mqtt_config = ahoy_config.get('mqtt', {}) mqtt_config = ahoy_config.get('mqtt', None)
if mqtt_config and not mqtt_config.get('disabled', False): if mqtt_config and not mqtt_config.get('disabled', False):
from .outputs import MqttOutputPlugin from .outputs import MqttOutputPlugin
mqtt_client = MqttOutputPlugin(mqtt_config) mqtt_client = MqttOutputPlugin(mqtt_config)
# create INFLUX - client object # create INFLUX - client object
influx_client = None influx_client = None
influx_config = ahoy_config.get('influxdb', {}) influx_config = ahoy_config.get('influxdb', None)
if influx_config and not influx_config.get('disabled', False): if influx_config and not influx_config.get('disabled', False):
from .outputs import InfluxOutputPlugin from .outputs import InfluxOutputPlugin
influx_client = InfluxOutputPlugin( influx_client = InfluxOutputPlugin(

50
tools/rpi/hoymiles/decoders/__init__.py

@ -99,6 +99,7 @@ class StatusResponse(Response):
frequency = None frequency = None
powerfactor = None powerfactor = None
event_count = None event_count = None
unpack_error = False
def unpack(self, fmt, base): def unpack(self, fmt, base):
""" """
@ -110,6 +111,10 @@ class StatusResponse(Response):
:rtype: tuple :rtype: tuple
""" """
size = struct.calcsize(fmt) size = struct.calcsize(fmt)
if (len(self.response) < base+size):
self.unpack_error = True
logging.error(f'base: {base} size: {size} len: {len(self.response)} fmt: {fmt} rep: {self.response}')
return [0]
return struct.unpack(fmt, self.response[base:base+size]) return struct.unpack(fmt, self.response[base:base+size])
@property @property
@ -150,6 +155,7 @@ class StatusResponse(Response):
s_exists = False s_exists = False
string_id = len(strings) string_id = len(strings)
string = {} string = {}
string['name'] = self.inv_strings[string_id]['s_name']
for key in self.string_keys: for key in self.string_keys:
prop = f'dc_{key}_{string_id}' prop = f'dc_{key}_{string_id}'
if hasattr(self, prop): if hasattr(self, prop):
@ -193,6 +199,7 @@ class StatusResponse(Response):
data['event_count'] = self.event_count data['event_count'] = self.event_count
data['time'] = self.time_rx data['time'] = self.time_rx
if not self.unpack_error:
return data return data
class UnknownResponse(Response): class UnknownResponse(Response):
@ -321,9 +328,9 @@ class EventsResponse(UnknownResponse):
#logging.debug(' payload has valid modbus crc') #logging.debug(' payload has valid modbus crc')
self.response = self.response[:-2] self.response = self.response[:-2]
status = struct.unpack('>H', self.response[:2])[0] self.status = struct.unpack('>H', self.response[:2])[0]
a_text = self.alarm_codes.get(status, 'N/A') self.a_text = self.alarm_codes.get(self.status, 'N/A')
logging.info (f' Inverter status: {a_text} ({status})') logging.info (f'Inverter status: {self.a_text} ({self.status})')
chunk_size = 12 chunk_size = 12
for i_chunk in range(2, len(self.response), chunk_size): for i_chunk in range(2, len(self.response), chunk_size):
@ -331,9 +338,12 @@ class EventsResponse(UnknownResponse):
logging.debug(' '.join([f'{byte:02x}' for byte in chunk]) + ': ') logging.debug(' '.join([f'{byte:02x}' for byte in chunk]) + ': ')
if (len(chunk[0:6]) < 6):
logging.error(f'length of chunk must be greater or equal 6 bytes: {chunk}')
return
opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6]) opcode, a_code, a_count, uptime_sec = struct.unpack('>BBHH', chunk[0:6])
a_text = self.alarm_codes.get(a_code, 'N/A') a_text = self.alarm_codes.get(a_code, 'N/A')
logging.debug(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}') logging.debug(f' uptime={timedelta(seconds=uptime_sec)} a_count={a_count} opcode={opcode} a_code={a_code} a_text={a_text}')
dbg = '' dbg = ''
@ -341,6 +351,14 @@ class EventsResponse(UnknownResponse):
dbg += f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk)) dbg += f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))
logging.debug(dbg) logging.debug(dbg)
def __dict__(self):
""" Base values, availabe in each __dict__ call """
data = super().__dict__()
data['inv_stat_num'] = self.status
data['inv_stat_txt'] = self.a_text
return data
class HardwareInfoResponse(UnknownResponse): class HardwareInfoResponse(UnknownResponse):
def __init__(self, *args, **params): def __init__(self, *args, **params):
super().__init__(*args, **params) super().__init__(*args, **params)
@ -361,9 +379,14 @@ class HardwareInfoResponse(UnknownResponse):
def __dict__(self): def __dict__(self):
""" Base values, availabe in each __dict__ call """ """ Base values, availabe in each __dict__ call """
responce_info = self.response data = super().__dict__()
logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}')
if (len(self.response) != 16):
logging.error(f'HardwareInfoResponse: data length should be 16 bytes - measured {len(self.response)} bytes')
logging.error(f'HardwareInfoResponse: data: {self.response}')
return data
logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", self.response[0:16])}')
fw_version, fw_build_yyyy, fw_build_mmdd, fw_build_hhmm, hw_id = struct.unpack('>HHHHH', self.response[0:10]) fw_version, fw_build_yyyy, fw_build_mmdd, fw_build_hhmm, hw_id = struct.unpack('>HHHHH', self.response[0:10])
fw_version_maj = int((fw_version / 10000)) fw_version_maj = int((fw_version / 10000))
@ -377,7 +400,6 @@ class HardwareInfoResponse(UnknownResponse):
f'build at {fw_build_dd:>02}/{fw_build_mm:>02}/{fw_build_yyyy}T{fw_build_HH:>02}:{fw_build_MM:>02}, '\ f'build at {fw_build_dd:>02}/{fw_build_mm:>02}/{fw_build_yyyy}T{fw_build_HH:>02}:{fw_build_MM:>02}, '\
f'HW revision {hw_id}') f'HW revision {hw_id}')
data = super().__dict__()
data['FW_ver_maj'] = fw_version_maj data['FW_ver_maj'] = fw_version_maj
data['FW_ver_min'] = fw_version_min data['FW_ver_min'] = fw_version_min
data['FW_ver_pat'] = fw_version_pat data['FW_ver_pat'] = fw_version_pat
@ -468,6 +490,8 @@ class Hm300Decode0B(StatusResponse):
""" String 1 irratiation in percent """ """ String 1 irratiation in percent """
if self.inv_strings is None: if self.inv_strings is None:
return None return None
if self.inv_strings[0]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
@property @property
@ -540,6 +564,8 @@ class Hm600Decode0B(StatusResponse):
""" String 1 irratiation in percent """ """ String 1 irratiation in percent """
if self.inv_strings is None: if self.inv_strings is None:
return None return None
if self.inv_strings[0]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) return round(self.unpack('>H', 6)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
@property @property
@ -567,6 +593,8 @@ class Hm600Decode0B(StatusResponse):
""" String 2 irratiation in percent """ """ String 2 irratiation in percent """
if self.inv_strings is None: if self.inv_strings is None:
return None return None
if self.inv_strings[1]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 12)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3) return round(self.unpack('>H', 12)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3)
@property @property
@ -647,6 +675,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 1 irratiation in percent """ """ String 1 irratiation in percent """
if self.inv_strings is None: if self.inv_strings is None:
return None return None
if self.inv_strings[0]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 8)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3) return round(self.unpack('>H', 8)[0]/10/self.inv_strings[0]['s_maxpower']*100, 3)
@property @property
@ -674,6 +704,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 2 irratiation in percent """ """ String 2 irratiation in percent """
if self.inv_strings is None: if self.inv_strings is None:
return None return None
if self.inv_strings[1]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 10)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3) return round(self.unpack('>H', 10)[0]/10/self.inv_strings[1]['s_maxpower']*100, 3)
@property @property
@ -701,6 +733,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 3 irratiation in percent """ """ String 3 irratiation in percent """
if self.inv_strings is None: if self.inv_strings is None:
return None return None
if self.inv_strings[2]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 30)[0]/10/self.inv_strings[2]['s_maxpower']*100, 3) return round(self.unpack('>H', 30)[0]/10/self.inv_strings[2]['s_maxpower']*100, 3)
@property @property
@ -728,6 +762,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 4 irratiation in percent """ """ String 4 irratiation in percent """
if self.inv_strings is None: if self.inv_strings is None:
return None return None
if self.inv_strings[3]['s_maxpower'] == 0:
return 0.00
return round(self.unpack('>H', 32)[0]/10/self.inv_strings[3]['s_maxpower']*100, 3) return round(self.unpack('>H', 32)[0]/10/self.inv_strings[3]['s_maxpower']*100, 3)
@property @property

72
tools/rpi/hoymiles/outputs.py

@ -9,6 +9,7 @@ import socket
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from hoymiles.decoders import StatusResponse, HardwareInfoResponse from hoymiles.decoders import StatusResponse, HardwareInfoResponse
from hoymiles import HOYMILES_TRANSACTION_LOGGING, HOYMILES_DEBUG_LOGGING
class OutputPluginFactory: class OutputPluginFactory:
def __init__(self, **params): def __init__(self, **params):
@ -39,6 +40,7 @@ class InfluxOutputPlugin(OutputPluginFactory):
def __init__(self, url, token, **params): def __init__(self, url, token, **params):
""" """
Initialize InfluxOutputPlugin Initialize InfluxOutputPlugin
https://influxdb-client.readthedocs.io/en/stable/api.html#influxdbclient
The following targets must be present in your InfluxDB. This does not The following targets must be present in your InfluxDB. This does not
automatically create anything for You. automatically create anything for You.
@ -68,8 +70,12 @@ class InfluxOutputPlugin(OutputPluginFactory):
self._org = params.get('org', '') self._org = params.get('org', '')
self._measurement = params.get('measurement', f'inverter,host={socket.gethostname()}') self._measurement = params.get('measurement', f'inverter,host={socket.gethostname()}')
client = InfluxDBClient(url, token, bucket=self._bucket) with InfluxDBClient(url, token, bucket=self._bucket) as self.client:
self.api = client.write_api() self.api = self.client.write_api()
def disco(self, **params):
self.client.close() # Shutdown the client
return
def store_status(self, response, **params): def store_status(self, response, **params):
""" """
@ -102,6 +108,9 @@ class InfluxOutputPlugin(OutputPluginFactory):
# InfluxDB requires nanoseconds # InfluxDB requires nanoseconds
ctime = int(utctime.timestamp() * 1e9) ctime = int(utctime.timestamp() * 1e9)
if HOYMILES_DEBUG_LOGGING:
logging.info(f'InfluxDB: utctime: {utctime}')
# AC Data # AC Data
phase_id = 0 phase_id = 0
for phase in data['phases']: for phase in data['phases']:
@ -135,6 +144,9 @@ class InfluxOutputPlugin(OutputPluginFactory):
data_stack.append(f'{measurement},type=YieldToday value={data["yield_today"]/1000:.3f} {ctime}') data_stack.append(f'{measurement},type=YieldToday value={data["yield_today"]/1000:.3f} {ctime}')
data_stack.append(f'{measurement},type=Efficiency value={data["efficiency"]:.2f} {ctime}') data_stack.append(f'{measurement},type=Efficiency value={data["efficiency"]:.2f} {ctime}')
if HOYMILES_DEBUG_LOGGING:
#logging.debug(f'INFLUX data to DB: {data_stack}')
pass
self.api.write(self._bucket, self._org, data_stack) self.api.write(self._bucket, self._org, data_stack)
class MqttOutputPlugin(OutputPluginFactory): class MqttOutputPlugin(OutputPluginFactory):
@ -196,6 +208,12 @@ class MqttOutputPlugin(OutputPluginFactory):
def disco(self, **params): def disco(self, **params):
self.client.loop_stop() # Stop loop self.client.loop_stop() # Stop loop
self.client.disconnect() # disconnect self.client.disconnect() # disconnect
return
def info2mqtt(self, mqtt_topic, mqtt_data):
for mqtt_key in mqtt_data:
self.client.publish(f'{mqtt_topic["topic"]}/{mqtt_key}', mqtt_data[mqtt_key], self.qos, self.ret)
return
def store_status(self, response, **params): def store_status(self, response, **params):
""" """
@ -209,13 +227,18 @@ class MqttOutputPlugin(OutputPluginFactory):
""" """
data = response.__dict__() data = response.__dict__()
topic = params.get('topic', None)
if not topic:
topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}' topic = f'{data.get("inverter_name", "hoymiles")}/{data.get("inverter_ser", None)}'
if HOYMILES_DEBUG_LOGGING:
logging.info(f'MQTT-topic: {topic} data-type: {type(response)}')
if isinstance(response, StatusResponse): if isinstance(response, StatusResponse):
# Global Head # Global Head
if data['time'] is not None: if data['time'] is not None:
self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%y - %H:%M:%S"), self.qos, self.ret) self.client.publish(f'{topic}/time', data['time'].strftime("%d.%m.%YT%H:%M:%S"), self.qos, self.ret)
# AC Data # AC Data
phase_id = 0 phase_id = 0
@ -233,12 +256,16 @@ class MqttOutputPlugin(OutputPluginFactory):
string_id = 0 string_id = 0
string_sum_power = 0 string_sum_power = 0
for string in data['strings']: for string in data['strings']:
self.client.publish(f'{topic}/emeter-dc/{string_id}/voltage', string['voltage'], self.qos, self.ret) if 'name' in string:
self.client.publish(f'{topic}/emeter-dc/{string_id}/current', string['current'], self.qos, self.ret) string_name = string['name'].replace(" ","_")
self.client.publish(f'{topic}/emeter-dc/{string_id}/power', string['power'], self.qos, self.ret) else:
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldDay', string['energy_daily'], self.qos, self.ret) string_name = string_id
self.client.publish(f'{topic}/emeter-dc/{string_id}/YieldTotal', string['energy_total']/1000, self.qos, self.ret) self.client.publish(f'{topic}/emeter-dc/{string_name}/voltage', string['voltage'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_id}/Irradiation', string['irradiation'], self.qos, self.ret) self.client.publish(f'{topic}/emeter-dc/{string_name}/current', string['current'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_name}/power', string['power'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldDay', string['energy_daily'], self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_name}/YieldTotal', string['energy_total']/1000, self.qos, self.ret)
self.client.publish(f'{topic}/emeter-dc/{string_name}/Irradiation', string['irradiation'], self.qos, self.ret)
string_id = string_id + 1 string_id = string_id + 1
string_sum_power += string['power'] string_sum_power += string['power']
@ -277,9 +304,10 @@ class VzInverterOutput:
self.channels = dict() self.channels = dict()
for channel in config.get('channels', []): for channel in config.get('channels', []):
uid = channel.get('uid') uid = channel.get('uid', None)
ctype = channel.get('type') ctype = channel.get('type')
if uid and ctype: # if uid and ctype:
if ctype:
self.channels[ctype] = uid self.channels[ctype] = uid
def store_status(self, data, session): def store_status(self, data, session):
@ -295,6 +323,9 @@ class VzInverterOutput:
ts = int(round(data['time'].timestamp() * 1000)) ts = int(round(data['time'].timestamp() * 1000))
if HOYMILES_DEBUG_LOGGING:
logging.info(f'Volkszaehler-Timestamp: {ts}')
# AC Data # AC Data
phase_id = 0 phase_id = 0
for phase in data['phases']: for phase in data['phases']:
@ -327,13 +358,24 @@ class VzInverterOutput:
if data['yield_today'] is not None: if data['yield_today'] is not None:
self.try_publish(ts, f'yield_today', data['yield_today']) self.try_publish(ts, f'yield_today', data['yield_today'])
self.try_publish(ts, f'efficiency', data['efficiency']) self.try_publish(ts, f'efficiency', data['efficiency'])
return
def try_publish(self, ts, ctype, value): def try_publish(self, ts, ctype, value):
if not ctype in self.channels: if not ctype in self.channels:
if HOYMILES_DEBUG_LOGGING:
logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml') logging.warning(f'ctype \"{ctype}\" not found in ahoy.yml')
return return
uid = self.channels[ctype] uid = self.channels[ctype]
url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}' url = f'{self.baseurl}/data/{uid}.json?operation=add&ts={ts}&value={value}'
if uid == None:
if HOYMILES_DEBUG_LOGGING:
logging.debug(f'ctype \"{ctype}\" has no configured uid-value in ahoy.yml')
return
if HOYMILES_DEBUG_LOGGING:
logging.debug(f'VZ-url: {url}')
try: try:
r = self.session.get(url) r = self.session.get(url)
if r.status_code == 404: if r.status_code == 404:
@ -344,6 +386,7 @@ class VzInverterOutput:
raise ValueError(f'Transmit result {url}') raise ValueError(f'Transmit result {url}')
except ConnectionError as e: except ConnectionError as e:
raise ValueError(f'Could not connect VZ-DB {type(e)} {e.keys()}') raise ValueError(f'Could not connect VZ-DB {type(e)} {e.keys()}')
return
class VolkszaehlerOutputPlugin(OutputPluginFactory): class VolkszaehlerOutputPlugin(OutputPluginFactory):
def __init__(self, config, **params): def __init__(self, config, **params):
@ -364,13 +407,17 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
exit(1) exit(1)
self.session = requests.Session() self.session = requests.Session()
self.inverters = dict()
self.inverters = dict()
for inverterconfig in config.get('inverters', []): for inverterconfig in config.get('inverters', []):
serial = inverterconfig.get('serial') serial = inverterconfig.get('serial')
output = VzInverterOutput(inverterconfig, self.session) output = VzInverterOutput(inverterconfig, self.session)
self.inverters[serial] = output self.inverters[serial] = output
def disco(self, **params):
self.session.close() # closing the connection
return
def store_status(self, response, **params): def store_status(self, response, **params):
""" """
Publish StatusResponse object Publish StatusResponse object
@ -395,3 +442,4 @@ class VolkszaehlerOutputPlugin(OutputPluginFactory):
output.store_status(data, self.session) output.store_status(data, self.session)
except ValueError as e: except ValueError as e:
logging.warning('Could not send data to volkszaehler instance: %s' % e) logging.warning('Could not send data to volkszaehler instance: %s' % e)
return

Loading…
Cancel
Save