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. 93
      src/app.cpp
  6. 31
      src/app.h
  7. 93
      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. 99
      src/publisher/pubMqtt.h
  21. 180
      src/web/RestApi.h
  22. 90
      src/web/html/api.js
  23. 27
      src/web/html/colorBright.css
  24. 27
      src/web/html/colorDark.css
  25. 95
      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. 112
      src/web/html/serial.html
  32. 566
      src/web/html/setup.html
  33. 485
      src/web/html/style.css
  34. 35
      src/web/html/system.html
  35. 54
      src/web/html/update.html
  36. 287
      src/web/html/visualization.html
  37. 309
      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. 41
      tools/rpi/hoymiles/__init__.py
  46. 62
      tools/rpi/hoymiles/__main__.py
  47. 52
      tools/rpi/hoymiles/decoders/__init__.py
  48. 76
      tools/rpi/hoymiles/outputs.py

1
.gitignore

@ -6,6 +6,7 @@
.vscode/extensions.json
src/config/config_override.h
src/web/html/h/*
src/web/html/tmp/*
/**/Debug
/**/v16/*
*.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 | 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 | | |
| tomquist | TSOL-M1600 | | 1.0.12 | 2020 | 06-24 | 100 | | |
| | | | | | | | | |
## Developer Information about Command Queue

22
src/CHANGES.md

@ -2,6 +2,28 @@
(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
* merged PR #684, #698, #705
* 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!

93
src/app.cpp

@ -21,16 +21,10 @@ void app::setup() {
resetSystem();
/*DBGPRINTLN("--- start");
DBGPRINTLN(String(ESP.getFreeHeap()));
DBGPRINTLN(String(ESP.getHeapFragmentation()));
DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));*/
mSettings.setup();
mSettings.getPtr(mConfig);
DPRINT(DBG_INFO, F("Settings valid: "));
if(mSettings.getValid())
if (mSettings.getValid())
DBGPRINTLN(F("true"));
else
DBGPRINTLN(F("false"));
@ -67,7 +61,7 @@ void app::setup() {
mMiPayload.setup(this, &mSys, &mNrfRadio, &mStat, mConfig->nrf.maxRetransPerPyld, &mTimestamp);
mMiPayload.enableSerialDebug(mConfig->serial.debug);
if(!mNrfRadio.isChipConnected())
if(!mNrfRadio.isChipConnected())
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
if(mConfig->cmt.enabled) {
@ -81,6 +75,8 @@ void app::setup() {
DBGPRINTLN(String(ESP.getHeapFragmentation()));
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
#if !defined(AP_ONLY)
@ -99,18 +95,18 @@ void app::setup() {
mApi.setup(this, &mSys, &mNrfRadio, mWeb.getWebSrvPtr(), mConfig);
// Plugins
if(mConfig->plugin.display.type != 0)
mMonoDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, 0xff, mVersion);
if (mConfig->plugin.display.type != 0)
mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, 0xff, mVersion);
mPubSerial.setup(mConfig, &mSys, &mTimestamp);
regularTickers();
/*DBGPRINTLN("--- end setup");
DBGPRINTLN(String(ESP.getFreeHeap()));
DBGPRINTLN(String(ESP.getHeapFragmentation()));
DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));*/
// DBGPRINTLN("--- end setup");
// DBGPRINTLN(String(ESP.getFreeHeap()));
// DBGPRINTLN(String(ESP.getHeapFragmentation()));
// DBGPRINTLN(String(ESP.getMaxFreeBlockSize()));
}
//-----------------------------------------------------------------------------
@ -137,8 +133,8 @@ void app::loopStandard(void) {
mStat.frmCnt++;
Inverter<> *iv = mSys.findInverter(&p->packet[1]);
if(NULL != iv) {
if(IV_HM == iv->ivGen)
if (NULL != iv) {
if (IV_HM == iv->ivGen)
mPayload.add(iv, p);
else
mMiPayload.add(iv, p);
@ -180,7 +176,7 @@ void app::loopStandard(void) {
mHmsPayload.loop();
#endif
if(mMqttEnabled)
if (mMqttEnabled)
mMqtt.loop();
}
@ -194,7 +190,7 @@ void app::loopWifi(void) {
void app::onWifi(bool gotIp) {
DPRINTLN(DBG_DEBUG, F("onWifi"));
ah::Scheduler::resetTicker();
regularTickers(); // reinstall regular tickers
regularTickers(); // reinstall regular tickers
if (gotIp) {
mInnerLoopCb = std::bind(&app::loopStandard, this);
every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval, "tSend");
@ -203,14 +199,13 @@ void app::onWifi(bool gotIp) {
everySec(std::bind(&CmtRadioType::tickSecond, &mCmtRadio), "tsCmt");
#endif
mMqttReconnect = true;
mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers!
mSunrise = 0; // needs to be set to 0, to reinstall sunrise and ivComm tickers!
once(std::bind(&app::tickNtpUpdate, this), 2, "ntp2");
if(WIFI_AP == WiFi.getMode()) {
if (WIFI_AP == WiFi.getMode()) {
mMqttEnabled = false;
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
}
}
else {
} else {
mInnerLoopCb = std::bind(&app::loopWifi, this);
everySec(std::bind(&ahoywifi::tickWifiLoop, &mWifi), "wifiL");
}
@ -221,8 +216,8 @@ void app::regularTickers(void) {
DPRINTLN(DBG_DEBUG, F("regularTickers"));
everySec(std::bind(&WebType::tickSecond, &mWeb), "webSc");
// Plugins
if(mConfig->plugin.display.type != 0)
everySec(std::bind(&MonoDisplayType::tickerSecond, &mMonoDisplay), "disp");
if (mConfig->plugin.display.type != 0)
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart");
}
@ -238,26 +233,26 @@ void app::tickNtpUpdate(void) {
}
// only install schedulers once even if NTP wasn't successful in first loop
if(mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed
if(mConfig->inst.rstValsNotAvail)
if (mMqttReconnect) { // @TODO: mMqttReconnect is variable which scope has changed
if (mConfig->inst.rstValsNotAvail)
everyMin(std::bind(&app::tickMinute, this), "tMin");
if(mConfig->inst.rstYieldMidNight) {
if (mConfig->inst.rstYieldMidNight) {
uint32_t localTime = gTimezone.toLocal(mTimestamp);
uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
uint32_t midTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
onceAt(std::bind(&app::tickMidnight, this), midTrig, "midNi");
}
}
nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min
nxtTrig = isOK ? 43200 : 60; // depending on NTP update success check again in 12 h or in 1 min
if((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) {
if ((mSunrise == 0) && (mConfig->sun.lat) && (mConfig->sun.lon)) {
mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
tickCalcSunrise();
}
// immediately start communicating
// @TODO: leads to reboot loops? not sure #674
if(isOK && mSendFirst) {
if (isOK && mSendFirst) {
mSendFirst = false;
once(std::bind(&app::tickSend, this), 2, "senOn");
}
@ -308,17 +303,17 @@ void app::tickIVCommunication(void) {
void app::tickSun(void) {
// only used and enabled by MQTT (see setup())
if (!mMqtt.tickerSun(mSunrise, mSunset, mConfig->sun.offsetSec, mConfig->sun.disNightCom))
once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry
once(std::bind(&app::tickSun, this), 1, "mqSun"); // MQTT not connected, retry
}
//-----------------------------------------------------------------------------
void app::tickComm(void) {
if((!mIVCommunicationOn) && (mConfig->inst.rstValsCommStop))
if ((!mIVCommunicationOn) && (mConfig->inst.rstValsCommStop))
once(std::bind(&app::tickZeroValues, this), mConfig->nrf.sendInterval, "tZero");
if (mMqttEnabled) {
if (!mMqtt.tickerComm(!mIVCommunicationOn))
once(std::bind(&app::tickComm, this), 5, "mqCom"); // MQTT not connected, retry after 5s
once(std::bind(&app::tickComm, this), 5, "mqCom"); // MQTT not connected, retry after 5s
}
}
@ -329,7 +324,7 @@ void app::tickZeroValues(void) {
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
continue; // skip to next inverter
mPayload.zeroInverterValues(iv);
}
@ -344,9 +339,9 @@ void app::tickMinute(void) {
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
continue; // skip to next inverter
if(!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled)
if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled)
mPayload.zeroInverterValues(iv);
}
}
@ -355,7 +350,7 @@ void app::tickMinute(void) {
void app::tickMidnight(void) {
// only triggered if 'reset values at midnight is enabled'
uint32_t localTime = gTimezone.toLocal(mTimestamp);
uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
uint32_t nxtTrig = gTimezone.toUTC(localTime - (localTime % 86400) + 86400); // next midnight local time
onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2");
Inverter<> *iv;
@ -363,7 +358,7 @@ void app::tickMidnight(void) {
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
continue; // skip to next inverter
mPayload.zeroInverterValues(iv);
mPayload.zeroYieldDay(iv);
@ -403,8 +398,8 @@ void app::tickSend(void) {
} while ((NULL == iv) && ((maxLoop--) > 0));
if (NULL != iv) {
if(iv->config->enabled) {
if(iv->ivGen == IV_HM)
if (iv->config->enabled) {
if (iv->ivGen == IV_HM)
mPayload.ivSend(iv);
else if(iv->ivGen == IV_MI)
mMiPayload.ivSend(iv);
@ -457,25 +452,25 @@ void app::setupLed(void) {
* PIN ---- |<----- 3.3V
*
* */
if(mConfig->led.led0 != 0xff) {
if (mConfig->led.led0 != 0xff) {
pinMode(mConfig->led.led0, OUTPUT);
digitalWrite(mConfig->led.led0, HIGH); // LED off
digitalWrite(mConfig->led.led0, HIGH); // LED off
}
if(mConfig->led.led1 != 0xff) {
if (mConfig->led.led1 != 0xff) {
pinMode(mConfig->led.led1, OUTPUT);
digitalWrite(mConfig->led.led1, HIGH); // LED off
digitalWrite(mConfig->led.led1, HIGH); // LED off
}
}
//-----------------------------------------------------------------------------
void app::updateLed(void) {
if(mConfig->led.led0 != 0xff) {
if (mConfig->led.led0 != 0xff) {
Inverter<> *iv = mSys.getInverterByPos(0);
if (NULL != iv) {
if(iv->isProducing(mTimestamp))
digitalWrite(mConfig->led.led0, LOW); // LED on
if (iv->isProducing(mTimestamp))
digitalWrite(mConfig->led.led0, LOW); // LED on
else
digitalWrite(mConfig->led.led0, HIGH); // LED off
digitalWrite(mConfig->led.led0, HIGH); // LED off
}
}
}

31
src/app.h

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

93
src/config/settings.h

@ -7,11 +7,12 @@
#define __SETTINGS_H__
#include <Arduino.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include <LittleFS.h>
#include "../defines.h"
#include "../utils/dbg.h"
#include "../utils/helper.h"
#include "../defines.h"
/**
* More info:
@ -51,6 +52,7 @@ typedef struct {
char deviceName[DEVNAME_LEN];
char adminPwd[PWD_LEN];
uint16_t protectionMask;
bool darkMode;
// wifi
char stationSsid[SSID_LEN];
@ -84,7 +86,7 @@ typedef struct {
typedef struct {
float lat;
float lon;
bool disNightCom; // disable night communication
bool disNightCom; // disable night communication
uint16_t offsetSec;
} cfgSun_t;
@ -95,8 +97,8 @@ typedef struct {
} cfgSerial_t;
typedef struct {
uint8_t led0; // first LED pin
uint8_t led1; // second LED pin
uint8_t led0; // first LED pin
uint8_t led1; // second LED pin
} cfgLed_t;
typedef struct {
@ -113,7 +115,7 @@ typedef struct {
char name[MAX_NAME_LENGTH];
serial_u serial;
uint16_t chMaxPwr[4];
int32_t yieldCor[4]; // signed YieldTotal correction value
int32_t yieldCor[4]; // signed YieldTotal correction value
char chName[4][MAX_NAME_LENGTH];
} cfgIv_t;
@ -129,14 +131,17 @@ typedef struct {
typedef struct {
uint8_t type;
bool pwrSaveAtIvOffline;
bool logoEn;
bool pxShift;
bool rot180;
uint16_t wakeUp;
uint16_t sleepAt;
uint8_t rot;
//uint16_t wakeUp;
//uint16_t sleepAt;
uint8_t contrast;
uint8_t pin0;
uint8_t pin1;
uint8_t disp_data;
uint8_t disp_clk;
uint8_t disp_cs;
uint8_t disp_reset;
uint8_t disp_busy;
uint8_t disp_dc;
} display_t;
typedef struct {
@ -228,7 +233,7 @@ class settings {
else {
//DPRINTLN(DBG_INFO, fp.readString());
//fp.seek(0, SeekSet);
DynamicJsonDocument root(4500);
DynamicJsonDocument root(5500);
DeserializationError err = deserializeJson(root, fp);
if(!err && (root.size() > 0)) {
mCfg.valid = true;
@ -262,7 +267,7 @@ class settings {
return false;
}
DynamicJsonDocument json(4500);
DynamicJsonDocument json(6500);
JsonObject root = json.to<JsonObject>();
jsonWifi(root.createNestedObject(F("wifi")), true);
jsonNrf(root.createNestedObject(F("nrf")), true);
@ -307,6 +312,7 @@ class settings {
memset(&mCfg, 0, sizeof(settings_t));
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;
mCfg.sys.darkMode = false;
// restore temp settings
if(keepWifi)
memcpy(&mCfg.sys, &tmp, sizeof(cfgSys_t));
@ -359,13 +365,16 @@ class settings {
memset(&mCfg.inst, 0, sizeof(cfgInst_t));
mCfg.plugin.display.pwrSaveAtIvOffline = false;
mCfg.plugin.display.contrast = 60;
mCfg.plugin.display.logoEn = true;
mCfg.plugin.display.pxShift = true;
mCfg.plugin.display.rot180 = false;
mCfg.plugin.display.pin0 = DEF_PIN_OFF; // SCL
mCfg.plugin.display.pin1 = DEF_PIN_OFF; // SDA
}
mCfg.plugin.display.contrast = 60;
mCfg.plugin.display.pxShift = true;
mCfg.plugin.display.rot = 0;
mCfg.plugin.display.disp_data = 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) {
if(set) {
@ -375,6 +384,7 @@ class settings {
obj[F("dev")] = mCfg.sys.deviceName;
obj[F("adm")] = mCfg.sys.adminPwd;
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.mask, buf); obj[F("mask")] = 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.adminPwd, PWD_LEN, "%s", obj[F("adm")].as<const char*>());
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.mask, obj[F("mask")].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");
disp[F("type")] = mCfg.plugin.display.type;
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("rot180")] = (bool)mCfg.plugin.display.rot180;
disp[F("wake")] = mCfg.plugin.display.wakeUp;
disp[F("sleep")] = mCfg.plugin.display.sleepAt;
disp[F("pxShift")] = (bool)mCfg.plugin.display.pxShift;
disp[F("rotation")] = mCfg.plugin.display.rot;
//disp[F("wake")] = mCfg.plugin.display.wakeUp;
//disp[F("sleep")] = mCfg.plugin.display.sleepAt;
disp[F("contrast")] = mCfg.plugin.display.contrast;
disp[F("pin0")] = mCfg.plugin.display.pin0;
disp[F("pin1")] = mCfg.plugin.display.pin1;
disp[F("data")] = mCfg.plugin.display.disp_data;
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 {
JsonObject disp = obj["disp"];
mCfg.plugin.display.type = disp[F("type")];
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.rot180 = (bool) disp[F("rot180")];
mCfg.plugin.display.wakeUp = disp[F("wake")];
mCfg.plugin.display.sleepAt = disp[F("sleep")];
mCfg.plugin.display.contrast = disp[F("contrast")];
mCfg.plugin.display.pin0 = disp[F("pin0")];
mCfg.plugin.display.pin1 = disp[F("pin1")];
mCfg.plugin.display.type = disp[F("type")];
mCfg.plugin.display.pwrSaveAtIvOffline = (bool)disp[F("pwrSafe")];
mCfg.plugin.display.pxShift = (bool)disp[F("pxShift")];
mCfg.plugin.display.rot = disp[F("rotation")];
//mCfg.plugin.display.wakeUp = disp[F("wake")];
//mCfg.plugin.display.sleepAt = disp[F("sleep")];
mCfg.plugin.display.contrast = disp[F("contrast")];
mCfg.plugin.display.disp_data = disp[F("data")];
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_MINOR 5
#define VERSION_PATCH 90
#define VERSION_PATCH 94
//-------------------------------------
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"};
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
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"};

4
src/hm/hmPayload.h

@ -9,6 +9,7 @@
#include "../utils/dbg.h"
#include "../utils/crc.h"
#include "../config/config.h"
#include "hmRadio.h"
#include <Arduino.h>
typedef struct {
@ -158,7 +159,8 @@ class HmPayload {
uint8_t cmd = iv->getQueuedCmd();
DPRINT(DBG_INFO, F("(#"));
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);
mPayload[iv->id].txCmd = cmd;
}

57
src/hm/miPayload.h

@ -92,14 +92,14 @@ class MiPayload {
}
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
mPayload[iv->id].stsa = true;
miStsDecode(iv, p);
} else if (p->packet[0] == (0x11 + SINGLE_FRAME)) { // 0x92; MI status response to 0x11
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
mPayload[iv->id].txId = p->packet[0];
miDataDecode(iv,p);
@ -359,12 +359,11 @@ class MiPayload {
(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
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!
//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(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)]){
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));
iv->enqueCommand<InfoCommand>(AlarmData);
}
/* Unclear how in HM inverters Info and alarm data is handled...
*/
/* for decoding see
/* int8_t offset = -2;
for decoding see
void MI600StsMsg (NRF24_packet_t *p){
STAT = (int)((p->packet[11] << 8) + p->packet[12]);
FCNT = (int)((p->packet[13] << 8) + p->packet[14]);
@ -393,23 +397,22 @@ class MiPayload {
#endif
}
*/
DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(": status msg ") + p->packet[0]);
}
void miDataDecode(Inverter<> *iv, packet_t *p) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the parser
rec->ts = mPayload[iv->id].ts;
uint8_t chan = ( p->packet[2] == 0x89 || p->packet[2] == (0x36 + ALL_FRAMES) ) ? 1 :
( p->packet[2] == 0x91 || p->packet[2] == (0x37 + ALL_FRAMES) ) ? 2 :
p->packet[2] == (0x38 + ALL_FRAMES) ? 3 :
4;
uint8_t datachan = ( p->packet[0] == 0x89 || p->packet[0] == (0x36 + ALL_FRAMES) ) ? CH1 :
( p->packet[0] == 0x91 || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 :
p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 :
CH4;
int8_t offset = -2;
// 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();
// 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();
// 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);
@ -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);
//yield();
// 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();
// 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();
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_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_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);
iv->setValue(iv->getPosByChFld(0, FLD_IRR, rec), rec, (float) (calcIrradiation(iv, datachan)));
yield();
//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] );
FCNT = (uint8_t)(p->packet[26]);
FCODE = (uint8_t)(p->packet[27]); // MI300: (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(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]));
FCODE = (uint8_t)(p->packet[27]); // MI300/Mi600 stsMsg:: (int)((p->packet[15] << 8) + p->packet[16]); */
iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, (uint8_t)(p->packet[25+offset]));
yield();
if (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->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();
notify(mPayload[iv->id].txCmd);
/*
@ -502,7 +505,7 @@ class MiPayload {
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) {

8
src/platformio.ini

@ -15,6 +15,7 @@ include_dir = .
[env]
framework = arduino
board_build.filesystem = littlefs
upload_speed = 921600
;build_flags =
; ;;;;; Possible Debug options ;;;;;;
@ -40,6 +41,7 @@ lib_deps =
bblanchon/ArduinoJson
https://github.com/JChristensen/Timezone
olikraus/U8g2
zinggjm/GxEPD2@^1.5.0
;esp8266/DNSServer
;esp8266/EEPROM
;esp8266/ESP8266WiFi
@ -54,8 +56,9 @@ board_build.f_cpu = 80000000L
build_flags = -D RELEASE
monitor_filters =
;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
esp8266_exception_decoder
[env:esp8266-debug]
platform = espressif8266
@ -98,8 +101,9 @@ build_flags = -D RELEASE -std=gnu++14
build_unflags = -std=gnu++11
monitor_filters =
;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
esp32_exception_decoder
[env:esp32-wroom32-debug]
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__*/

99
src/publisher/pubMqtt.h

@ -406,7 +406,7 @@ class PubMqtt {
return (pos >= DEVICE_CLS_ASSIGN_LIST_LEN) ? NULL : stateClasses[deviceFieldAssignment[pos].stateClsId];
}
bool processIvStatus() {
bool processIvStatus() {
// returns true if any inverter is available
bool allAvail = true; // shows if all enabled inverters are available
bool anyAvail = false; // shows if at least one enabled inverter is available
@ -419,17 +419,19 @@ class PubMqtt {
iv = mSys->getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
if (!iv->config->enabled)
continue; // skip to next inverter
rec = iv->getRecordStruct(RealTimeRunData_Debug);
// inverter status
uint8_t status = MQTT_STATUS_NOT_AVAIL_NOT_PROD;
if (iv->config->enabled) {
if (iv->isAvailable(*mUtcTimestamp))
status = (iv->isProducing(*mUtcTimestamp)) ? MQTT_STATUS_AVAIL_PROD : MQTT_STATUS_AVAIL_NOT_PROD;
else // inverter is enabled but not available
allAvail = false;
if (iv->isAvailable(*mUtcTimestamp)) {
anyAvail = true;
status = (iv->isProducing(*mUtcTimestamp)) ? MQTT_STATUS_AVAIL_PROD : MQTT_STATUS_AVAIL_NOT_PROD;
}
else // inverter is enabled but not available
allAvail = false;
if(mLastIvState[id] != status) {
// if status changed from producing to not producing send last data immediately
@ -439,11 +441,11 @@ class PubMqtt {
mLastIvState[id] = status;
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);
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));
publish(topic, val, true);
}
@ -451,7 +453,7 @@ class PubMqtt {
if(changed) {
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;
@ -474,24 +476,26 @@ class PubMqtt {
char topic[7 + MQTT_TOPIC_LEN], val[40];
record_t<> *rec = iv->getRecordStruct(curInfoCmd);
for (uint8_t i = 0; i < rec->length; i++) {
bool retained = false;
if (curInfoCmd == RealTimeRunData_Debug) {
switch (rec->assign[i].fieldId) {
case FLD_YT:
case FLD_YD:
if ((rec->assign[i].ch == CH0) && (!iv->isProducing(*mUtcTimestamp))) // avoids returns to 0 on restart
continue;
retained = true;
break;
if (iv->getLastTs(rec) > 0) {
for (uint8_t i = 0; i < rec->length; i++) {
bool retained = false;
if (curInfoCmd == RealTimeRunData_Debug) {
switch (rec->assign[i].fieldId) {
case FLD_YT:
case FLD_YD:
if ((rec->assign[i].ch == CH0) && (!iv->isProducing(*mUtcTimestamp))) // avoids returns to 0 on restart
continue;
retained = true;
break;
}
}
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec)));
publish(topic, val, retained);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec)));
publish(topic, val, retained);
yield();
yield();
}
}
}
@ -512,42 +516,49 @@ class PubMqtt {
uint8_t curInfoCmd = mSendList.front();
if ((curInfoCmd != RealTimeRunData_Debug) || !RTRDataHasBeenSent) { // send RTR Data only once
bool sendTotals = (curInfoCmd == RealTimeRunData_Debug);
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
if (!iv->config->enabled)
continue; // skip to next inverter
// 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);
// calculate total values for RealTimeRunData_Debug
if (curInfoCmd == RealTimeRunData_Debug) {
if (sendTotals) {
record_t<> *rec = iv->getRecordStruct(curInfoCmd);
for (uint8_t i = 0; i < rec->length; i++) {
if (CH0 == rec->assign[i].ch) {
switch (rec->assign[i].fieldId) {
case FLD_PAC:
total[0] += iv->getValue(i, rec);
break;
case FLD_YT:
total[1] += iv->getValue(i, rec);
break;
case FLD_YD:
total[2] += iv->getValue(i, rec);
break;
case FLD_PDC:
total[3] += iv->getValue(i, rec);
break;
sendTotals &= (iv->getLastTs(rec) > 0);
if (sendTotals) {
for (uint8_t i = 0; i < rec->length; i++) {
if (CH0 == rec->assign[i].ch) {
switch (rec->assign[i].fieldId) {
case FLD_PAC:
total[0] += iv->getValue(i, rec);
break;
case FLD_YT:
total[1] += iv->getValue(i, rec);
break;
case FLD_YD:
total[2] += iv->getValue(i, rec);
break;
case FLD_PDC:
total[3] += iv->getValue(i, rec);
break;
}
}
}
}
yield();
}
yield();
}
if (curInfoCmd == RealTimeRunData_Debug) {
if (sendTotals) {
uint8_t fieldId;
for (uint8_t i = 0; i < 4; i++) {
switch (i) {
@ -565,7 +576,7 @@ class PubMqtt {
fieldId = FLD_PDC;
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]));
publish(topic, val, true);
}

180
src/web/RestApi.h

@ -8,22 +8,24 @@
#include "../utils/dbg.h"
#ifdef ESP32
#include "AsyncTCP.h"
#include "AsyncTCP.h"
#else
#include "ESPAsyncTCP.h"
#include "ESPAsyncTCP.h"
#endif
#include "ESPAsyncWebServer.h"
#include "AsyncJson.h"
#include "../appInterface.h"
#include "../hm/hmSystem.h"
#include "../utils/helper.h"
#include "../appInterface.h"
#include "AsyncJson.h"
#include "ESPAsyncWebServer.h"
#if defined(F) && defined(ESP32)
#undef F
#define F(sl) (sl)
#undef F
#define F(sl) (sl)
#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>
class RestApi {
public:
@ -72,7 +74,7 @@ class RestApi {
mHeapFrag = ESP.getHeapFragmentation();
#endif
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192);
AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000);
JsonObject root = response->getRoot();
String path = request->url().substring(5);
@ -84,7 +86,6 @@ class RestApi {
else if(path == "reboot") getReboot(root);
else if(path == "statistics") getStatistics(root);
else if(path == "inverter/list") getInverterList(root);
else if(path == "menu") getMenu(root);
else if(path == "index") getIndex(root);
else if(path == "setup") getSetup(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/config") getRecord(root, SystemConfigPara);
else if(path == "record/live") getRecord(root, RealTimeRunData_Debug);
else
getNotFound(root, F("http://") + request->host() + F("/api/"));
else {
if(path.substring(0, 12) == "inverter/id/")
getInverter(root, request->url().substring(17).toInt());
else
getNotFound(root, F("http://") + request->host() + F("/api/"));
}
//DPRINTLN(DBG_INFO, "API mem usage: " + String(root.memoryUsage()));
response->addHeader("Access-Control-Allow-Origin", "*");
@ -154,13 +159,14 @@ class RestApi {
ep[F("record/live")] = url + F("record/live");
}
void onDwnldSetup(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response;
File fp = LittleFS.open("/settings.json", "r");
if(!fp) {
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 {
String tmp = fp.readString();
@ -173,7 +179,7 @@ class RestApi {
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");
@ -184,10 +190,11 @@ class RestApi {
}
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("ts_uptime")] = mApp->getUptime();
obj[F("wifi_rssi")] = (WiFi.status() != WL_CONNECTED) ? 0 : WiFi.RSSI();
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)
obj[F("esp_type")] = F("ESP32");
@ -199,6 +206,7 @@ class RestApi {
void getSysInfo(JsonObject obj) {
obj[F("ssid")] = mConfig->sys.stationSsid;
obj[F("device_name")] = mConfig->sys.deviceName;
obj[F("dark_mode")] = (bool)mConfig->sys.darkMode;
obj[F("mac")] = WiFi.macAddress();
obj[F("hostname")] = mConfig->sys.deviceName;
@ -245,15 +253,12 @@ class RestApi {
}
void getHtmlSystem(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getSysInfo(obj.createNestedObject(F("system")));
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>");
}
void getHtmlLogout(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic")));
obj[F("refresh")] = 3;
obj[F("refresh_url")] = "/";
@ -261,7 +266,6 @@ class RestApi {
}
void getHtmlSave(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic")));
obj[F("refresh")] = 2;
obj[F("refresh_url")] = "/setup";
@ -269,7 +273,6 @@ class RestApi {
}
void getReboot(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic")));
obj[F("refresh")] = 10;
obj[F("refresh_url")] = "/";
@ -316,6 +319,41 @@ class RestApi {
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) {
obj[F("broker")] = String(mConfig->mqtt.broker);
obj[F("port")] = String(mConfig->mqtt.port);
@ -376,64 +414,21 @@ class RestApi {
}
void getDisplay(JsonObject obj) {
obj[F("disp_type")] = (uint8_t)mConfig->plugin.display.type;
obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline;
obj[F("logo_en")] = (bool)mConfig->plugin.display.logoEn;
obj[F("px_shift")] = (bool)mConfig->plugin.display.pxShift;
obj[F("rot180")] = (bool)mConfig->plugin.display.rot180;
obj[F("contrast")] = (uint8_t)mConfig->plugin.display.contrast;
obj[F("pinDisp0")] = mConfig->plugin.display.pin0;
obj[F("pinDisp1")] = mConfig->plugin.display.pin1;
}
void getMenu(JsonObject obj) {
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";
}
}
obj[F("disp_typ")] = (uint8_t)mConfig->plugin.display.type;
obj[F("disp_pwr")] = (bool)mConfig->plugin.display.pwrSaveAtIvOffline;
obj[F("disp_pxshift")] = (bool)mConfig->plugin.display.pxShift;
obj[F("disp_rot")] = (uint8_t)mConfig->plugin.display.rot;
obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast;
obj[F("disp_clk")] = mConfig->plugin.display.disp_clk;
obj[F("disp_data")] = mConfig->plugin.display.disp_data;
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;
obj[F("disp_bsy")] = mConfig->plugin.display.disp_busy;
}
void getIndex(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic")));
obj[F("ts_now")] = mApp->getTimestamp();
obj[F("ts_sunrise")] = mApp->getSunrise();
obj[F("ts_sunset")] = mApp->getSunset();
@ -482,10 +477,9 @@ class RestApi {
}
void getSetup(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic")));
getSysInfo(obj.createNestedObject(F("system")));
getInverterList(obj.createNestedObject(F("inverter")));
//getInverterList(obj.createNestedObject(F("inverter")));
getMqtt(obj.createNestedObject(F("mqtt")));
getNtp(obj.createNestedObject(F("ntp")));
getSun(obj.createNestedObject(F("sun")));
@ -502,14 +496,29 @@ class RestApi {
}
void getLive(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getGeneric(obj.createNestedObject(F("generic")));
JsonArray invArr = obj.createNestedArray(F("inverter"));
obj["refresh_interval"] = mConfig->nrf.sendInterval;
//JsonArray invArr = obj.createNestedArray(F("inverter"));
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;
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;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i);
@ -553,7 +562,7 @@ class RestApi {
}
}
}
}
}*/
}
void getRecord(JsonObject obj, uint8_t recType) {
@ -632,8 +641,7 @@ class RestApi {
mTimezoneOffset = jsonIn[F("val")];
else if(F("discovery_cfg") == jsonIn[F("cmd")]) {
mApp->setMqttDiscoveryFlag(); // for homeassistant
}
else {
} else {
jsonOut[F("error")] = F("unknown cmd");
return false;
}

90
src/web/html/api.js

@ -33,23 +33,66 @@ iconSuccess = [
/**
* 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 topnav() {
toggle("topnav");
}
function parseMenu(obj) {
var e = document.getElementById("topnav");
e.innerHTML = "";
for(var i = 0; i < obj["name"].length; i ++) {
if(obj["name"][i] == "-")
e.appendChild(span("", ["seperator"]));
else {
var l = link(obj["link"][i], obj["name"][i], obj["trgt"][i]);
if(obj["link"][i] == window.location.pathname)
l.classList.add("active");
e.appendChild(l);
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() {
toggle("topnav", "mobile");
}
function parseNav(obj) {
for(i = 0; i < 10; i++) {
if(i == 2)
continue;
var l = document.getElementById("nav"+i);
if(window.location.pathname == "/" + l.href.split('/').pop())
l.classList.add("active");
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) {
document.getElementById("esp_type").innerHTML="Board: " + obj["esp_type"];
document.getElementById("esp_type").append(
document.createTextNode("Board: " + obj["esp_type"])
);
}
function parseRssi(obj) {
@ -69,7 +114,7 @@ function parseRssi(obj) {
icon = iconWifi1;
else if(obj["wifi_rssi"] <= -70)
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) {
@ -82,12 +127,12 @@ function setHide(id, hide) {
elm.classList.remove('hide');
}
function toggle(id) {
function toggle(id, cl="hide") {
var e = document.getElementById(id);
if(!e.classList.contains("hide"))
e.classList.add("hide");
if(!e.classList.contains(cl))
e.classList.add(cl);
else
e.classList.remove('hide');
e.classList.remove(cl);
}
function getAjax(url, ptr, method="GET", json=null) {
@ -198,11 +243,10 @@ function link(dst, text, target=null) {
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');
s.setAttribute('width', w);
s.setAttribute('height', h);
s.setAttribute('fill', color);
s.setAttribute('viewBox', '0 0 16 16');
if(null != cl) s.setAttribute('class', cl);
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;
}

95
src/web/html/convert.py

@ -2,10 +2,67 @@ import re
import os
import gzip
import glob
import shutil
from datetime import date
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()
def convert2Header(inFile):
f = open(nav, "r")
n = f.read().strip()
f.close()
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]
define = inFile.split(".")[0].upper()
define2 = inFile.split(".")[1].upper()
@ -17,14 +74,19 @@ def convert2Header(inFile):
Path("html/h").mkdir(exist_ok=True)
else:
outName = "h/" + inFileVarName + ".h"
Path("h").mkdir(exist_ok=True)
data = ""
if fileType == "ico":
f = open(inFile, "rb")
data = f.read()
f.close()
else:
f = open(inFile, "r")
data = f.read()
f.close()
if fileType == "html":
data = htmlParts(inFile, "includes/header.html", "includes/nav.html", "includes/footer.html", version)
else:
f = open(inFile, "r")
data = f.read()
f.close()
if fileType == "css":
data = data.replace('\n', '')
@ -53,13 +115,17 @@ def convert2Header(inFile):
f.close()
# delete all files in the 'h' dir
dir = 'h'
wd = 'h'
if os.getcwd()[-4:] != "html":
dir = "web/html/" + dir
wd = "web/html/" + wd
if os.path.exists(dir):
for f in os.listdir(dir):
os.remove(os.path.join(dir, f))
if os.path.exists(wd):
for f in os.listdir(wd):
os.remove(os.path.join(wd, f))
wd += "/tmp"
if os.path.exists(wd):
for f in os.listdir(wd):
os.remove(os.path.join(wd, f))
# grab all files with following extensions
if os.getcwd()[-4:] != "html":
@ -69,6 +135,11 @@ files_grabbed = []
for files in types:
files_grabbed.extend(glob.glob(files))
Path("h").mkdir(exist_ok=True)
Path("tmp").mkdir(exist_ok=True) # created to check if webpages are valid with all replacements
shutil.copyfile("style.css", "tmp/style.css")
version = readVersion("../../defines.h")
# go throw the array
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>
<head>
<title>Index</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
{#HTML_HEADER}
</head>
<body>
<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="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
{#HTML_NAV}
<div id="wrapper">
<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>
<span class="des">Uptime: </span><span id="uptime"></span><br/>
<span class="des">ESP-Time: </span><span id="date"></span>
@ -60,22 +36,7 @@
</div>
</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">
<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>
{#HTML_FOOTER}
<script type="text/javascript">
var exeOnce = true;
var tickCnt = 0;
@ -106,12 +67,8 @@
}
function parseGeneric(obj) {
// Disclaimer
//if(obj["disclaimer"] == false) sessionStorage.setItem("gDisclaimer", promptFunction());
if(exeOnce){
parseVersion(obj);
if(exeOnce)
parseESP(obj);
}
parseRssi(obj);
}
@ -163,14 +120,14 @@
var p = div(["none"]);
for(var i of obj) {
var icon = iconWarn;
var color = "#F70";
var cl = "icon-warn";
avail = "";
if(false == i["enabled"]) {
avail = "disabled";
}
else if(false == i["is_avail"]) {
icon = iconInfo;
color = "#00d";
cl = "icon-info";
avail = "not yet available";
}
else if(0 == i["ts_last_success"]) {
@ -183,12 +140,12 @@
if(false == i["is_producing"])
avail += "not ";
else
color = "#090";
cl = "icon-success";
avail += "producing";
}
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),
br()
);
@ -203,25 +160,25 @@
document.getElementById("iv").replaceChildren(p);
}
function parseWarnInfo(warn, success, version) {
function parseWarnInfo(warn, success) {
var p = div(["none"]);
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) {
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)
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(getVerInt(version) < getVerInt(release))
p.append(svg(iconInfo, 20, 20, "#00d", "icon"), span("Update available, current released version: " + release), br());
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());
if(getVerInt("{#VERSION}") < getVerInt(release))
p.append(svg(iconInfo, 30, 30, "icon icon-info"), span("Update available, current released version: " + release), br());
else if(getVerInt("{#VERSION}") > getVerInt(release))
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
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);
@ -239,11 +196,11 @@
function parse(obj) {
if(null != obj) {
if(exeOnce)
parseMenu(obj["menu"]);
parseNav(obj["generic"]);
parseGeneric(obj["generic"]);
parseSys(obj);
parseIv(obj["inverter"]);
parseWarnInfo(obj["warnings"], obj["infos"], obj["generic"]["version"]);
parseWarnInfo(obj["warnings"], obj["infos"]);
if(exeOnce) {
window.setInterval("tick()", 1000);
exeOnce = false;

35
src/web/html/login.html

@ -2,41 +2,22 @@
<html>
<head>
<title>Login</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
{#HTML_HEADER}
</head>
<body>
<div id="wrapper">
<div id="login">
<div class="pad">
<div class="p-4">
<form action="/login" method="post">
<h2>AhoyDTU</h2>
<input type="password" name="pwd" value="" autofocus>
<input type="submit" name="login" value="login" class="btn">
<div class="row"><h2>AhoyDTU</h2></div>
<div class="row">
<div class="col-8"><input type="password" name="pwd" autofocus></div>
<div class="col-4"><input type="submit" name="login" value="login" class="btn"></div>
</div>
</form>
</div>
</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>
<script type="text/javascript">
function parse(obj) {
parseVersion(obj["general"]);
}
getAjax("/api/generic", parse);
</script>
{#HTML_FOOTER}
</body>
</html>

112
src/web/html/serial.html

@ -2,73 +2,66 @@
<html>
<head>
<title>Serial Console</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
{#HTML_HEADER}
</head>
<body>
<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="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<div class="serial">
<textarea id="serial" cols="80" rows="20" readonly></textarea><br/>
connected: <span class="dot" id="connected"></span>
Uptime: <span id="uptime"></span>
<input type="button" value="clear" class="btn" id="clear"/>
<input type="button" value="autoscroll" class="btn" id="scroll"/>
<div class="hr mt-3 mb-3"></div>
<div class="row">
<textarea id="serial" class="mt-3" cols="80" rows="20" readonly></textarea>
</div>
<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="autoscroll" class="btn" id="scroll"/>
</div>
</div>
<div class="hr my-3"></div>
<div class="row mb-3">
<h3>Commands</h3>
<label for="iv">Select Inverter:</label>
<select name="iv" id="InvID">
</select>
<label for="pwrlimval">Power Limit Value</label>
<input type="number" class="text" name="pwrlimval" maxlength="4"/>
<label for="pwrlimctrl">Power Limit Command</label>
<select name="pwrlimctrl">
<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_relative">relative non persistent [%]</option>
<option value="limit_persistent_absolute">absolute persistent [W]</option>
<option value="limit_persistent_relative">relative persistent [%]</option>
</select>
<br/>
<input type="button" value="Send Power Limit" class="btn" id="sendpwrlim"/>
<div class="hr mt-3 mb-3"></div>
<div id="power" class="mt-3">
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Select Inverter</div>
<div class="col-12 col-sm-9"><select name="iv" id="InvID"></select></div>
</div>
<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">
<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_relative">relative non persistent [%]</option>
<option value="limit_persistent_absolute">absolute persistent [W]</option>
<option value="limit_persistent_relative">relative persistent [%]</option>
</select>
</div>
</div>
<div class="row mb-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="Turn Off" class="btn" id="power_off"/>
<input type="button" value="Turn On" class="btn" id="power_on"/>
</div>
<br/>
<p>Ctrl result: <span id="result">n/a</span></p>
</div>
<div class="row mb-5">
<div class="col-3 my-2">Ctrl result</div>
<div class="col-9 my-2"><span id="result">n/a</span></div>
</div>
</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">
<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>
{#HTML_FOOTER}
<script type="text/javascript">
var mAutoScroll = true;
var con = document.getElementById("serial");
@ -87,22 +80,21 @@
parseRssi(obj);
if(true == exeOnce) {
parseVersion(obj);
parseNav(obj);
parseESP(obj);
window.setInterval("getAjax('/api/generic', parseGeneric)", 10000);
exeOnce = false;
getAjax("/api/setup", parse);
getAjax("/api/inverter/list", parse);
}
}
function parse(root) {
parseMenu(root["menu"]);
select = document.getElementById('InvID');
if(null == root) return;
root = root.inverter;
for(var i = 0; i < root.inverter.length; i++) {
inv = root.inverter[i];
for(var i = 0; i < root.length; i++) {
inv = root[i];
var opt = document.createElement('option');
opt.value = inv.id;
opt.innerHTML = inv.name;

566
src/web/html/setup.html

@ -2,9 +2,7 @@
<html>
<head>
<title>Setup</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
{#HTML_HEADER}
<script type="text/javascript">
function load() {
for(it of document.getElementsByClassName("s_collapsible")) {
@ -18,67 +16,116 @@
</script>
</head>
<body onload="load()">
<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="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<form method="post" action="/save">
<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>
<label for="device">Device Name</label>
<input type="text" name="device" class="text"/>
<div class="row mb-3">
<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>
</div>
<button type="button" class="s_collapsible">Network</button>
<div class="s_content">
<fieldset>
<fieldset class="mb-2">
<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>
<label for="scanbtn">Search Networks</label>
<input type="button" name="scanbtn" id="scanbtn" class="btn" value="scan" onclick="scan()"/><br/>
<label for="networks">Avail Networks</label>
<select name="networks" id="networks" onChange="selNet()">
<option value="-1" selected disabled hidden>not scanned</option>
</select>
<label for="ssid">SSID</label>
<input type="text" name="ssid" class="text"/>
<label for="pwd">Password</label>
<input type="password" class="text" name="pwd" value="{PWD}"/>
<div class="row mb-3">
<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()">
<option value="-1" selected disabled hidden>not scanned</option>
</select>
</div>
</div>
<div class="row mb-2 mb-sm-3">
<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 class="mb-4">
<legend class="des">Static IP (optional)</legend>
<p>
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>
<label for="ipAddr">IP Address</label>
<input type="text" name="ipAddr" class="text" maxlength="15" />
<label for="ipMask">Submask</label>
<input type="text" name="ipMask" class="text" maxlength="15" />
<label for="ipDns1">DNS 1</label>
<input type="text" name="ipDns1" class="text" maxlength="15" />
<label for="ipDns2">DNS 2</label>
<input type="text" name="ipDns2" class="text" maxlength="15" />
<label for="ipGateway">Gateway</label>
<input type="text" name="ipGateway" class="text" maxlength="15" />
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">IP Address</div>
<div class="col-12 col-sm-9"><input type="text" name="ipAddr" maxlength="15" /></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Submask</div>
<div class="col-12 col-sm-9"><input type="text" name="ipMask" maxlength="15" /></div>
</div>
<div class="row mb-3">
<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>
</div>
<button type="button" class="s_collapsible">Protection</button>
<div class="s_content">
<fieldset>
<legend class="des">Protection</legend>
<label for="adminpwd">Admin Password</label>
<input type="password" name="adminpwd" class="text" value="{PWD}"/>
<input type="hidden" name="disclaimer" value="false" id="disclaimer">
<fieldset class="mb-4">
<legend class="des mx-2">Protection</legend>
<div class="row mb-3">
<div class="col-12 col-sm-3 mb-2 mt-2">Admin Password</div>
<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>
<div id="prot_mask"></div>
</fieldset>
@ -86,163 +133,174 @@
<button type="button" class="s_collapsible">Inverter</button>
<div class="s_content">
<fieldset>
<fieldset class="mb-4">
<legend class="des">Inverter</legend>
<div id="inverter"></div><br/>
<input type="button" id="btnAdd" class="btn" value="Add Inverter"/>
<p class="subdes">General</p>
<label for="invInterval">Interval [s]</label>
<input type="text" class="text" name="invInterval" pattern="[0-9]+" title="Invalid input"/>
<label for="invRetry">Max retries per Payload</label>
<input type="text" class="text" name="invRetry"/>
<label for="invRstMid">Reset values and YieldDay at midnight</label>
<input type="checkbox" class="cb" name="invRstMid"/><br/>
<label for="invRstComStop">Reset values when inverter polling stops at sunset</label>
<input type="checkbox" class="cb" name="invRstComStop"/><br/>
<label for="invRstNotAvail">Reset values when inverter status is 'not available'</label>
<input type="checkbox" class="cb" name="invRstNotAvail"/><br/>
<div id="inverter"></div>
<div class="row mb-2">
<div class="col-12 col-sm-3"></div>
<div class="col-12 col-sm-9"><input type="button" id="btnAdd" class="btn" value="Add Inverter"/></div>
</div>
<div class="row mb-2">
<div class="col-12 col-sm-3"><p class="subdes">General</p></div>
<div class="col-12 col-sm-9"></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="invInterval" pattern="[0-9]+" title="Invalid input"/></div>
</div>
<div class="row mb-3">
<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>
</div>
<button type="button" class="s_collapsible">NTP Server</button>
<div class="s_content">
<fieldset>
<legend class="des">NTP Server</legend>
<label for="ntpAddr">NTP Server / IP</label>
<input type="text" class="text" name="ntpAddr"/>
<label for="ntpPort">NTP Port</label>
<input type="text" class="text" name="ntpPort"/>
<label for="ntpBtn">set system time</label>
<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()"/>
<span id="apiResultNtp"></span>
<fieldset class="mb-4">
<legend class="des">NTP Server</legend>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">NTP Server / IP</div>
<div class="col-12 col-sm-9"><input type="text" name="ntpAddr"/></div>
</div>
<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="ntpSync" id="ntpSync" class="btn" value="sync NTP" onclick="syncTime()"/>
<span id="apiResultNtp"></span>
</div>
</div>
</fieldset>
</div>
<button type="button" class="s_collapsible">Sunrise & Sunset</button>
<div class="s_content">
<fieldset>
<legend class="des">Sunrise & Sunset</legend>
<p>
Use a decimal separator: '.' (dot) for Latitude and Longitude
</p>
<label for="sunLat">Latitude (decimal)</label>
<input type="text" class="text" name="sunLat"/>
<label for="sunLon">Longitude (decimal)</label>
<input type="text" class="text" name="sunLon"/>
<label for="sunOffs">Offset (pre sunrise, post sunset)</label>
<select name="sunOffs"></select>
<br>
<label for="sunDisNightCom">Stop polling inverters during night</label>
<input type="checkbox" class="cb" name="sunDisNightCom"/><br/>
<fieldset class="mb-4">
<legend class="des">Sunrise & Sunset</legend>
<p>Use a decimal separator: '.' (dot) for Latitude and Longitude</p>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Latitude (decimal)</div>
<div class="col-12 col-sm-9"><input type="text" name="sunLat"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Longitude (decimal)</div>
<div class="col-12 col-sm-9"><input type="text" name="sunLon"/></div>
</div>
<div class="row mb-3">
<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>
</div>
<button type="button" class="s_collapsible">MQTT</button>
<div class="s_content">
<fieldset>
<legend class="des">MQTT</legend>
<label for="mqttAddr">Broker / Server IP</label>
<input type="text" class="text" name="mqttAddr"/>
<label for="mqttPort">Port</label>
<input type="text" class="text" name="mqttPort"/>
<label for="mqttUser">Username (optional)</label>
<input type="text" class="text" name="mqttUser"/>
<label for="mqttPwd">Password (optional)</label>
<input type="password" class="text" name="mqttPwd"/>
<label for="mqttTopic">Topic</label>
<input type="text" class="text" name="mqttTopic" pattern="[A-Za-z0-9./#$%&=+_-]+" title="Invalid input" />
<fieldset class="mb-4">
<legend class="des">MQTT</legend>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Broker / Server IP</div>
<div class="col-12 col-sm-9"><input type="text" name="mqttAddr"/></div>
</div>
<div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Port</div>
<div class="col-12 col-sm-9"><input type="text" name="mqttPort"/></div>
</div>
<div class="row mb-3">
<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>
<label for="mqttIntvl">Interval [s]</label>
<input type="text" class="text" name="mqttInterval" pattern="[0-9]+" title="Invalid input" />
<label for="mqttBtn">Discovery Config (homeassistant)</label>
<input type="button" name="mqttDiscovery" id="mqttDiscovery" class="btn" value="send" onclick="sendDiscoveryConfig()"/>
<span id="apiResultMqtt"></span>
</fieldset>
</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"/>
<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="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()"/>
<span id="apiResultMqtt"></span>
</div>
</div>
</fieldset>
</div>
<button type="button" class="s_collapsible">Display Config</button>
<div class="s_content">
<fieldset>
<fieldset class="mb-4">
<legend class="des">Display Config</legend>
<div id="dispType"></div>
<label for="logoEn">Show Logo</label>
<input type="checkbox" class="cb" name="logoEn"/><br/>
<label for="dispPwr">Turn off while inverters are offline</label>
<input type="checkbox" class="cb" name="dispPwr"/><br/>
<label for="dispPxSh">Enable pixel shifting</label>
<input type="checkbox" class="cb" name="dispPxSh"/><br/>
<label for="disp180">Rotate 180 degree</label>
<input type="checkbox" class="cb" name="disp180"/><br/>
<label for="dispCont">Contrast</label>
<select name="dispCont" id="contrast"></select>
<div id="dispRot"></div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Turn off while inverters are offline</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="disp_pwr"/></div>
</div>
<div class="row mb-3">
<div class="col-8 col-sm-3">Enable Screensaver (pixel shifting)</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="disp_pxshift"/></div>
</div>
<div class="row mb-3">
<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>
<div id="dispPins"></div>
</fieldset>
</div>
<div class="mt-3">
<label for="reboot">Reboot device after successful save</label>
<input type="checkbox" class="cb" name="reboot" checked />
<input type="submit" value="save" class="btn right"/>
<div class="row mb-4 mt-4">
<div class="col-8 col-sm-3">Reboot device after successful save</div>
<div class="col-2 col-md-6">
<input type="checkbox" name="reboot" checked />
<input type="submit" value="save" class="btn right"/>
</div>
</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>
<fieldset>
<fieldset class="mb-4">
<legend class="des">Upload / Store JSON Settings</legend>
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="upload">
<input type="button" class="btn" value="Upload" onclick="hide()">
</form>
</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>
</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">
<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>
{#HTML_FOOTER}
<script type="text/javascript">
var highestId = 0;
var maxInv = 0;
@ -372,22 +430,38 @@
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) {
highestId = id + 1;
if(highestId == maxInv)
setHide("btnAdd", true);
iv = document.getElementById("inverter");
var iv = document.getElementById("inverter");
iv.appendChild(des("Inverter " + 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");
iv.appendChild(addr);
iv.append(
mlCb(id + "Enable", "Communication Enable", obj["enabled"]),
mlE("Serial Number (12 digits)*", addr)
);
['keyup', 'change'].forEach(function(evt) {
addr.addEventListener(evt, (e) => {
var serial = addr.value.substring(0,4);
@ -397,9 +471,9 @@
setHide(id+"ModName"+i, true);
setHide(id+"YieldCor"+i, true);
}
setHide("lbl"+id+"ModPwr", true);
setHide("lbl"+id+"ModName", true);
setHide("lbl"+id+"YieldCor", true);
setHide("row"+id+"ModPwr", true);
setHide("row"+id+"ModName", true);
setHide("row"+id+"YieldCor", true);
if(serial.charAt(0) == 1) {
if((serial.charAt(1) == 0) || (serial.charAt(1) == 1)) {
@ -423,39 +497,44 @@
setHide(id+"ModName"+i, false);
setHide(id+"YieldCor"+i, false);
}
setHide("lbl"+id+"ModPwr", false);
setHide("lbl"+id+"ModName", false);
setHide("lbl"+id+"YieldCor", false);
setHide("row"+id+"ModPwr", false);
setHide("row"+id+"ModName", false);
setHide("row"+id+"YieldCor", false);
}
})
});
iv.append(
lbl(id + "Name", "Name*"),
inp(id + "Name", obj["name"], 32, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input")
);
iv.append(mlE("Name*", inp(id + "Name", obj["name"], 16, ["text"], null, "text", "[A-Za-z0-9./#$%&=+_-]+", "Invalid input")));
for(var j of [
["ModPwr", "ch_max_power", "Max Module Power (Wp)", 4, "[0-9]+"],
["ModName", "ch_name", "Module Name", 16, null],
["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 16, "[0-9-]+"]]) {
var cl = (re.test(obj["serial"])) ? null : ["hide"];
iv.appendChild(lbl(null, j[2], cl, "lbl" + id + j[0]));
d = div([j[0]]);
["ModName", "ch_name", "Module Name", 15, null],
["YieldCor", "ch_yield_cor", "Yield Total Correction [kWh]", 8, "[0-9-]+"]]) {
var cl = (re.test(obj["serial"])) ? "" : " hide";
i = 0;
cl = (re.test(obj["serial"])) ? ["text", "sh"] : ["text", "sh", "hide"];
arrIn = [];
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++;
}
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");
del.addEventListener("click", delIv);
iv.append(
lbl(id + "lbldel", "Delete"),
del
);
iv.append(mlE("Delete", del));
}
function ivGlob(obj) {
@ -468,24 +547,22 @@
function parseSys(obj) {
for(var i of [["device", "device_name"], ["ssid", "ssid"]])
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"])
e.value = "";
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++) {
var chkd = ((obj["prot_mask"] & (1 << i)) == (1 << i));
var sp = lbl("protMask" + i, a[i]);
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());
var chk = ((obj["prot_mask"] & (1 << i)) == (1 << i));
el.push(mlCb("protMask" + i, a[i], chk))
}
d.append(...el);
}
function parseGeneric(obj) {
parseVersion(obj);
parseNav(obj);
parseESP(obj);
parseRssi(obj);
}
@ -528,35 +605,30 @@
pins = [['led0', 'pinLed0'], ['led1', 'pinLed1']];
for(p of pins) {
e.append(
lbl(p[1], p[0].toUpperCase()),
sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[0]])
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]])
)
])
);
}
}
function parseNrfRadio(obj, type) {
var e = document.getElementById("rf24");
var en = inp("rf24Enable", null, null, ["cb"], "rf24Enable", "checkbox");
en.checked = obj["en"];
e.append(
lbl("rf24Enable", "NRF24 Enable"),
en, br(),
lbl("rf24Power", "Amplifier Power Level"),
sel("rf24Power", [
[0, "MIN"],
[1, "LOW"],
[2, "HIGH"],
[3, "MAX"]
], obj["power_level"])
var e = document.getElementById("rf24").append(
ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, "Power Level"),
ml("div", {class: "col-12 col-sm-9"},
sel("rf24Power", [
[0, "MIN"],
[1, "LOW"],
[2, "HIGH"],
[3, "MAX"]
], 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) {
@ -584,35 +656,56 @@
}
function parseDisplay(obj, type) {
for(var i of [["logoEn", "logo_en"], ["dispPwr", "disp_pwr"], ["dispPxSh", "px_shift"], ["disp180", "rot180"]])
document.getElementsByName(i[0])[0].checked = obj[i[1]];
for(var i of ["disp_pwr", "disp_pxshift"])
document.getElementsByName(i)[0].checked = obj[i];
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) {
e.appendChild(lbl(p[1], p[0].toUpperCase()));
e.appendChild(sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]]));
if(("ESP8266" == type) && p[0] == "cs")
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(
lbl("dispType", "Type"),
sel("dispType", opts, obj["disp_type"])
ml("div", {class: "row mb-3"}, [
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");
for(var i = 30; i < 101; i += 2) {
e.appendChild(opt(i, i, (i == obj["contrast"])));
opts = [[0, "0&deg;"], [2, "180&deg;"]];
if("ESP32" == type) {
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) {
if(null != root) {
parseMenu(root["menu"]);
parseSys(root["system"]);
parseGeneric(root["generic"]);
parseStaticIp(root["static_ip"]);
parseIv(root["inverter"]);
parseMqtt(root["mqtt"]);
parseNtp(root["ntp"]);
parseSun(root["sun"]);
@ -622,6 +715,7 @@
parseCmtRadio(root["radioCmt"], root["system"]["esp_type"]);
parseSerial(root["serial"]);
parseDisplay(root["display"], root["system"]["esp_type"]);
getAjax("/api/inverter/list", parseIv);
}
}
@ -644,11 +738,7 @@
e.value = s.value;
}
hiddenInput = document.getElementById("disclaimer")
hiddenInput.value = sessionStorage.getItem("gDisclaimer");
getAjax("/api/setup", parse);
</script>
</body>
</html>

485
src/web/html/style.css

@ -4,26 +4,39 @@ html, body {
padding: 0;
height: 100%;
min-height: 100%;
background-color: var(--bg);
color: var(--fg);
}
h2 {
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 {
background-color: #333;
background-color: var(--nav-bg);
position: fixed;
top: 0;
width: 100%;
}
.topnav a {
color: #fff;
color: var(--fg2);
padding: 14px 14px;
text-decoration: none;
font-size: 17px;
display: block;
height: 20px;
}
#topnav a {
@ -33,23 +46,26 @@ h2 {
.topnav a.icon {
top: 0;
left: 0;
background: #333;
background: var(--nav-bg);
display: block;
position: absolute;
}
.topnav a:hover {
background-color: #044e86 !important;
color: #000;
background-color: var(--primary-hover) !important;
}
.topnav .info {
color: #fff;
color: var(--fg2);
position: absolute;
right: 24px;
top: 5px;
}
.topnav .mobile {
display: none;
}
svg.icon {
vertical-align: middle;
display: inline-block;
@ -57,8 +73,24 @@ svg.icon {
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 {
background-color: #006ec0;
background-color: var(--primary);
color: #fff !important;
padding-left: 80px !important
}
@ -74,7 +106,7 @@ svg.icon {
}
.topnav .active {
background-color: #555;
background-color: var(--nav-active);
}
span.seperator {
@ -85,6 +117,197 @@ span.seperator {
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 {
min-height: 100%;
}
@ -97,7 +320,6 @@ span.seperator {
#footer {
height: 121px;
margin-top: -121px;
background-color: #555;
width: 100%;
font-size: 13px;
}
@ -131,7 +353,7 @@ span.seperator {
}
.hide {
display: none;
display: none !important;
}
@media only screen and (min-width: 992px) {
@ -152,7 +374,7 @@ span.seperator {
padding-left: 24px !important;
}
.topnav .hide {
.topnav .mobile {
display: block;
}
@ -172,13 +394,6 @@ span.seperator {
}
}
/** old CSS below **/
p {
text-align: justify;
font-size: 13pt;
}
p.lic, p.lic a {
font-size: 8pt;
color: #999;
@ -187,11 +402,11 @@ p.lic, p.lic a {
.des {
margin-top: 20px;
font-size: 13pt;
color: #006ec0;
color: var(--secondary);
}
.s_active, .s_collapsible:hover {
background-color: #044e86;
background-color: var(--primary-hover);
color: #fff;
}
@ -201,34 +416,34 @@ p.lic, p.lic a {
}
.s_collapsible {
background-color: #006ec0;
background-color: var(--primary);
color: white;
cursor: pointer;
padding: 18px;
padding: 12px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
margin-bottom: 4px;
margin-bottom: 5px;
}
.subdes {
font-size: 12pt;
color: #006ec0;
color: var(--secondary);
margin-left: 7px;
}
.subsubdes {
font-size:12pt;
color:#006ec0;
color:var(--secondary);
margin: 0 0 7px 12px;
}
a:link, a:visited {
text-decoration: none;
font-size: 13pt;
color: #006ec0;
color: var(--secondary);
}
a:hover, a:focus {
@ -236,14 +451,14 @@ a:hover, a:focus {
}
a.btn {
background-color: #006ec0;
background-color: var(--primary);
color: #fff;
padding: 7px 15px 7px 15px;
display: inline-block;
}
a.btn:hover {
background-color: #044e86 !important;
background-color: var(--primary-hover) !important;
}
input, select {
@ -251,11 +466,13 @@ input, select {
font-size: 13pt;
}
input.text, select {
width: 70%;
input[type=text], input[type=password], select, input[type=number] {
width: 100%;
box-sizing: border-box;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: var(--input-bg);
color: var(--fg);
}
input.sh {
@ -268,7 +485,7 @@ input.btnDel {
}
input.btn {
background-color: #006ec0;
background-color: var(--primary);
color: #fff;
border: 0px;
padding: 7px 20px 7px 20px;
@ -299,10 +516,6 @@ pre {
white-space: pre-wrap;
}
fieldset {
margin-bottom: 15px;
}
.left {
float: left;
}
@ -311,88 +524,11 @@ fieldset {
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 {
float: left;
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 {
width:70%;
display: inline-block;
@ -443,104 +579,55 @@ div.hr {
}
#login {
width: 300px;
width: 450px;
height: 200px;
border: 1px solid #ccc;
background-color: #eee;
background-color: var(--input-bg);
position: absolute;
top: 50%;
left: 50%;
margin-top: -160px;
margin-left: -150px;
}
#login .pad {
padding: 20px;
}
#login .pad input {
width: 100%;
padding: 7px 0 7px 0;
border: 0px;
margin-bottom: 10px;
margin-left: -225px;
}
.head {
background-color: #006ec0;
background-color: var(--primary);
color: #fff;
}
.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; }
.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{
position: relative;
}
/* 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%; }
.css-tooltip:hover:after{
content:attr(data-tooltip);
background:#000;
padding:5px;
border-radius:3px;
display: inline-block;
position: absolute;
transform: translate(-50%,-100%);
margin:0 auto;
color:#FFF;
min-width:100px;
min-width:150px;
top:-5px;
left: 50%;
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>
<head>
<title>System</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
{#HTML_HEADER}
</head>
<body>
<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="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<pre id="stat"></pre>
@ -26,25 +15,10 @@
<div id="html" class="mt-3 mb-3"></div>
</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">
<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>
{#HTML_FOOTER}
<script type="text/javascript">
function parseGeneric(obj) {
parseVersion(obj);
parseNav(obj);
parseESP(obj);
parseRssi(obj);
}
@ -123,7 +97,6 @@
function parse(obj) {
if(null != obj) {
parseMenu(obj["menu"]);
parseGeneric(obj["generic"]);
if(null != obj["refresh"]) {

54
src/web/html/update.html

@ -2,66 +2,36 @@
<html>
<head>
<title>Update</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="text/javascript" src="api.js"></script>
{#HTML_HEADER}
</head>
<body>
<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="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="update">
<input type="button" class="btn" value="Update" onclick="hide()">
</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">
<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>
<fieldset>
<legend class="des">Select firmware file (*.bin)</legend>
<form id="form" method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="update">
<input type="button" class="btn" value="Update" onclick="hide()">
</form>
</fieldset>
</div>
</div>
{#HTML_FOOTER}
<script type="text/javascript">
function parseGeneric(obj) {
parseVersion(obj);
parseNav(obj);
parseESP(obj);
parseRssi(obj);
}
function parse(obj) {
if(null != obj) {
parseMenu(obj["menu"]);
parseGeneric(obj["generic"]);
}
}
function hide() {
document.getElementById("form").submit();
var e = document.getElementById("content");
e.replaceChildren(span("update started"));
}
getAjax("/api/index", parse);
getAjax("/api/generic", parseGeneric);
</script>
</body>
</html>

287
src/web/html/visualization.html

@ -2,144 +2,227 @@
<html>
<head>
<title>Live</title>
<link rel="stylesheet" type="text/css" href="style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
{#HTML_HEADER}
<meta name="apple-mobile-web-app-capable" content="yes">
<script type="text/javascript" src="api.js"></script>
</head>
<body>
<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="hide"></div>
<div id="wifiicon" class="info"></div>
</div>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<div id="live"></div>
<p>Every <span id="refresh"></span> seconds the values are updated</p>
</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">
<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>
{#HTML_FOOTER}
<script type="text/javascript">
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) {
if(true == exeOnce){
parseVersion(obj);
parseNav(obj);
parseESP(obj);
}
parseRssi(obj);
}
function parseIv(obj, root) {
var ivHtml = [];
var tDiv = div(["ch-all", "iv"]);
tDiv.appendChild(span("Total", ["head"]));
var total = new Array(root.ch0_fld_names.length).fill(0);
if(obj.length > 1)
ivHtml.push(tDiv);
for(var iv of obj) {
if(iv["enabled"]) {
main = div(["iv"]);
var ch0 = div(["ch-iv"]);
var limit = iv["power_limit_read"] + "%";
if(limit == "65535%")
limit = "n/a";
ch0.appendChild(span(iv["name"] + " Limit " + limit, ["head"]));
function numBig(val, unit, des) {
return ml("div", {class: "col-6 col-sm-4 a-c"}, [
ml("div", {class: "row"},
ml("div", {class: "col"}, [
ml("span", {class: "fs-5 fs-md-4"}, String(val)),
ml("span", {class: "fs-6 fs-md-7 mx-1"}, unit)
])),
ml("div", {class: "row"},
ml("div", {class: "col"},
ml("span", {class: "fs-9 px-1"}, des)
)
)
]);
}
for(var j = 0; j < root.ch0_fld_names.length; j++) {
var val = Math.round(iv["ch"][0][j] * 100) / 100;
var sub = div(["subgrp"]);
sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"]));
sub.appendChild(span(root["ch0_fld_names"][j], ["info"]));
ch0.appendChild(sub);
function numMid(val, unit, des) {
return ml("div", {class: "col-6 col-sm-4 col-md-3 mb-2"}, [
ml("div", {class: "row"},
ml("div", {class: "col"}, [
ml("span", {class: "fs-6"}, String(val)),
ml("span", {class: "fs-8 mx-1"}, unit)
])),
ml("div", {class: "row"},
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
}
}
main.appendChild(ch0);
function totals() {
for(var i = 0; i < 5; i++) {
total[i] = Math.round(total[i] * 100) / 100;
}
return ml("div", {class: "row mt-3 mb-5"},
ml("div", {class: "col"}, [
ml("div", {class: "p-2 total-h"},
ml("div", {class: "row"},
ml("div", {class: "col mx-2 mx-md-1"}, "TOTAL")
),
),
ml("div", {class: "p-2 total-bg"}, [
ml("div", {class: "row"}, [
numBig(total[0], "W", "AC Power"),
numBig(total[1], "Wh", "Yield Day"),
numBig(total[2], "kWh", "Yield Total")
]),
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")
])
])
])
);
}
for(var i = 1; i < (iv["channels"] + 1); i++) {
var ch = div(["ch"]);
ch.appendChild(span(("" == iv["ch_names"][i]) ? ("CHANNEL " + i) : iv["ch_names"][i], ["head"]));
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)
)
)
]);
}
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"]));
ch.appendChild(span(root["fld_names"][j], ["info"]));
}
main.appendChild(ch);
}
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")
])
])
]);
}
var ts = div(["ts"]);
var ageInfo = "Last received data requested at: ";
if(iv["ts_last_success"] > 0) {
var date = new Date(iv["ts_last_success"] * 1000);
ageInfo += date.toLocaleString('de-DE');
}
else
ageInfo += "nothing received";
function tsInfo(ts) {
var ageInfo = "Last received data requested at: ";
if(ts > 0) {
var date = new Date(ts * 1000);
ageInfo += date.toLocaleString('de-DE');
}
else
ageInfo += "nothing received";
return ml("div", {class: "mb-5"}, [
ml("div", {class: "row p-1 ts-h mx-2"},
ml("div", {class: "col"}, "")
),
ml("div", {class: "row p-2 ts-bg mx-2"},
ml("div", {class: "col mx-2"}, ageInfo)
)
]);
}
ts.innerHTML = ageInfo;
function parseIv(obj) {
mNum++;
main.appendChild(ts);
ivHtml.push(main);
}
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]));
}
// total
if(obj.length > 1) {
for(var j = 0; j < root.ch0_fld_names.length; j++) {
var val = Math.round(total[j] * 100) / 100;
if((j == 2) || (j == 6) || (j == 7) || (j == 8) || (j == 10)) {
var sub = div(["subgrp"]);
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);
}
mIvHtml.push(
ml("div", {}, [
ivHead(obj),
ml("div", {class: "row mb-2"}, chn),
tsInfo(obj.ts_last_success)
])
);
var last = true;
for(var i = obj.id + 1; i < ivEn.length; i++) {
if((i != ivEn.length) && ivEn[i]) {
last = false;
getAjax("/api/inverter/id/" + i, parseIv);
break;
}
}
document.getElementById("live").replaceChildren(...ivHtml);
if(last) {
if(mNum > 1)
mIvHtml.unshift(totals());
document.getElementById("live").replaceChildren(...mIvHtml);
}
}
function parse(obj) {
if(null != obj) {
if(true == exeOnce)
parseMenu(obj["menu"]);
parseGeneric(obj["generic"]);
parseIv(obj["inverter"], obj);
document.getElementById("refresh").innerHTML = obj["refresh_interval"];
units = Object.assign({}, obj["fld_units"]);
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) {
window.setInterval("getAjax('/api/live', parse)", obj["refresh_interval"] * 1000);
window.setInterval("getAjax('/api/live', parse)", obj["refresh"] * 1000);
exeOnce = false;
}
}

309
src/web/web.h

@ -8,36 +8,35 @@
#include "../utils/dbg.h"
#ifdef ESP32
#include "AsyncTCP.h"
#include "Update.h"
#include "AsyncTCP.h"
#include "Update.h"
#else
#include "ESPAsyncTCP.h"
#include "ESPAsyncTCP.h"
#endif
#include "ESPAsyncWebServer.h"
#include "../appInterface.h"
#include "../hm/hmSystem.h"
#include "../utils/helper.h"
#include "html/h/index_html.h"
#include "html/h/login_html.h"
#include "html/h/style_css.h"
#include "ESPAsyncWebServer.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/setup_html.h"
#include "html/h/visualization_html.h"
#include "html/h/update_html.h"
#include "html/h/index_html.h"
#include "html/h/login_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/update_html.h"
#include "html/h/visualization_html.h"
#define WEB_SERIAL_BUF_SIZE 2048
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinLed0", "pinLed1", "pinCsb", "pinFcsb", "pinGpio3"};
template<class HMSYSTEM>
template <class HMSYSTEM>
class Web {
public:
public:
Web(void) : mWeb(80), mEvts("/events") {
mProtected = true;
mLogoutTimeout = 0;
@ -57,6 +56,7 @@ class Web {
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("/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("/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));
@ -99,18 +99,18 @@ class Web {
}
void tickSecond() {
if(0 != mLogoutTimeout) {
if (0 != mLogoutTimeout) {
mLogoutTimeout -= 1;
if(0 == mLogoutTimeout) {
if(strlen(mConfig->sys.adminPwd) > 0)
if (0 == mLogoutTimeout) {
if (strlen(mConfig->sys.adminPwd) > 0)
mProtected = true;
}
DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout));
}
if(mSerialClientConnnected) {
if(mSerialBufFill > 0) {
if (mSerialClientConnnected) {
if (mSerialBufFill > 0) {
mEvts.send(mSerialBuf, "serial", millis());
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0;
@ -133,23 +133,23 @@ class Web {
void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
mApp->setOnUpdate();
if(!index) {
if (!index) {
Serial.printf("Update Start: %s\n", filename.c_str());
#ifndef ESP32
#ifndef ESP32
Update.runAsync(true);
#endif
if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
#endif
if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
Update.printError(Serial);
}
}
if(!Update.hasError()) {
if(Update.write(data, len) != len){
if (!Update.hasError()) {
if (Update.write(data, len) != len) {
Update.printError(Serial);
}
}
if(final) {
if(Update.end(true)) {
Serial.printf("Update Success: %uB\n", index+len);
if (final) {
if (Update.end(true)) {
Serial.printf("Update Success: %uB\n", index + len);
} else {
Update.printError(Serial);
}
@ -157,27 +157,26 @@ class Web {
}
void onUpload2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if(!index) {
if (!index) {
mUploadFail = false;
mUploadFp = LittleFS.open("/tmp.json", "w");
if(!mUploadFp) {
if (!mUploadFp) {
DPRINTLN(DBG_ERROR, F("can't open file!"));
mUploadFail = true;
mUploadFp.close();
}
}
mUploadFp.write(data, len);
if(final) {
if (final) {
mUploadFp.close();
File fp = LittleFS.open("/tmp.json", "r");
if(!fp)
if (!fp)
mUploadFail = true;
else {
if(!mApp->readSettings("tmp.json")) {
if (!mApp->readSettings("tmp.json")) {
mUploadFail = true;
DPRINTLN(DBG_ERROR, F("upload JSON error!"));
}
else
} else
mApp->saveSettings();
}
DPRINTLN(DBG_INFO, F("upload finished!"));
@ -185,18 +184,17 @@ class Web {
}
void serialCb(String msg) {
if(!mSerialClientConnnected)
if (!mSerialClientConnnected)
return;
msg.replace("\r\n", "<rn>");
if(mSerialAddTime) {
if((9 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
if(mApp->getTimestamp() > 0) {
if (mSerialAddTime) {
if ((9 + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
if (mApp->getTimestamp() > 0) {
strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9);
mSerialBufFill += 9;
}
}
else {
} else {
mSerialBufFill = 0;
mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis());
return;
@ -204,32 +202,44 @@ class Web {
mSerialAddTime = false;
}
if(msg.endsWith("<rn>"))
if (msg.endsWith("<rn>"))
mSerialAddTime = true;
uint16_t length = msg.length();
if((length + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
if ((length + mSerialBufFill) < WEB_SERIAL_BUF_SIZE) {
strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length);
mSerialBufFill += length;
}
else {
} else {
mSerialBufFill = 0;
mEvts.send("webSerial, buffer overflow!<rn>", "serial", millis());
}
}
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) {
DPRINTLN(DBG_VERBOSE, F("onUpdate"));
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) {
if(mProtected) {
request->redirect("/login");
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_UPDATE)) {
if (mProtected) {
checkRedirect(request);
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");
request->send(response);
}
@ -238,34 +248,32 @@ class Web {
bool reboot = !Update.hasError();
String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Update: ");
if(reboot)
if (reboot)
html += "success";
else
html += "failed";
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");
request->send(response);
//if(reboot)
mApp->setRebootFlag();
mApp->setRebootFlag();
}
void onUpload(AsyncWebServerRequest *request) {
bool reboot = !mUploadFail;
String html = F("<!doctype html><html><head><title>Upload</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Upload: ");
if(reboot)
if (reboot)
html += "success";
else
html += "failed";
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");
request->send(response);
//if(reboot)
mApp->setRebootFlag();
mApp->setRebootFlag();
}
void onConnect(AsyncEventSourceClient *client) {
@ -273,7 +281,7 @@ class Web {
mSerialClientConnnected = true;
if(client->lastId())
if (client->lastId())
DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId()));
client->send("hello!", NULL, millis(), 1000);
@ -282,14 +290,14 @@ class Web {
void onIndex(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onIndex"));
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) {
if(mProtected) {
request->redirect("/login");
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_INDEX)) {
if (mProtected) {
checkRedirect(request);
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");
request->send(response);
}
@ -297,14 +305,14 @@ class Web {
void onLogin(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLogin"));
if(request->args() > 0) {
if(String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) {
if (request->args() > 0) {
if (String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) {
mProtected = false;
request->redirect("/");
}
}
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");
request->send(response);
}
@ -312,14 +320,25 @@ class Web {
void onLogout(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLogout"));
if(mProtected) {
request->redirect("/login");
if (mProtected) {
checkRedirect(request);
return;
}
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");
request->send(response);
}
@ -348,22 +367,22 @@ class Web {
}
void showNotFound(AsyncWebServerRequest *request) {
if(mProtected)
request->redirect("/login");
if (mProtected)
checkRedirect(request);
else
request->redirect("/setup");
}
void onReboot(AsyncWebServerRequest *request) {
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");
request->send(response);
}
void showErase(AsyncWebServerRequest *request) {
if(mProtected) {
request->redirect("/login");
if (mProtected) {
checkRedirect(request);
return;
}
@ -373,34 +392,32 @@ class Web {
}
void showFactoryRst(AsyncWebServerRequest *request) {
if(mProtected) {
request->redirect("/login");
if (mProtected) {
checkRedirect(request);
return;
}
DPRINTLN(DBG_VERBOSE, F("showFactoryRst"));
String content = "";
int refresh = 3;
if(request->args() > 0) {
if(request->arg("reset").toInt() == 1) {
if (request->args() > 0) {
if (request->arg("reset").toInt() == 1) {
refresh = 10;
if(mApp->eraseSettings(true))
if (mApp->eraseSettings(true))
content = F("factory reset: success\n\nrebooting ... ");
else
content = F("factory reset: failed\n\nrebooting ... ");
}
else {
} else {
content = F("factory reset: aborted");
refresh = 3;
}
}
else {
} else {
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>");
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>"));
if(refresh == 10) {
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) {
delay(1000);
ESP.restart();
}
@ -409,14 +426,14 @@ class Web {
void onSetup(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSetup"));
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) {
if(mProtected) {
request->redirect("/login");
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SETUP)) {
if (mProtected) {
checkRedirect(request);
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");
request->send(response);
}
@ -424,32 +441,33 @@ class Web {
void showSave(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showSave"));
if(mProtected) {
request->redirect("/login");
if (mProtected) {
checkRedirect(request);
return;
}
if(request->args() == 0)
if (request->args() == 0)
return;
char buf[20] = {0};
// general
if(request->arg("ssid") != "")
if (request->arg("ssid") != "")
request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN);
if(request->arg("pwd") != "{PWD}")
if (request->arg("pwd") != "{PWD}")
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);
mConfig->sys.darkMode = (request->arg("darkMode") == "on");
// protection
if(request->arg("adminpwd") != "{PWD}") {
if (request->arg("adminpwd") != "{PWD}") {
request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN);
mProtected = (strlen(mConfig->sys.adminPwd) > 0);
}
mConfig->sys.protectionMask = 0x0000;
for(uint8_t i = 0; i < 6; i++) {
if(request->arg("protMask" + String(i)) == "on")
for (uint8_t i = 0; i < 6; i++) {
if (request->arg("protMask" + String(i)) == "on")
mConfig->sys.protectionMask |= (1 << i);
}
@ -465,16 +483,15 @@ class Web {
request->arg("ipGateway").toCharArray(buf, 20);
ah::ip2Arr(mConfig->sys.ip.gateway, buf);
// inverter
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
for (uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
iv = mSys->getInverterByPos(i, false);
// enable communication
iv->config->enabled = (request->arg("inv" + String(i) + "Enable") == "on");
// address
request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20);
if(strlen(buf) == 0)
if (strlen(buf) == 0)
memset(buf, 0, 20);
iv->config->serial.u64 = ah::Serial2u64(buf);
switch(iv->config->serial.b[4]) {
@ -488,7 +505,7 @@ class Web {
request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH);
// max channel power / name
for(uint8_t j = 0; j < 4; j++) {
for (uint8_t j = 0; j < 4; j++) {
iv->config->yieldCor[j] = request->arg("inv" + String(i) + "YieldCor" + String(j)).toInt();
iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff;
request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH);
@ -496,13 +513,13 @@ class Web {
iv->initialized = true;
}
if(request->arg("invInterval") != "")
if (request->arg("invInterval") != "")
mConfig->nrf.sendInterval = request->arg("invInterval").toInt();
if(request->arg("invRetry") != "")
if (request->arg("invRetry") != "")
mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt();
mConfig->inst.rstYieldMidNight = (request->arg("invRstMid") == "on");
mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on");
mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on");
mConfig->inst.rstValsCommStop = (request->arg("invRstComStop") == "on");
mConfig->inst.rstValsNotAvail = (request->arg("invRstNotAvail") == "on");
// pinout
uint8_t pin;
@ -528,13 +545,13 @@ class Web {
mConfig->cmt.enabled = (request->arg("cmtEnable") == "on");
// ntp
if(request->arg("ntpAddr") != "") {
if (request->arg("ntpAddr") != "") {
request->arg("ntpAddr").toCharArray(mConfig->ntp.addr, NTP_ADDR_LEN);
mConfig->ntp.port = request->arg("ntpPort").toInt() & 0xffff;
}
// sun
if(request->arg("sunLat") == "" || (request->arg("sunLon") == "")) {
if (request->arg("sunLat") == "" || (request->arg("sunLon") == "")) {
mConfig->sun.lat = 0.0;
mConfig->sun.lon = 0.0;
mConfig->sun.disNightCom = false;
@ -547,47 +564,48 @@ class Web {
}
// mqtt
if(request->arg("mqttAddr") != "") {
if (request->arg("mqttAddr") != "") {
String addr = request->arg("mqttAddr");
addr.trim();
addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN);
}
else
} else
mConfig->mqtt.broker[0] = '\0';
request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN);
if(request->arg("mqttPwd") != "{PWD}")
if (request->arg("mqttPwd") != "{PWD}")
request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN);
request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN);
mConfig->mqtt.port = request->arg("mqttPort").toInt();
mConfig->mqtt.interval = request->arg("mqttInterval").toInt();
// serial console
if(request->arg("serIntvl") != "") {
if (request->arg("serIntvl") != "") {
mConfig->serial.interval = request->arg("serIntvl").toInt() & 0xffff;
mConfig->serial.debug = (request->arg("serDbg") == "on");
mConfig->serial.debug = (request->arg("serDbg") == "on");
mConfig->serial.showIv = (request->arg("serEn") == "on");
// Needed to log TX buffers to serial console
// mSys->Radio.mSerialDebug = mConfig->serial.debug;
}
// display
mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("dispPwr") == "on");
mConfig->plugin.display.logoEn = (request->arg("logoEn") == "on");
mConfig->plugin.display.pxShift = (request->arg("dispPxSh") == "on");
mConfig->plugin.display.rot180 = (request->arg("disp180") == "on");
mConfig->plugin.display.type = request->arg("dispType").toInt();
mConfig->plugin.display.contrast = request->arg("dispCont").toInt();
mConfig->plugin.display.pin0 = request->arg("pinDisp0").toInt();
mConfig->plugin.display.pin1 = request->arg("pinDisp1").toInt();
mConfig->plugin.display.pwrSaveAtIvOffline = (request->arg("disp_pwr") == "on");
mConfig->plugin.display.pxShift = (request->arg("disp_pxshift") == "on");
mConfig->plugin.display.rot = request->arg("disp_rot").toInt();
mConfig->plugin.display.type = request->arg("disp_typ").toInt();
mConfig->plugin.display.contrast = request->arg("disp_cont").toInt();
mConfig->plugin.display.disp_data = request->arg("disp_data").toInt();
mConfig->plugin.display.disp_clk = request->arg("disp_clk").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();
if(request->arg("reboot") == "on")
if (request->arg("reboot") == "on")
onReboot(request);
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");
request->send(response);
}
@ -596,15 +614,17 @@ class Web {
void onLive(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLive"));
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) {
if(mProtected) {
request->redirect("/login");
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_LIVE)) {
if (mProtected) {
checkRedirect(request);
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-type"), "text/html; charset=UTF-8");
request->send(response);
}
@ -675,21 +695,21 @@ class Web {
void onDebug(AsyncWebServerRequest *request) {
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);
}
void onSerial(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSerial"));
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) {
if(mProtected) {
request->redirect("/login");
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SERIAL)) {
if (mProtected) {
checkRedirect(request);
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");
request->send(response);
}
@ -697,36 +717,36 @@ class Web {
void onSystem(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSystem"));
if(CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) {
if(mProtected) {
request->redirect("/login");
if (CHECK_MASK(mConfig->sys.protectionMask, PROT_MASK_SYSTEM)) {
if (mProtected) {
checkRedirect(request);
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");
request->send(response);
}
#ifdef ENABLE_JSON_EP
#ifdef ENABLE_JSON_EP
void showJson(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showJson"));
String modJson;
Inverter<> *iv;
record_t<> *rec;
char topic[40], val[25];
char topic[40], val[25];
modJson = F("{\n");
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
iv = mSys->getInverterByPos(id);
if(NULL == iv)
if (NULL == iv)
continue;
rec = iv->getRecordStruct(RealTimeRunData_Debug);
snprintf(topic, 30, "\"%s\": {\n", iv->config->name);
modJson += String(topic);
for(uint8_t i = 0; i < rec->length; i++) {
for (uint8_t i = 0; i < rec->length; i++) {
snprintf(topic, 40, "\t\"ch%d/%s\"", rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i, rec), iv->getUnit(i, rec));
modJson += String(topic) + ": " + String(val) + F(",\n");
@ -853,8 +873,7 @@ class Web {
// TODO: find the right one channel with the alarm id
alarmChannelId = 0;
// printf("AlarmData Length %d\n",rec->length);
if (alarmChannelId < rec->length)
{
if (alarmChannelId < rec->length) {
//uint8_t channel = rec->assign[alarmChannelId].ch;
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());

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.
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
-----------------------
Some modules are not installed by default on a RaspberryPi, therefore add them manually:
```
```code
pip install crcmod pyyaml paho-mqtt SunTimes
```
@ -112,7 +179,7 @@ Python parameters
The application describes itself
```
```code
python3 -m hoymiles --help
usage: hoymiles [-h] -c [CONFIG_FILE] [--log-transactions] [--verbose]
@ -180,7 +247,7 @@ Todo
- Ability to talk to multiple inverters
- MQTT gateway
- understand channel hopping
- configurable polling interval
- ~~configurable polling interval~~ done: interval ist configurable in ahoy.yml
- commands
- picture of setup!
- python module

6
tools/rpi/ahoy.service

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

13
tools/rpi/ahoy.yml.example

@ -31,7 +31,7 @@ ahoy:
QoS: 0
Retain: True
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!"
# Influx2 output
@ -96,6 +96,7 @@ ahoy:
dtu:
serial: 99978563001
name: my_DTU_name
inverters:
- name: 'balkon'
@ -103,14 +104,14 @@ ahoy:
txpower: 'low' # txpower per inverter (min,low,high,max)
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
- 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_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_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_maxpower: 410 # String 4 max power in Wp
s_maxpower: 410 # String 4 max power in inverter

41
tools/rpi/hoymiles/__init__.py

@ -11,8 +11,28 @@ import re
from datetime import datetime
import logging
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 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_crc8 = crcmod.mkCrcFun(0x101, initCrc=0, xorOut=0)
@ -158,15 +178,26 @@ class ResponseDecoder(ResponseDecoderFactory):
model = self.inverter_model
command = self.request_command
c_datetime = self.time_rx.strftime("%Y-%m-%d %H:%M:%S.%f")
logging.info(f'{c_datetime} model_decoder: {model}Decode{command.upper()}')
if HOYMILES_DEBUG_LOGGING:
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')
if hasattr(model_decoders, f'{model}Decode{command.upper()}'):
device = getattr(model_decoders, f'{model}Decode{command.upper()}')
else:
if HOYMILES_DEBUG_LOGGING:
device = getattr(model_decoders, 'DebugDecodeAny')
device = getattr(model_decoders, 'DebugDecodeAny')
return device(self.response,
time_rx=self.time_rx,

62
tools/rpi/hoymiles/__main__.py

@ -33,6 +33,12 @@ def signal_handler(sig_num, frame):
if mqtt_client:
mqtt_client.disco()
if influx_client:
influx_client.disco()
if volkszaehler_client:
volkszaehler_client.disco()
sys.exit(0)
signal(SIGINT, signal_handler) # Interrupt from keyboard (CTRL + C)
@ -75,7 +81,6 @@ class SunsetHandler:
else:
logging.info('Sunset disabled.')
def checkWaitForSunrise(self):
if not self.suntimes:
return
@ -94,6 +99,23 @@ class SunsetHandler:
time.sleep(time_to_sleep)
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):
"""Main loop"""
inverters = [
@ -101,7 +123,9 @@ def main_loop(ahoy_config):
if not inverter.get('disabled', False)]
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)
try:
@ -112,6 +136,11 @@ def main_loop(ahoy_config):
t_loop_start = time.time()
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:
logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}')
poll_inverter(inverter, dtu_ser, do_init, 3)
@ -122,8 +151,6 @@ def main_loop(ahoy_config):
if time_to_sleep > 0:
time.sleep(time_to_sleep)
except KeyboardInterrupt:
sys.exit()
except Exception as e:
logging.fatal('Exception catched: %s' % e)
logging.fatal(traceback.print_exc())
@ -174,13 +201,14 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
response = com.get_payload()
payload_ttl = 0
except Exception as e_all:
logging.error(f'Error while retrieving data: {e_all}')
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
logging.error(f'Error while retrieving data: {e_all}')
pass
# Handle the response data if any
if response:
c_datetime = datetime.now()
if hoymiles.HOYMILES_DEBUG_LOGGING:
if hoymiles.HOYMILES_TRANSACTION_LOGGING:
c_datetime = datetime.now()
logging.debug(f'{c_datetime} Payload: ' + hoymiles.hexify_payload(response))
# prepare decoder object
@ -195,7 +223,7 @@ def poll_inverter(inverter, dtu_ser, do_init, retries):
# get decoder object
result = decoder.decode()
if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.info(f'{c_datetime} Decoded: {result.__dict__()}')
logging.info(f'Decoded: {result.__dict__()}')
# check decoder object for output
if isinstance(result, hoymiles.decoders.StatusResponse):
@ -281,7 +309,13 @@ def init_logging(ahoy_config):
lvl = logging.WARNING
elif level == '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)
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__':
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}')
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:
hoymiles.HOYMILES_TRANSACTION_LOGGING=True
if global_config.verbose:
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
for radio_config in ahoy_config.get('nrf', [{}]):
hmradio = hoymiles.HoymilesNRF(**radio_config)
# create MQTT - client object
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):
from .outputs import MqttOutputPlugin
mqtt_client = MqttOutputPlugin(mqtt_config)
# create INFLUX - client object
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):
from .outputs import InfluxOutputPlugin
influx_client = InfluxOutputPlugin(

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

@ -99,6 +99,7 @@ class StatusResponse(Response):
frequency = None
powerfactor = None
event_count = None
unpack_error = False
def unpack(self, fmt, base):
"""
@ -110,6 +111,10 @@ class StatusResponse(Response):
:rtype: tuple
"""
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])
@property
@ -150,6 +155,7 @@ class StatusResponse(Response):
s_exists = False
string_id = len(strings)
string = {}
string['name'] = self.inv_strings[string_id]['s_name']
for key in self.string_keys:
prop = f'dc_{key}_{string_id}'
if hasattr(self, prop):
@ -193,7 +199,8 @@ class StatusResponse(Response):
data['event_count'] = self.event_count
data['time'] = self.time_rx
return data
if not self.unpack_error:
return data
class UnknownResponse(Response):
"""
@ -321,9 +328,9 @@ class EventsResponse(UnknownResponse):
#logging.debug(' payload has valid modbus crc')
self.response = self.response[:-2]
status = struct.unpack('>H', self.response[:2])[0]
a_text = self.alarm_codes.get(status, 'N/A')
logging.info (f' Inverter status: {a_text} ({status})')
self.status = struct.unpack('>H', self.response[:2])[0]
self.a_text = self.alarm_codes.get(self.status, 'N/A')
logging.info (f'Inverter status: {self.a_text} ({self.status})')
chunk_size = 12
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]) + ': ')
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])
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}')
dbg = ''
@ -341,6 +351,14 @@ class EventsResponse(UnknownResponse):
dbg += f' {fmt:7}: ' + str(struct.unpack('>' + fmt, chunk))
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):
def __init__(self, *args, **params):
super().__init__(*args, **params)
@ -361,9 +379,14 @@ class HardwareInfoResponse(UnknownResponse):
def __dict__(self):
""" Base values, availabe in each __dict__ call """
responce_info = self.response
logging.info(f'HardwareInfoResponse: {struct.unpack(">HHHHHHHH", responce_info)}')
data = super().__dict__()
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_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'HW revision {hw_id}')
data = super().__dict__()
data['FW_ver_maj'] = fw_version_maj
data['FW_ver_min'] = fw_version_min
data['FW_ver_pat'] = fw_version_pat
@ -468,6 +490,8 @@ class Hm300Decode0B(StatusResponse):
""" String 1 irratiation in percent """
if self.inv_strings is 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)
@property
@ -540,6 +564,8 @@ class Hm600Decode0B(StatusResponse):
""" String 1 irratiation in percent """
if self.inv_strings is 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)
@property
@ -567,6 +593,8 @@ class Hm600Decode0B(StatusResponse):
""" String 2 irratiation in percent """
if self.inv_strings is 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)
@property
@ -647,6 +675,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 1 irratiation in percent """
if self.inv_strings is 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)
@property
@ -674,6 +704,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 2 irratiation in percent """
if self.inv_strings is 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)
@property
@ -701,6 +733,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 3 irratiation in percent """
if self.inv_strings is 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)
@property
@ -728,6 +762,8 @@ class Hm1200Decode0B(StatusResponse):
""" String 4 irratiation in percent """
if self.inv_strings is 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)
@property

76
tools/rpi/hoymiles/outputs.py

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

Loading…
Cancel
Save