mirror of
https://github.com/lumapu/ahoy.git
synced 2025-05-24 22:36:10 +02:00
Merge branch 'development03' into improv
This commit is contained in:
commit
1c36638226
33 changed files with 1327 additions and 655 deletions
|
@ -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<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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
|
30
src/app.cpp
30
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);
|
||||
|
|
|
@ -93,6 +93,10 @@ class app : public IApp, public ah::Scheduler {
|
|||
return mSettings.getLastSaveSucceed();
|
||||
}
|
||||
|
||||
bool getShouldReboot() {
|
||||
return mSaveReboot;
|
||||
}
|
||||
|
||||
statistics_t *getStatistics() {
|
||||
return &mStat;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
#ifndef __SETTINGS_H__
|
||||
#define __SETTINGS_H__
|
||||
|
||||
#if defined(F) && defined(ESP32)
|
||||
#undef F
|
||||
#define F(sl) (sl)
|
||||
#endif
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <LittleFS.h>
|
||||
|
@ -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<const char*>());
|
||||
snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as<const char*>());
|
||||
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as<const char*>());
|
||||
snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as<const char*>());
|
||||
mCfg.sys.protectionMask = obj[F("prot_mask")];
|
||||
mCfg.sys.darkMode = obj[F("dark")];
|
||||
ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>());
|
||||
ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>());
|
||||
ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>());
|
||||
ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as<const char*>());
|
||||
ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as<const char*>());
|
||||
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<uint16_t>(obj, F("prot_mask"), &mCfg.sys.protectionMask);
|
||||
getVal<bool>(obj, F("dark"), &mCfg.sys.darkMode);
|
||||
if(obj.containsKey(F("ip"))) ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>());
|
||||
if(obj.containsKey(F("mask"))) ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>());
|
||||
if(obj.containsKey(F("dns1"))) ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>());
|
||||
if(obj.containsKey(F("dns2"))) ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].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)
|
||||
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<uint16_t>(obj, F("intvl"), &mCfg.nrf.sendInterval);
|
||||
getVal<uint8_t>(obj, F("maxRetry"), &mCfg.nrf.maxRetransPerPyld);
|
||||
getVal<uint8_t>(obj, F("cs"), &mCfg.nrf.pinCs);
|
||||
getVal<uint8_t>(obj, F("ce"), &mCfg.nrf.pinCe);
|
||||
getVal<uint8_t>(obj, F("irq"), &mCfg.nrf.pinIrq);
|
||||
getVal<uint8_t>(obj, F("sclk"), &mCfg.nrf.pinSclk);
|
||||
getVal<uint8_t>(obj, F("mosi"), &mCfg.nrf.pinMosi);
|
||||
getVal<uint8_t>(obj, F("miso"), &mCfg.nrf.pinMiso);
|
||||
getVal<uint8_t>(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<const char*>());
|
||||
mCfg.ntp.port = obj[F("port")];
|
||||
getChar(obj, F("addr"), mCfg.ntp.addr, NTP_ADDR_LEN);
|
||||
getVal<uint16_t>(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<float>(obj, F("lat"), &mCfg.sun.lat);
|
||||
getVal<float>(obj, F("lon"), &mCfg.sun.lon);
|
||||
getVal<bool>(obj, F("dis"), &mCfg.sun.disNightCom);
|
||||
getVal<uint16_t>(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<uint16_t>(obj, F("intvl"), &mCfg.serial.interval);
|
||||
getVal<bool>(obj, F("show"), &mCfg.serial.showIv);
|
||||
getVal<bool>(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<const char*>());
|
||||
snprintf(mCfg.mqtt.user, MQTT_USER_LEN, "%s", obj[F("user")].as<const char*>());
|
||||
snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", obj[F("pwd")].as<const char*>());
|
||||
snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", obj[F("topic")].as<const char*>());
|
||||
getVal<uint16_t>(obj, F("port"), &mCfg.mqtt.port);
|
||||
getVal<uint16_t>(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<uint8_t>(obj, F("0"), &mCfg.led.led0);
|
||||
getVal<uint8_t>(obj, F("1"), &mCfg.led.led1);
|
||||
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;
|
||||
} 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<uint8_t>(disp, F("type"), &mCfg.plugin.display.type);
|
||||
getVal<bool>(disp, F("pwrSafe"), &mCfg.plugin.display.pwrSaveAtIvOffline);
|
||||
getVal<bool>(disp, F("pxShift"), &mCfg.plugin.display.pxShift);
|
||||
getVal<uint8_t>(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<uint8_t>(disp, F("contrast"), &mCfg.plugin.display.contrast);
|
||||
getVal<uint8_t>(disp, F("data"), &mCfg.plugin.display.disp_data);
|
||||
getVal<uint8_t>(disp, F("clock"), &mCfg.plugin.display.disp_clk);
|
||||
getVal<uint8_t>(disp, F("cs"), &mCfg.plugin.display.disp_cs);
|
||||
getVal<uint8_t>(disp, F("reset"), &mCfg.plugin.display.disp_reset);
|
||||
getVal<uint8_t>(disp, F("busy"), &mCfg.plugin.display.disp_busy);
|
||||
getVal<uint8_t>(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<bool>(obj, F("en"), &mCfg.inst.enabled);
|
||||
getVal<bool>(obj, F("rstMidNight"), &mCfg.inst.rstYieldMidNight);
|
||||
getVal<bool>(obj, F("rstNotAvail"), &mCfg.inst.rstValsNotAvail);
|
||||
getVal<bool>(obj, F("rstComStop"), &mCfg.inst.rstValsCommStop);
|
||||
}
|
||||
|
||||
JsonArray ivArr;
|
||||
|
@ -582,13 +587,10 @@ 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())
|
||||
} else if(!obj[F("iv")][i].isNull())
|
||||
jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void jsonIv(JsonObject obj, cfgIv_t *cfg, bool set = false) {
|
||||
if(set) {
|
||||
|
@ -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<const char*>());
|
||||
cfg->serial.u64 = obj[F("sn")];
|
||||
getVal<bool>(obj, F("en"), &cfg->enabled);
|
||||
getChar(obj, F("name"), cfg->name, MAX_NAME_LENGTH);
|
||||
getVal<uint64_t>(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<const char*>());
|
||||
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<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;
|
||||
bool mLastSaveSucceed;
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
#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 HMSYSTEM>
|
||||
|
@ -26,14 +29,27 @@ class Display {
|
|||
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);
|
||||
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)
|
||||
#if defined(ESP32)
|
||||
mRefreshCycle = 0;
|
||||
mEpaper.config(mCfg->rot);
|
||||
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
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +58,9 @@ class Display {
|
|||
}
|
||||
|
||||
void tickerSecond() {
|
||||
mMono.loop();
|
||||
if (mMono != NULL)
|
||||
mMono->loop();
|
||||
|
||||
if (mNewPayload || ((++mLoopCnt % 10) == 0)) {
|
||||
mNewPayload = false;
|
||||
mLoopCnt = 0;
|
||||
|
@ -79,21 +97,21 @@ class Display {
|
|||
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
|
||||
}
|
||||
|
||||
if ((0 < mCfg->type) && (mCfg->type < 10)) {
|
||||
mMono.disp(totalPower, totalYieldDay, totalYieldTotal, isprod);
|
||||
if ((0 < mCfg->type) && (mCfg->type < 10) && (mMono != NULL)) {
|
||||
mMono->disp(totalPower, totalYieldDay, totalYieldTotal, isprod);
|
||||
} else if (mCfg->type >= 10) {
|
||||
#if defined(ESP32)
|
||||
#if defined(ESP32)
|
||||
mEpaper.loop(totalPower, totalYieldDay, totalYieldTotal, isprod);
|
||||
mRefreshCycle++;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
#if defined(ESP32)
|
||||
if (mRefreshCycle > 480) {
|
||||
mEpaper.fullRefresh();
|
||||
mRefreshCycle = 0;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
// private member variables
|
||||
|
@ -105,10 +123,10 @@ class Display {
|
|||
HMSYSTEM *mSys;
|
||||
uint16_t mRefreshCycle;
|
||||
|
||||
#if defined(ESP32)
|
||||
#if defined(ESP32)
|
||||
DisplayEPaper mEpaper;
|
||||
#endif
|
||||
DisplayMono mMono;
|
||||
#endif
|
||||
DisplayMono *mMono;
|
||||
};
|
||||
|
||||
#endif /*__DISPLAY__*/
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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>
|
||||
#define DISP_DEFAULT_TIMEOUT 60 // in seconds
|
||||
#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 {
|
||||
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];};
|
||||
|
|
155
src/plugins/Display/Display_Mono_128X32.h
Normal file
155
src/plugins/Display/Display_Mono_128X32.h
Normal file
|
@ -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
Normal file
138
src/plugins/Display/Display_Mono_128X64.h
Normal file
|
@ -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
Normal file
132
src/plugins/Display/Display_Mono_84X48.h
Normal file
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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,15 +153,19 @@ 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");
|
||||
}
|
||||
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);
|
||||
|
@ -148,7 +175,7 @@ void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, fl
|
|||
y = _display->height() / 2;
|
||||
_display->setCursor(5, y);
|
||||
_display->print("today:");
|
||||
snprintf(_fmtText, _display->width(), "%.0f", _totalYieldDay);
|
||||
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);
|
||||
|
@ -159,7 +186,7 @@ void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, fl
|
|||
y = y + tbh + 7;
|
||||
_display->setCursor(5, y);
|
||||
_display->print("total:");
|
||||
snprintf(_fmtText, _display->width(), "%.1f", _totalYieldTotal);
|
||||
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);
|
||||
|
@ -168,9 +195,10 @@ void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, fl
|
|||
_display->println("kWh");
|
||||
|
||||
_display->setCursor(10, _display->height() - (mHeadFootPadding + 10));
|
||||
snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod);
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<HMSYSTEM> mSendIvData;
|
||||
|
||||
uint32_t *mUtcTimestamp;
|
||||
uint32_t mRxCnt, mTxCnt;
|
||||
std::queue<uint8_t> mSendList;
|
||||
|
|
208
src/publisher/pubMqttIvData.h
Normal file
208
src/publisher/pubMqttIvData.h
Normal file
|
@ -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__*/
|
|
@ -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__
|
||||
|
|
|
@ -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<const char*>(), 6)) {
|
||||
iv->powerLimit[0] = jsonIn["val"];
|
||||
|
|
|
@ -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"])
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
<meta charset="UTF-8">
|
||||
<script type="text/javascript" src="api.js?v={#VERSION}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="colors.css?v={#VERSION}"/>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
|
|
|
@ -8,11 +8,13 @@
|
|||
{#HTML_NAV}
|
||||
<div id="wrapper">
|
||||
<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>
|
||||
{#HTML_FOOTER}
|
||||
<script type="text/javascript">
|
||||
var intervalId = null;
|
||||
|
||||
function parseGeneric(obj) {
|
||||
parseNav(obj);
|
||||
parseESP(obj);
|
||||
|
@ -21,18 +23,24 @@
|
|||
|
||||
function parseHtml(obj) {
|
||||
var html = "";
|
||||
if(obj.pending)
|
||||
html = "saving settings ...";
|
||||
else {
|
||||
if(obj.success)
|
||||
html = "settings successfully saved";
|
||||
else
|
||||
html = "failed saving settings";
|
||||
|
||||
if(!obj.pending) {
|
||||
if(intervalId != null) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
if(obj.success) {
|
||||
var meta = document.createElement('meta');
|
||||
meta.httpEquiv = "refresh"
|
||||
meta.content = 1 + "; URL=/setup";
|
||||
if(!obj.reboot) {
|
||||
html = "Settings successfully saved. Automatic page reload in 3 seconds.";
|
||||
meta.content = 3;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
@ -41,11 +49,9 @@
|
|||
if(null != obj) {
|
||||
parseGeneric(obj["generic"]);
|
||||
parseHtml(obj);
|
||||
window.setInterval("getAjax('/api/html/save', parse)", 1100);
|
||||
}
|
||||
}
|
||||
|
||||
getAjax("/api/html/save", parse);
|
||||
intervalId = window.setInterval("getAjax('/api/html/save', parse)", 2500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Serial Console</title>
|
||||
{#HTML_HEADER}
|
||||
|
|
|
@ -1,25 +1,14 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Setup</title>
|
||||
{#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>
|
||||
<body onload="load()">
|
||||
<body>
|
||||
{#HTML_NAV}
|
||||
<div id="wrapper">
|
||||
<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>
|
||||
<div class="s_content">
|
||||
<fieldset class="mb-2">
|
||||
|
@ -31,6 +20,7 @@
|
|||
<div class="row mb-3">
|
||||
<div class="col-8 col-sm-3">Dark Mode</div>
|
||||
<div class="col-4 col-sm-9"><input type="checkbox" name="darkMode"/></div>
|
||||
<div class="col-8 col-sm-3">(empty browser cache or use CTRL + F5 after reboot to apply this setting)</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="mb-4">
|
||||
|
@ -146,11 +136,11 @@
|
|||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-sm-3 my-2">Interval [s]</div>
|
||||
<div class="col-12 col-sm-9"><input type="text" name="invInterval" pattern="[0-9]+" title="Invalid input"/></div>
|
||||
<div class="col-12 col-sm-9"><input type="number" name="invInterval" title="Invalid input"/></div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-sm-3 my-2">Max retries per Payload</div>
|
||||
<div class="col-12 col-sm-9"><input type="text" name="invRetry"/></div>
|
||||
<div class="col-12 col-sm-9"><input type="number" name="invRetry"/></div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-8 col-sm-3 mb-2">Reset values and YieldDay at midnight</div>
|
||||
|
@ -177,7 +167,7 @@
|
|||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-sm-3 my-2">NTP Port</div>
|
||||
<div class="col-12 col-sm-9"><input type="text" name="ntpPort"/></div>
|
||||
<div class="col-12 col-sm-9"><input type="number" name="ntpPort"/></div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-sm-3 my-2">set system time</div>
|
||||
|
@ -194,15 +184,13 @@
|
|||
<div class="s_content">
|
||||
<fieldset class="mb-4">
|
||||
<legend class="des">Sunrise & Sunset</legend>
|
||||
<p>Use a decimal separator: '.' (dot) for Latitude and Longitude</p>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-sm-3 my-2">Latitude (decimal)</div>
|
||||
<div class="col-12 col-sm-9"><input type="text" name="sunLat"/></div>
|
||||
<div class="col-12 col-sm-9"><input type="number" name="sunLat" step="any"/></div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-sm-3 my-2">Longitude (decimal)</div>
|
||||
<div class="col-12 col-sm-9"><input type="text" name="sunLon"/></div>
|
||||
<div class="col-12 col-sm-9"><input type="number" name="sunLon" step="any"/></div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-sm-3 my-2">Offset (pre sunrise, post sunset)</div>
|
||||
|
@ -225,7 +213,7 @@
|
|||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-sm-3 my-2">Port</div>
|
||||
<div class="col-12 col-sm-9"><input type="text" name="mqttPort"/></div>
|
||||
<div class="col-12 col-sm-9"><input type="number" name="mqttPort"/></div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<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>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-sm-3 my-2">Interval [s]</div>
|
||||
<div class="col-12 col-sm-9"><input type="text" name="mqttInterval" pattern="[0-9]+" title="Invalid input" /></div>
|
||||
<div class="col-12 col-sm-9"><input type="number" name="mqttInterval" title="Invalid input" /></div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-sm-3 my-2">Discovery Config (homeassistant)</div>
|
||||
|
@ -291,11 +279,13 @@
|
|||
<fieldset class="mb-4">
|
||||
<legend class="des">Import / Export JSON Settings</legend>
|
||||
<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">
|
||||
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
|
||||
<input type="file" name="upload">
|
||||
<input type="button" class="btn" value="Import" onclick="hide()">
|
||||
<div class="row">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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
|
||||
|
|
191
src/web/web.h
191
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);
|
||||
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;
|
||||
|
||||
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);
|
||||
case metricsStateInverter2: // Information about all inverters configured : fit to one packet
|
||||
metrics += "# TYPE ahoy_solar_inverter_is_enabled gauge\n";
|
||||
metrics += inverterMetric(topic, sizeof(topic),"ahoy_solar_inverter_is_enabled {inverter=\"%s\"} %d\n",
|
||||
[](Inverter<> *iv,IApp *mApp)-> uint64_t {return iv->config->enabled;});
|
||||
|
||||
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
|
||||
|
||||
// Start Realtime Data Channel loop for this inverter
|
||||
metricsChannelId = 0;
|
||||
metricsStep = metricStateRealtimeData;
|
||||
}
|
||||
} else {
|
||||
metricsStep = metricsStateEnd;
|
||||
}
|
||||
metricsStep = metricsStateInverter3;
|
||||
break;
|
||||
|
||||
case metricStateRealtimeData: // Realtime Data Channel loop
|
||||
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 {
|
||||
metrics = "# Info: all realtime fields processed\n";
|
||||
metricsStep = metricsStateAlarmData;
|
||||
}
|
||||
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
|
||||
break;
|
||||
|
||||
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);
|
||||
if (metricsChannelId < rec->length) {
|
||||
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));
|
||||
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(metricsChannelId, rec), promUnit.c_str(), promType.c_str());
|
||||
// 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", iv->getValue(metricsChannelId, rec));
|
||||
len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val);
|
||||
|
||||
metricsChannelId++;
|
||||
} else {
|
||||
len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends.
|
||||
|
||||
// All realtime data channels processed --> try alarm data
|
||||
metricsStep = metricsStateAlarmData;
|
||||
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 {
|
||||
metrics = "# Info: All inverters for field #"+String(metricsFieldId)+" processed.\n";
|
||||
metricsFieldId++; // Process next field Id
|
||||
metricsStep = metricStateRealtimeFieldId;
|
||||
}
|
||||
len = snprintf((char *)buffer,maxLen,"%s",metrics.c_str());
|
||||
break;
|
||||
|
||||
case metricsStateAlarmData: // Alarm Info loop
|
||||
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;
|
||||
// printf("AlarmData Length %d\n",rec->length);
|
||||
if (alarmChannelId < rec->length) {
|
||||
//uint8_t channel = rec->assign[alarmChannelId].ch;
|
||||
std::tie(promUnit, promType) = convertToPromUnits(iv->getUnit(alarmChannelId, rec));
|
||||
snprintf(type, sizeof(type), "# TYPE ahoy_solar_%s%s %s", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), promType.c_str());
|
||||
snprintf(topic, sizeof(topic), "ahoy_solar_%s%s{inverter=\"%s\"}", iv->getFieldName(alarmChannelId, rec), promUnit.c_str(), iv->config->name);
|
||||
snprintf(val, sizeof(val), "%.3f", iv->getValue(alarmChannelId, rec));
|
||||
len = snprintf((char*)buffer,maxLen,"%s\n%s %s\n",type,topic,val);
|
||||
} else {
|
||||
len = snprintf((char*)buffer,maxLen,"#\n"); // At least one char to send otherwise the transmission ends.
|
||||
snprintf(val, sizeof(val), " %.3f\n", iv->getValue(alarmChannelId, rec));
|
||||
metrics += topic;
|
||||
metrics += val;
|
||||
}
|
||||
// alarm channel processed --> try next inverter
|
||||
metricsInverterId++;
|
||||
metricsStep = metricsStateInverter;
|
||||
}
|
||||
}
|
||||
len = snprintf((char*)buffer,maxLen,"%s",metrics.c_str());
|
||||
metricsStep = metricsStateEnd;
|
||||
break;
|
||||
|
||||
case metricsStateEnd:
|
||||
|
@ -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<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) {
|
||||
char type[60], topic[80], val[25];
|
||||
snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}')
|
||||
|
||||
|
|
|
@ -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 """
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue