diff --git a/Getting_Started.md b/Getting_Started.md index 3a031af0..d66aeca0 100644 --- a/Getting_Started.md +++ b/Getting_Started.md @@ -217,6 +217,14 @@ Once your Ahoy DTU is running, you can use the Over The Air (OTA) capabilities t ! ATTENTION: If you update from a very low version to the newest, please make sure to wipe all flash data! +#### Flashing on Linux with `esptool.py` (ESP32) +1. install [esptool.py](https://docs.espressif.com/projects/esptool/en/latest/esp32/) if you haven't already. +2. download and extract the latest release bin-file from [ahoy_](https://github.com/grindylow/ahoy/releases) +3. `cd ahoy_v && cp *esp32.bin esp32.bin` +4. Perhaps you need to replace `/dev/ttyUSB0` to match your acual device in the following command. Execute it afterwards: `esptool.py --port /dev/ttyUSB0 --chip esp32 --before default_reset --after hard_reset write_flash --flash_mode dout --flash_freq 40m --flash_size detect 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 esp32.bin` +5. Unplug and replug your device. +6. Open a serial monitor (e.g. Putty) @ 115200 Baud. You should see some messages regarding wifi. + ## Connect to your Ahoy DTU When everything is wired up and the firmware is flashed, it is time to connect to your Ahoy DTU. @@ -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 | | /visualization | displays the information from your converter | | 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 | -| /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 diff --git a/User_Manual.md b/User_Manual.md index 242a2809..a7be8f80 100644 --- a/User_Manual.md +++ b/User_Manual.md @@ -321,6 +321,19 @@ Send Power Limit: - A persistent limit is only needed if you want to throttle your inverter permanently or you can use it to set a start value on the battery, which is then always the switch-on limit when switching on, otherwise it would ramp up to 100% without regulation, which is continuous load is not healthy. - You can set a new limit in the turn-off state, which is then used for on (switching on again), otherwise the last limit from before the turn-off is used, but of course this only applies if DC voltage is applied the whole time. - If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit. +### Update your AHOY-DTU Firmware +To update your AHOY-DTU, you have to download the latest firmware package. +Here are the [latest stable releases](https://github.com/lumapu/ahoy/releases/) and [latest development builds](https://nightly.link/lumapu/ahoy/workflows/compile_development/development03/ahoydtu_dev.zip) available for download. +As soon as you have downloaded the firmware package, unzip it. On the WebUI, navigate to Update and press on select firmware file. +From the unzipped files, select the right .bin file for your hardware and needs. +- If you use an ESP8266, select the file ending with esp8266.bin +- If you use an ESP8266 with prometheus, select the file ending with esp8266_prometheus.bin +- If you use an ESP32, select the file ending with esp32.bin +- If you use an ESP32 with prometheus, select the file ending with esp32_prometheus.bin + +Note: if you want to use prometheus, the usage of an ESP32 is recommended, since the ESP8266 is at its performance limits and therefore can cause stability issues. + +After selecting the right firmware file, press update. Your AHOY-DTU will now install the new firmware and reboot. ## Additional Notes ### MI Inverters diff --git a/doc/prometheus_ep_description.md b/doc/prometheus_ep_description.md index 8fb9e002..755fd1e4 100644 --- a/doc/prometheus_ep_description.md +++ b/doc/prometheus_ep_description.md @@ -12,7 +12,7 @@ Prometheus metrics provided at `/metrics`. | name | Inverter name from setup | | serial | Serial number of inverter | | 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 | Metric name | Type | Description | Labels | diff --git a/src/CHANGES.md b/src/CHANGES.md index c9b426b9..6b97e319 100644 --- a/src/CHANGES.md +++ b/src/CHANGES.md @@ -1,20 +1,25 @@ # Development Changes -## 0.6.4 - 2023-04-06 -* merge PR #846, improved NRF24 communication and MI, thx @beegee3 & @rejoe2 -* merge PR #859, fix burger menu height, thx @ThomasPohl +## 0.6.15 - 2023-05-25 +* improved Prometheus Endpoint PR #958 +* fix turn off ePaper only if setting was set #956 +* improved reset values and update MqTT #957 -## 0.6.3 - 2023-04-04 -* fix login, password length was not checked #852 -* merge PR #854 optimize browser caching, thx @tastendruecker123 #828 -* fix WiFi reconnect not working #851 -* updated issue templates #822 +## 0.6.14 - 2023-05-21 +* merge PR #902 Mono-Display -## 0.6.2 - 2023-04-04 -* fix login from multiple clients #819 -* fix login screen on small displays +## 0.6.13 - 2023-05-16 +* merge PR #934 (fix JSON API) and #944 (update manual) -## 0.6.1 - 2023-04-01 -* merge LED fix - LED1 shows MqTT state, LED configureable active high/low #839 -* only publish new inverter data #826 -* potential fix of WiFi hostname during boot up #752 +## 0.6.12 - 2023-04-28 +* improved MqTT +* fix menu active item + +## 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 diff --git a/src/app.cpp b/src/app.cpp index 001b18f6..57e3fed6 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -175,6 +175,7 @@ void app::regularTickers(void) { everySec(std::bind(&DisplayType::tickerSecond, &mDisplay), "disp"); every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval, "uart"); //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) { Inverter<> *iv; + bool changed = false; // set values to zero, except yields for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); @@ -283,7 +285,11 @@ void app::tickZeroValues(void) { continue; // skip to next inverter 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' Inverter<> *iv; + bool changed = false; // set values to zero, except yields for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL == iv) continue; // skip to next inverter - if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) + if (!iv->isAvailable(mTimestamp) && !iv->isProducing(mTimestamp) && iv->config->enabled) { mPayload.zeroInverterValues(iv); + changed = true; } } + if(changed) + payloadEventListener(RealTimeRunData_Debug); +} + //----------------------------------------------------------------------------- void app::tickMidnight(void) { // 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"); Inverter<> *iv; + bool changed = false; // set values to zero, except yield total for (uint8_t id = 0; id < mSys.getNumInverters(); id++) { iv = mSys.getInverterByPos(id); if (NULL == iv) continue; // skip to next inverter - mPayload.zeroInverterValues(iv); - mPayload.zeroYieldDay(iv); + mPayload.zeroInverterValues(iv, false); + changed = true; } + if(changed) + payloadEventListener(RealTimeRunData_Debug); + if (mMqttEnabled) mMqtt.tickerMidnight(); } @@ -393,8 +409,7 @@ void app::mqttSubRxCb(JsonObject obj) { //----------------------------------------------------------------------------- void app::setupLed(void) { - - uint8_t led_off = (mConfig->led.led_high_active != 0) ? LOW : HIGH; + uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH; if (mConfig->led.led0 != 0xff) { pinMode(mConfig->led.led0, OUTPUT); @@ -408,9 +423,8 @@ void app::setupLed(void) { //----------------------------------------------------------------------------- void app::updateLed(void) { - - uint8_t led_off = (mConfig->led.led_high_active != 0) ? LOW : HIGH; - uint8_t led_on = (mConfig->led.led_high_active != 0) ? HIGH : LOW; + uint8_t led_off = (mConfig->led.led_high_active) ? LOW : HIGH; + uint8_t led_on = (mConfig->led.led_high_active) ? HIGH : LOW; if (mConfig->led.led0 != 0xff) { Inverter<> *iv = mSys.getInverterByPos(0); diff --git a/src/app.h b/src/app.h index 9a73ddcd..aa6a4915 100644 --- a/src/app.h +++ b/src/app.h @@ -93,6 +93,10 @@ class app : public IApp, public ah::Scheduler { return mSettings.getLastSaveSucceed(); } + bool getShouldReboot() { + return mSaveReboot; + } + statistics_t *getStatistics() { return &mStat; } diff --git a/src/appInterface.h b/src/appInterface.h index a3240189..7111bb86 100644 --- a/src/appInterface.h +++ b/src/appInterface.h @@ -20,6 +20,7 @@ class IApp { virtual bool eraseSettings(bool eraseWifi) = 0; virtual bool getSavePending() = 0; virtual bool getLastSaveSucceed() = 0; + virtual bool getShouldReboot() = 0; virtual void setOnUpdate() = 0; virtual void setRebootFlag() = 0; virtual const char *getVersion() = 0; diff --git a/src/config/settings.h b/src/config/settings.h index e895a8a9..d04fdce5 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -6,6 +6,11 @@ #ifndef __SETTINGS_H__ #define __SETTINGS_H__ +#if defined(F) && defined(ESP32) + #undef F + #define F(sl) (sl) +#endif + #include #include #include @@ -100,7 +105,7 @@ typedef struct { typedef struct { uint8_t led0; // first 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; typedef struct { @@ -245,15 +250,15 @@ class settings { root.shrinkToFit(); if(!err && (root.size() > 0)) { mCfg.valid = true; - jsonWifi(root[F("wifi")]); - jsonNrf(root[F("nrf")]); - jsonNtp(root[F("ntp")]); - jsonSun(root[F("sun")]); - jsonSerial(root[F("serial")]); - jsonMqtt(root[F("mqtt")]); - jsonLed(root[F("led")]); - jsonPlugin(root[F("plugin")]); - jsonInst(root[F("inst")]); + if(root.containsKey(F("wifi"))) jsonWifi(root[F("wifi")]); + if(root.containsKey(F("nrf"))) jsonNrf(root[F("nrf")]); + if(root.containsKey(F("ntp"))) jsonNtp(root[F("ntp")]); + if(root.containsKey(F("sun"))) jsonSun(root[F("sun")]); + if(root.containsKey(F("serial"))) jsonSerial(root[F("serial")]); + if(root.containsKey(F("mqtt"))) jsonMqtt(root[F("mqtt")]); + if(root.containsKey(F("led"))) jsonLed(root[F("led")]); + if(root.containsKey(F("plugin"))) jsonPlugin(root[F("plugin")]); + if(root.containsKey(F("inst"))) jsonInst(root[F("inst")]); } else { Serial.println(F("failed to parse json, using default config")); @@ -379,7 +384,7 @@ class settings { mCfg.led.led0 = 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)); @@ -410,17 +415,17 @@ class settings { ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf); ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf); } else { - snprintf(mCfg.sys.stationSsid, SSID_LEN, "%s", obj[F("ssid")].as()); - snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as()); - snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as()); - snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as()); - mCfg.sys.protectionMask = obj[F("prot_mask")]; - mCfg.sys.darkMode = obj[F("dark")]; - ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as()); - ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as()); - ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as()); - ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as()); - ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as()); + getChar(obj, F("ssid"), mCfg.sys.stationSsid, SSID_LEN); + getChar(obj, F("pwd"), mCfg.sys.stationPwd, PWD_LEN); + getChar(obj, F("dev"), mCfg.sys.deviceName, DEVNAME_LEN); + getChar(obj, F("adm"), mCfg.sys.adminPwd, PWD_LEN); + getVal(obj, F("prot_mask"), &mCfg.sys.protectionMask); + getVal(obj, F("dark"), &mCfg.sys.darkMode); + if(obj.containsKey(F("ip"))) ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as()); + if(obj.containsKey(F("mask"))) ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as()); + if(obj.containsKey(F("dns1"))) ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as()); + if(obj.containsKey(F("dns2"))) ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as()); + if(obj.containsKey(F("gtwy"))) ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as()); if(mCfg.sys.protectionMask == 0) 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("pwr")] = mCfg.nrf.amplifierPower; } else { - mCfg.nrf.sendInterval = obj[F("intvl")]; - mCfg.nrf.maxRetransPerPyld = obj[F("maxRetry")]; - mCfg.nrf.pinCs = obj[F("cs")]; - mCfg.nrf.pinCe = obj[F("ce")]; - mCfg.nrf.pinIrq = obj[F("irq")]; - mCfg.nrf.pinSclk = obj[F("sclk")]; - mCfg.nrf.pinMosi = obj[F("mosi")]; - mCfg.nrf.pinMiso = obj[F("miso")]; - mCfg.nrf.amplifierPower = obj[F("pwr")]; + getVal(obj, F("intvl"), &mCfg.nrf.sendInterval); + getVal(obj, F("maxRetry"), &mCfg.nrf.maxRetransPerPyld); + getVal(obj, F("cs"), &mCfg.nrf.pinCs); + getVal(obj, F("ce"), &mCfg.nrf.pinCe); + getVal(obj, F("irq"), &mCfg.nrf.pinIrq); + getVal(obj, F("sclk"), &mCfg.nrf.pinSclk); + getVal(obj, F("mosi"), &mCfg.nrf.pinMosi); + getVal(obj, F("miso"), &mCfg.nrf.pinMiso); + getVal(obj, F("pwr"), &mCfg.nrf.amplifierPower); if((obj[F("cs")] == obj[F("ce")])) { mCfg.nrf.pinCs = DEF_CS_PIN; mCfg.nrf.pinCe = DEF_CE_PIN; @@ -465,8 +470,8 @@ class settings { obj[F("addr")] = mCfg.ntp.addr; obj[F("port")] = mCfg.ntp.port; } else { - snprintf(mCfg.ntp.addr, NTP_ADDR_LEN, "%s", obj[F("addr")].as()); - mCfg.ntp.port = obj[F("port")]; + getChar(obj, F("addr"), mCfg.ntp.addr, NTP_ADDR_LEN); + getVal(obj, F("port"), &mCfg.ntp.port); } } @@ -477,10 +482,10 @@ class settings { obj[F("dis")] = mCfg.sun.disNightCom; obj[F("offs")] = mCfg.sun.offsetSec; } else { - mCfg.sun.lat = obj[F("lat")]; - mCfg.sun.lon = obj[F("lon")]; - mCfg.sun.disNightCom = obj[F("dis")]; - mCfg.sun.offsetSec = obj[F("offs")]; + getVal(obj, F("lat"), &mCfg.sun.lat); + getVal(obj, F("lon"), &mCfg.sun.lon); + getVal(obj, F("dis"), &mCfg.sun.disNightCom); + getVal(obj, F("offs"), &mCfg.sun.offsetSec); } } @@ -490,9 +495,9 @@ class settings { obj[F("show")] = mCfg.serial.showIv; obj[F("debug")] = mCfg.serial.debug; } else { - mCfg.serial.interval = obj[F("intvl")]; - mCfg.serial.showIv = obj[F("show")]; - mCfg.serial.debug = obj[F("debug")]; + getVal(obj, F("intvl"), &mCfg.serial.interval); + getVal(obj, F("show"), &mCfg.serial.showIv); + getVal(obj, F("debug"), &mCfg.serial.debug); } } @@ -506,12 +511,12 @@ class settings { obj[F("intvl")] = mCfg.mqtt.interval; } else { - mCfg.mqtt.port = obj[F("port")]; - mCfg.mqtt.interval = obj[F("intvl")]; - snprintf(mCfg.mqtt.broker, MQTT_ADDR_LEN, "%s", obj[F("broker")].as()); - snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as()); - snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as()); - snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", obj[F("topic")].as()); + getVal(obj, F("port"), &mCfg.mqtt.port); + getVal(obj, F("intvl"), &mCfg.mqtt.interval); + getChar(obj, F("broker"), mCfg.mqtt.broker, MQTT_ADDR_LEN); + getChar(obj, F("user"), mCfg.mqtt.user, MQTT_USER_LEN); + getChar(obj, F("pwd"), mCfg.mqtt.pwd, MQTT_PWD_LEN); + getChar(obj, F("topic"), mCfg.mqtt.topic, MQTT_TOPIC_LEN); } } @@ -519,11 +524,11 @@ class settings { if(set) { obj[F("0")] = mCfg.led.led0; 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 { - mCfg.led.led0 = obj[F("0")]; - mCfg.led.led1 = obj[F("1")]; - mCfg.led.led_high_active = obj[F("led_high_active")]; + getVal(obj, F("0"), &mCfg.led.led0); + getVal(obj, F("1"), &mCfg.led.led1); + getVal(obj, F("act_high"), &mCfg.led.led_high_active); } } @@ -545,19 +550,19 @@ class settings { disp[F("dc")] = mCfg.plugin.display.disp_dc; } else { JsonObject disp = obj["disp"]; - mCfg.plugin.display.type = disp[F("type")]; - mCfg.plugin.display.pwrSaveAtIvOffline = (bool)disp[F("pwrSafe")]; - mCfg.plugin.display.pxShift = (bool)disp[F("pxShift")]; - mCfg.plugin.display.rot = disp[F("rotation")]; + getVal(disp, F("type"), &mCfg.plugin.display.type); + getVal(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline); + getVal(disp, F("pxShift"), &mCfg.plugin.display.pxShift); + getVal(disp, F("rotation"), &mCfg.plugin.display.rot); //mCfg.plugin.display.wakeUp = disp[F("wake")]; //mCfg.plugin.display.sleepAt = disp[F("sleep")]; - mCfg.plugin.display.contrast = disp[F("contrast")]; - mCfg.plugin.display.disp_data = disp[F("data")]; - mCfg.plugin.display.disp_clk = disp[F("clock")]; - mCfg.plugin.display.disp_cs = disp[F("cs")]; - mCfg.plugin.display.disp_reset = disp[F("reset")]; - mCfg.plugin.display.disp_busy = disp[F("busy")]; - mCfg.plugin.display.disp_dc = disp[F("dc")]; + getVal(disp, F("contrast"), &mCfg.plugin.display.contrast); + getVal(disp, F("data"), &mCfg.plugin.display.disp_data); + getVal(disp, F("clock"), &mCfg.plugin.display.disp_clk); + getVal(disp, F("cs"), &mCfg.plugin.display.disp_cs); + getVal(disp, F("reset"), &mCfg.plugin.display.disp_reset); + getVal(disp, F("busy"), &mCfg.plugin.display.disp_busy); + getVal(disp, F("dc"), &mCfg.plugin.display.disp_dc); } } @@ -569,10 +574,10 @@ class settings { obj[F("rstComStop")] = (bool)mCfg.inst.rstValsCommStop; } else { - mCfg.inst.enabled = (bool)obj[F("en")]; - mCfg.inst.rstYieldMidNight = (bool)obj["rstMidNight"]; - mCfg.inst.rstValsNotAvail = (bool)obj["rstNotAvail"]; - mCfg.inst.rstValsCommStop = (bool)obj["rstComStop"]; + getVal(obj, F("en"), &mCfg.inst.enabled); + getVal(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight); + getVal(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail); + getVal(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop); } JsonArray ivArr; @@ -582,11 +587,8 @@ class settings { if(set) { if(mCfg.inst.iv[i].serial.u64 != 0ULL) jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); - } - else { - if(!obj[F("iv")][i].isNull()) - jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]); - } + } else 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]; } } else { - cfg->enabled = (bool)obj[F("en")]; - snprintf(cfg->name, MAX_NAME_LENGTH, "%s", obj[F("name")].as()); - cfg->serial.u64 = obj[F("sn")]; + getVal(obj, F("en"), &cfg->enabled); + getChar(obj, F("name"), cfg->name, MAX_NAME_LENGTH); + getVal(obj, F("sn"), &cfg->serial.u64); for(uint8_t i = 0; i < 4; i++) { - cfg->yieldCor[i] = obj[F("yield")][i]; - cfg->chMaxPwr[i] = obj[F("pwr")][i]; - snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as()); + if(obj.containsKey(F("yield"))) cfg->yieldCor[i] = obj[F("yield")][i]; + if(obj.containsKey(F("pwr"))) cfg->chMaxPwr[i] = obj[F("pwr")][i]; + if(obj.containsKey(F("chName"))) snprintf(cfg->chName[i], MAX_NAME_LENGTH, "%s", obj[F("chName")][i].as()); } } } + #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()); + } + + template + 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()); + } + + template + void getVal(JsonObject obj, const __FlashStringHelper *key, T *dst) { + if(obj.containsKey(key)) + *dst = obj[key]; + } + #endif + settings_t mCfg; bool mLastSaveSucceed; }; diff --git a/src/defines.h b/src/defines.h index 1ff3e1b7..baaea8b6 100644 --- a/src/defines.h +++ b/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 //----------------------------------------------------------------------------- @@ -13,7 +13,7 @@ //------------------------------------- #define VERSION_MAJOR 0 #define VERSION_MINOR 6 -#define VERSION_PATCH 4 +#define VERSION_PATCH 15 //------------------------------------- typedef struct { diff --git a/src/hm/hmPayload.h b/src/hm/hmPayload.h index b04ee8f3..5c66f1f8 100644 --- a/src/hm/hmPayload.h +++ b/src/hm/hmPayload.h @@ -70,17 +70,30 @@ class HmPayload { } } - void zeroYieldDay(Inverter<> *iv) { - DPRINTLN(DBG_DEBUG, F("zeroYieldDay")); - record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); - uint8_t pos; - for(uint8_t ch = 0; ch < iv->channels; ch++) { - pos = iv->getPosByChFld(CH0, FLD_YD, rec); - iv->setValue(pos, rec, 0.0f); - } - } + /*void simulation() { + uint8_t pay[] = { + 0x00, 0x01, 0x01, 0x24, 0x02, 0x28, 0x02, 0x33, + 0x06, 0x49, 0x06, 0x6a, 0x00, 0x05, 0x5f, 0x1b, + 0x00, 0x06, 0x66, 0x9a, 0x03, 0xfd, 0x04, 0x0b, + 0x01, 0x23, 0x02, 0x28, 0x02, 0x28, 0x06, 0x41, + 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 + }; - void zeroInverterValues(Inverter<> *iv) { + 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, bool skipYieldDay = true) { DPRINTLN(DBG_DEBUG, F("zeroInverterValues")); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); for(uint8_t ch = 0; ch <= iv->channels; ch++) { @@ -88,15 +101,18 @@ class HmPayload { for(uint8_t fld = 0; fld < FLD_EVT; fld++) { switch(fld) { case FLD_YD: + if(skipYieldDay) + continue; + else + break; case FLD_YT: continue; } pos = iv->getPosByChFld(ch, fld, rec); iv->setValue(pos, rec, 0.0f); } + iv->doCalculations(); } - - notify(RealTimeRunData_Debug); } void ivSendHighPrio(Inverter<> *iv) { diff --git a/src/platformio.ini b/src/platformio.ini index 5d4c655d..904f6120 100644 --- a/src/platformio.ini +++ b/src/platformio.ini @@ -16,17 +16,6 @@ include_dir = . framework = arduino board_build.filesystem = littlefs 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 extra_scripts = @@ -38,10 +27,10 @@ lib_deps = nrf24/RF24 @ ^1.4.5 paulstoffregen/Time @ ^1.6.1 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 - olikraus/U8g2 @ ^2.34.16 - zinggjm/GxEPD2 @ ^1.5.0 + olikraus/U8g2 @ ^2.34.17 + zinggjm/GxEPD2 @ ^1.5.2 [env:esp8266-release] @@ -95,7 +84,13 @@ platform = espressif8266 board = esp8285 board_build.ldscript = eagle.flash.1m64.ld 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 monitor_filters = ;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 [env:esp32-wroom32-release] -platform = espressif32 +platform = espressif32@6.1.0 board = lolin_d32 -build_flags = -D RELEASE -std=gnu++14 +build_flags = -D RELEASE -std=gnu++17 build_unflags = -std=gnu++11 monitor_filters = ;default ; Remove typical terminal control codes from input @@ -114,9 +109,11 @@ monitor_filters = esp32_exception_decoder [env:esp32-wroom32-release-prometheus] -platform = espressif32 +platform = espressif32@6.1.0 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 monitor_filters = ;default ; Remove typical terminal control codes from input @@ -125,9 +122,16 @@ monitor_filters = esp32_exception_decoder [env:esp32-wroom32-debug] -platform = espressif32 +platform = espressif32@6.1.0 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_type = debug monitor_filters = @@ -136,13 +140,13 @@ monitor_filters = log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory [env:opendtufusionv1-release] -platform = espressif32 +platform = espressif32@6.1.0 board = esp32-s3-devkitc-1 upload_protocol = esp-builtin upload_speed = 115200 debug_tool = esp-builtin debug_speed = 12000 -build_flags = -D RELEASE -std=gnu++14 +build_flags = -D RELEASE -std=gnu++17 build_unflags = -std=gnu++11 monitor_filters = ;default ; Remove typical terminal control codes from input diff --git a/src/plugins/Display/Display.h b/src/plugins/Display/Display.h index 1a0222b2..ba187c7d 100644 --- a/src/plugins/Display/Display.h +++ b/src/plugins/Display/Display.h @@ -7,108 +7,126 @@ #include "../../hm/hmSystem.h" #include "../../utils/helper.h" #include "Display_Mono.h" +#include "Display_Mono_128X32.h" +#include "Display_Mono_128X64.h" +#include "Display_Mono_84X48.h" #include "Display_ePaper.h" template class Display { public: - Display() {} + Display() {} - void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) { - mCfg = cfg; - mSys = sys; - mUtcTs = utcTs; + 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(); + + if (mNewPayload || ((++mLoopCnt % 10) == 0)) { mNewPayload = false; mLoopCnt = 0; - mVersion = version; + DataScreen(); + } + } - if (mCfg->type == 0) - return; + private: + void DataScreen() { + if (mCfg->type == 0) + return; + if (*mUtcTs == 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 - } + 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 payloadEventListener(uint8_t cmd) { - mNewPayload = true; + if ((0 < mCfg->type) && (mCfg->type < 10) && (mMono != NULL)) { + mMono->disp(totalPower, totalYieldDay, totalYieldTotal, isprod); + } else if (mCfg->type >= 10) { +#if defined(ESP32) + mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod); + mRefreshCycle++; +#endif } - void tickerSecond() { - mMono.loop(); - if (mNewPayload || ((++mLoopCnt % 10) == 0)) { - mNewPayload = false; - mLoopCnt = 0; - DataScreen(); - } +#if defined(ESP32) + if (mRefreshCycle > 480) { + mEpaper.fullRefresh(); + mRefreshCycle = 0; } +#endif + } - private: - void DataScreen() { - if (mCfg->type == 0) - return; - if (*mUtcTs == 0) - return; + // private member variables + bool mNewPayload; + uint8_t mLoopCnt; + uint32_t *mUtcTs; + const char *mVersion; + display_t *mCfg; + HMSYSTEM *mSys; + uint16_t mRefreshCycle; - 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 - } - - // private member variables - bool mNewPayload; - uint8_t mLoopCnt; - uint32_t *mUtcTs; - const char *mVersion; - display_t *mCfg; - HMSYSTEM *mSys; - uint16_t mRefreshCycle; - - #if defined(ESP32) - DisplayEPaper mEpaper; - #endif - DisplayMono mMono; +#if defined(ESP32) + DisplayEPaper mEpaper; +#endif + DisplayMono *mMono; }; #endif /*__DISPLAY__*/ diff --git a/src/plugins/Display/Display_Mono.cpp b/src/plugins/Display/Display_Mono.cpp deleted file mode 100644 index d55b6061..00000000 --- a/src/plugins/Display/Display_Mono.cpp +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#include "Display_Mono.h" - -#ifdef ESP8266 - #include -#elif defined(ESP32) - #include -#endif -#include "../../utils/helper.h" - -//#ifdef U8X8_HAVE_HW_SPI -//#include -//#endif -//#ifdef U8X8_HAVE_HW_I2C -//#include -//#endif - -DisplayMono::DisplayMono() { - mEnPowerSafe = true; - mEnScreenSaver = true; - mLuminance = 60; - _dispY = 0; - mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) - mUtcTs = NULL; - mType = 0; -} - - - -void DisplayMono::init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version) { - if ((0 < type) && (type < 4)) { - u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); - mType = type; - switch(type) { - case 1: - mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); - break; - default: - case 2: - mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); - break; - case 3: - mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset); - break; - } - - mUtcTs = utcTs; - - mDisplay->begin(); - - mIsLarge = (mDisplay->getWidth() > 120); - calcLineHeights(); - - mDisplay->clearBuffer(); - if (3 != mType) - mDisplay->setContrast(mLuminance); - printText("AHOY!", 0, 35); - printText("ahoydtu.de", 2, 20); - printText(version, 3, 46); - mDisplay->sendBuffer(); - } -} - -void DisplayMono::config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { - mEnPowerSafe = enPowerSafe; - mEnScreenSaver = enScreenSaver; - mLuminance = lum; -} - -void DisplayMono::loop(void) { - if (mEnPowerSafe) - if(mTimeout != 0) - mTimeout--; -} - -void DisplayMono::disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { - - - mDisplay->clearBuffer(); - - // set Contrast of the Display to raise the lifetime - if (3 != mType) - mDisplay->setContrast(mLuminance); - - if ((totalPower > 0) && (isprod > 0)) { - mTimeout = DISP_DEFAULT_TIMEOUT; - mDisplay->setPowerSave(false); - if (totalPower > 999) { - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); - } else { - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); - } - printText(_fmtText, 0); - } else { - printText("offline", 0, 25); - // check if it's time to enter power saving mode - if (mTimeout == 0) - mDisplay->setPowerSave(mEnPowerSafe); - } - - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); - printText(_fmtText, 1); - - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); - printText(_fmtText, 2); - - IPAddress ip = WiFi.localIP(); - if (!(_mExtra % 10) && (ip)) { - printText(ip.toString().c_str(), 3); - } else if (!(_mExtra % 5)) { - snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); - printText(_fmtText, 3); - } else { - if(mIsLarge && (NULL != mUtcTs)) - printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); - else - printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); - } - - mDisplay->sendBuffer(); - - _dispY = 0; - _mExtra++; -} - -void DisplayMono::calcLineHeights() { - uint8_t yOff = 0; - for (uint8_t i = 0; i < 4; i++) { - setFont(i); - yOff += (mDisplay->getMaxCharHeight()); - mLineOffsets[i] = yOff; - } -} - -inline void DisplayMono::setFont(uint8_t line) { - switch (line) { - case 0: - mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB14_tr : u8g2_font_logisoso16_tr); - break; - case 3: - mDisplay->setFont(u8g2_font_5x8_tr); - break; - default: - mDisplay->setFont((mIsLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr); - break; - } -} - -void DisplayMono::printText(const char* text, uint8_t line, uint8_t dispX) { - if (!mIsLarge) { - dispX = (line == 0) ? 10 : 5; - } - setFont(line); - - dispX += (mEnScreenSaver) ? (_mExtra % 7) : 0; - mDisplay->drawStr(dispX, mLineOffsets[line], text); -} diff --git a/src/plugins/Display/Display_Mono.h b/src/plugins/Display/Display_Mono.h index ad04c9f4..ed9154af 100644 --- a/src/plugins/Display/Display_Mono.h +++ b/src/plugins/Display/Display_Mono.h @@ -1,38 +1,45 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- +#pragma once #include #define DISP_DEFAULT_TIMEOUT 60 // in seconds -#define DISP_FMT_TEXT_LEN 32 +#define DISP_FMT_TEXT_LEN 32 +#define BOTTOM_MARGIN 5 + + +#ifdef ESP8266 +#include +#elif defined(ESP32) +#include +#endif +#include "../../utils/helper.h" class DisplayMono { public: - DisplayMono(); + 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: - void calcLineHeights(); - void setFont(uint8_t line); - void printText(const char* text, uint8_t line, uint8_t dispX = 5); + virtual void init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t* utcTs, const char* version) = 0; + virtual void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) = 0; + virtual void loop(void) = 0; + virtual void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) = 0; + protected: U8G2* mDisplay; uint8_t mType; bool mEnPowerSafe, mEnScreenSaver; uint8_t mLuminance; - bool mIsLarge = false; uint8_t mLoopCnt; uint32_t* mUtcTs; - uint8_t mLineOffsets[5]; + uint8_t mLineXOffsets[5]; + uint8_t mLineYOffsets[5]; - uint16_t _dispY; + uint16_t mDispY; - uint8_t _mExtra; + uint8_t mExtra; uint16_t mTimeout; - char _fmtText[DISP_FMT_TEXT_LEN]; -}; + char mFmtText[DISP_FMT_TEXT_LEN];}; diff --git a/src/plugins/Display/Display_Mono_128X32.h b/src/plugins/Display/Display_Mono_128X32.h new file mode 100644 index 00000000..9d5ade7e --- /dev/null +++ b/src/plugins/Display/Display_Mono_128X32.h @@ -0,0 +1,155 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#pragma once +#include "Display_Mono.h" + +class DisplayMono128X32 : public DisplayMono { + public: + DisplayMono128X32() : DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + mExtra = 0; + mDispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; + } + + + void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) { + if((0 == type) || (type > 4)) + return; + + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + mDisplay = new U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C(rot, reset, clock, data); + + mUtcTs = utcTs; + + mDisplay->begin(); + + calcLinePositions(); + + mDisplay->clearBuffer(); + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0); + printText("ahoydtu.de", 2); + printText(version, 3); + mDisplay->sendBuffer(); + } + + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; + } + + void loop(void) { + if (mEnPowerSafe) { + if (mTimeout != 0) + mTimeout--; + } + } + + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + if (totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + + printText(mFmtText, 0); + } else { + printText("offline", 0); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(mFmtText, 1); + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(mFmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(mExtra % 10) && (ip)) + printText(ip.toString().c_str(), 3); + else if (!(mExtra % 5)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(mFmtText, 3); + } else if (NULL != mUtcTs) + printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + + mDisplay->sendBuffer(); + + mDispY = 0; + mExtra++; + } + + private: + void calcLinePositions() { + uint8_t yOff[] = {0, 0}; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff[getColumn(i)] += (mDisplay->getMaxCharHeight()); + mLineYOffsets[i] = yOff[getColumn(i)]; + if (isTwoRowLine(i)) + yOff[getColumn(i)] += mDisplay->getMaxCharHeight(); + yOff[getColumn(i)] += BOTTOM_MARGIN; + mLineXOffsets[i] = (getColumn(i) == 1 ? 80 : 0); + } + } + + inline void setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont(u8g2_font_9x15_tf); + break; + case 3: + mDisplay->setFont(u8g2_font_tom_thumb_4x6_tf); + break; + default: + mDisplay->setFont(u8g2_font_tom_thumb_4x6_tf); + break; + } + } + + inline uint8_t getColumn(uint8_t line) { + if (line >= 1 && line <= 2) + return 1; + else + return 0; + } + + inline bool isTwoRowLine(uint8_t line) { + return ((line >= 1) && (line <= 2)); + } + + void printText(const char *text, uint8_t line) { + setFont(line); + + uint8_t dispX = mLineXOffsets[line] + ((mEnScreenSaver) ? (mExtra % 7) : 0); + + if (isTwoRowLine(line)) { + String stringText = String(text); + int space = stringText.indexOf(" "); + mDisplay->drawStr(dispX, mLineYOffsets[line], stringText.substring(0, space).c_str()); + if (space > 0) + mDisplay->drawStr(dispX, mLineYOffsets[line] + mDisplay->getMaxCharHeight(), stringText.substring(space + 1).c_str()); + } else + mDisplay->drawStr(dispX, mLineYOffsets[line], text); + } +}; diff --git a/src/plugins/Display/Display_Mono_128X64.h b/src/plugins/Display/Display_Mono_128X64.h new file mode 100644 index 00000000..3d4f91ee --- /dev/null +++ b/src/plugins/Display/Display_Mono_128X64.h @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#pragma once +#include "Display_Mono.h" + +class DisplayMono128X64 : public DisplayMono { + public: + DisplayMono128X64() : DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + mDispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; + } + + void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) { + if((0 == type) || (type > 4)) + return; + + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + + switch (type) { + case 1: + mDisplay = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); + break; + default: + case 2: + mDisplay = new U8G2_SH1106_128X64_NONAME_F_HW_I2C(rot, reset, clock, data); + break; + } + + mUtcTs = utcTs; + + mDisplay->begin(); + calcLinePositions(); + + mDisplay->clearBuffer(); + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0, 35); + printText("ahoydtu.de", 2, 20); + printText(version, 3, 46); + mDisplay->sendBuffer(); + } + + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; + } + + void loop(void) { + if (mEnPowerSafe) { + if (mTimeout != 0) + mTimeout--; + } + } + + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + + if (totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + + printText(mFmtText, 0); + } else { + printText("offline", 0, 25); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(mFmtText, 1); + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(mFmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(mExtra % 10) && (ip)) + printText(ip.toString().c_str(), 3); + else if (!(mExtra % 5)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(mFmtText, 3); + } else if (NULL != mUtcTs) + printText(ah::getDateTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + + mDisplay->sendBuffer(); + + mDispY = 0; + mExtra++; + } + + private: + void calcLinePositions() { + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (mDisplay->getMaxCharHeight()); + mLineYOffsets[i] = yOff; + } + } + + inline void setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont(u8g2_font_ncenB14_tr); + break; + case 3: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + default: + mDisplay->setFont(u8g2_font_ncenB10_tr); + break; + } + } + void printText(const char *text, uint8_t line, uint8_t dispX = 5) { + setFont(line); + + dispX += (mEnScreenSaver) ? (mExtra % 7) : 0; + mDisplay->drawStr(dispX, mLineYOffsets[line], text); + } +}; diff --git a/src/plugins/Display/Display_Mono_84X48.h b/src/plugins/Display/Display_Mono_84X48.h new file mode 100644 index 00000000..82aa83fa --- /dev/null +++ b/src/plugins/Display/Display_Mono_84X48.h @@ -0,0 +1,132 @@ +//----------------------------------------------------------------------------- +// 2023 Ahoy, https://ahoydtu.de +// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed +//----------------------------------------------------------------------------- + +#pragma once +#include "Display_Mono.h" + +class DisplayMono84X48 : public DisplayMono { + public: + DisplayMono84X48() : DisplayMono() { + mEnPowerSafe = true; + mEnScreenSaver = true; + mLuminance = 60; + mExtra = 0; + mDispY = 0; + mTimeout = DISP_DEFAULT_TIMEOUT; // interval at which to power save (milliseconds) + mUtcTs = NULL; + mType = 0; + } + + void init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char *version) { + if((0 == type) || (type > 4)) + return; + + u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0); + mType = type; + mDisplay = new U8G2_PCD8544_84X48_F_4W_SW_SPI(rot, clock, data, cs, dc, reset); + + mUtcTs = utcTs; + + mDisplay->begin(); + calcLinePositions(); + + mDisplay->clearBuffer(); + if (3 != mType) + mDisplay->setContrast(mLuminance); + printText("AHOY!", 0); + printText("ahoydtu.de", 2); + printText(version, 3); + mDisplay->sendBuffer(); + } + + void config(bool enPowerSafe, bool enScreenSaver, uint8_t lum) { + mEnPowerSafe = enPowerSafe; + mEnScreenSaver = enScreenSaver; + mLuminance = lum; + } + + void loop(void) { + if (mEnPowerSafe) { + if (mTimeout != 0) + mTimeout--; + } + } + + void disp(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { + mDisplay->clearBuffer(); + + // set Contrast of the Display to raise the lifetime + if (3 != mType) + mDisplay->setContrast(mLuminance); + + if ((totalPower > 0) && (isprod > 0)) { + mTimeout = DISP_DEFAULT_TIMEOUT; + mDisplay->setPowerSave(false); + + if (totalPower > 999) + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%2.2f kW", (totalPower / 1000)); + else + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%3.0f W", totalPower); + + printText(mFmtText, 0); + } else { + printText("offline", 0); + // check if it's time to enter power saving mode + if (mTimeout == 0) + mDisplay->setPowerSave(mEnPowerSafe); + } + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "today: %4.0f Wh", totalYieldDay); + printText(mFmtText, 1); + + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "total: %.1f kWh", totalYieldTotal); + printText(mFmtText, 2); + + IPAddress ip = WiFi.localIP(); + if (!(mExtra % 10) && (ip)) + printText(ip.toString().c_str(), 3); + else if (!(mExtra % 5)) { + snprintf(mFmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod); + printText(mFmtText, 3); + } else if (NULL != mUtcTs) + printText(ah::getTimeStr(gTimezone.toLocal(*mUtcTs)).c_str(), 3); + + mDisplay->sendBuffer(); + + mExtra = 1; + } + + private: + void calcLinePositions() { + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (mDisplay->getMaxCharHeight()); + mLineYOffsets[i] = yOff; + } + } + + inline void setFont(uint8_t line) { + switch (line) { + case 0: + mDisplay->setFont(u8g2_font_logisoso16_tr); + break; + case 3: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + default: + mDisplay->setFont(u8g2_font_5x8_tr); + break; + } + } + + void printText(const char *text, uint8_t line) { + uint8_t dispX = (line == 0) ? 10 : 5; + setFont(line); + + dispX += (mEnScreenSaver) ? (mExtra % 7) : 0; + mDisplay->drawStr(dispX, mLineYOffsets[line], text); + } +}; diff --git a/src/plugins/Display/Display_ePaper.cpp b/src/plugins/Display/Display_ePaper.cpp index 99d35ed8..924961a3 100644 --- a/src/plugins/Display/Display_ePaper.cpp +++ b/src/plugins/Display/Display_ePaper.cpp @@ -57,8 +57,9 @@ void DisplayEPaper::init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, u } } -void DisplayEPaper::config(uint8_t rotation) { +void DisplayEPaper::config(uint8_t rotation, bool enPowerSafe) { mDisplayRotation = rotation; + mEnPowerSafe = enPowerSafe; } //*************************************************************************** @@ -120,7 +121,29 @@ void DisplayEPaper::lastUpdatePaged() { } while (_display->nextPage()); } //*************************************************************************** -void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod) { +void DisplayEPaper::offlineFooter() { + int16_t tbx, tby; + uint16_t tbw, tbh; + + _display->setFont(&FreeSans9pt7b); + _display->setTextColor(GxEPD_WHITE); + + _display->setPartialWindow(0, _display->height() - mHeadFootPadding, _display->width(), mHeadFootPadding); + _display->fillScreen(GxEPD_BLACK); + do { + if (NULL != mUtcTs) { + snprintf(_fmtText, sizeof(_fmtText), "offline"); + + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + uint16_t x = ((_display->width() - tbw) / 2) - tbx; + + _display->setCursor(x, (_display->height() - 3)); + _display->println(_fmtText); + } + } while (_display->nextPage()); +} +//*************************************************************************** +void DisplayEPaper::actualPowerPaged(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod) { int16_t tbx, tby; uint16_t tbw, tbh, x, y; @@ -130,47 +153,52 @@ void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, fl _display->setPartialWindow(0, mHeadFootPadding, _display->width(), _display->height() - (mHeadFootPadding * 2)); _display->fillScreen(GxEPD_WHITE); do { - if (_totalPower > 9999) { - snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (_totalPower / 10000)); + if (totalPower > 9999) { + snprintf(_fmtText, sizeof(_fmtText), "%.1f kW", (totalPower / 10000)); _changed = true; - } else if ((_totalPower > 0) && (_totalPower <= 9999)) { - snprintf(_fmtText, sizeof(_fmtText), "%.0f W", _totalPower); + } else if ((totalPower > 0) && (totalPower <= 9999)) { + snprintf(_fmtText, sizeof(_fmtText), "%.0f W", totalPower); _changed = true; } else { snprintf(_fmtText, sizeof(_fmtText), "offline"); } - _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); - x = ((_display->width() - tbw) / 2) - tbx; - _display->setCursor(x, mHeadFootPadding + tbh + 10); - _display->print(_fmtText); + if (totalPower == 0){ + _display->fillRect(0, mHeadFootPadding, 200,200, GxEPD_BLACK); + _display->drawBitmap(0, 0, logo, 200, 200, GxEPD_WHITE); + } else { + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, mHeadFootPadding + tbh + 10); + _display->print(_fmtText); - _display->setFont(&FreeSans12pt7b); - y = _display->height() / 2; - _display->setCursor(5, y); - _display->print("today:"); - snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay); - _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); - x = ((_display->width() - tbw) / 2) - tbx; - _display->setCursor(x, y); - _display->print(_fmtText); - _display->setCursor(_display->width() - 38, y); - _display->println("Wh"); + _display->setFont(&FreeSans12pt7b); + y = _display->height() / 2; + _display->setCursor(5, y); + _display->print("today:"); + snprintf(_fmtText, _display->width(), "%.0f", totalYieldDay); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - 38, y); + _display->println("Wh"); - y = y + tbh + 7; - _display->setCursor(5, y); - _display->print("total:"); - snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal); - _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); - x = ((_display->width() - tbw) / 2) - tbx; - _display->setCursor(x, y); - _display->print(_fmtText); - _display->setCursor(_display->width() - 50, y); - _display->println("kWh"); + y = y + tbh + 7; + _display->setCursor(5, y); + _display->print("total:"); + snprintf(_fmtText, _display->width(), "%.1f", totalYieldTotal); + _display->getTextBounds(_fmtText, 0, 0, &tbx, &tby, &tbw, &tbh); + x = ((_display->width() - tbw) / 2) - tbx; + _display->setCursor(x, y); + _display->print(_fmtText); + _display->setCursor(_display->width() - 50, y); + _display->println("kWh"); - _display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); - snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod); - _display->println(_fmtText); + _display->setCursor(10, _display->height() - (mHeadFootPadding + 10)); + snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", isprod); + _display->println(_fmtText); + } } while (_display->nextPage()); } //*************************************************************************** @@ -185,11 +213,12 @@ void DisplayEPaper::loop(float totalPower, float totalYieldDay, float totalYield // call the PowerPage to change the PV Power Values actualPowerPaged(totalPower, totalYieldDay, totalYieldTotal, isprod); - // if there was an change and the Inverter is producing set a new Timestam in the footline + // if there was an change and the Inverter is producing set a new Timestamp in the footline if ((isprod > 0) && (_changed)) { _changed = false; lastUpdatePaged(); - } + } else if((0 == totalPower) && (mEnPowerSafe)) + offlineFooter(); _display->powerOff(); } diff --git a/src/plugins/Display/Display_ePaper.h b/src/plugins/Display/Display_ePaper.h index b2729f25..ad422b26 100644 --- a/src/plugins/Display/Display_ePaper.h +++ b/src/plugins/Display/Display_ePaper.h @@ -31,7 +31,7 @@ class DisplayEPaper { DisplayEPaper(); void fullRefresh(); void init(uint8_t type, uint8_t _CS, uint8_t _DC, uint8_t _RST, uint8_t _BUSY, uint8_t _SCK, uint8_t _MOSI, uint32_t *utcTs, const char* version); - void config(uint8_t rotation); + void config(uint8_t rotation, bool enPowerSafe); void loop(float totalPower, float totalYieldDay, float totalYieldTotal, uint8_t isprod); @@ -39,6 +39,7 @@ class DisplayEPaper { void headlineIP(); void actualPowerPaged(float _totalPower, float _totalYieldDay, float _totalYieldTotal, uint8_t _isprod); void lastUpdatePaged(); + void offlineFooter(); uint8_t mDisplayRotation; bool _changed = false; @@ -47,6 +48,7 @@ class DisplayEPaper { uint8_t mHeadFootPadding; GxEPD2_GFX* _display; uint32_t *mUtcTs; + bool mEnPowerSafe; }; #endif // ESP32 diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index e717ee7c..1f8519f5 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -22,6 +22,7 @@ #include "../hm/hmSystem.h" #include "pubMqttDefs.h" +#include "pubMqttIvData.h" #define QOS_0 0 @@ -63,6 +64,10 @@ class PubMqtt { mUtcTimestamp = utcTs; 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; snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic); @@ -88,6 +93,8 @@ class PubMqtt { } void loop() { + mSendIvData.loop(); + #if defined(ESP8266) mClient.loop(); yield(); @@ -247,6 +254,8 @@ class PubMqtt { subscribe(mVal); snprintf(mVal, 20, "ctrl/restart/%d", i); subscribe(mVal); + snprintf(mVal, 20, "ctrl/power/%d", i); + subscribe(mVal); } subscribe(subscr[MQTT_SUBS_SET_TIME]); } @@ -555,94 +564,12 @@ class PubMqtt { void sendIvData() { bool anyAvail = processIvStatus(); if (mLastAnyAvail != anyAvail) - mSendList.push(RealTimeRunData_Debug); // makes shure that total values are calculated + mSendList.push(RealTimeRunData_Debug); // makes sure that total values are calculated if(mSendList.empty()) return; - float total[4]; - 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 - } - + mSendIvData.start(); mLastAnyAvail = anyAvail; } @@ -653,6 +580,8 @@ class PubMqtt { #endif HMSYSTEM *mSys; + PubMqttIvData mSendIvData; + uint32_t *mUtcTimestamp; uint32_t mRxCnt, mTxCnt; std::queue mSendList; diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h new file mode 100644 index 00000000..947cd2f4 --- /dev/null +++ b/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 pubMqttPublisherType; + +template +class PubMqttIvData { + public: + void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue *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 *mSendList; +}; + +#endif /*__PUB_MQTT_IV_DATA_H__*/ diff --git a/src/utils/dbg.h b/src/utils/dbg.h index 8ce23db9..6c861329 100644 --- a/src/utils/dbg.h +++ b/src/utils/dbg.h @@ -1,6 +1,6 @@ //----------------------------------------------------------------------------- // 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__ diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 2d617847..812bb640 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -276,6 +276,7 @@ class RestApi { getGeneric(request, obj.createNestedObject(F("generic"))); obj["pending"] = (bool)mApp->getSavePending(); obj["success"] = (bool)mApp->getLastSaveSucceed(); + obj["reboot"] = (bool)mApp->getShouldReboot(); } void getReboot(AsyncWebServerRequest *request, JsonObject obj) { @@ -552,7 +553,7 @@ class RestApi { if(F("power") == jsonIn[F("cmd")]) accepted = iv->setDevControlRequest((jsonIn[F("val")] == 1) ? TurnOn : TurnOff); - else if(F("restart") == jsonIn[F("restart")]) + else if(F("restart") == jsonIn[F("cmd")]) accepted = iv->setDevControlRequest(Restart); else if(0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) { iv->powerLimit[0] = jsonIn["val"]; diff --git a/src/web/html/api.js b/src/web/html/api.js index 5ccb0e15..1dd5422e 100644 --- a/src/web/html/api.js +++ b/src/web/html/api.js @@ -78,7 +78,7 @@ function parseNav(obj) { if(i == 2) continue; 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"); if(obj["menu_protEn"]) { @@ -103,7 +103,7 @@ function parseVersion(obj) { } function parseESP(obj) { - document.getElementById("esp_type").append( + document.getElementById("esp_type").replaceChildren( document.createTextNode("Board: " + obj["esp_type"]) ); } diff --git a/src/web/html/includes/header.html b/src/web/html/includes/header.html index d591eb42..ab3b0545 100644 --- a/src/web/html/includes/header.html +++ b/src/web/html/includes/header.html @@ -2,4 +2,5 @@ - \ No newline at end of file + + diff --git a/src/web/html/save.html b/src/web/html/save.html index 54d43d7f..4c924c40 100644 --- a/src/web/html/save.html +++ b/src/web/html/save.html @@ -8,11 +8,13 @@ {#HTML_NAV}
-
+
Saving settings...
{#HTML_FOOTER} diff --git a/src/web/html/serial.html b/src/web/html/serial.html index da9d2816..442ba24a 100644 --- a/src/web/html/serial.html +++ b/src/web/html/serial.html @@ -1,5 +1,5 @@ - + Serial Console {#HTML_HEADER} diff --git a/src/web/html/setup.html b/src/web/html/setup.html index 97bf5608..427152e3 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -1,25 +1,14 @@ - + Setup {#HTML_HEADER} - - + {#HTML_NAV}
-
+
@@ -31,6 +20,7 @@
Dark Mode
+
(empty browser cache or use CTRL + F5 after reboot to apply this setting)
@@ -146,11 +136,11 @@
Interval [s]
-
+
Max retries per Payload
-
+
Reset values and YieldDay at midnight
@@ -177,7 +167,7 @@
NTP Port
-
+
set system time
@@ -194,15 +184,13 @@
Sunrise & Sunset -

Use a decimal separator: '.' (dot) for Latitude and Longitude

-
Latitude (decimal)
-
+
Longitude (decimal)
-
+
Offset (pre sunrise, post sunset)
@@ -225,7 +213,7 @@
Port
-
+
Username (optional)
@@ -242,7 +230,7 @@

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)

Interval [s]
-
+
Discovery Config (homeassistant)
@@ -291,11 +279,13 @@
Import / Export JSON Settings
-
Import
+
Import
- - +
+
+
+
@@ -418,6 +408,24 @@ 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() { 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); @@ -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) opts.push([10, "ePaper"]); var dispType = sel("disp_typ", opts, obj["disp_typ"]); @@ -761,7 +769,7 @@ if(0 == dispType) cl.add("hide"); - else if(dispType <= 2) { // OLED + else if(dispType <= 2 || dispType == 4) { // OLED if(i < 2) cl.remove("hide"); else diff --git a/src/web/web.h b/src/web/web.h index f8abc0f7..d8804b54 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -515,7 +515,7 @@ class Web { // pinout 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(); switch(i) { default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break; @@ -627,14 +627,22 @@ class Web { #ifdef ENABLE_PROMETHEUS_EP + // Note + // Prometheus exposition format is defined here: https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md + // TODO: Check packetsize for MAX_NUM_INVERTERS. Successfull Tested with 4 Inverters (each with 4 channels) enum { - metricsStateStart, metricsStateInverter, metricStateRealtimeData,metricsStateAlarmData,metricsStateEnd + metricsStateStart, + metricsStateInverter1, metricsStateInverter2, metricsStateInverter3, metricsStateInverter4, + metricStateRealtimeFieldId, metricStateRealtimeInverterId, + metricsStateAlarmData, + metricsStateEnd } metricsStep; - int metricsInverterId,metricsChannelId; + int metricsInverterId; + uint8_t metricsFieldId; + bool metricDeclared; void showMetrics(AsyncWebServerRequest *request) { DPRINTLN(DBG_VERBOSE, F("web::showMetrics")); - metricsStep = metricsStateStart; AsyncWebServerResponse *response = request->beginChunkedResponse(F("text/plain"), [this](uint8_t *buffer, size_t maxLen, size_t filledLength) -> size_t @@ -647,7 +655,11 @@ class Web { char type[60], topic[100], val[25]; size_t len = 0; int alarmChannelId; + int metricsChannelId; + // Perform grouping on metrics according to format specification + // Each step must return at least one character. Otherwise the processing of AsyncWebServerResponse stops. + // So several "Info:" blocks are used to keep the transmission going switch (metricsStep) { case metricsStateStart: // System Info & NRF Statistics : fit to one packet snprintf(type,sizeof(type),"# TYPE ahoy_solar_info gauge\n"); @@ -676,88 +688,138 @@ class Web { metrics += radioStatistic(F("tx_cnt"), mSys->Radio.mSendCnt); len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - // Start Inverter loop + // Next is Inverter information metricsInverterId = 0; - metricsStep = metricsStateInverter; + metricsStep = metricsStateInverter1; break; - case metricsStateInverter: // Inverter loop - if (metricsInverterId < mSys->getNumInverters()) { - iv = mSys->getInverterByPos(metricsInverterId); - if(NULL != iv) { - // Inverter info : fit to one packet - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_info gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", - iv->config->name, iv->config->serial.u64); - metrics = String(type) + String(topic); - - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_enabled gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",iv->config->name,iv->config->enabled); - metrics += String(type) + String(topic); - - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_available gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n",iv->config->name,iv->isAvailable(mApp->getTimestamp())); - metrics += String(type) + String(topic); - - snprintf(type,sizeof(type),"# TYPE ahoy_solar_inverter_is_producing gauge\n"); - snprintf(topic,sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n",iv->config->name,iv->isProducing(mApp->getTimestamp())); - metrics += String(type) + String(topic); - - len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); - - // Start Realtime Data Channel loop for this inverter - metricsChannelId = 0; - metricsStep = metricStateRealtimeData; - } - } else { - metricsStep = metricsStateEnd; - } + case metricsStateInverter1: // Information about all inverters configured : fit to one packet + metrics = "# TYPE ahoy_solar_inverter_info gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_info{name=\"%s\",serial=\"%12llx\"} 1\n", + [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->serial.u64;}); + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateInverter2; break; - case metricStateRealtimeData: // Realtime Data Channel loop - iv = mSys->getInverterByPos(metricsInverterId); - rec = iv->getRecordStruct(RealTimeRunData_Debug); - if (metricsChannelId < rec->length) { - uint8_t channel = rec->assign[metricsChannelId].ch; - std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); - if (0 == channel) { - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); - } else { - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); - } - snprintf(val, sizeof(val), "%.3f", iv->getValue(metricsChannelId, rec)); - len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); + case metricsStateInverter2: // Information about all inverters configured : fit to one packet + metrics += "# TYPE ahoy_solar_inverter_is_enabled gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n", + [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->enabled;}); - metricsChannelId++; + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateInverter3; + break; + + case metricsStateInverter3: // Information about all inverters configured : fit to one packet + metrics += "# TYPE ahoy_solar_inverter_is_available gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_available {inverter=\"%s\"} %d\n", + [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isAvailable(mApp->getTimestamp());}); + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateInverter4; + break; + + case metricsStateInverter4: // Information about all inverters configured : fit to one packet + metrics += "# TYPE ahoy_solar_inverter_is_producing gauge\n"; + metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_producing {inverter=\"%s\"} %d\n", + [](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->isProducing(mApp->getTimestamp());}); + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + // Start Realtime Field loop + metricsFieldId = FLD_UDC; + metricsStep = metricStateRealtimeFieldId; + break; + + case metricStateRealtimeFieldId: // Iterate over all defined fields + if (metricsFieldId < FLD_LAST_ALARM_CODE) { + metrics = "# Info: processing realtime field #"+String(metricsFieldId)+"\n"; + metricDeclared = false; + + metricsInverterId = 0; + metricsStep = metricStateRealtimeInverterId; } else { - len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. - - // All realtime data channels processed --> try alarm data + metrics = "# Info: all realtime fields processed\n"; metricsStep = metricsStateAlarmData; } + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); break; - case metricsStateAlarmData: // Alarm Info loop - iv = mSys->getInverterByPos(metricsInverterId); - rec = iv->getRecordStruct(AlarmData); - // simple hack : there is only one channel with alarm data - // TODO: find the right one channel with the alarm id - alarmChannelId = 0; - // printf("AlarmData Length %d\n",rec->length); - if (alarmChannelId < rec->length) { - //uint8_t channel = rec->assign[alarmChannelId].ch; - std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); - snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str()); - snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); - snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec)); - len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val); + case metricStateRealtimeInverterId: // Iterate over all inverters for this field + metrics = ""; + if (metricsInverterId < mSys->getNumInverters()) { + // process all channels of this inverter + + iv = mSys->getInverterByPos(metricsInverterId); + if (NULL != iv) { + rec = iv->getRecordStruct(RealTimeRunData_Debug); + for (metricsChannelId=0; metricsChannelId < rec->length;metricsChannelId++) { + uint8_t channel = rec->assign[metricsChannelId].ch; + + // Try inverter channel (channel 0) or any channel with maxPwr > 0 + if (0 == channel || 0 != iv->config->chMaxPwr[channel-1]) { + + if (metricsFieldId == iv->getByteAssign(metricsChannelId, rec)->fieldId) { + // This is the correct field to report + std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(metricsChannelId, rec)); + // Declare metric only once + if (!metricDeclared) { + snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s\n", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str()); + metrics += type; + metricDeclared = true; + } + // report value + if (0 == channel) { + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name); + } else { + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\",channel=\"%s\"}", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), iv->config->name,iv->config->chName[channel-1]); + } + snprintf(val, sizeof(val), " %.3f\n", iv->getValue(metricsChannelId, rec)); + metrics += topic; + metrics += val; + } + } + } + if (metrics.length() < 1) { + metrics = "# Info: Field #"+String(metricsFieldId)+" not available for inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; + metricsFieldId++; // Process next field Id + metricsStep = metricStateRealtimeFieldId; + } + } else { + metrics = "# Info: No data for field #"+String(metricsFieldId)+" of inverter #"+String(metricsInverterId)+". Skipping remaining inverters\n"; + metricsFieldId++; // Process next field Id + metricsStep = metricStateRealtimeFieldId; + } + // Stay in this state and try next inverter + metricsInverterId++; } else { - len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends. + metrics = "# Info: All inverters for field #"+String(metricsFieldId)+" processed.\n"; + metricsFieldId++; // Process next field Id + metricsStep = metricStateRealtimeFieldId; } - // alarm channel processed --> try next inverter - metricsInverterId++; - metricsStep = metricsStateInverter; + len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str()); + break; + + case metricsStateAlarmData: // Alarm Info loop : fit to one packet + // Perform grouping on metrics according to Prometheus exposition format specification + snprintf(type, sizeof(type),"# TYPE ahoy_solar_%s gauge\n",fields[FLD_LAST_ALARM_CODE]); + metrics = type; + + for (metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { + iv = mSys->getInverterByPos(metricsInverterId); + if (NULL != iv) { + rec = iv->getRecordStruct(AlarmData); + // simple hack : there is only one channel with alarm data + // TODO: find the right one channel with the alarm id + alarmChannelId = 0; + if (alarmChannelId < rec->length) { + std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec)); + snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name); + snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec)); + metrics += topic; + metrics += val; + } + } + } + len = snprintf((char*)buffer,maxLen,"%s",metrics.c_str()); + metricsStep = metricsStateEnd; break; case metricsStateEnd: @@ -770,6 +832,21 @@ class Web { request->send(response); } + + // Traverse all inverters and collect the metric via valueFunc + String inverterMetric(char *buffer, size_t len, const char *format, std::function *iv, IApp *mApp)> valueFunc) { + Inverter<> *iv; + String metric = ""; + for (int metricsInverterId = 0; metricsInverterId < mSys->getNumInverters();metricsInverterId++) { + iv = mSys->getInverterByPos(metricsInverterId); + if (NULL != iv) { + snprintf(buffer,len,format,iv->config->name, valueFunc(iv,mApp)); + metric += String(buffer); + } + } + return metric; + } + String radioStatistic(String statistic, uint32_t value) { char type[60], topic[80], val[25]; snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str()); diff --git a/tools/rpi/ahoy.yml.example b/tools/rpi/ahoy.yml.example index 9301067f..00d52511 100644 --- a/tools/rpi/ahoy.yml.example +++ b/tools/rpi/ahoy.yml.example @@ -2,11 +2,14 @@ ahoy: interval: 5 + transmit_retries: 5 logging: filename: 'hoymiles.log' # DEBUG, INFO, WARNING, ERROR, FATAL level: 'INFO' + max_log_filesize: 1000000 + max_log_files: 1 sunset: disabled: false diff --git a/tools/rpi/hoymiles/__init__.py b/tools/rpi/hoymiles/__init__.py index 210bed65..688e271d 100644 --- a/tools/rpi/hoymiles/__init__.py +++ b/tools/rpi/hoymiles/__init__.py @@ -297,8 +297,8 @@ class InverterPacketFragment: class HoymilesNRF: """Hoymiles NRF24 Interface""" - tx_channel_id = 0 - tx_channel_list = [40] + tx_channel_id = 2 + tx_channel_list = [3,23,40,61,75] rx_channel_id = 0 rx_channel_list = [3,23,40,61,75] rx_channel_ack = False @@ -332,6 +332,12 @@ class HoymilesNRF: :rtype: bool """ + self.next_tx_channel() + + if HOYMILES_TRANSACTION_LOGGING: + c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") + logging.debug(f'{c_datetime} Transmit {len(packet)} bytes channel {self.tx_channel}: {hexify_payload(packet)}') + if not txpower: txpower = self.txpower @@ -363,13 +369,13 @@ class HoymilesNRF: """ Receive Packets - :param timeout: receive timeout in nanoseconds (default: 12e8) + :param timeout: receive timeout in nanoseconds (default: 5e8) :type timeout: int :yields: fragment """ if not timeout: - timeout=12e8 + timeout=5e8 self.radio.setChannel(self.rx_channel) self.radio.setAutoAck(False) @@ -415,7 +421,7 @@ class HoymilesNRF: self.radio.setChannel(self.rx_channel) self.radio.startListening() - time.sleep(0.005) + time.sleep(0.004) def next_rx_channel(self): """ @@ -433,6 +439,15 @@ class HoymilesNRF: return True return False + def next_tx_channel(self): + """ + Select next channel from hop list + + """ + self.tx_channel_id = self.tx_channel_id + 1 + if self.tx_channel_id >= len(self.tx_channel_list): + self.tx_channel_id = 0 + @property def tx_channel(self): """ @@ -612,10 +627,6 @@ class InverterTransaction: packet = self.tx_queue.pop(0) - if HOYMILES_TRANSACTION_LOGGING: - c_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") - logging.debug(f'{c_datetime} Transmit {len(packet)} | {hexify_payload(packet)}') - self.radio.transmit(packet, txpower=self.txpower) wait = False diff --git a/tools/rpi/hoymiles/__main__.py b/tools/rpi/hoymiles/__main__.py index 7de4a1a2..3589de1a 100644 --- a/tools/rpi/hoymiles/__main__.py +++ b/tools/rpi/hoymiles/__main__.py @@ -19,6 +19,7 @@ import yaml from yaml.loader import SafeLoader import hoymiles import logging +from logging.handlers import RotatingFileHandler ################################################################################ """ Signal Handler """ @@ -127,6 +128,7 @@ def main_loop(ahoy_config): dtu_name = ahoy_config.get('dtu', {}).get('name', 'hoymiles-dtu') sunset.sun_status2mqtt(dtu_ser, dtu_name) loop_interval = ahoy_config.get('interval', 1) + transmit_retries = ahoy_config.get('transmit_retries', 5) try: do_init = True @@ -143,7 +145,7 @@ def main_loop(ahoy_config): sys.exit(999) if hoymiles.HOYMILES_DEBUG_LOGGING: logging.info(f'Poll inverter name={inverter["name"]} ser={inverter["serial"]}') - poll_inverter(inverter, dtu_ser, do_init, 3) + poll_inverter(inverter, dtu_ser, do_init, transmit_retries) do_init = False if loop_interval > 0: @@ -298,6 +300,8 @@ def init_logging(ahoy_config): log_config = ahoy_config.get('logging') fn = 'hoymiles.log' lvl = logging.ERROR + max_log_filesize = 1000000 + max_log_files = 1 if log_config: fn = log_config.get('filename', fn) level = log_config.get('level', 'ERROR') @@ -311,9 +315,11 @@ def init_logging(ahoy_config): lvl = logging.ERROR elif level == 'FATAL': lvl = logging.FATAL + max_log_filesize = log_config.get('max_log_filesize', max_log_filesize) + max_log_files = log_config.get('max_log_files', max_log_files) if hoymiles.HOYMILES_TRANSACTION_LOGGING: lvl = logging.DEBUG - logging.basicConfig(filename=fn, format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl) + logging.basicConfig(handlers=[RotatingFileHandler(fn, maxBytes=max_log_filesize, backupCount=max_log_files)], format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=lvl) dtu_name = ahoy_config.get('dtu',{}).get('name','hoymiles-dtu') logging.info(f'start logging for {dtu_name} with level: {logging.root.level}') diff --git a/tools/rpi/hoymiles/decoders/__init__.py b/tools/rpi/hoymiles/decoders/__init__.py index bb32fb07..ad49d664 100644 --- a/tools/rpi/hoymiles/decoders/__init__.py +++ b/tools/rpi/hoymiles/decoders/__init__.py @@ -515,9 +515,17 @@ class Hm300Decode0B(StatusResponse): """ reactive power """ return self.unpack('>H', 20)[0]/10 @property + def powerfactor(self): + """ Powerfactor """ + return self.unpack('>H', 24)[0]/1000 + @property def temperature(self): """ Inverter temperature in °C """ return self.unpack('>h', 26)[0]/10 + @property + def event_count(self): + """ Event counter """ + return self.unpack('>H', 28)[0] class Hm300Decode0C(Hm300Decode0B): """ 1121-series mirco-inverters status data """