Browse Source

Merge branch 'development03' into improv

pull/1027/head
lumapu 1 year ago
parent
commit
1c36638226
  1. 11
      Getting_Started.md
  2. 13
      User_Manual.md
  3. 2
      doc/prometheus_ep_description.md
  4. 35
      src/CHANGES.md
  5. 30
      src/app.cpp
  6. 4
      src/app.h
  7. 1
      src/appInterface.h
  8. 178
      src/config/settings.h
  9. 4
      src/defines.h
  10. 38
      src/hm/hmPayload.h
  11. 50
      src/platformio.ini
  12. 194
      src/plugins/Display/Display.h
  13. 157
      src/plugins/Display/Display_Mono.cpp
  14. 45
      src/plugins/Display/Display_Mono.h
  15. 155
      src/plugins/Display/Display_Mono_128X32.h
  16. 138
      src/plugins/Display/Display_Mono_128X64.h
  17. 132
      src/plugins/Display/Display_Mono_84X48.h
  18. 107
      src/plugins/Display/Display_ePaper.cpp
  19. 4
      src/plugins/Display/Display_ePaper.h
  20. 97
      src/publisher/pubMqtt.h
  21. 208
      src/publisher/pubMqttIvData.h
  22. 2
      src/utils/dbg.h
  23. 3
      src/web/RestApi.h
  24. 4
      src/web/html/api.js
  25. 3
      src/web/html/includes/header.html
  26. 38
      src/web/html/save.html
  27. 2
      src/web/html/serial.html
  28. 64
      src/web/html/setup.html
  29. 221
      src/web/web.h
  30. 3
      tools/rpi/ahoy.yml.example
  31. 29
      tools/rpi/hoymiles/__init__.py
  32. 10
      tools/rpi/hoymiles/__main__.py
  33. 8
      tools/rpi/hoymiles/decoders/__init__.py

11
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! ! 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<XXX> && 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 ## Connect to your Ahoy DTU
When everything is wired up and the firmware is flashed, it is time to connect to your Ahoy DTU. When everything is wired up and the firmware is flashed, it is time to connect to your Ahoy DTU.
@ -264,9 +272,8 @@ When everything is wired up and the firmware is flashed, it is time to connect t
| /cmdstat | show stat from the home page | | yes | | /cmdstat | show stat from the home page | | yes |
| /visualization | displays the information from your converter | | yes | | /visualization | displays the information from your converter | | yes |
| /livedata | displays the live data | | yes | | /livedata | displays the live data | | yes |
| /json | gets live-data in JSON format | json output from the livedata | no - enable via config_override.h |
| /metrics | gets live-data for prometheus | prometheus metrics from the livedata | no - enable via config_override.h | | /metrics | gets live-data for prometheus | prometheus metrics from the livedata | no - enable via config_override.h |
| /api | | | yes | | /api | gets configuration and live-data in JSON format | json output from the configuration or livedata | yes |
## MQTT command to set the DTU without webinterface ## MQTT command to set the DTU without webinterface

13
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. - 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. - 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. - 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 ## Additional Notes
### MI Inverters ### MI Inverters

2
doc/prometheus_ep_description.md

@ -12,7 +12,7 @@ Prometheus metrics provided at `/metrics`.
| name | Inverter name from setup | | name | Inverter name from setup |
| serial | Serial number of inverter | | serial | Serial number of inverter |
| inverter | Inverter name from setup | | inverter | Inverter name from setup |
| channel | Channel name from setup | | channel | Channel (Module) name from setup. Label only available if max power level of module is set to non-zero. Be sure to have a cannel name set in configuration. |
## Exported Metrics ## Exported Metrics
| Metric name | Type | Description | Labels | | Metric name | Type | Description | Labels |

35
src/CHANGES.md

@ -1,20 +1,25 @@
# Development Changes # Development Changes
## 0.6.4 - 2023-04-06 ## 0.6.15 - 2023-05-25
* merge PR #846, improved NRF24 communication and MI, thx @beegee3 & @rejoe2 * improved Prometheus Endpoint PR #958
* merge PR #859, fix burger menu height, thx @ThomasPohl * fix turn off ePaper only if setting was set #956
* improved reset values and update MqTT #957
## 0.6.3 - 2023-04-04 ## 0.6.14 - 2023-05-21
* fix login, password length was not checked #852 * merge PR #902 Mono-Display
* merge PR #854 optimize browser caching, thx @tastendruecker123 #828
* fix WiFi reconnect not working #851
* updated issue templates #822
## 0.6.2 - 2023-04-04 ## 0.6.13 - 2023-05-16
* fix login from multiple clients #819 * merge PR #934 (fix JSON API) and #944 (update manual)
* fix login screen on small displays
## 0.6.1 - 2023-04-01 ## 0.6.12 - 2023-04-28
* merge LED fix - LED1 shows MqTT state, LED configureable active high/low #839 * improved MqTT
* only publish new inverter data #826 * fix menu active item
* potential fix of WiFi hostname during boot up #752
## 0.6.11 - 2023-04-27
* added MqTT class for publishing all values in Arduino `loop`
## 0.6.10 - HMS
* Version available in `HMS` branch
## 0.6.9
* last Relaese

30
src/app.cpp

@ -175,6 +175,7 @@ void app::regularTickers(void) {
everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp");
every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart");
//everySec(std::bind(&Improv::tickSerial, &mImprov), "impro"); //everySec(std::bind(&Improv::tickSerial, &mImprov), "impro");
// every([this]() {mPayload.simulation();}, 15, "simul");
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -276,6 +277,7 @@ void app::tickComm(void) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::tickZeroValues(void) { void app::tickZeroValues(void) {
Inverter<> *iv; Inverter<> *iv;
bool changed = false;
// set values to zero, except yields // set values to zero, except yields
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id); iv = mSys.getInverterByPos(id);
@ -283,7 +285,11 @@ void app::tickZeroValues(void) {
continue; // skip to next inverter continue; // skip to next inverter
mPayload.zeroInverterValues(iv); mPayload.zeroInverterValues(iv);
changed = true;
} }
if(changed)
payloadEventListener(RealTimeRunData_Debug);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -291,17 +297,23 @@ void app::tickMinute(void) {
// only triggered if 'reset values on no avail is enabled' // only triggered if 'reset values on no avail is enabled'
Inverter<> *iv; Inverter<> *iv;
bool changed = false;
// set values to zero, except yields // set values to zero, except yields
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id); iv = mSys.getInverterByPos(id);
if (NULL == iv) if (NULL == iv)
continue; // skip to next inverter continue; // skip to next inverter
if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) {
mPayload.zeroInverterValues(iv); mPayload.zeroInverterValues(iv);
changed = true;
} }
} }
if(changed)
payloadEventListener(RealTimeRunData_Debug);
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::tickMidnight(void) { void app::tickMidnight(void) {
// only triggered if 'reset values at midnight is enabled' // only triggered if 'reset values at midnight is enabled'
@ -310,16 +322,20 @@ void app::tickMidnight(void) {
onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2"); onceAt(std::bind(&app::tickMidnight, this), nxtTrig, "mid2");
Inverter<> *iv; Inverter<> *iv;
bool changed = false;
// set values to zero, except yield total // set values to zero, except yield total
for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { for (uint8_t id = 0; id < mSys.getNumInverters(); id++) {
iv = mSys.getInverterByPos(id); iv = mSys.getInverterByPos(id);
if (NULL == iv) if (NULL == iv)
continue; // skip to next inverter continue; // skip to next inverter
mPayload.zeroInverterValues(iv); mPayload.zeroInverterValues(iv, false);
mPayload.zeroYieldDay(iv); changed = true;
} }
if(changed)
payloadEventListener(RealTimeRunData_Debug);
if (mMqttEnabled) if (mMqttEnabled)
mMqtt.tickerMidnight(); mMqtt.tickerMidnight();
} }
@ -393,8 +409,7 @@ void app::mqttSubRxCb(JsonObject obj) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::setupLed(void) { void app::setupLed(void) {
uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH;
uint8_t led_off = (mConfig->led.led_high_active != 0) ? LOW : HIGH;
if (mConfig->led.led0 != 0xff) { if (mConfig->led.led0 != 0xff) {
pinMode(mConfig->led.led0, OUTPUT); pinMode(mConfig->led.led0, OUTPUT);
@ -408,9 +423,8 @@ void app::setupLed(void) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::updateLed(void) { void app::updateLed(void) {
uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH;
uint8_t led_off = (mConfig->led.led_high_active != 0) ? LOW : HIGH; uint8_t led_on = (mConfig->led.led_high_active) ? HIGH : LOW;
uint8_t led_on = (mConfig->led.led_high_active != 0) ? HIGH : LOW;
if (mConfig->led.led0 != 0xff) { if (mConfig->led.led0 != 0xff) {
Inverter<> *iv = mSys.getInverterByPos(0); Inverter<> *iv = mSys.getInverterByPos(0);

4
src/app.h

@ -93,6 +93,10 @@ class app : public IApp, public ah::Scheduler {
return mSettings.getLastSaveSucceed(); return mSettings.getLastSaveSucceed();
} }
bool getShouldReboot() {
return mSaveReboot;
}
statistics_t *getStatistics() { statistics_t *getStatistics() {
return &mStat; return &mStat;
} }

1
src/appInterface.h

@ -20,6 +20,7 @@ class IApp {
virtual bool eraseSettings(bool eraseWifi) = 0; virtual bool eraseSettings(bool eraseWifi) = 0;
virtual bool getSavePending() = 0; virtual bool getSavePending() = 0;
virtual bool getLastSaveSucceed() = 0; virtual bool getLastSaveSucceed() = 0;
virtual bool getShouldReboot() = 0;
virtual void setOnUpdate() = 0; virtual void setOnUpdate() = 0;
virtual void setRebootFlag() = 0; virtual void setRebootFlag() = 0;
virtual const char *getVersion() = 0; virtual const char *getVersion() = 0;

178
src/config/settings.h

@ -6,6 +6,11 @@
#ifndef __SETTINGS_H__ #ifndef __SETTINGS_H__
#define __SETTINGS_H__ #define __SETTINGS_H__
#if defined(F) && defined(ESP32)
#undef F
#define F(sl) (sl)
#endif
#include <Arduino.h> #include <Arduino.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <LittleFS.h> #include <LittleFS.h>
@ -100,7 +105,7 @@ typedef struct {
typedef struct { typedef struct {
uint8_t led0; // first LED pin uint8_t led0; // first LED pin
uint8_t led1; // second LED pin uint8_t led1; // second LED pin
uint8_t led_high_active; // determines if LEDs are high or low active bool led_high_active; // determines if LEDs are high or low active
} cfgLed_t; } cfgLed_t;
typedef struct { typedef struct {
@ -245,15 +250,15 @@ class settings {
root.shrinkToFit(); root.shrinkToFit();
if(!err && (root.size() > 0)) { if(!err && (root.size() > 0)) {
mCfg.valid = true; mCfg.valid = true;
jsonWifi(root[F("wifi")]); if(root.containsKey(F("wifi"))) jsonWifi(root[F("wifi")]);
jsonNrf(root[F("nrf")]); if(root.containsKey(F("nrf"))) jsonNrf(root[F("nrf")]);
jsonNtp(root[F("ntp")]); if(root.containsKey(F("ntp"))) jsonNtp(root[F("ntp")]);
jsonSun(root[F("sun")]); if(root.containsKey(F("sun"))) jsonSun(root[F("sun")]);
jsonSerial(root[F("serial")]); if(root.containsKey(F("serial"))) jsonSerial(root[F("serial")]);
jsonMqtt(root[F("mqtt")]); if(root.containsKey(F("mqtt"))) jsonMqtt(root[F("mqtt")]);
jsonLed(root[F("led")]); if(root.containsKey(F("led"))) jsonLed(root[F("led")]);
jsonPlugin(root[F("plugin")]); if(root.containsKey(F("plugin"))) jsonPlugin(root[F("plugin")]);
jsonInst(root[F("inst")]); if(root.containsKey(F("inst"))) jsonInst(root[F("inst")]);
} }
else { else {
Serial.println(F("failed to parse json, using default config")); Serial.println(F("failed to parse json, using default config"));
@ -379,7 +384,7 @@ class settings {
mCfg.led.led0 = DEF_PIN_OFF; mCfg.led.led0 = DEF_PIN_OFF;
mCfg.led.led1 = DEF_PIN_OFF; mCfg.led.led1 = DEF_PIN_OFF;
mCfg.led.led_high_active = LOW; mCfg.led.led_high_active = false;
memset(&mCfg.inst, 0, sizeof(cfgInst_t)); memset(&mCfg.inst, 0, sizeof(cfgInst_t));
@ -410,17 +415,17 @@ class settings {
ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf); ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf);
ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf); ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf);
} else { } else {
snprintf(mCfg.sys.stationSsid, SSID_LEN, "%s", obj[F("ssid")].as<const char*>()); getChar(obj, F("ssid"), mCfg.sys.stationSsid, SSID_LEN);
snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as<const char*>()); getChar(obj, F("pwd"), mCfg.sys.stationPwd, PWD_LEN);
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as<const char*>()); getChar(obj, F("dev"), mCfg.sys.deviceName, DEVNAME_LEN);
snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as<const char*>()); getChar(obj, F("adm"), mCfg.sys.adminPwd, PWD_LEN);
mCfg.sys.protectionMask = obj[F("prot_mask")]; getVal<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask);
mCfg.sys.darkMode = obj[F("dark")]; getVal<bool>(obj, F("dark"), &mCfg.sys.darkMode);
ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>()); if(obj.containsKey(F("ip"))) ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>());
ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>()); if(obj.containsKey(F("mask"))) ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>());
ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>()); if(obj.containsKey(F("dns1"))) ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>());
ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as<const char*>()); if(obj.containsKey(F("dns2"))) ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as<const char*>());
ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as<const char*>()); if(obj.containsKey(F("gtwy"))) ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as<const char*>());
if(mCfg.sys.protectionMask == 0) if(mCfg.sys.protectionMask == 0)
mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP mCfg.sys.protectionMask = DEF_PROT_INDEX | DEF_PROT_LIVE | DEF_PROT_SERIAL | DEF_PROT_SETUP
@ -440,15 +445,15 @@ class settings {
obj[F("miso")] = mCfg.nrf.pinMiso; obj[F("miso")] = mCfg.nrf.pinMiso;
obj[F("pwr")] = mCfg.nrf.amplifierPower; obj[F("pwr")] = mCfg.nrf.amplifierPower;
} else { } else {
mCfg.nrf.sendInterval = obj[F("intvl")]; getVal<uint16_t>(obj, F("intvl"), &mCfg.nrf.sendInterval);
mCfg.nrf.maxRetransPerPyld = obj[F("maxRetry")]; getVal<uint8_t>(obj, F("maxRetry"), &mCfg.nrf.maxRetransPerPyld);
mCfg.nrf.pinCs = obj[F("cs")]; getVal<uint8_t>(obj, F("cs"), &mCfg.nrf.pinCs);
mCfg.nrf.pinCe = obj[F("ce")]; getVal<uint8_t>(obj, F("ce"), &mCfg.nrf.pinCe);
mCfg.nrf.pinIrq = obj[F("irq")]; getVal<uint8_t>(obj, F("irq"), &mCfg.nrf.pinIrq);
mCfg.nrf.pinSclk = obj[F("sclk")]; getVal<uint8_t>(obj, F("sclk"), &mCfg.nrf.pinSclk);
mCfg.nrf.pinMosi = obj[F("mosi")]; getVal<uint8_t>(obj, F("mosi"), &mCfg.nrf.pinMosi);
mCfg.nrf.pinMiso = obj[F("miso")]; getVal<uint8_t>(obj, F("miso"), &mCfg.nrf.pinMiso);
mCfg.nrf.amplifierPower = obj[F("pwr")]; getVal<uint8_t>(obj, F("pwr"), &mCfg.nrf.amplifierPower);
if((obj[F("cs")] == obj[F("ce")])) { if((obj[F("cs")] == obj[F("ce")])) {
mCfg.nrf.pinCs = DEF_CS_PIN; mCfg.nrf.pinCs = DEF_CS_PIN;
mCfg.nrf.pinCe = DEF_CE_PIN; mCfg.nrf.pinCe = DEF_CE_PIN;
@ -465,8 +470,8 @@ class settings {
obj[F("addr")] = mCfg.ntp.addr; obj[F("addr")] = mCfg.ntp.addr;
obj[F("port")] = mCfg.ntp.port; obj[F("port")] = mCfg.ntp.port;
} else { } else {
snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", obj[F("addr")].as<const char*>()); getChar(obj, F("addr"), mCfg.ntp.addr, NTP_ADDR_LEN);
mCfg.ntp.port = obj[F("port")]; getVal<uint16_t>(obj, F("port"), &mCfg.ntp.port);
} }
} }
@ -477,10 +482,10 @@ class settings {
obj[F("dis")] = mCfg.sun.disNightCom; obj[F("dis")] = mCfg.sun.disNightCom;
obj[F("offs")] = mCfg.sun.offsetSec; obj[F("offs")] = mCfg.sun.offsetSec;
} else { } else {
mCfg.sun.lat = obj[F("lat")]; getVal<float>(obj, F("lat"), &mCfg.sun.lat);
mCfg.sun.lon = obj[F("lon")]; getVal<float>(obj, F("lon"), &mCfg.sun.lon);
mCfg.sun.disNightCom = obj[F("dis")]; getVal<bool>(obj, F("dis"), &mCfg.sun.disNightCom);
mCfg.sun.offsetSec = obj[F("offs")]; getVal<uint16_t>(obj, F("offs"), &mCfg.sun.offsetSec);
} }
} }
@ -490,9 +495,9 @@ class settings {
obj[F("show")] = mCfg.serial.showIv; obj[F("show")] = mCfg.serial.showIv;
obj[F("debug")] = mCfg.serial.debug; obj[F("debug")] = mCfg.serial.debug;
} else { } else {
mCfg.serial.interval = obj[F("intvl")]; getVal<uint16_t>(obj, F("intvl"), &mCfg.serial.interval);
mCfg.serial.showIv = obj[F("show")]; getVal<bool>(obj, F("show"), &mCfg.serial.showIv);
mCfg.serial.debug = obj[F("debug")]; getVal<bool>(obj, F("debug"), &mCfg.serial.debug);
} }
} }
@ -506,12 +511,12 @@ class settings {
obj[F("intvl")] = mCfg.mqtt.interval; obj[F("intvl")] = mCfg.mqtt.interval;
} else { } else {
mCfg.mqtt.port = obj[F("port")]; getVal<uint16_t>(obj, F("port"), &mCfg.mqtt.port);
mCfg.mqtt.interval = obj[F("intvl")]; getVal<uint16_t>(obj, F("intvl"), &mCfg.mqtt.interval);
snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", obj[F("broker")].as<const char*>()); getChar(obj, F("broker"), mCfg.mqtt.broker, MQTT_ADDR_LEN);
snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as<const char*>()); getChar(obj, F("user"), mCfg.mqtt.user, MQTT_USER_LEN);
snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as<const char*>()); getChar(obj, F("pwd"), mCfg.mqtt.pwd, MQTT_PWD_LEN);
snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", obj[F("topic")].as<const char*>()); getChar(obj, F("topic"), mCfg.mqtt.topic, MQTT_TOPIC_LEN);
} }
} }
@ -519,11 +524,11 @@ class settings {
if(set) { if(set) {
obj[F("0")] = mCfg.led.led0; obj[F("0")] = mCfg.led.led0;
obj[F("1")] = mCfg.led.led1; obj[F("1")] = mCfg.led.led1;
obj[F("led_high_active")] = mCfg.led.led_high_active; obj[F("act_high")] = mCfg.led.led_high_active;
} else { } else {
mCfg.led.led0 = obj[F("0")]; getVal<uint8_t>(obj, F("0"), &mCfg.led.led0);
mCfg.led.led1 = obj[F("1")]; getVal<uint8_t>(obj, F("1"), &mCfg.led.led1);
mCfg.led.led_high_active = obj[F("led_high_active")]; getVal<bool>(obj, F("act_high"), &mCfg.led.led_high_active);
} }
} }
@ -545,19 +550,19 @@ class settings {
disp[F("dc")] = mCfg.plugin.display.disp_dc; disp[F("dc")] = mCfg.plugin.display.disp_dc;
} else { } else {
JsonObject disp = obj["disp"]; JsonObject disp = obj["disp"];
mCfg.plugin.display.type = disp[F("type")]; getVal<uint8_t>(disp, F("type"), &mCfg.plugin.display.type);
mCfg.plugin.display.pwrSaveAtIvOffline = (bool)disp[F("pwrSafe")]; getVal<bool>(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline);
mCfg.plugin.display.pxShift = (bool)disp[F("pxShift")]; getVal<bool>(disp, F("pxShift"), &mCfg.plugin.display.pxShift);
mCfg.plugin.display.rot = disp[F("rotation")]; getVal<uint8_t>(disp, F("rotation"), &mCfg.plugin.display.rot);
//mCfg.plugin.display.wakeUp = disp[F("wake")]; //mCfg.plugin.display.wakeUp = disp[F("wake")];
//mCfg.plugin.display.sleepAt = disp[F("sleep")]; //mCfg.plugin.display.sleepAt = disp[F("sleep")];
mCfg.plugin.display.contrast = disp[F("contrast")]; getVal<uint8_t>(disp, F("contrast"), &mCfg.plugin.display.contrast);
mCfg.plugin.display.disp_data = disp[F("data")]; getVal<uint8_t>(disp, F("data"), &mCfg.plugin.display.disp_data);
mCfg.plugin.display.disp_clk = disp[F("clock")]; getVal<uint8_t>(disp, F("clock"), &mCfg.plugin.display.disp_clk);
mCfg.plugin.display.disp_cs = disp[F("cs")]; getVal<uint8_t>(disp, F("cs"), &mCfg.plugin.display.disp_cs);
mCfg.plugin.display.disp_reset = disp[F("reset")]; getVal<uint8_t>(disp, F("reset"), &mCfg.plugin.display.disp_reset);
mCfg.plugin.display.disp_busy = disp[F("busy")]; getVal<uint8_t>(disp, F("busy"), &mCfg.plugin.display.disp_busy);
mCfg.plugin.display.disp_dc = disp[F("dc")]; getVal<uint8_t>(disp, F("dc"), &mCfg.plugin.display.disp_dc);
} }
} }
@ -569,10 +574,10 @@ class settings {
obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop;
} }
else { else {
mCfg.inst.enabled = (bool)obj[F("en")]; getVal<bool>(obj, F("en"), &mCfg.inst.enabled);
mCfg.inst.rstYieldMidNight = (bool)obj["rstMidNight"]; getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight);
mCfg.inst.rstValsNotAvail = (bool)obj["rstNotAvail"]; getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail);
mCfg.inst.rstValsCommStop = (bool)obj["rstComStop"]; getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop);
} }
JsonArray ivArr; JsonArray ivArr;
@ -582,11 +587,8 @@ class settings {
if(set) { if(set) {
if(mCfg.inst.iv[i].serial.u64 != 0ULL) if(mCfg.inst.iv[i].serial.u64 != 0ULL)
jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true);
} } else if(!obj[F("iv")][i].isNull())
else { jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]);
if(!obj[F("iv")][i].isNull())
jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]);
}
} }
} }
@ -601,17 +603,41 @@ class settings {
obj[F("chName")][i] = cfg->chName[i]; obj[F("chName")][i] = cfg->chName[i];
} }
} else { } else {
cfg->enabled = (bool)obj[F("en")]; getVal<bool>(obj, F("en"), &cfg->enabled);
snprintf(cfg->name, MAX_NAME_LENGTH, "%s", obj[F("name")].as<const char*>()); getChar(obj, F("name"), cfg->name, MAX_NAME_LENGTH);
cfg->serial.u64 = obj[F("sn")]; getVal<uint64_t>(obj, F("sn"), &cfg->serial.u64);
for(uint8_t i = 0; i < 4; i++) { for(uint8_t i = 0; i < 4; i++) {
cfg->yieldCor[i] = obj[F("yield")][i]; if(obj.containsKey(F("yield"))) cfg->yieldCor[i] = obj[F("yield")][i];
cfg->chMaxPwr[i] = obj[F("pwr")][i]; if(obj.containsKey(F("pwr"))) cfg->chMaxPwr[i] = obj[F("pwr")][i];
snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as<const char*>()); if(obj.containsKey(F("chName"))) snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as<const char*>());
} }
} }
} }
#if defined(ESP32)
void getChar(JsonObject obj, const char *key, char *dst, int maxLen) {
if(obj.containsKey(key))
snprintf(dst, maxLen, "%s", obj[key].as<const char*>());
}
template<typename T=uint8_t>
void getVal(JsonObject obj, const char *key, T *dst) {
if(obj.containsKey(key))
*dst = obj[key];
}
#else
void getChar(JsonObject obj, const __FlashStringHelper *key, char *dst, int maxLen) {
if(obj.containsKey(key))
snprintf(dst, maxLen, "%s", obj[key].as<const char*>());
}
template<typename T=uint8_t>
void getVal(JsonObject obj, const __FlashStringHelper *key, T *dst) {
if(obj.containsKey(key))
*dst = obj[key];
}
#endif
settings_t mCfg; settings_t mCfg;
bool mLastSaveSucceed; bool mLastSaveSucceed;
}; };

4
src/defines.h

@ -1,5 +1,5 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 // 2023 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -13,7 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 6 #define VERSION_MINOR 6
#define VERSION_PATCH 4 #define VERSION_PATCH 15
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {

38
src/hm/hmPayload.h

@ -70,17 +70,30 @@ class HmPayload {
} }
} }
void zeroYieldDay(Inverter<> *iv) { /*void simulation() {
DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); uint8_t pay[] = {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); 0x00, 0x01, 0x01, 0x24, 0x02, 0x28, 0x02, 0x33,
uint8_t pos; 0x06, 0x49, 0x06, 0x6a, 0x00, 0x05, 0x5f, 0x1b,
for(uint8_t ch = 0; ch < iv->channels; ch++) { 0x00, 0x06, 0x66, 0x9a, 0x03, 0xfd, 0x04, 0x0b,
pos = iv->getPosByChFld(CH0, FLD_YD, rec); 0x01, 0x23, 0x02, 0x28, 0x02, 0x28, 0x06, 0x41,
iv->setValue(pos, rec, 0.0f); 0x06, 0x43, 0x00, 0x05, 0xdc, 0x2c, 0x00, 0x06,
0x2e, 0x3f, 0x04, 0x01, 0x03, 0xfb, 0x09, 0x78,
0x13, 0x86, 0x18, 0x15, 0x00, 0xcf, 0x00, 0xfe,
0x03, 0xe7, 0x01, 0x42, 0x00, 0x03
};
Inverter<> *iv = mSys->getInverterByPos(0);
record_t<> *rec = iv->getRecordStruct(0x0b);
rec->ts = *mTimestamp;
for (uint8_t i = 0; i < rec->length; i++) {
iv->addValue(i, pay, rec);
yield();
} }
} iv->doCalculations();
notify(0x0b);
}*/
void zeroInverterValues(Inverter<> *iv) { void zeroInverterValues(Inverter<> *iv, bool skipYieldDay = true) {
DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); DPRINTLN(DBG_DEBUG, F("zeroInverterValues"));
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
for(uint8_t ch = 0; ch <= iv->channels; ch++) { for(uint8_t ch = 0; ch <= iv->channels; ch++) {
@ -88,15 +101,18 @@ class HmPayload {
for(uint8_t fld = 0; fld < FLD_EVT; fld++) { for(uint8_t fld = 0; fld < FLD_EVT; fld++) {
switch(fld) { switch(fld) {
case FLD_YD: case FLD_YD:
if(skipYieldDay)
continue;
else
break;
case FLD_YT: case FLD_YT:
continue; continue;
} }
pos = iv->getPosByChFld(ch, fld, rec); pos = iv->getPosByChFld(ch, fld, rec);
iv->setValue(pos, rec, 0.0f); iv->setValue(pos, rec, 0.0f);
} }
iv->doCalculations();
} }
notify(RealTimeRunData_Debug);
} }
void ivSendHighPrio(Inverter<> *iv) { void ivSendHighPrio(Inverter<> *iv) {

50
src/platformio.ini

@ -16,17 +16,6 @@ include_dir = .
framework = arduino framework = arduino
board_build.filesystem = littlefs board_build.filesystem = littlefs
upload_speed = 921600 upload_speed = 921600
;build_flags =
; ;;;;; Possible Debug options ;;;;;;
; https://docs.platformio.org/en/latest/platforms/espressif8266.html#debug-level
;-DDEBUG_ESP_PORT=Serial
;-DDEBUG_ESP_CORE
;-DDEBUG_ESP_WIFI
;-DDEBUG_ESP_HTTP_CLIENT
;-DDEBUG_ESP_HTTP_SERVER
;-DDEBUG_ESP_OOM
monitor_speed = 115200 monitor_speed = 115200
extra_scripts = extra_scripts =
@ -38,10 +27,10 @@ lib_deps =
nrf24/RF24 @ ^1.4.5 nrf24/RF24 @ ^1.4.5
paulstoffregen/Time @ ^1.6.1 paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.4.2 https://github.com/bertmelis/espMqttClient#v1.4.2
bblanchon/ArduinoJson @ ^6.21.0 bblanchon/ArduinoJson @ ^6.21.2
https://github.com/JChristensen/Timezone @ ^1.2.4 https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 @ ^2.34.16 olikraus/U8g2 @ ^2.34.17
zinggjm/GxEPD2 @ ^1.5.0 zinggjm/GxEPD2 @ ^1.5.2
[env:esp8266-release] [env:esp8266-release]
@ -95,7 +84,13 @@ platform = espressif8266
board = esp8285 board = esp8285
board_build.ldscript = eagle.flash.1m64.ld board_build.ldscript = eagle.flash.1m64.ld
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial build_flags = -DDEBUG_LEVEL=DBG_DEBUG
-DDEBUG_ESP_CORE
-DDEBUG_ESP_WIFI
-DDEBUG_ESP_HTTP_CLIENT
-DDEBUG_ESP_HTTP_SERVER
-DDEBUG_ESP_OOM
-DDEBUG_ESP_PORT=Serial
build_type = debug build_type = debug
monitor_filters = monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input
@ -103,9 +98,9 @@ monitor_filters =
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
[env:esp32-wroom32-release] [env:esp32-wroom32-release]
platform = espressif32 platform = espressif32@6.1.0
board = lolin_d32 board = lolin_d32
build_flags = -D RELEASE -std=gnu++14 build_flags = -D RELEASE -std=gnu++17
build_unflags = -std=gnu++11 build_unflags = -std=gnu++11
monitor_filters = monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input
@ -114,9 +109,11 @@ monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-wroom32-release-prometheus] [env:esp32-wroom32-release-prometheus]
platform = espressif32 platform = espressif32@6.1.0
board = lolin_d32 board = lolin_d32
build_flags = -D RELEASE -std=gnu++14 -DENABLE_PROMETHEUS_EP build_flags = -D RELEASE
-std=gnu++17
-DENABLE_PROMETHEUS_EP
build_unflags = -std=gnu++11 build_unflags = -std=gnu++11
monitor_filters = monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input
@ -125,9 +122,16 @@ monitor_filters =
esp32_exception_decoder esp32_exception_decoder
[env:esp32-wroom32-debug] [env:esp32-wroom32-debug]
platform = espressif32 platform = espressif32@6.1.0
board = lolin_d32 board = lolin_d32
build_flags = -DDEBUG_LEVEL=DBG_DEBUG -DDEBUG_ESP_CORE -DDEBUG_ESP_WIFI -DDEBUG_ESP_HTTP_CLIENT -DDEBUG_ESP_HTTP_SERVER -DDEBUG_ESP_OOM -DDEBUG_ESP_PORT=Serial -std=gnu++14 build_flags = -DDEBUG_LEVEL=DBG_DEBUG
-DDEBUG_ESP_CORE
-DDEBUG_ESP_WIFI
-DDEBUG_ESP_HTTP_CLIENT
-DDEBUG_ESP_HTTP_SERVER
-DDEBUG_ESP_OOM
-DDEBUG_ESP_PORT=Serial
-std=gnu++17
build_unflags = -std=gnu++11 build_unflags = -std=gnu++11
build_type = debug build_type = debug
monitor_filters = monitor_filters =
@ -136,13 +140,13 @@ monitor_filters =
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
[env:opendtufusionv1-release] [env:opendtufusionv1-release]
platform = espressif32 platform = espressif32@6.1.0
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
upload_protocol = esp-builtin upload_protocol = esp-builtin
upload_speed = 115200 upload_speed = 115200
debug_tool = esp-builtin debug_tool = esp-builtin
debug_speed = 12000 debug_speed = 12000
build_flags = -D RELEASE -std=gnu++14 build_flags = -D RELEASE -std=gnu++17
build_unflags = -std=gnu++11 build_unflags = -std=gnu++11
monitor_filters = monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input

194
src/plugins/Display/Display.h

@ -7,108 +7,126 @@
#include "../../hm/hmSystem.h" #include "../../hm/hmSystem.h"
#include "../../utils/helper.h" #include "../../utils/helper.h"
#include "Display_Mono.h" #include "Display_Mono.h"
#include "Display_Mono_128X32.h"
#include "Display_Mono_128X64.h"
#include "Display_Mono_84X48.h"
#include "Display_ePaper.h" #include "Display_ePaper.h"
template <class HMSYSTEM> template <class HMSYSTEM>
class Display { class Display {
public: 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) { if (mNewPayload || ((++mLoopCnt % 10) == 0)) {
mCfg = cfg;
mSys = sys;
mUtcTs = utcTs;
mNewPayload = false; mNewPayload = false;
mLoopCnt = 0; mLoopCnt = 0;
mVersion = version; DataScreen();
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
}
} }
}
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() { if ((0 < mCfg->type) && (mCfg->type < 10) && (mMono != NULL)) {
mMono.loop(); mMono->disp(totalPower, totalYieldDay, totalYieldTotal, isprod);
if (mNewPayload || ((++mLoopCnt % 10) == 0)) { } else if (mCfg->type >= 10) {
mNewPayload = false; #if defined(ESP32)
mLoopCnt = 0; mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod);
DataScreen(); mRefreshCycle++;
} #endif
} }
private: #if defined(ESP32)
void DataScreen() { if (mRefreshCycle > 480) {
if (mCfg->type == 0) mEpaper.fullRefresh();
return; mRefreshCycle = 0;
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
} }
#endif
// private member variables }
bool mNewPayload;
uint8_t mLoopCnt; // private member variables
uint32_t *mUtcTs; bool mNewPayload;
const char *mVersion; uint8_t mLoopCnt;
display_t *mCfg; uint32_t *mUtcTs;
HMSYSTEM *mSys; const char *mVersion;
uint16_t mRefreshCycle; display_t *mCfg;
HMSYSTEM *mSys;
#if defined(ESP32) uint16_t mRefreshCycle;
DisplayEPaper mEpaper;
#endif #if defined(ESP32)
DisplayMono mMono; DisplayEPaper mEpaper;
#endif
DisplayMono *mMono;
}; };
#endif /*__DISPLAY__*/ #endif /*__DISPLAY__*/

157
src/plugins/Display/Display_Mono.cpp

@ -1,157 +0,0 @@
// 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;
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);
}

45
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 <U8g2lib.h> #include <U8g2lib.h>
#define DISP_DEFAULT_TIMEOUT 60 // in seconds #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 <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#include "../../utils/helper.h"
class DisplayMono { class DisplayMono {
public: public:
DisplayMono(); 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);
private: 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;
void calcLineHeights(); virtual void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) = 0;
void setFont(uint8_t line); virtual void loop(void) = 0;
void printText(const char* text, uint8_t line, uint8_t dispX = 5); virtual void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) = 0;
protected:
U8G2* mDisplay; U8G2* mDisplay;
uint8_t mType; uint8_t mType;
bool mEnPowerSafe, mEnScreenSaver; bool mEnPowerSafe, mEnScreenSaver;
uint8_t mLuminance; uint8_t mLuminance;
bool mIsLarge = false;
uint8_t mLoopCnt; uint8_t mLoopCnt;
uint32_t* mUtcTs; 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; uint16_t mTimeout;
char _fmtText[DISP_FMT_TEXT_LEN]; char mFmtText[DISP_FMT_TEXT_LEN];};
};

155
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);
}
};

138
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);
}
};

132
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);
}
};

107
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; mDisplayRotation = rotation;
mEnPowerSafe = enPowerSafe;
} }
//*************************************************************************** //***************************************************************************
@ -120,7 +121,29 @@ void DisplayEPaper::lastUpdatePaged() {
} while (_display->nextPage()); } 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; int16_t tbx, tby;
uint16_t tbw, tbh, x, y; 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->setPartialWindow(0, mHeadFootPadding, _display->width(), _display->height() - (mHeadFootPadding * 2));
_display->fillScreen(GxEPD_WHITE); _display->fillScreen(GxEPD_WHITE);
do { do {
if (_totalPower > 9999) { if (totalPower > 9999) {
snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (_totalPower / 10000)); snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (totalPower / 10000));
_changed = true; _changed = true;
} else if ((_totalPower > 0) && (_totalPower <= 9999)) { } else if ((totalPower > 0) && (totalPower <= 9999)) {
snprintf(_fmtText, sizeof(_fmtText), "%.0f W", _totalPower); snprintf(_fmtText, sizeof(_fmtText), "%.0f W", totalPower);
_changed = true; _changed = true;
} else { } else {
snprintf(_fmtText, sizeof(_fmtText), "offline"); snprintf(_fmtText, sizeof(_fmtText), "offline");
} }
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); if (totalPower == 0){
x = ((_display->width() - tbw) / 2) - tbx; _display->fillRect(0, mHeadFootPadding, 200,200, GxEPD_BLACK);
_display->setCursor(x, mHeadFootPadding + tbh + 10); _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE);
_display->print(_fmtText); } else {
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
_display->setFont(&FreeSans12pt7b); x = ((_display->width() - tbw) / 2) - tbx;
y = _display->height() / 2; _display->setCursor(x, mHeadFootPadding + tbh + 10);
_display->setCursor(5, y); _display->print(_fmtText);
_display->print("today:");
snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay); _display->setFont(&FreeSans12pt7b);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); y = _display->height() / 2;
x = ((_display->width() - tbw) / 2) - tbx; _display->setCursor(5, y);
_display->setCursor(x, y); _display->print("today:");
_display->print(_fmtText); snprintf(_fmtText, _display->width(), "%.0f", totalYieldDay);
_display->setCursor(_display->width() - 38, y); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
_display->println("Wh"); x = ((_display->width() - tbw) / 2) - tbx;
_display->setCursor(x, y);
y = y + tbh + 7; _display->print(_fmtText);
_display->setCursor(5, y); _display->setCursor(_display->width() - 38, y);
_display->print("total:"); _display->println("Wh");
snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal);
_display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); y = y + tbh + 7;
x = ((_display->width() - tbw) / 2) - tbx; _display->setCursor(5, y);
_display->setCursor(x, y); _display->print("total:");
_display->print(_fmtText); snprintf(_fmtText, _display->width(), "%.1f", totalYieldTotal);
_display->setCursor(_display->width() - 50, y); _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh);
_display->println("kWh"); x = ((_display->width() - tbw) / 2) - tbx;
_display->setCursor(x, y);
_display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); _display->print(_fmtText);
snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod); _display->setCursor(_display->width() - 50, y);
_display->println(_fmtText); _display->println("kWh");
_display->setCursor(10, _display->height() - (mHeadFootPadding + 10));
snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", isprod);
_display->println(_fmtText);
}
} while (_display->nextPage()); } 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 // call the PowerPage to change the PV Power Values
actualPowerPaged(totalPower, totalYieldDay, totalYieldTotal, isprod); 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)) { if ((isprod > 0) && (_changed)) {
_changed = false; _changed = false;
lastUpdatePaged(); lastUpdatePaged();
} } else if((0 == totalPower) && (mEnPowerSafe))
offlineFooter();
_display->powerOff(); _display->powerOff();
} }

4
src/plugins/Display/Display_ePaper.h

@ -31,7 +31,7 @@ class DisplayEPaper {
DisplayEPaper(); DisplayEPaper();
void fullRefresh(); 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 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); void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod);
@ -39,6 +39,7 @@ class DisplayEPaper {
void headlineIP(); void headlineIP();
void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod);
void lastUpdatePaged(); void lastUpdatePaged();
void offlineFooter();
uint8_t mDisplayRotation; uint8_t mDisplayRotation;
bool _changed = false; bool _changed = false;
@ -47,6 +48,7 @@ class DisplayEPaper {
uint8_t mHeadFootPadding; uint8_t mHeadFootPadding;
GxEPD2_GFX* _display; GxEPD2_GFX* _display;
uint32_t *mUtcTs; uint32_t *mUtcTs;
bool mEnPowerSafe;
}; };
#endif // ESP32 #endif // ESP32

97
src/publisher/pubMqtt.h

@ -22,6 +22,7 @@
#include "../hm/hmSystem.h" #include "../hm/hmSystem.h"
#include "pubMqttDefs.h" #include "pubMqttDefs.h"
#include "pubMqttIvData.h"
#define QOS_0 0 #define QOS_0 0
@ -63,6 +64,10 @@ class PubMqtt {
mUtcTimestamp = utcTs; mUtcTimestamp = utcTs;
mIntervalTimeout = 1; mIntervalTimeout = 1;
mSendIvData.setup(sys, utcTs, &mSendList);
mSendIvData.setPublishFunc([this](const char *subTopic, const char *payload, bool retained) {
publish(subTopic, payload, retained);
});
mDiscovery.running = false; mDiscovery.running = false;
snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic); snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic);
@ -88,6 +93,8 @@ class PubMqtt {
} }
void loop() { void loop() {
mSendIvData.loop();
#if defined(ESP8266) #if defined(ESP8266)
mClient.loop(); mClient.loop();
yield(); yield();
@ -247,6 +254,8 @@ class PubMqtt {
subscribe(mVal); subscribe(mVal);
snprintf(mVal, 20, "ctrl/restart/%d", i); snprintf(mVal, 20, "ctrl/restart/%d", i);
subscribe(mVal); subscribe(mVal);
snprintf(mVal, 20, "ctrl/power/%d", i);
subscribe(mVal);
} }
subscribe(subscr[MQTT_SUBS_SET_TIME]); subscribe(subscr[MQTT_SUBS_SET_TIME]);
} }
@ -555,94 +564,12 @@ class PubMqtt {
void sendIvData() { void sendIvData() {
bool anyAvail = processIvStatus(); bool anyAvail = processIvStatus();
if (mLastAnyAvail != anyAvail) 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()) if(mSendList.empty())
return; return;
float total[4]; mSendIvData.start();
bool RTRDataHasBeenSent = false;
while(!mSendList.empty()) {
memset(total, 0, sizeof(float) * 4);
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_NOT_AVAIL_NOT_PROD != mLastIvState[id]))
sendData(iv, curInfoCmd);
// calculate total values for RealTimeRunData_Debug
if (sendTotals) {
record_t<> *rec = iv->getRecordStruct(curInfoCmd);
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();
}
if (sendTotals) {
uint8_t fieldId;
for (uint8_t i = 0; i < 4; i++) {
bool retained = true;
switch (i) {
default:
case 0:
fieldId = FLD_PAC;
retained = false;
break;
case 1:
fieldId = FLD_YT;
break;
case 2:
fieldId = FLD_YD;
break;
case 3:
fieldId = FLD_PDC;
retained = false;
break;
}
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]);
snprintf(mVal, 40, "%g", ah::round3(total[i]));
publish(mSubTopic, mVal, retained);
}
RTRDataHasBeenSent = true;
yield();
}
}
mSendList.pop(); // remove from list once all inverters were processed
}
mLastAnyAvail = anyAvail; mLastAnyAvail = anyAvail;
} }
@ -653,6 +580,8 @@ class PubMqtt {
#endif #endif
HMSYSTEM *mSys; HMSYSTEM *mSys;
PubMqttIvData<HMSYSTEM> mSendIvData;
uint32_t *mUtcTimestamp; uint32_t *mUtcTimestamp;
uint32_t mRxCnt, mTxCnt; uint32_t mRxCnt, mTxCnt;
std::queue<uint8_t> mSendList; std::queue<uint8_t> mSendList;

208
src/publisher/pubMqttIvData.h

@ -0,0 +1,208 @@
//-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//-----------------------------------------------------------------------------
#ifndef __PUB_MQTT_IV_DATA_H__
#define __PUB_MQTT_IV_DATA_H__
#include "../utils/dbg.h"
#include "../hm/hmSystem.h"
#include "pubMqttDefs.h"
typedef std::function<void(const char *subTopic, const char *payload, bool retained)> pubMqttPublisherType;
template<class HMSYSTEM>
class PubMqttIvData {
public:
void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue<uint8_t> *sendList) {
mSys = sys;
mUtcTimestamp = utcTs;
mSendList = sendList;
mState = IDLE;
memset(mIvLastRTRpub, 0, MAX_NUM_INVERTERS * 4);
mRTRDataHasBeenSent = false;
mTable[IDLE] = &PubMqttIvData::stateIdle;
mTable[START] = &PubMqttIvData::stateStart;
mTable[FIND_NXT_IV] = &PubMqttIvData::stateFindNxtIv;
mTable[SEND_DATA] = &PubMqttIvData::stateSend;
mTable[SEND_TOTALS] = &PubMqttIvData::stateSendTotals;
}
void loop() {
(this->*mTable[mState])();
yield();
}
bool start(void) {
if(IDLE != mState)
return false;
mRTRDataHasBeenSent = false;
mState = START;
return true;
}
void setPublishFunc(pubMqttPublisherType cb) {
mPublish = cb;
}
private:
enum State {IDLE, START, FIND_NXT_IV, SEND_DATA, SEND_TOTALS, NUM_STATES};
typedef void (PubMqttIvData::*StateFunction)();
void stateIdle() {
; // nothing to do
}
void stateStart() {
mLastIvId = 0;
if(!mSendList->empty()) {
mCmd = mSendList->front();
if((RealTimeRunData_Debug != mCmd) || !mRTRDataHasBeenSent) {
mSendTotals = (RealTimeRunData_Debug == mCmd);
memset(mTotal, 0, sizeof(float) * 4);
mState = FIND_NXT_IV;
} else
mSendList->pop();
} else
mState = IDLE;
}
void stateFindNxtIv() {
bool found = false;
for (; mLastIvId < mSys->getNumInverters(); mLastIvId++) {
mIv = mSys->getInverterByPos(mLastIvId);
if (NULL != mIv) {
if (mIv->config->enabled) {
found = true;
break;
}
}
}
mLastIvId++;
mPos = 0;
if(found)
mState = SEND_DATA;
else if(mSendTotals)
mState = SEND_TOTALS;
else {
mSendList->pop();
mState = START;
}
}
void stateSend() {
record_t<> *rec = mIv->getRecordStruct(mCmd);
uint32_t lastTs = mIv->getLastTs(rec);
bool pubData = (lastTs > 0);
if (mCmd == RealTimeRunData_Debug)
pubData &= (lastTs != mIvLastRTRpub[mIv->id]);
if (pubData) {
if(mPos < rec->length) {
bool retained = false;
if (mCmd == RealTimeRunData_Debug) {
switch (rec->assign[mPos].fieldId) {
case FLD_YT:
case FLD_YD:
if ((rec->assign[mPos].ch == CH0) && (!mIv->isProducing(*mUtcTimestamp))) { // avoids returns to 0 on restart
mPos++;
return;
}
retained = true;
break;
}
// calculate total values for RealTimeRunData_Debug
if (CH0 == rec->assign[mPos].ch) {
switch (rec->assign[mPos].fieldId) {
case FLD_PAC:
mTotal[0] += mIv->getValue(mPos, rec);
break;
case FLD_YT:
mTotal[1] += mIv->getValue(mPos, rec);
break;
case FLD_YD:
mTotal[2] += mIv->getValue(mPos, rec);
break;
case FLD_PDC:
mTotal[3] += mIv->getValue(mPos, rec);
break;
}
}
} else
mIvLastRTRpub[mIv->id] = lastTs;
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]);
snprintf(mVal, 40, "%g", ah::round3(mIv->getValue(mPos, rec)));
mPublish(mSubTopic, mVal, retained);
mPos++;
} else
mState = FIND_NXT_IV;
} else
mState = FIND_NXT_IV;
}
void stateSendTotals() {
uint8_t fieldId;
if(mPos < 4) {
bool retained = true;
switch (mPos) {
default:
case 0:
fieldId = FLD_PAC;
retained = false;
break;
case 1:
fieldId = FLD_YT;
break;
case 2:
fieldId = FLD_YD;
break;
case 3:
fieldId = FLD_PDC;
retained = false;
break;
}
snprintf(mSubTopic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]);
snprintf(mVal, 40, "%g", ah::round3(mTotal[mPos]));
mPublish(mSubTopic, mVal, retained);
mPos++;
} else {
mSendList->pop();
mState = START;
}
mRTRDataHasBeenSent = true;
}
HMSYSTEM *mSys;
uint32_t *mUtcTimestamp;
pubMqttPublisherType mPublish;
State mState;
StateFunction mTable[NUM_STATES];
uint8_t mCmd;
uint8_t mLastIvId;
bool mSendTotals;
float mTotal[4];
Inverter<> *mIv;
uint8_t mPos;
uint32_t mIvLastRTRpub[MAX_NUM_INVERTERS];
bool mRTRDataHasBeenSent;
char mSubTopic[32 + MAX_NAME_LENGTH + 1];
char mVal[40];
std::queue<uint8_t> *mSendList;
};
#endif /*__PUB_MQTT_IV_DATA_H__*/

2
src/utils/dbg.h

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 // 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef __DBG_H__ #ifndef __DBG_H__

3
src/web/RestApi.h

@ -276,6 +276,7 @@ class RestApi {
getGeneric(request, obj.createNestedObject(F("generic"))); getGeneric(request, obj.createNestedObject(F("generic")));
obj["pending"] = (bool)mApp->getSavePending(); obj["pending"] = (bool)mApp->getSavePending();
obj["success"] = (bool)mApp->getLastSaveSucceed(); obj["success"] = (bool)mApp->getLastSaveSucceed();
obj["reboot"] = (bool)mApp->getShouldReboot();
} }
void getReboot(AsyncWebServerRequest *request, JsonObject obj) { void getReboot(AsyncWebServerRequest *request, JsonObject obj) {
@ -552,7 +553,7 @@ class RestApi {
if(F("power") == jsonIn[F("cmd")]) if(F("power") == jsonIn[F("cmd")])
accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); 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); accepted = iv->setDevControlRequest(Restart);
else if(0 == strncmp("limit_", jsonIn[F("cmd")].as<const char*>(), 6)) { else if(0 == strncmp("limit_", jsonIn[F("cmd")].as<const char*>(), 6)) {
iv->powerLimit[0] = jsonIn["val"]; iv->powerLimit[0] = jsonIn["val"];

4
src/web/html/api.js

@ -78,7 +78,7 @@ function parseNav(obj) {
if(i == 2) if(i == 2)
continue; continue;
var l = document.getElementById("nav"+i); var l = document.getElementById("nav"+i);
if(window.location.pathname == "/" + l.href.split('/').pop()) if(window.location.pathname == "/" + l.href.substring(0, l.href.indexOf("?")).split('/').pop())
l.classList.add("active"); l.classList.add("active");
if(obj["menu_protEn"]) { if(obj["menu_protEn"]) {
@ -103,7 +103,7 @@ function parseVersion(obj) {
} }
function parseESP(obj) { function parseESP(obj) {
document.getElementById("esp_type").append( document.getElementById("esp_type").replaceChildren(
document.createTextNode("Board: " + obj["esp_type"]) document.createTextNode("Board: " + obj["esp_type"])
); );
} }

3
src/web/html/includes/header.html

@ -2,4 +2,5 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8"> <meta charset="UTF-8">
<script type="text/javascript" src="api.js?v={#VERSION}"></script> <script type="text/javascript" src="api.js?v={#VERSION}"></script>
<link rel="stylesheet" type="text/css" href="colors.css?v={#VERSION}"/> <link rel="stylesheet" type="text/css" href="colors.css?v={#VERSION}"/>
<meta name="robots" content="noindex, nofollow" />

38
src/web/html/save.html

@ -8,11 +8,13 @@
{#HTML_NAV} {#HTML_NAV}
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<div id="html" class="mt-3 mb-3"></div> <div id="html" class="mt-3 mb-3">Saving settings...</div>
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}
<script type="text/javascript"> <script type="text/javascript">
var intervalId = null;
function parseGeneric(obj) { function parseGeneric(obj) {
parseNav(obj); parseNav(obj);
parseESP(obj); parseESP(obj);
@ -21,18 +23,24 @@
function parseHtml(obj) { function parseHtml(obj) {
var html = ""; var html = "";
if(obj.pending) if(!obj.pending) {
html = "saving settings ..."; if(intervalId != null) {
else { clearInterval(intervalId);
if(obj.success) }
html = "settings successfully saved"; if(obj.success) {
else var meta = document.createElement('meta');
html = "failed saving settings"; meta.httpEquiv = "refresh"
if(!obj.reboot) {
var meta = document.createElement('meta'); html = "Settings successfully saved. Automatic page reload in 3 seconds.";
meta.httpEquiv = "refresh" meta.content = 3;
meta.content = 1 + "; URL=/setup"; } else {
document.getElementsByTagName('head')[0].appendChild(meta); html = "Settings successfully saved. Rebooting. Automatic redirect in 20 seconds.";
meta.content = 20 + "; URL=/";
}
document.getElementsByTagName('head')[0].appendChild(meta);
} else {
html = "Failed saving settings.";
}
} }
document.getElementById("html").innerHTML = html; document.getElementById("html").innerHTML = html;
} }
@ -41,11 +49,9 @@
if(null != obj) { if(null != obj) {
parseGeneric(obj["generic"]); parseGeneric(obj["generic"]);
parseHtml(obj); parseHtml(obj);
window.setInterval("getAjax('/api/html/save', parse)", 1100);
} }
} }
intervalId = window.setInterval("getAjax('/api/html/save', parse)", 2500);
getAjax("/api/html/save", parse);
</script> </script>
</body> </body>
</html> </html>

2
src/web/html/serial.html

@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html> <html lang="en">
<head> <head>
<title>Serial Console</title> <title>Serial Console</title>
{#HTML_HEADER} {#HTML_HEADER}

64
src/web/html/setup.html

@ -1,25 +1,14 @@
<!doctype html> <!doctype html>
<html> <html lang="en">
<head> <head>
<title>Setup</title> <title>Setup</title>
{#HTML_HEADER} {#HTML_HEADER}
<script type="text/javascript">
function load() {
for(it of document.getElementsByClassName("s_collapsible")) {
it.addEventListener("click", function() {
this.classList.toggle("active");
var content = this.nextElementSibling;
content.style.display = (content.style.display === "block") ? "none" : "block";
});
}
}
</script>
</head> </head>
<body onload="load()"> <body>
{#HTML_NAV} {#HTML_NAV}
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<form method="post" action="/save"> <form method="post" action="/save" id="settings">
<button type="button" class="s_collapsible mt-4">System Config</button> <button type="button" class="s_collapsible mt-4">System Config</button>
<div class="s_content"> <div class="s_content">
<fieldset class="mb-2"> <fieldset class="mb-2">
@ -31,6 +20,7 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3">Dark Mode</div> <div class="col-8 col-sm-3">Dark Mode</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="darkMode"/></div> <div class="col-4 col-sm-9"><input type="checkbox" name="darkMode"/></div>
<div class="col-8 col-sm-3">(empty browser cache or use CTRL + F5 after reboot to apply this setting)</div>
</div> </div>
</fieldset> </fieldset>
<fieldset class="mb-4"> <fieldset class="mb-4">
@ -146,11 +136,11 @@
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Interval [s]</div> <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 class="col-12 col-sm-9"><input type="number" name="invInterval" title="Invalid input"/></div>
</div> </div>
<div class="row mb-3"> <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-3 my-2">Max retries per Payload</div>
<div class="col-12 col-sm-9"><input type="text" name="invRetry"/></div> <div class="col-12 col-sm-9"><input type="number" name="invRetry"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3 mb-2">Reset values and YieldDay at midnight</div> <div class="col-8 col-sm-3 mb-2">Reset values and YieldDay at midnight</div>
@ -177,7 +167,7 @@
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">NTP Port</div> <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 class="col-12 col-sm-9"><input type="number" name="ntpPort"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">set system time</div> <div class="col-12 col-sm-3 my-2">set system time</div>
@ -194,15 +184,13 @@
<div class="s_content"> <div class="s_content">
<fieldset class="mb-4"> <fieldset class="mb-4">
<legend class="des">Sunrise & Sunset</legend> <legend class="des">Sunrise & Sunset</legend>
<p>Use a decimal separator: '.' (dot) for Latitude and Longitude</p>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Latitude (decimal)</div> <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 class="col-12 col-sm-9"><input type="number" name="sunLat" step="any"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Longitude (decimal)</div> <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 class="col-12 col-sm-9"><input type="number" name="sunLon" step="any"/></div>
</div> </div>
<div class="row mb-3"> <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-3 my-2">Offset (pre sunrise, post sunset)</div>
@ -225,7 +213,7 @@
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Port</div> <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 class="col-12 col-sm-9"><input type="number" name="mqttPort"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Username (optional)</div> <div class="col-12 col-sm-3 my-2">Username (optional)</div>
@ -242,7 +230,7 @@
<p class="des">Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)</p> <p class="des">Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)</p>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Interval [s]</div> <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 class="col-12 col-sm-9"><input type="number" name="mqttInterval" title="Invalid input" /></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Discovery Config (homeassistant)</div> <div class="col-12 col-sm-3 my-2">Discovery Config (homeassistant)</div>
@ -291,11 +279,13 @@
<fieldset class="mb-4"> <fieldset class="mb-4">
<legend class="des">Import / Export JSON Settings</legend> <legend class="des">Import / Export JSON Settings</legend>
<div class="row mb-4 mt-4"> <div class="row mb-4 mt-4">
<div class="col-12 col-sm-3 my-2">Import</div> <div class="col-12 col-sm-3">Import</div>
<div class="col-12 col-sm-9"> <div class="col-12 col-sm-9">
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8"> <form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="upload"> <div class="row">
<input type="button" class="btn" value="Import" onclick="hide()"> <div class="col-12 col-sm-8 my-2"><input type="file" name="upload"></div>
<div class="col-12 col-sm-4 my-2"><input type="button" class="btn" value="Import" onclick="hide()"></div>
</div>
</form> </form>
</div> </div>
</div> </div>
@ -418,6 +408,24 @@
const re = /11[2,4,6]1.*/; const re = /11[2,4,6]1.*/;
window.onload = function() {
for(it of document.getElementsByClassName("s_collapsible")) {
it.addEventListener("click", function() {
this.classList.toggle("active");
var content = this.nextElementSibling;
content.style.display = (content.style.display === "block") ? "none" : "block";
});
}
document.getElementById("settings").addEventListener("submit", function() {
var inputs = document.querySelectorAll("input[type='number']");
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].value.indexOf(",") != -1)
inputs[i].value = inputs[i].value.replace(",", ".");
}
});
}
document.getElementById("btnAdd").addEventListener("click", function() { document.getElementById("btnAdd").addEventListener("click", function() {
if(highestId <= (maxInv-1)) { if(highestId <= (maxInv-1)) {
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_pwr":[0,0,0,0],"ch_name":["","","",""],"ch_yield_cor":[0,0,0,0]}'), highestId); ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_pwr":[0,0,0,0],"ch_name":["","","",""],"ch_yield_cor":[0,0,0,0]}'), highestId);
@ -725,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) if("ESP32" == type)
opts.push([10, "ePaper"]); opts.push([10, "ePaper"]);
var dispType = sel("disp_typ", opts, obj["disp_typ"]); var dispType = sel("disp_typ", opts, obj["disp_typ"]);
@ -761,7 +769,7 @@
if(0 == dispType) if(0 == dispType)
cl.add("hide"); cl.add("hide");
else if(dispType <= 2) { // OLED else if(dispType <= 2 || dispType == 4) { // OLED
if(i < 2) if(i < 2)
cl.remove("hide"); cl.remove("hide");
else else

221
src/web/web.h

@ -515,7 +515,7 @@ class Web {
// pinout // pinout
uint8_t pin; uint8_t pin;
for (uint8_t i = 0; i < 8; i++) { for (uint8_t i = 0; i < 9; i++) {
pin = request->arg(String(pinArgNames[i])).toInt(); pin = request->arg(String(pinArgNames[i])).toInt();
switch(i) { switch(i) {
default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break; default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break;
@ -627,14 +627,22 @@ class Web {
#ifdef ENABLE_PROMETHEUS_EP #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 { enum {
metricsStateStart, metricsStateInverter, metricStateRealtimeData,metricsStateAlarmData,metricsStateEnd metricsStateStart,
metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4,
metricStateRealtimeFieldId, metricStateRealtimeInverterId,
metricsStateAlarmData,
metricsStateEnd
} metricsStep; } metricsStep;
int metricsInverterId,metricsChannelId; int metricsInverterId;
uint8_t metricsFieldId;
bool metricDeclared;
void showMetrics(AsyncWebServerRequest *request) { void showMetrics(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));
metricsStep = metricsStateStart; metricsStep = metricsStateStart;
AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"), AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"),
[this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t [this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t
@ -647,7 +655,11 @@ class Web {
char type[60], topic[100], val[25]; char type[60], topic[100], val[25];
size_t len = 0; size_t len = 0;
int alarmChannelId; 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) { switch (metricsStep) {
case metricsStateStart: // System Info & NRF Statistics : fit to one packet case metricsStateStart: // System Info & NRF Statistics : fit to one packet
snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n"); snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n");
@ -676,88 +688,138 @@ class Web {
metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt); metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt);
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
// Start Inverter loop // Next is Inverter information
metricsInverterId = 0; metricsInverterId = 0;
metricsStep = metricsStateInverter; metricsStep = metricsStateInverter1;
break; break;
case metricsStateInverter: // Inverter loop case metricsStateInverter1: // Information about all inverters configured : fit to one packet
if (metricsInverterId < mSys->getNumInverters()) { metrics = "# TYPE ahoy_solar_inverter_info gauge\n";
iv = mSys->getInverterByPos(metricsInverterId); metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n",
if(NULL != iv) { [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->serial.u64;});
// Inverter info : fit to one packet len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_info gauge\n"); metricsStep = metricsStateInverter2;
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", break;
iv->config->name, iv->config->serial.u64);
metrics = String(type) + String(topic); case metricsStateInverter2: // Information about all inverters configured : fit to one packet
metrics += "# TYPE ahoy_solar_inverter_is_enabled gauge\n";
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_enabled gauge\n"); metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",iv->config->name,iv->config->enabled); [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->enabled;});
metrics += String(type) + String(topic);
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_available gauge\n"); metricsStep = metricsStateInverter3;
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",iv->config->name,iv->isAvailable(mApp->getTimestamp())); break;
metrics += String(type) + String(topic);
case metricsStateInverter3: // Information about all inverters configured : fit to one packet
snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_producing gauge\n"); metrics += "# TYPE ahoy_solar_inverter_is_available gauge\n";
snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",iv->config->name,iv->isProducing(mApp->getTimestamp())); metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",
metrics += String(type) + String(topic); [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isAvailable(mApp->getTimestamp());});
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); metricsStep = metricsStateInverter4;
break;
// Start Realtime Data Channel loop for this inverter
metricsChannelId = 0; case metricsStateInverter4: // Information about all inverters configured : fit to one packet
metricsStep = metricStateRealtimeData; 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 { } else {
metricsStep = metricsStateEnd; metrics = "# Info: all realtime fields processed\n";
metricsStep = metricsStateAlarmData;
} }
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
break; break;
case metricStateRealtimeData: // Realtime Data Channel loop case metricStateRealtimeInverterId: // Iterate over all inverters for this field
iv = mSys->getInverterByPos(metricsInverterId); metrics = "";
rec = iv->getRecordStruct(RealTimeRunData_Debug); if (metricsInverterId < mSys->getNumInverters()) {
if (metricsChannelId < rec->length) { // process all channels of this inverter
uint8_t channel = rec->assign[metricsChannelId].ch;
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); iv = mSys->getInverterByPos(metricsInverterId);
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); if (NULL != iv) {
if (0 == channel) { rec = iv->getRecordStruct(RealTimeRunData_Debug);
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); 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;
}
} else { } 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]); metrics = "# Info: No data for field #"+String(metricsFieldId)+" of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n";
metricsFieldId++; // Process next field Id
metricsStep = metricStateRealtimeFieldId;
} }
snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec)); // Stay in this state and try next inverter
len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); metricsInverterId++;
metricsChannelId++;
} else { } else {
len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. metrics = "# Info: All inverters for field #"+String(metricsFieldId)+" processed.\n";
metricsFieldId++; // Process next field Id
// All realtime data channels processed --> try alarm data metricsStep = metricStateRealtimeFieldId;
metricsStep = metricsStateAlarmData;
} }
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
break; break;
case metricsStateAlarmData: // Alarm Info loop case metricsStateAlarmData: // Alarm Info loop : fit to one packet
iv = mSys->getInverterByPos(metricsInverterId); // Perform grouping on metrics according to Prometheus exposition format specification
rec = iv->getRecordStruct(AlarmData); snprintf(type, sizeof(type),"# TYPE ahoy_solar_%s gauge\n",fields[FLD_LAST_ALARM_CODE]);
// simple hack : there is only one channel with alarm data metrics = type;
// TODO: find the right one channel with the alarm id
alarmChannelId = 0; for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) {
// printf("AlarmData Length %d\n",rec->length); iv = mSys->getInverterByPos(metricsInverterId);
if (alarmChannelId < rec->length) { if (NULL != iv) {
//uint8_t channel = rec->assign[alarmChannelId].ch; rec = iv->getRecordStruct(AlarmData);
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); // simple hack : there is only one channel with alarm data
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str()); // TODO: find the right one channel with the alarm id
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); alarmChannelId = 0;
snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec)); if (alarmChannelId < rec->length) {
len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec));
} else { snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name);
len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec));
metrics += topic;
metrics += val;
}
}
} }
// alarm channel processed --> try next inverter len = snprintf((char*)buffer,maxLen,"%s",metrics.c_str());
metricsInverterId++; metricsStep = metricsStateEnd;
metricsStep = metricsStateInverter;
break; break;
case metricsStateEnd: case metricsStateEnd:
@ -770,6 +832,21 @@ class Web {
request->send(response); request->send(response);
} }
// Traverse all inverters and collect the metric via valueFunc
String inverterMetric(char *buffer, size_t len, const char *format, std::function<uint64_t(Inverter<> *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) { String radioStatistic(String statistic, uint32_t value) {
char type[60], topic[80], val[25]; char type[60], topic[80], val[25];
snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str()); snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str());

3
tools/rpi/ahoy.yml.example

@ -2,11 +2,14 @@
ahoy: ahoy:
interval: 5 interval: 5
transmit_retries: 5
logging: logging:
filename: 'hoymiles.log' filename: 'hoymiles.log'
# DEBUG, INFO, WARNING, ERROR, FATAL # DEBUG, INFO, WARNING, ERROR, FATAL
level: 'INFO' level: 'INFO'
max_log_filesize: 1000000
max_log_files: 1
sunset: sunset:
disabled: false disabled: false

29
tools/rpi/hoymiles/__init__.py

@ -297,8 +297,8 @@ class InverterPacketFragment:
class HoymilesNRF: class HoymilesNRF:
"""Hoymiles NRF24 Interface""" """Hoymiles NRF24 Interface"""
tx_channel_id = 0 tx_channel_id = 2
tx_channel_list = [40] tx_channel_list = [3,23,40,61,75]
rx_channel_id = 0 rx_channel_id = 0
rx_channel_list = [3,23,40,61,75] rx_channel_list = [3,23,40,61,75]
rx_channel_ack = False rx_channel_ack = False
@ -332,6 +332,12 @@ class HoymilesNRF:
:rtype: bool :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: if not txpower:
txpower = self.txpower txpower = self.txpower
@ -363,13 +369,13 @@ class HoymilesNRF:
""" """
Receive Packets Receive Packets
:param timeout: receive timeout in nanoseconds (default: 12e8) :param timeout: receive timeout in nanoseconds (default: 5e8)
:type timeout: int :type timeout: int
:yields: fragment :yields: fragment
""" """
if not timeout: if not timeout:
timeout=12e8 timeout=5e8
self.radio.setChannel(self.rx_channel) self.radio.setChannel(self.rx_channel)
self.radio.setAutoAck(False) self.radio.setAutoAck(False)
@ -415,7 +421,7 @@ class HoymilesNRF:
self.radio.setChannel(self.rx_channel) self.radio.setChannel(self.rx_channel)
self.radio.startListening() self.radio.startListening()
time.sleep(0.005) time.sleep(0.004)
def next_rx_channel(self): def next_rx_channel(self):
""" """
@ -433,6 +439,15 @@ class HoymilesNRF:
return True return True
return False 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 @property
def tx_channel(self): def tx_channel(self):
""" """
@ -612,10 +627,6 @@ class InverterTransaction:
packet = self.tx_queue.pop(0) 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) self.radio.transmit(packet, txpower=self.txpower)
wait = False wait = False

10
tools/rpi/hoymiles/__main__.py

@ -19,6 +19,7 @@ import yaml
from yaml.loader import SafeLoader from yaml.loader import SafeLoader
import hoymiles import hoymiles
import logging import logging
from logging.handlers import RotatingFileHandler
################################################################################ ################################################################################
""" Signal Handler """ """ Signal Handler """
@ -127,6 +128,7 @@ def main_loop(ahoy_config):
dtu_name = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu') dtu_name = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu')
sunset.sun_status2mqtt(dtu_ser, dtu_name) sunset.sun_status2mqtt(dtu_ser, dtu_name)
loop_interval = ahoy_config.get('interval', 1) loop_interval = ahoy_config.get('interval', 1)
transmit_retries = ahoy_config.get('transmit_retries', 5)
try: try:
do_init = True do_init = True
@ -143,7 +145,7 @@ def main_loop(ahoy_config):
sys.exit(999) sys.exit(999)
if hoymiles.HOYMILES_DEBUG_LOGGING: if hoymiles.HOYMILES_DEBUG_LOGGING:
logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}') logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}')
poll_inverter(inverter, dtu_ser, do_init, 3) poll_inverter(inverter, dtu_ser, do_init, transmit_retries)
do_init = False do_init = False
if loop_interval > 0: if loop_interval > 0:
@ -298,6 +300,8 @@ def init_logging(ahoy_config):
log_config = ahoy_config.get('logging') log_config = ahoy_config.get('logging')
fn = 'hoymiles.log' fn = 'hoymiles.log'
lvl = logging.ERROR lvl = logging.ERROR
max_log_filesize = 1000000
max_log_files = 1
if log_config: if log_config:
fn = log_config.get('filename', fn) fn = log_config.get('filename', fn)
level = log_config.get('level', 'ERROR') level = log_config.get('level', 'ERROR')
@ -311,9 +315,11 @@ def init_logging(ahoy_config):
lvl = logging.ERROR lvl = logging.ERROR
elif level == 'FATAL': elif level == 'FATAL':
lvl = logging.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: if hoymiles.HOYMILES_TRANSACTION_LOGGING:
lvl = logging.DEBUG 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') dtu_name = ahoy_config.get('dtu',{}).get('name','hoymiles-dtu')
logging.info(f'start logging for {dtu_name} with level: {logging.root.level}') logging.info(f'start logging for {dtu_name} with level: {logging.root.level}')

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

@ -515,9 +515,17 @@ class Hm300Decode0B(StatusResponse):
""" reactive power """ """ reactive power """
return self.unpack('>H', 20)[0]/10 return self.unpack('>H', 20)[0]/10
@property @property
def powerfactor(self):
""" Powerfactor """
return self.unpack('>H', 24)[0]/1000
@property
def temperature(self): def temperature(self):
""" Inverter temperature in °C """ """ Inverter temperature in °C """
return self.unpack('>h', 26)[0]/10 return self.unpack('>h', 26)[0]/10
@property
def event_count(self):
""" Event counter """
return self.unpack('>H', 28)[0]
class Hm300Decode0C(Hm300Decode0B): class Hm300Decode0C(Hm300Decode0B):
""" 1121-series mirco-inverters status data """ """ 1121-series mirco-inverters status data """

Loading…
Cancel
Save