diff --git a/Getting_Started.md b/Getting_Started.md index 4d35903c..d66aeca0 100644 --- a/Getting_Started.md +++ b/Getting_Started.md @@ -217,6 +217,14 @@ Once your Ahoy DTU is running, you can use the Over The Air (OTA) capabilities t ! ATTENTION: If you update from a very low version to the newest, please make sure to wipe all flash data! +#### Flashing on Linux with `esptool.py` (ESP32) +1. install [esptool.py](https://docs.espressif.com/projects/esptool/en/latest/esp32/) if you haven't already. +2. download and extract the latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases) +3. `cd ahoy_v && cp *esp32.bin esp32.bin` +4. Perhaps you need to replace `/dev/ttyUSB0` to match your acual device in the following command. Execute it afterwards: `esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset write_flash --flash_mode dout --flash_freq 40m --flash_size detect 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 esp32.bin` +5. Unplug and replug your device. +6. Open a serial monitor (e.g. Putty) @ 115200 Baud. You should see some messages regarding wifi. + ## Connect to your Ahoy DTU When everything is wired up and the firmware is flashed, it is time to connect to your Ahoy DTU. diff --git a/User_Manual.md b/User_Manual.md index 242a2809..a7be8f80 100644 --- a/User_Manual.md +++ b/User_Manual.md @@ -321,6 +321,19 @@ Send Power Limit: - A persistent limit is only needed if you want to throttle your inverter permanently or you can use it to set a start value on the battery, which is then always the switch-on limit when switching on, otherwise it would ramp up to 100% without regulation, which is continuous load is not healthy. - You can set a new limit in the turn-off state, which is then used for on (switching on again), otherwise the last limit from before the turn-off is used, but of course this only applies if DC voltage is applied the whole time. - If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit. +### Update your AHOY-DTU Firmware +To update your AHOY-DTU, you have to download the latest firmware package. +Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip) available for download. +As soon as you have downloaded the firmware package, unzip it. On the WebUI, navigate to Update and press on select firmware file. +From the unzipped files, select the right .bin file for your hardware and needs. +- If you use an ESP8266, select the file ending with esp8266.bin +- If you use an ESP8266 with prometheus, select the file ending with esp8266_prometheus.bin +- If you use an ESP32, select the file ending with esp32.bin +- If you use an ESP32 with prometheus, select the file ending with esp32_prometheus.bin + +Note: if you want to use prometheus, the usage of an ESP32 is recommended, since the ESP8266 is at its performance limits and therefore can cause stability issues. + +After selecting the right firmware file, press update. Your AHOY-DTU will now install the new firmware and reboot. ## Additional Notes ### MI Inverters diff --git a/src/CHANGES.md b/src/CHANGES.md index 9f83f7e7..6b97e319 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,5 +1,16 @@ # Development Changes +## 0.6.15 - 2023-05-25 +* improved Prometheus Endpoint PR #958 +* fix turn off ePaper only if setting was set #956 +* improved reset values and update MqTT #957 + +## 0.6.14 - 2023-05-21 +* merge PR #902 Mono-Display + +## 0.6.13 - 2023-05-16 +* merge PR #934 (fix JSON API) and #944 (update manual) + ## 0.6.12 - 2023-04-28 * improved MqTT * fix menu active item diff --git a/src/app.cpp b/src/app.cpp index 173eb50c..eaebb8a8 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -325,6 +325,7 @@ void app::tickComm(void) { //----------------------------------------------------------------------------- void app::tickZeroValues(void) { Inverter<> *iv; + bool changed = false; // set values to zero, except yields for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); @@ -332,7 +333,11 @@ void app::tickZeroValues(void) { continue; // skip to next inverter mPayload.zeroInverterValues(iv); + changed = true; } + + if(changed) + payloadEventListener(RealTimeRunData_Debug); } //----------------------------------------------------------------------------- @@ -340,15 +345,21 @@ void app::tickMinute(void) { // only triggered if 'reset values on no avail is enabled' Inverter<> *iv; + bool changed = false; // set values to zero, except yields for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL == iv) 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); + changed = true; + } } + + if(changed) + payloadEventListener(RealTimeRunData_Debug); } //----------------------------------------------------------------------------- @@ -359,16 +370,20 @@ void app::tickMidnight(void) { onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); Inverter<> *iv; + bool changed = false; // set values to zero, except yield total for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL == iv) continue; // skip to next inverter - mPayload.zeroInverterValues(iv); - mPayload.zeroYieldDay(iv); + mPayload.zeroInverterValues(iv, false); + changed = true; } + if(changed) + payloadEventListener(RealTimeRunData_Debug); + if (mMqttEnabled) mMqtt.tickerMidnight(); } diff --git a/src/defines.h b/src/defines.h index cf9395d5..baaea8b6 100644 --- a/src/defines.h +++ b/src/defines.h @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 6 -#define VERSION_PATCH 12 +#define VERSION_PATCH 15 //------------------------------------- typedef struct { diff --git a/src/hm/hmPayload.h b/src/hm/hmPayload.h index 0ec40d22..5c66f1f8 100644 --- a/src/hm/hmPayload.h +++ b/src/hm/hmPayload.h @@ -93,17 +93,7 @@ class HmPayload { notify(0x0b); }*/ - void zeroYieldDay(Inverter<> *iv) { - DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - uint8_t pos; - for(uint8_t ch = 0; ch <= iv->channels; ch++) { - pos = iv->getPosByChFld(ch, FLD_YD, rec); - iv->setValue(pos, rec, 0.0f); - } - } - - void zeroInverterValues(Inverter<> *iv) { + void zeroInverterValues(Inverter<> *iv, bool skipYieldDay = true) { DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); for(uint8_t ch = 0; ch <= iv->channels; ch++) { @@ -111,15 +101,18 @@ class HmPayload { for(uint8_t fld = 0; fld < FLD_EVT; fld++) { switch(fld) { case FLD_YD: + if(skipYieldDay) + continue; + else + break; case FLD_YT: continue; } pos = iv->getPosByChFld(ch, fld, rec); iv->setValue(pos, rec, 0.0f); } + iv->doCalculations(); } - - notify(RealTimeRunData_Debug); } void ivSendHighPrio(Inverter<> *iv) { diff --git a/src/platformio.ini b/src/platformio.ini index e8f31e18..46e46498 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -30,7 +30,7 @@ lib_deps = bblanchon/ArduinoJson @ ^6.21.2 https://github.com/JChristensen/Timezone @ ^1.2.4 olikraus/U8g2 @ ^2.34.17 - zinggjm/GxEPD2 @ ^1.5.0 + zinggjm/GxEPD2 @ ^1.5.2 [env:esp8266-release] @@ -98,7 +98,7 @@ monitor_filters = log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory [env:esp32-wroom32-release] -platform = espressif32@>=6.1.0 +platform = espressif32@6.1.0 board = lolin_d32 build_flags = -D RELEASE -std=gnu++17 build_unflags = -std=gnu++11 @@ -109,7 +109,7 @@ monitor_filters = esp32_exception_decoder [env:esp32-wroom32-release-prometheus] -platform = espressif32@>=6.1.0 +platform = espressif32@6.1.0 board = lolin_d32 build_flags = -D RELEASE -std=gnu++17 @@ -122,7 +122,7 @@ monitor_filters = esp32_exception_decoder [env:esp32-wroom32-debug] -platform = espressif32@>=6.1.0 +platform = espressif32@6.1.0 board = lolin_d32 build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE @@ -166,7 +166,7 @@ monitor_filters = esp32_exception_decoder [env:opendtufusionv1-release] -platform = espressif32@>=6.1.0 +platform = espressif32@6.1.0 board = esp32-s3-devkitc-1 upload_protocol = esp-builtin upload_speed = 115200 diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 1a0222b2..ba187c7d 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -7,108 +7,126 @@ #include "../../hm/hmSystem.h" #include "../../utils/helper.h" #include "Display_Mono.h" +#include "Display_Mono_128X32.h" +#include "Display_Mono_128X64.h" +#include "Display_Mono_84X48.h" #include "Display_ePaper.h" template class Display { public: - Display() {} + Display() {} + + void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { + mCfg = cfg; + mSys = sys; + mUtcTs = utcTs; + mNewPayload = false; + mLoopCnt = 0; + mVersion = version; + + if (mCfg->type == 0) + return; + + if ((0 < mCfg->type) && (mCfg->type < 10)) { + switch (mCfg->type) { + case 2: + case 1: + default: + mMono = new DisplayMono128X64(); + break; + case 3: + mMono = new DisplayMono84X48(); + break; + case 4: + mMono = new DisplayMono128X32(); + break; + } + mMono->config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); + mMono->init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); + } else if (mCfg->type >= 10) { +#if defined(ESP32) + mRefreshCycle = 0; + mEpaper.config(mCfg->rot, mCfg->pwrSaveAtIvOffline); + 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 (mMono != NULL) + mMono->loop(); - void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { - mCfg = cfg; - mSys = sys; - mUtcTs = utcTs; + if (mNewPayload || ((++mLoopCnt % 10) == 0)) { mNewPayload = false; mLoopCnt = 0; - mVersion = version; - - if (mCfg->type == 0) - return; - - if ((0 < 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, 0xff, 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 - } + DataScreen(); } - - void payloadEventListener(uint8_t cmd) { - mNewPayload = true; + } + + 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); } - void tickerSecond() { - mMono.loop(); - if (mNewPayload || ((++mLoopCnt % 10) == 0)) { - mNewPayload = false; - mLoopCnt = 0; - DataScreen(); - } + if ((0 < mCfg->type) && (mCfg->type < 10) && (mMono != NULL)) { + mMono->disp(totalPower, totalYieldDay, totalYieldTotal, isprod); + } else if (mCfg->type >= 10) { +#if defined(ESP32) + mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); + mRefreshCycle++; +#endif } - 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 ((0 < mCfg->type) && (mCfg->type < 10)) { - mMono.disp(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 +#if defined(ESP32) + if (mRefreshCycle > 480) { + mEpaper.fullRefresh(); + mRefreshCycle = 0; } - - // 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 + } + + // 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__*/ diff --git a/src/plugins/Display/Display_Mono.cpp b/src/plugins/Display/Display_Mono.cpp deleted file mode 100644 index d55b6061..00000000 --- a/src/plugins/Display/Display_Mono.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#include "Display_Mono.h" - -#ifdef ESP8266 - #include -#elif defined(ESP32) - #include -#endif -#include "../../utils/helper.h" - -//#ifdef U8X8_HAVE_HW_SPI -//#include -//#endif -//#ifdef U8X8_HAVE_HW_I2C -//#include -//#endif - -DisplayMono::DisplayMono() { - mEnPowerSafe = true; - mEnScreenSaver = true; - mLuminance = 60; - _dispY = 0; - mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) - mUtcTs = NULL; - mType = 0; -} - - - -void DisplayMono::init(uint8_t type, uint8_t rotation, 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 *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); - mType = type; - switch(type) { - case 1: - mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); - break; - default: - case 2: - mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); - break; - case 3: - mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset); - break; - } - - mUtcTs = utcTs; - - mDisplay->begin(); - - mIsLarge = (mDisplay->getWidth() > 120); - calcLineHeights(); - - mDisplay->clearBuffer(); - if (3 != mType) - 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(void) { - if (mEnPowerSafe) - if(mTimeout != 0) - mTimeout--; -} - -void DisplayMono::disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { - - - mDisplay->clearBuffer(); - - // set Contrast of the Display to raise the lifetime - if (3 != mType) - 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 on", 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 += (mEnScreenSaver) ? (_mExtra % 7) : 0; - mDisplay->drawStr(dispX, mLineOffsets[line], text); -} diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index ad04c9f4..ed9154af 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -1,38 +1,45 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- +#pragma once #include #define DISP_DEFAULT_TIMEOUT 60 // in seconds -#define DISP_FMT_TEXT_LEN 32 +#define DISP_FMT_TEXT_LEN 32 +#define BOTTOM_MARGIN 5 + + +#ifdef ESP8266 +#include +#elif defined(ESP32) +#include +#endif +#include "../../utils/helper.h" 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(void); - void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); + DisplayMono() {}; - private: - void calcLineHeights(); - void setFont(uint8_t line); - void printText(const char* text, uint8_t line, uint8_t dispX = 5); + virtual 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) = 0; + virtual void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) = 0; + virtual void loop(void) = 0; + virtual void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) = 0; + protected: U8G2* mDisplay; uint8_t mType; bool mEnPowerSafe, mEnScreenSaver; uint8_t mLuminance; - bool mIsLarge = false; uint8_t mLoopCnt; uint32_t* mUtcTs; - uint8_t mLineOffsets[5]; + uint8_t mLineXOffsets[5]; + uint8_t mLineYOffsets[5]; - uint16_t _dispY; + uint16_t mDispY; - uint8_t _mExtra; + uint8_t mExtra; uint16_t mTimeout; - char _fmtText[DISP_FMT_TEXT_LEN]; -}; + char mFmtText[DISP_FMT_TEXT_LEN];}; diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h new file mode 100644 index 00000000..9d5ade7e --- /dev/null +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -0,0 +1,155 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#pragma once +#include "Display_Mono.h" + +class DisplayMono128X32 : public DisplayMono { + public: + DisplayMono128X32() : DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + mExtra = 0; + mDispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; + } + + + void init(uint8_t type, uint8_t rotation, 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)) + return; + + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + mDisplay = new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, reset, clock, data); + + mUtcTs = utcTs; + + mDisplay->begin(); + + calcLinePositions(); + + mDisplay->clearBuffer(); + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0); + printText("ahoydtu.de", 2); + printText(version, 3); + mDisplay->sendBuffer(); + } + + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; + } + + void loop(void) { + if (mEnPowerSafe) { + if (mTimeout != 0) + mTimeout--; + } + } + + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + if (totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + + printText(mFmtText, 0); + } else { + printText("offline", 0); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(mFmtText, 1); + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(mFmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(mExtra % 10) && (ip)) + printText(ip.toString().c_str(), 3); + else if (!(mExtra % 5)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(mFmtText, 3); + } else if (NULL != mUtcTs) + printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + + mDisplay->sendBuffer(); + + mDispY = 0; + mExtra++; + } + + private: + void calcLinePositions() { + uint8_t yOff[] = {0, 0}; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff[getColumn(i)] += (mDisplay->getMaxCharHeight()); + mLineYOffsets[i] = yOff[getColumn(i)]; + if (isTwoRowLine(i)) + yOff[getColumn(i)] += mDisplay->getMaxCharHeight(); + yOff[getColumn(i)] += BOTTOM_MARGIN; + mLineXOffsets[i] = (getColumn(i) == 1 ? 80 : 0); + } + } + + inline void setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont(u8g2_font_9x15_tf); + break; + case 3: + mDisplay->setFont(u8g2_font_tom_thumb_4x6_tf); + break; + default: + mDisplay->setFont(u8g2_font_tom_thumb_4x6_tf); + break; + } + } + + inline uint8_t getColumn(uint8_t line) { + if (line >= 1 && line <= 2) + return 1; + else + return 0; + } + + inline bool isTwoRowLine(uint8_t line) { + return ((line >= 1) && (line <= 2)); + } + + void printText(const char *text, uint8_t line) { + setFont(line); + + uint8_t dispX = mLineXOffsets[line] + ((mEnScreenSaver) ? (mExtra % 7) : 0); + + if (isTwoRowLine(line)) { + String stringText = String(text); + int space = stringText.indexOf(" "); + mDisplay->drawStr(dispX, mLineYOffsets[line], stringText.substring(0, space).c_str()); + if (space > 0) + mDisplay->drawStr(dispX, mLineYOffsets[line] + mDisplay->getMaxCharHeight(), stringText.substring(space + 1).c_str()); + } else + mDisplay->drawStr(dispX, mLineYOffsets[line], text); + } +}; diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h new file mode 100644 index 00000000..3d4f91ee --- /dev/null +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#pragma once +#include "Display_Mono.h" + +class DisplayMono128X64 : public DisplayMono { + public: + DisplayMono128X64() : DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + mDispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; + } + + void init(uint8_t type, uint8_t rotation, 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)) + return; + + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + + switch (type) { + case 1: + mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); + break; + default: + case 2: + mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); + break; + } + + mUtcTs = utcTs; + + mDisplay->begin(); + calcLinePositions(); + + mDisplay->clearBuffer(); + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0, 35); + printText("ahoydtu.de", 2, 20); + printText(version, 3, 46); + mDisplay->sendBuffer(); + } + + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; + } + + void loop(void) { + if (mEnPowerSafe) { + if (mTimeout != 0) + mTimeout--; + } + } + + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + + if (totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + + printText(mFmtText, 0); + } else { + printText("offline", 0, 25); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(mFmtText, 1); + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(mFmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(mExtra % 10) && (ip)) + printText(ip.toString().c_str(), 3); + else if (!(mExtra % 5)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(mFmtText, 3); + } else if (NULL != mUtcTs) + printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + + mDisplay->sendBuffer(); + + mDispY = 0; + mExtra++; + } + + private: + void calcLinePositions() { + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (mDisplay->getMaxCharHeight()); + mLineYOffsets[i] = yOff; + } + } + + inline void setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont(u8g2_font_ncenB14_tr); + break; + case 3: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + default: + mDisplay->setFont(u8g2_font_ncenB10_tr); + break; + } + } + void printText(const char *text, uint8_t line, uint8_t dispX = 5) { + setFont(line); + + dispX += (mEnScreenSaver) ? (mExtra % 7) : 0; + mDisplay->drawStr(dispX, mLineYOffsets[line], text); + } +}; diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h new file mode 100644 index 00000000..82aa83fa --- /dev/null +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -0,0 +1,132 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#pragma once +#include "Display_Mono.h" + +class DisplayMono84X48 : public DisplayMono { + public: + DisplayMono84X48() : DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + mExtra = 0; + mDispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; + } + + void init(uint8_t type, uint8_t rotation, 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)) + return; + + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset); + + mUtcTs = utcTs; + + mDisplay->begin(); + calcLinePositions(); + + mDisplay->clearBuffer(); + if (3 != mType) + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0); + printText("ahoydtu.de", 2); + printText(version, 3); + mDisplay->sendBuffer(); + } + + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; + } + + void loop(void) { + if (mEnPowerSafe) { + if (mTimeout != 0) + mTimeout--; + } + } + + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + + if (totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + + printText(mFmtText, 0); + } else { + printText("offline", 0); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(mFmtText, 1); + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(mFmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(mExtra % 10) && (ip)) + printText(ip.toString().c_str(), 3); + else if (!(mExtra % 5)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(mFmtText, 3); + } else if (NULL != mUtcTs) + printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + + mDisplay->sendBuffer(); + + mExtra = 1; + } + + private: + void calcLinePositions() { + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (mDisplay->getMaxCharHeight()); + mLineYOffsets[i] = yOff; + } + } + + inline void setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont(u8g2_font_logisoso16_tr); + break; + case 3: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + default: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + } + } + + void printText(const char *text, uint8_t line) { + uint8_t dispX = (line == 0) ? 10 : 5; + setFont(line); + + dispX += (mEnScreenSaver) ? (mExtra % 7) : 0; + mDisplay->drawStr(dispX, mLineYOffsets[line], text); + } +}; diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp index 99d35ed8..924961a3 100644 --- a/src/plugins/Display/Display_ePaper.cpp +++ b/src/plugins/Display/Display_ePaper.cpp @@ -57,8 +57,9 @@ void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, u } } -void DisplayEPaper::config(uint8_t rotation) { +void DisplayEPaper::config(uint8_t rotation, bool enPowerSafe) { mDisplayRotation = rotation; + mEnPowerSafe = enPowerSafe; } //*************************************************************************** @@ -120,7 +121,29 @@ void DisplayEPaper::lastUpdatePaged() { } while (_display->nextPage()); } //*************************************************************************** -void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod) { +void DisplayEPaper::offlineFooter() { + 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), "offline"); + + _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; @@ -130,47 +153,52 @@ void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, fl _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)); + 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); + } 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(5, 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() - 38, y); - _display->println("Wh"); - - y = y + tbh + 7; - _display->setCursor(5, 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() - 50, y); - _display->println("kWh"); - - _display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); - snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod); - _display->println(_fmtText); + if (totalPower == 0){ + _display->fillRect(0, mHeadFootPadding, 200,200, GxEPD_BLACK); + _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); + } else { + _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(5, 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() - 38, y); + _display->println("Wh"); + + y = y + tbh + 7; + _display->setCursor(5, 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() - 50, y); + _display->println("kWh"); + + _display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); + snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", isprod); + _display->println(_fmtText); + } } while (_display->nextPage()); } //*************************************************************************** @@ -185,11 +213,12 @@ void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYield // 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 there was an change and the Inverter is producing set a new Timestamp in the footline if ((isprod > 0) && (_changed)) { _changed = false; lastUpdatePaged(); - } + } else if((0 == totalPower) && (mEnPowerSafe)) + offlineFooter(); _display->powerOff(); } diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h index b2729f25..ad422b26 100644 --- a/src/plugins/Display/Display_ePaper.h +++ b/src/plugins/Display/Display_ePaper.h @@ -31,7 +31,7 @@ class DisplayEPaper { 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 config(uint8_t rotation, bool enPowerSafe); void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); @@ -39,6 +39,7 @@ class DisplayEPaper { void headlineIP(); void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); void lastUpdatePaged(); + void offlineFooter(); uint8_t mDisplayRotation; bool _changed = false; @@ -47,6 +48,7 @@ class DisplayEPaper { uint8_t mHeadFootPadding; GxEPD2_GFX* _display; uint32_t *mUtcTs; + bool mEnPowerSafe; }; #endif // ESP32 diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 049554d7..1f8519f5 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -564,7 +564,7 @@ class PubMqtt { void sendIvData() { bool anyAvail = processIvStatus(); if (mLastAnyAvail != anyAvail) - mSendList.push(RealTimeRunData_Debug); // makes shure that total values are calculated + mSendList.push(RealTimeRunData_Debug); // makes sure that total values are calculated if(mSendList.empty()) return; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index ffdc4df9..f0e34222 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -563,7 +563,7 @@ class RestApi { if(F("power") == jsonIn[F("cmd")]) accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); - else if(F("restart") == jsonIn[F("restart")]) + else if(F("restart") == jsonIn[F("cmd")]) accepted = iv->setDevControlRequest(Restart); else if(0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) { iv->powerLimit[0] = jsonIn["val"]; diff --git a/src/web/html/setup.html b/src/web/html/setup.html index c86ee980..427152e3 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -733,7 +733,7 @@ ); } - var opts = [[0, "None"], [1, "SSD1306 0.96\""], [2, "SH1106 1.3\""], [3, "Nokia5110"]]; + var opts = [[0, "None"], [1, "SSD1306 0.96\" 128X64"], [2, "SH1106 1.3\""], [3, "Nokia5110"], [4, "SSD1306 0.96\" 128X32"]]; if("ESP32" == type) opts.push([10, "ePaper"]); var dispType = sel("disp_typ", opts, obj["disp_typ"]); @@ -769,7 +769,7 @@ if(0 == dispType) cl.add("hide"); - else if(dispType <= 2) { // OLED + else if(dispType <= 2 || dispType == 4) { // OLED if(i < 2) cl.remove("hide"); else diff --git a/src/web/web.h b/src/web/web.h index c083f505..bef6491c 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -639,14 +639,22 @@ class Web { #ifdef ENABLE_PROMETHEUS_EP + // Note + // Prometheus exposition format is defined here: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md + // TODO: Check packetsize for MAX_NUM_INVERTERS. Successfull Tested with 4 Inverters (each with 4 channels) enum { - metricsStateStart, metricsStateInverter, metricStateRealtimeData,metricsStateAlarmData,metricsStateEnd + metricsStateStart, + metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4, + metricStateRealtimeFieldId, metricStateRealtimeInverterId, + metricsStateAlarmData, + metricsStateEnd } metricsStep; - int metricsInverterId,metricsChannelId; + int metricsInverterId; + uint8_t metricsFieldId; + bool metricDeclared; void showMetrics(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); - metricsStep = metricsStateStart; AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"), [this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t @@ -659,7 +667,11 @@ class Web { char type[60], topic[100], val[25]; size_t len = 0; int alarmChannelId; + int metricsChannelId; + // Perform grouping on metrics according to format specification + // Each step must return at least one character. Otherwise the processing of AsyncWebServerResponse stops. + // So several "Info:" blocks are used to keep the transmission going switch (metricsStep) { case metricsStateStart: // System Info & NRF Statistics : fit to one packet snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n"); @@ -688,93 +700,138 @@ class Web { metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - // Start Inverter loop + // Next is Inverter information metricsInverterId = 0; - metricsStep = metricsStateInverter; + metricsStep = metricsStateInverter1; break; - case metricsStateInverter: // Inverter loop - if (metricsInverterId < mSys->getNumInverters()) { - iv = mSys->getInverterByPos(metricsInverterId); - if(NULL != iv) { - // Inverter info : fit to one packet - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_info gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", - iv->config->name, iv->config->serial.u64); - metrics = String(type) + String(topic); - - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_enabled gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",iv->config->name,iv->config->enabled); - metrics += String(type) + String(topic); - - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_available gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",iv->config->name,iv->isAvailable(mApp->getTimestamp())); - metrics += String(type) + String(topic); - - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_producing gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",iv->config->name,iv->isProducing(mApp->getTimestamp())); - metrics += String(type) + String(topic); - - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - - // Start Realtime Data Channel loop for this inverter - metricsChannelId = 0; - metricsStep = metricStateRealtimeData; - } + case metricsStateInverter1: // Information about all inverters configured : fit to one packet + metrics = "# TYPE ahoy_solar_inverter_info gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", + [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->serial.u64;}); + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateInverter2; + break; + + case metricsStateInverter2: // Information about all inverters configured : fit to one packet + metrics += "# TYPE ahoy_solar_inverter_is_enabled gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n", + [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->enabled;}); + + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateInverter3; + break; + + case metricsStateInverter3: // Information about all inverters configured : fit to one packet + metrics += "# TYPE ahoy_solar_inverter_is_available gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n", + [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isAvailable(mApp->getTimestamp());}); + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateInverter4; + break; + + case metricsStateInverter4: // Information about all inverters configured : fit to one packet + metrics += "# TYPE ahoy_solar_inverter_is_producing gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n", + [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isProducing(mApp->getTimestamp());}); + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + // Start Realtime Field loop + metricsFieldId = FLD_UDC; + metricsStep = metricStateRealtimeFieldId; + break; + + case metricStateRealtimeFieldId: // Iterate over all defined fields + if (metricsFieldId < FLD_LAST_ALARM_CODE) { + metrics = "# Info: processing realtime field #"+String(metricsFieldId)+"\n"; + metricDeclared = false; + + metricsInverterId = 0; + metricsStep = metricStateRealtimeInverterId; } else { - metricsStep = metricsStateEnd; + metrics = "# Info: all realtime fields processed\n"; + metricsStep = metricsStateAlarmData; } + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); break; - case metricStateRealtimeData: // Realtime Data Channel loop - iv = mSys->getInverterByPos(metricsInverterId); - rec = iv->getRecordStruct(RealTimeRunData_Debug); - if (metricsChannelId < rec->length) { - uint8_t channel = rec->assign[metricsChannelId].ch; - // Skip entry if maxPwr is 0 and it's not the inverter channel (channel 0) - if (0 == channel || 0 != iv->config->chMaxPwr[channel-1]) { - std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); - if (0 == channel) { - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); - } else { - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); + case metricStateRealtimeInverterId: // Iterate over all inverters for this field + metrics = ""; + if (metricsInverterId < mSys->getNumInverters()) { + // process all channels of this inverter + + iv = mSys->getInverterByPos(metricsInverterId); + if (NULL != iv) { + rec = iv->getRecordStruct(RealTimeRunData_Debug); + for (metricsChannelId=0; metricsChannelId < rec->length;metricsChannelId++) { + uint8_t channel = rec->assign[metricsChannelId].ch; + + // Try inverter channel (channel 0) or any channel with maxPwr > 0 + if (0 == channel || 0 != iv->config->chMaxPwr[channel-1]) { + + if (metricsFieldId == iv->getByteAssign(metricsChannelId, rec)->fieldId) { + // This is the correct field to report + std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); + // Declare metric only once + if (!metricDeclared) { + snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); + metrics += type; + metricDeclared = true; + } + // report value + if (0 == channel) { + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); + } else { + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); + } + snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec)); + metrics += topic; + metrics += val; + } + } + } + if (metrics.length() < 1) { + metrics = "# Info: Field #"+String(metricsFieldId)+" not available for inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; + metricsFieldId++; // Process next field Id + metricsStep = metricStateRealtimeFieldId; } - snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec)); - len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); } else { - len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. + metrics = "# Info: No data for field #"+String(metricsFieldId)+" of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; + metricsFieldId++; // Process next field Id + metricsStep = metricStateRealtimeFieldId; } - - metricsChannelId++; + // Stay in this state and try next inverter + metricsInverterId++; } else { - len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. - - // All realtime data channels processed --> try alarm data - metricsStep = metricsStateAlarmData; + metrics = "# Info: All inverters for field #"+String(metricsFieldId)+" processed.\n"; + metricsFieldId++; // Process next field Id + metricsStep = metricStateRealtimeFieldId; } + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); break; - case metricsStateAlarmData: // Alarm Info loop - iv = mSys->getInverterByPos(metricsInverterId); - rec = iv->getRecordStruct(AlarmData); - // simple hack : there is only one channel with alarm data - // TODO: find the right one channel with the alarm id - alarmChannelId = 0; - // printf("AlarmData Length %d\n",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()); - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); - snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec)); - len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); - } else { - len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. + case metricsStateAlarmData: // Alarm Info loop : fit to one packet + // Perform grouping on metrics according to Prometheus exposition format specification + snprintf(type, sizeof(type),"# TYPE ahoy_solar_%s gauge\n",fields[FLD_LAST_ALARM_CODE]); + metrics = type; + + for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { + iv = mSys->getInverterByPos(metricsInverterId); + if (NULL != iv) { + rec = iv->getRecordStruct(AlarmData); + // simple hack : there is only one channel with alarm data + // TODO: find the right one channel with the alarm id + alarmChannelId = 0; + if (alarmChannelId < rec->length) { + std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); + snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec)); + metrics += topic; + metrics += val; + } + } } - // alarm channel processed --> try next inverter - metricsInverterId++; - metricsStep = metricsStateInverter; + len = snprintf((char*)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateEnd; break; case metricsStateEnd: @@ -787,6 +844,21 @@ class Web { request->send(response); } + + // Traverse all inverters and collect the metric via valueFunc + String inverterMetric(char *buffer, size_t len, const char *format, std::function *iv, IApp *mApp)> valueFunc) { + Inverter<> *iv; + String metric = ""; + for (int metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { + iv = mSys->getInverterByPos(metricsInverterId); + if (NULL != iv) { + snprintf(buffer,len,format,iv->config->name, valueFunc(iv,mApp)); + metric += String(buffer); + } + } + return metric; + } + String radioStatistic(String statistic, uint32_t value) { char type[60], topic[80], val[25]; snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str()); diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 9301067f..00d52511 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -2,11 +2,14 @@ ahoy: interval: 5 + transmit_retries: 5 logging: filename: 'hoymiles.log' # DEBUG, INFO, WARNING, ERROR, FATAL level: 'INFO' + max_log_filesize: 1000000 + max_log_files: 1 sunset: disabled: false diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index 210bed65..688e271d 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -297,8 +297,8 @@ class InverterPacketFragment: class HoymilesNRF: """Hoymiles NRF24 Interface""" - tx_channel_id = 0 - tx_channel_list = [40] + tx_channel_id = 2 + tx_channel_list = [3,23,40,61,75] rx_channel_id = 0 rx_channel_list = [3,23,40,61,75] rx_channel_ack = False @@ -332,6 +332,12 @@ class HoymilesNRF: :rtype: bool """ + self.next_tx_channel() + + if HOYMILES_TRANSACTION_LOGGING: + c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") + logging.debug(f'{c_datetime} Transmit {len(packet)} bytes channel {self.tx_channel}: {hexify_payload(packet)}') + if not txpower: txpower = self.txpower @@ -363,13 +369,13 @@ class HoymilesNRF: """ Receive Packets - :param timeout: receive timeout in nanoseconds (default: 12e8) + :param timeout: receive timeout in nanoseconds (default: 5e8) :type timeout: int :yields: fragment """ if not timeout: - timeout=12e8 + timeout=5e8 self.radio.setChannel(self.rx_channel) self.radio.setAutoAck(False) @@ -415,7 +421,7 @@ class HoymilesNRF: self.radio.setChannel(self.rx_channel) self.radio.startListening() - time.sleep(0.005) + time.sleep(0.004) def next_rx_channel(self): """ @@ -433,6 +439,15 @@ class HoymilesNRF: return True return False + def next_tx_channel(self): + """ + Select next channel from hop list + + """ + self.tx_channel_id = self.tx_channel_id + 1 + if self.tx_channel_id >= len(self.tx_channel_list): + self.tx_channel_id = 0 + @property def tx_channel(self): """ @@ -612,10 +627,6 @@ class InverterTransaction: packet = self.tx_queue.pop(0) - if HOYMILES_TRANSACTION_LOGGING: - c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") - logging.debug(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}') - self.radio.transmit(packet, txpower=self.txpower) wait = False diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 7de4a1a2..3589de1a 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -19,6 +19,7 @@ import yaml from yaml.loader import SafeLoader import hoymiles import logging +from logging.handlers import RotatingFileHandler ################################################################################ """ Signal Handler """ @@ -127,6 +128,7 @@ def main_loop(ahoy_config): dtu_name = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu') sunset.sun_status2mqtt(dtu_ser, dtu_name) loop_interval = ahoy_config.get('interval', 1) + transmit_retries = ahoy_config.get('transmit_retries', 5) try: do_init = True @@ -143,7 +145,7 @@ def main_loop(ahoy_config): 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) + poll_inverter(inverter, dtu_ser, do_init, transmit_retries) do_init = False if loop_interval > 0: @@ -298,6 +300,8 @@ def init_logging(ahoy_config): log_config = ahoy_config.get('logging') fn = 'hoymiles.log' lvl = logging.ERROR + max_log_filesize = 1000000 + max_log_files = 1 if log_config: fn = log_config.get('filename', fn) level = log_config.get('level', 'ERROR') @@ -311,9 +315,11 @@ def init_logging(ahoy_config): lvl = logging.ERROR elif level == 'FATAL': lvl = logging.FATAL + max_log_filesize = log_config.get('max_log_filesize', max_log_filesize) + max_log_files = log_config.get('max_log_files', max_log_files) if hoymiles.HOYMILES_TRANSACTION_LOGGING: lvl = logging.DEBUG - logging.basicConfig(filename=fn, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl) + logging.basicConfig(handlers=[RotatingFileHandler(fn, maxBytes=max_log_filesize, backupCount=max_log_files)], 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}') diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index bb32fb07..ad49d664 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -515,9 +515,17 @@ class Hm300Decode0B(StatusResponse): """ reactive power """ return self.unpack('>H', 20)[0]/10 @property + def powerfactor(self): + """ Powerfactor """ + return self.unpack('>H', 24)[0]/1000 + @property def temperature(self): """ Inverter temperature in °C """ return self.unpack('>h', 26)[0]/10 + @property + def event_count(self): + """ Event counter """ + return self.unpack('>H', 28)[0] class Hm300Decode0C(Hm300Decode0B): """ 1121-series mirco-inverters status data """