diff --git a/.github/workflows/compile_development.yml b/.github/workflows/compile_development.yml
index 274a6c73..8d8ab45e 100644
--- a/.github/workflows/compile_development.yml
+++ b/.github/workflows/compile_development.yml
@@ -47,7 +47,7 @@ jobs:
run: python convert.py
- name: Run PlatformIO
- run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp32-wroom32-release
+ run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp8266-nokia5110 --environment esp8266-ssd1306 --environment esp32-wroom32-release --environment esp32-wroom32-nokia5110 --environment esp32-wroom32-ssd1306
- name: Rename Binary files
id: rename-binary-files
@@ -68,7 +68,7 @@ jobs:
- name: Create Artifact
uses: actions/upload-artifact@v3
with:
- name: ${{ steps.rename-binary-files.outputs.name }}_dev_build
+ name: ahoydtu_dev
path: |
src/firmware/*
src/User_Manual.md
diff --git a/.github/workflows/compile_release.yml b/.github/workflows/compile_release.yml
index 91ae35bf..556f7b7d 100644
--- a/.github/workflows/compile_release.yml
+++ b/.github/workflows/compile_release.yml
@@ -51,7 +51,7 @@ jobs:
run: python convert.py
- name: Run PlatformIO
- run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp32-wroom32-release
+ run: pio run -d tools/esp8266 --environment esp8266-release --environment esp8285-release --environment esp8266-nokia5110 --environment esp8266-ssd1306 --environment esp32-wroom32-release --environment esp32-wroom32-nokia5110 --environment esp32-wroom32-ssd1306
- name: Rename Binary files
id: rename-binary-files
diff --git a/Getting_Started.md b/Getting_Started.md
index 797d0093..fd4de83e 100644
--- a/Getting_Started.md
+++ b/Getting_Started.md
@@ -253,6 +253,7 @@ When everything is wired up and the firmware is flashed, it is time to connect t
| `RF24` | 1.4.5 | GPL-2.0 |
| `PubSubClient` | 2.8 | MIT |
| `ArduinoJson` | 6.19.4 | MIT |
+| `ESP Async WebServer` | 4.3.0 | ? |
## Contact
diff --git a/User_Manual.md b/User_Manual.md
index 3b51a3e1..5f34676f 100644
--- a/User_Manual.md
+++ b/User_Manual.md
@@ -1,4 +1,4 @@
-# User Manual Ahoy DTU (on ESP8266)
+# User Manual AhoyDTU (on ESP8266)
Version #{VERSION}#
## Introduction
See the repository [README.md](Getting_Started.md)
@@ -9,9 +9,9 @@ In the initial case or after click "erase settings" the fields for the inverter
Set at least the serial number and a name for each inverter, check "reboot after save" and click the "Save" button.
-## MQTT Output
-The ahoy dtu will publish on the following topics
-`//ch0/#`
+## MQTT Output
+The AhoyDTU will publish on the following topics
+`//ch0/#`
| Topic | Example Value | Remarks |
|---|---|---|
@@ -34,7 +34,7 @@ The ahoy dtu will publish on the following topics
|PowerLimit | 80.000|actual set point for power limit control AC active power in percent|
|LastAlarmCode | 1.000| Last Alarm Code eg. "inverter start"|
-`//ch/#`
+`//ch/#`
`` is in the range 1 to 4 depending on the inverter type
@@ -47,7 +47,8 @@ The ahoy dtu will publish on the following topics
|YieldTotal | 110.819 | Energy converted to AC since reset Watt hours per module/channel (measured on DC) |
|Irradiation |5.65 | ratio DC Power over set maximum power per module/channel in percent |
-## Active Power Limit via Setup Page
+## Active Power Limit via Serial / Control Page
+URL: `/serial`
If you leave the field "Active Power Limit" empty during the setup and reboot the ahoy-dtu will set a value of 65535 in the setup.
That is the value you have to fill in case you want to operate the inverter without a active power limit.
If the value is 65535 or -1 after another reboot the value will be set automatically to "100" and in the drop-down menu "relative in percent persistent" will be set. Of course you can do this also by your self.
@@ -68,160 +69,182 @@ after a power cycle of the inverter (P_DC=0 and P_AC=0 for at least 10 seconds)
The user has to ensure correct settings. Remember that for the inverters of 3rd generation the relative active power limit is in the range of 2% up to 100%.
Also an absolute active power limit below approx. 30 Watt seems to be not meanful because of the control capabilities and reactive power load.
-## Active Power Limit via MQTT
-The ahoy-dtu subscribes on the topic `/devcontrol/#` if the mqtt broker is set-up correctly. The default topic is `inverter/devcontrol/#`.
+## Control via MQTT
-To set the active power limit (controled value is the AC Power of the inverter) you have four options. (Only single phase inverters are actually in focus).
+### Generic Information
-| topic | payload | active power limit in | Condition |
-| --------------------------------------------------------------- | ----------- | -------------------------------------------- | -------------- |
-| /devcontrol//11 OR /devcontrol//11/0 | [0..65535] | Watt | not persistent |
-| /devcontrol//11/256 | [0..65535] | Watt | persistent |
-| /devcontrol//11/1 | [2..100] | % | not persistent |
-| /devcontrol//11/257 | [2..100] | % | persistent |
+The AhoyDTU subscribes on three topics `/ctrl/#`, `/setup` and `/status`.
+
+👆 `` can be set on setup page, default is `inverter`.
👆 `` is the number of the specific inverter in the setup page.
-* First inverter --> `` = 0
-* Second inverter --> `` = 1
-* ...
-
-### Developer Information MQTT Interface
-`/devcontrol///`
-
-The implementation allows to set any of the available `` Commands:
-```C
-typedef enum {
- TurnOn = 0, // 0x00
- TurnOff = 1, // 0x01
- Restart = 2, // 0x02
- Lock = 3, // 0x03
- Unlock = 4, // 0x04
- ActivePowerContr = 11, // 0x0b
- ReactivePowerContr = 12, // 0x0c
- PFSet = 13, // 0x0d
- CleanState_LockAndAlarm = 20, // 0x14
- SelfInspection = 40, // 0x28, self-inspection of grid-connected protection files
- Init = 0xff
-} DevControlCmdType;
+
+### Inverter Power (On / Off)
+```mqtt
+/ctrl/power/
```
-The MQTT payload will be set on first to bytes and ``, which is taken from the topic path will be set on the second two bytes if the corresponding DevControlCmdType supports 4 byte data.
-See here the actual implementation to set the send buffer bytes.
-```C
-void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data) {
- sendCmdPacket(invId, TX_REQ_DEVCONTROL, ALL_FRAMES, false);
- int cnt = 0;
- // cmd --> 0x0b => Type_ActivePowerContr, 0 on, 1 off, 2 restart, 12 reactive power, 13 power factor
- mTxBuf[10] = cmd;
- mTxBuf[10 + (++cnt)] = 0x00;
- if (cmd >= ActivePowerContr && cmd <= PFSet){
- mTxBuf[10 + (++cnt)] = ((data[0] * 10) >> 8) & 0xff; // power limit || high byte from MQTT payload
- mTxBuf[10 + (++cnt)] = ((data[0] * 10) ) & 0xff; // power limit || low byte from MQTT payload
- mTxBuf[10 + (++cnt)] = ((data[1] ) >> 8) & 0xff; // high byte from MQTT topic value
- mTxBuf[10 + (++cnt)] = ((data[1] ) ) & 0xff; // low byte from MQTT topic value
- }
- // crc control data
- uint16_t crc = Hoymiles::crc16(&mTxBuf[10], cnt+1);
- mTxBuf[10 + (++cnt)] = (crc >> 8) & 0xff;
- mTxBuf[10 + (++cnt)] = (crc ) & 0xff;
- // crc over all
- cnt +=1;
- mTxBuf[10 + cnt] = Hoymiles::crc8(mTxBuf, 10 + cnt);
-
- sendPacket(invId, mTxBuf, 10 + (++cnt), true);
-}
+with payload `1` = `ON` and `0` = `OFF`
+
+Example:
+```mqtt
+inverter/ctrl/power/0 1
+```
+
+### Inverter restart
+```mqtt
+/ctrl/restart/
+```
+Example:
+```mqtt
+inverter/ctrl/restart/0
+```
+
+### Power Limit relative persistent [%]
+
+```mqtt
+/ctrl/limit_persistent_relative/
+```
+with a payload `[2 .. 100]`
+
+Example:
+```mqtt
+inverter/ctrl/limit_persistent_relative/0 70
```
-So as example sending any payload on `inverter/devcontrol/0/1` will switch off the inverter.
+### Power Limit absolute persistent [Watts]
+```mqtt
+/ctrl/limit_persistent_absolute/
+```
+with a payload `[0 .. 65535]`
+
+Example:
+```mqtt
+inverter/ctrl/limit_persistent_absolute/0 600
+```
+
+### Power Limit relative non persistent [%]
+```mqtt
+/ctrl/limit_nonpersistent_relative/
+```
+with a payload `[2 .. 100]`
+
+Example:
+```mqtt
+inverter/ctrl/limit_nonpersistent_relative/0 70
+```
+
+### Power Limit absolute non persistent [Watts]
+```mqtt
+/ctrl/limit_nonpersistent_absolute/
+```
+with a payload `[0 .. 65535]`
+
+Example:
+```mqtt
+inverter/ctrl/limit_nonpersistent_absolute/0 600
+```
+
+## Control via REST API
+
+### Generic Information
+
+The rest API works with *JSON* POST requests. All the following instructions must be sent to the `/api` endpoint of the AhoyDTU.
+
+👆 `` is the number of the specific inverter in the setup page.
+
+### Inverter Power (On / Off)
-## Active Power Limit via REST API
-It is also implemented to set the power limit via REST API call. Therefore send a POST request to the endpoint /api.
-The response will always be a json with {success:true}
-The payload shall be a json formated string in the following manner
```json
{
- "inverter":,
- "tx_request": ,
- "cmd": ,
- "payload": ,
- "payload2":
+ "id": ,
+ "cmd": "power",
+ "val":
}
```
-With the following value ranges
+The `` should be set to `1` = `ON` and `0` = `OFF`
-| Value | range | note |
-| --------------------------- | ----------- | ------------------------------- |
-| | 81 or 21 | integer uint8, (0x15 or 0x51) |
-| | [0...255] | integer uint8, subcmds eg. 0x0b |
-| | [0...65535] | uint16 |
-| | [0...3] | integer uint8 |
+### Inverter restart
-Example to set the active power limit non persistent to 10%
```json
{
- "inverter":0,
- "tx_request": 81,
- "cmd": 11,
- "payload": 10,
- "payload2": 1
+ "id": ,
+ "cmd": "restart"
}
```
-Example to set the active power limit persistent to 600Watt
+
+
+### Power Limit relative persistent [%]
+
```json
{
- "inverter":0,
- "tx_request": 81,
- "cmd": 11,
- "payload": 600,
- "payload2": 256
+ "id": ,
+ "cmd": "limit_persistent_relative",
+ "val":
}
```
+The `VALUE` represents a percent number in a range of `[2 .. 100]`
+
+
+### Power Limit absolute persistent [Watts]
-### Developer Information REST API
-In the same approach as for MQTT any other SubCmd and also MainCmd can be applied and the response payload can be observed in the serial logs. Eg. request the Alarm-Data from the Alarm-Index 5 from inverter 0 will look like this:
```json
{
- "inverter":0,
- "tx_request": 21,
- "cmd": 17,
- "payload": 5,
- "payload2": 0
+ "id": ,
+ "cmd": "limit_persistent_absolute",
+ "val":
}
```
+The `VALUE` represents watts in a range of `[0 .. 65535]`
-## Zero Export Control
-* You can use the mqtt topic `/devcontrol//11` with a number as payload (eg. 300 -> 300 Watt) to set the power limit to the published number in Watt. (In regular cases the inverter will use the new set point within one intervall period; to verify this see next bullet)
-* You can check the inverter set point for the power limit control on the topic `//ch0/PowerLimit` 👆 This value is ALWAYS in percent of the maximum power limit of the inverter. In regular cases this value will be updated within approx. 15 seconds. (depends on request intervall)
-* You can monitor the actual AC power by subscribing to the topic `//ch0/P_AC` 👆 This value is ALWAYS in Watt
-## Issues and Debuging for active power limit settings
+### Power Limit relative non persistent [%]
-Turn on the serial debugging in the setup. Try to have find out if the behavior is deterministic. That means can you reproduce the behavior. Be patient and wait on inverter reactions at least some minutes and beware that the DC-Power is sufficient.
+```json
+{
+ "id": ,
+ "cmd": "limit_nonpersistent_relative",
+ "val":
+}
+```
+The `VALUE` represents a percent number in a range of `[2 .. 100]`
-In case of issues please report:
-1. Version of firmware
-2. The output of the serial debug esp. the TX messages starting with "0x51" and the RX messages starting with "0xD1" or "0xF1"
-3. Which case you have tried: Setup-Page, MQTT, REST API and at what was shown on the "Visualization Page" at the Location "Limit"
-4. The setting means payload, relative, absolute, persistent, not persistent (see tables above)
+### Power Limit absolute non persistent [Watts]
+
+```json
+{
+ "id": ,
+ "cmd": "limit_nonpersistent_absolute",
+ "val":
+}
+```
+The `VALUE` represents watts in a range of `[0 .. 65535]`
-**Developer Information General for Active Power Limit**
-⚡The following was verified by field tests and feedback from users
-Internally this values will be set for the second two bytes for MainCmd: 0x51 SubCmd: 0x0b --> DevControl set ActivePowerLimit
-```C
-typedef enum {
- AbsolutNonPersistent = 0x0000, // 0
- RelativNonPersistent = 0x0001, // 1
- AbsolutPersistent = 0x0100, // 256
- RelativPersistent = 0x0101 // 257
-} PowerLimitControlType;
+### Developer Information REST API (obsolete)
+In the same approach as for MQTT any other SubCmd and also MainCmd can be applied and the response payload can be observed in the serial logs. Eg. request the Alarm-Data from the Alarm-Index 5 from inverter 0 will look like this:
+```json
+{
+ "inverter":0,
+ "tx_request": 21,
+ "cmd": 17,
+ "payload": 5,
+ "payload2": 0
+}
```
+## Zero Export Control (needs rework)
+* You can use the mqtt topic `/devcontrol//11` with a number as payload (eg. 300 -> 300 Watt) to set the power limit to the published number in Watt. (In regular cases the inverter will use the new set point within one intervall period; to verify this see next bullet)
+* You can check the inverter set point for the power limit control on the topic `//ch0/PowerLimit` 👆 This value is ALWAYS in percent of the maximum power limit of the inverter. In regular cases this value will be updated within approx. 15 seconds. (depends on request intervall)
+* You can monitor the actual AC power by subscribing to the topic `//ch0/P_AC` 👆 This value is ALWAYS in Watt
+
+
## Firmware Version collection
Gather user inverter information here to understand what differs between some inverters.
+To get the information open the URL `/api/record/info` on your AhoyDTU. The information will only be present once the AhoyDTU was able to communicate with an inverter.
| Name | Inverter Typ | Bootloader V. | FWVersion | FWBuild [YYYY] | FWBuild [MM-DD] | HWPartId | | |
| ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------- | --------- |
diff --git a/scripts/getVersion.py b/scripts/getVersion.py
index 2346966d..f7c825ce 100644
--- a/scripts/getVersion.py
+++ b/scripts/getVersion.py
@@ -1,4 +1,6 @@
import os
+import shutil
+import gzip
from datetime import date
def genOtaBin(path):
@@ -24,6 +26,11 @@ def genOtaBin(path):
with open(path + "ota.bin", "wb") as f:
f.write(bytearray(arr))
+# write gzip firmware file
+def gzip_bin(bin_file, gzip_file):
+ with open(bin_file,"rb") as fp:
+ with gzip.open(gzip_file, "wb", compresslevel = 9) as f:
+ shutil.copyfileobj(fp, f)
def readVersion(path, infile):
f = open(path + infile, "r")
@@ -44,21 +51,43 @@ def readVersion(path, infile):
os.mkdir(path + "firmware/")
sha = os.getenv("SHA",default="sha")
+
versionout = version[:-1] + "_esp8266_" + sha + ".bin"
src = path + ".pio/build/esp8266-release/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
+
+ versionout = version[:-1] + "_esp8266_nokia5110_" + sha + ".bin"
+ src = path + ".pio/build/esp8266-nokia5110/firmware.bin"
+ dst = path + "firmware/" + versionout
+ os.rename(src, dst)
+
+ versionout = version[:-1] + "_esp8266_ssd1306_" + sha + ".bin"
+ src = path + ".pio/build/esp8266-ssd1306/firmware.bin"
+ dst = path + "firmware/" + versionout
+ os.rename(src, dst)
- versionout = version[:-1] + "_esp8266_1m_" + sha + ".bin"
+ versionout = version[:-1] + "_esp8285_" + sha + ".bin"
src = path + ".pio/build/esp8285-release/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
+ gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_esp32_" + sha + ".bin"
src = path + ".pio/build/esp32-wroom32-release/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
+ versionout = version[:-1] + "_esp32_nokia5110_" + sha + ".bin"
+ src = path + ".pio/build/esp32-wroom32-nokia5110/firmware.bin"
+ dst = path + "firmware/" + versionout
+ os.rename(src, dst)
+
+ versionout = version[:-1] + "_esp32_ssd1306_" + sha + ".bin"
+ src = path + ".pio/build/esp32-wroom32-ssd1306/firmware.bin"
+ dst = path + "firmware/" + versionout
+ os.rename(src, dst)
+
# other ESP32 bin files
src = path + ".pio/build/esp32-wroom32-release/"
dst = path + "firmware/"
diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json
index 6af36bae..3b7ffafd 100644
--- a/src/.vscode/settings.json
+++ b/src/.vscode/settings.json
@@ -24,6 +24,55 @@
"typeinfo": "cpp",
"string": "cpp",
"istream": "cpp",
- "ostream": "cpp"
+ "ostream": "cpp",
+ "array": "cpp",
+ "atomic": "cpp",
+ "*.tcc": "cpp",
+ "bitset": "cpp",
+ "cctype": "cpp",
+ "chrono": "cpp",
+ "clocale": "cpp",
+ "cmath": "cpp",
+ "cstdarg": "cpp",
+ "cstddef": "cpp",
+ "cstdint": "cpp",
+ "cstdio": "cpp",
+ "cstdlib": "cpp",
+ "cstring": "cpp",
+ "ctime": "cpp",
+ "cwchar": "cpp",
+ "cwctype": "cpp",
+ "deque": "cpp",
+ "list": "cpp",
+ "unordered_map": "cpp",
+ "unordered_set": "cpp",
+ "vector": "cpp",
+ "exception": "cpp",
+ "algorithm": "cpp",
+ "functional": "cpp",
+ "iterator": "cpp",
+ "map": "cpp",
+ "memory": "cpp",
+ "memory_resource": "cpp",
+ "numeric": "cpp",
+ "optional": "cpp",
+ "random": "cpp",
+ "ratio": "cpp",
+ "regex": "cpp",
+ "string_view": "cpp",
+ "system_error": "cpp",
+ "tuple": "cpp",
+ "type_traits": "cpp",
+ "utility": "cpp",
+ "fstream": "cpp",
+ "initializer_list": "cpp",
+ "iomanip": "cpp",
+ "iosfwd": "cpp",
+ "limits": "cpp",
+ "new": "cpp",
+ "sstream": "cpp",
+ "stdexcept": "cpp",
+ "streambuf": "cpp",
+ "cinttypes": "cpp"
},
}
\ No newline at end of file
diff --git a/src/CHANGES.md b/src/CHANGES.md
index 4bf99ac5..ce80ed95 100644
--- a/src/CHANGES.md
+++ b/src/CHANGES.md
@@ -1,13 +1,71 @@
# Changelog
-* fix browser sync NTP button
-* added login feature (protect web ui)
-* added static IP option
-* improved initial boot - don't connect to `YOUR_WIFI_SSID` any more, directly boot into AP mode
-* added status LED support
-* improved MQTT handling (boot, periodic updates, no zero values any more)
-* replaced deprecated workflow functions
-* refactored code to make it more clearly
-* added scheduler to register functions which need to be run each second / minute / ...
-* changed settings to littlefs (-> no currupt settings in future on memory layout changes)
-* added a lot of system infos to `System` page for support
+## 0.5.52
+* improved ahoyWifi class
+* added interface class for app
+* refactored web and webApi -> RestApi
+* fix calcSunrise was not called every day
+* added MQTT RX counter to index.html
+* all values are displayed on /live even if they are 0
+* added MQTT /status to show status over all inverters
+
+## 0.5.51
+* improved scheduler, @beegee3 #483
+* refactored get NTP time, @beegee3 #483
+* generate `bin.gz` only for 1M device ESP8285
+* fix calcSunrise was not called every day
+* incresed number of allowed characters for MQTT user, broker and password, @DanielR92
+* added NRF24 info to Systeminfo, @DanielR92
+* added timezone for monochrome displays, @gh-fx2
+* added support for second inverter for monochrome displays, @gh-fx2
+
+## 0.5.50
+* fixed scheduler, uptime and timestamp counted too fast
+* added / renamed automatically build outputs
+* fixed MQTT ESP uptime on reconnect (not zero any more)
+* changed uptime on index.html to count each second, synced with ESP each 10 seconds
+
+## 0.5.49
+* fixed AP mode on brand new ESP modules
+* fixed `last_success` MQTT message
+* fixed MQTT inverter available status at sunset
+* reordered enqueue commands after boot up to prevent same payload length for successive commands
+* added automatic build for Nokia5110 and SSD1306 displays (ESP8266)
+
+## 0.5.48
+* added MQTT message send at sunset
+* added monochrome display support
+* added `once` and `onceAt` to scheduler to make code cleaner
+* improved sunrise / sunset calculation
+
+## 0.5.47
+* refactored ahoyWifi class: AP is opened on every boot, once station connection is successful the AP will be closed
+* improved NTP sync after boot, faster sync
+* fix NRF24 details only on valid SPI connection
+
+## 0.5.46
+* fix sunrise / sunset calculation
+* improved setup.html: `reboot on save` is checked as default
+
+## 0.5.45
+* changed MQTT last will topic from `status` to `mqtt`
+* fix sunrise / sunset calculation
+* fix time of serial web console
+
+## 0.5.44
+* marked some MQTT messages as retained
+* moved global functions to global location (no duplicates)
+* changed index.html inverval to static 10 seconds
+* fix static IP
+* fix NTP with static IP
+* print MQTT info only if MQTT was configured
+
+## 0.5.43
+* updated REST API and MQTT (both of them use the same functionality)
+* added ESP-heap information as MQTT message
+* changed output name of automatic development build to fixed name (to have a static link from https://ahoydtu.de)
+* updated user manual to latest MQTT and API changes
+
+## 0.5.42
+* fix web logout (auto logout)
+* switched MQTT library
diff --git a/src/app.cpp b/src/app.cpp
index 5fc3d089..24b5fceb 100644
--- a/src/app.cpp
+++ b/src/app.cpp
@@ -13,47 +13,86 @@
#include "utils/sun.h"
//-----------------------------------------------------------------------------
-void app::setup(uint32_t timeout) {
+app::app() : ah::Scheduler() {}
+
+
+//-----------------------------------------------------------------------------
+void app::setup() {
Serial.begin(115200);
while (!Serial)
yield();
- addListener(EVERY_SEC, std::bind(&app::uptimeTick, this));
- addListener(EVERY_MIN, std::bind(&app::minuteTick, this));
- addListener(EVERY_12H, std::bind(&app::ntpUpdateTick, this));
+ ah::Scheduler::setup();
resetSystem();
+
mSettings.setup();
mSettings.getPtr(mConfig);
DPRINTLN(DBG_INFO, F("Settings valid: ") + String((mSettings.getValid()) ? F("true") : F("false")));
- mWifi = new ahoywifi(mConfig);
- mWifi->setup(timeout, mSettings.getValid());
+
+ everySec(std::bind(&app::tickSecond, this));
+ every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval);
+ #if !defined(AP_ONLY)
+ once(std::bind(&app::tickNtpUpdate, this), 2);
+ if((mConfig->sun.lat) && (mConfig->sun.lon)) {
+ mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
+ once(std::bind(&app::tickCalcSunrise, this), 5);
+ }
+ #endif
mSys = new HmSystemType();
mSys->enableDebug();
mSys->setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs);
- mSys->addInverters(&mConfig->inst);
- mPayload.setup(mSys);
- mPayload.enableSerialDebug(mConfig->serial.debug);
-#if !defined(AP_ONLY)
+ #if !defined(AP_ONLY)
+ mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, mSys, &mTimestamp, &mSunrise, &mSunset);
+ #endif
+
+ mWifi.setup(mConfig, &mTimestamp);
+
+ if(mSys->Radio.isChipConnected()) {
+ mSys->addInverters(&mConfig->inst);
+ mPayload.setup(mSys);
+ mPayload.enableSerialDebug(mConfig->serial.debug);
+ }
+ else
+ DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
+
+ // when WiFi is in client mode, then enable mqtt broker
+ #if !defined(AP_ONLY)
if (mConfig->mqtt.broker[0] > 0) {
- mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, mSys, &mUtcTimestamp, &mSunrise, &mSunset);
mPayload.addListener(std::bind(&PubMqttType::payloadEventListener, &mMqtt, std::placeholders::_1));
- addListener(EVERY_SEC, std::bind(&PubMqttType::tickerSecond, &mMqtt));
- addListener(EVERY_MIN, std::bind(&PubMqttType::tickerMinute, &mMqtt));
- addListener(EVERY_HR, std::bind(&PubMqttType::tickerHour, &mMqtt));
+ everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt));
+ everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt));
+ mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
}
-#endif
+ #endif
setupLed();
- mWeb = new web(this, mConfig, &mStat, mVersion);
- mWeb->setup();
- mWeb->setProtection(strlen(mConfig->sys.adminPwd) != 0);
- addListener(EVERY_SEC, std::bind(&web::tickSecond, mWeb));
-
- //addListener(EVERY_MIN, std::bind(&PubSerialType::tickerMinute, &mPubSerial));
+ mWeb.setup(this, mSys, mConfig);
+ mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
+ everySec(std::bind(&WebType::tickSecond, &mWeb));
+
+ mApi.setup(this, mSys, mWeb.getWebSrvPtr(), mConfig);
+ /*mApi.registerCb(apiCbScanNetworks, std::bind(&app::scanAvailNetworks, this));
+ #if !defined(AP_ONLY)
+ mApi.registerCb(apiCbMqttTxCnt, std::bind(&PubMqttType::getTxCnt, &mMqtt));
+ mApi.registerCb(apiCbMqttRxCnt, std::bind(&PubMqttType::getRxCnt, &mMqtt));
+ mApi.registerCb(apiCbMqttIsCon, std::bind(&PubMqttType::isConnected, &mMqtt));
+ mApi.registerCb(apiCbMqttDiscvry, std::bind(&PubMqttType::sendDiscoveryConfig, &mMqtt));
+ //mApi.registerCb(apiCbMqttDiscvry, std::bind(&app::setMqttDiscoveryFlag, this));
+ #endif*/
+
+ // Plugins
+ #if defined(ENA_NOKIA) || defined(ENA_SSD1306)
+ mMonoDisplay.setup(mSys, &mTimestamp);
+ mPayload.addListener(std::bind(&MonoDisplayType::payloadEventListener, &mMonoDisplay, std::placeholders::_1));
+ everySec(std::bind(&MonoDisplayType::tickerSecond, &mMonoDisplay));
+ #endif
+
+ mPubSerial.setup(mConfig, mSys, &mTimestamp);
+ every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval);
}
//-----------------------------------------------------------------------------
@@ -62,11 +101,15 @@ void app::loop(void) {
ah::Scheduler::loop();
- mWeb->loop();
+ #if !defined(AP_ONLY)
+ mWifi.loop();
+ #endif
+
+ mWeb.loop();
if (mFlagSendDiscoveryConfig) {
mFlagSendDiscoveryConfig = false;
- mMqtt.sendMqttDiscoveryConfig(mConfig->mqtt.topic);
+ mMqtt.sendDiscoveryConfig();
}
mSys->Radio.loop();
@@ -99,87 +142,103 @@ void app::loop(void) {
}
mMqtt.loop();
+}
- if (ah::checkTicker(&mTicker, 1000)) {
- if (mUtcTimestamp > 946684800 && mConfig->sun.lat && mConfig->sun.lon && (mUtcTimestamp + mCalculatedTimezoneOffset) / 86400 != (mLatestSunTimestamp + mCalculatedTimezoneOffset) / 86400) { // update on reboot or midnight
- if (!mLatestSunTimestamp) { // first call: calculate time zone from longitude to refresh at local midnight
- mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
- }
- ah::calculateSunriseSunset(mUtcTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
- mLatestSunTimestamp = mUtcTimestamp;
+//-----------------------------------------------------------------------------
+void app::tickNtpUpdate(void) {
+ uint32_t nxtTrig = 5; // default: check again in 5 sec
+ if (mWifi.getNtpTime())
+ nxtTrig = 43200; // check again in 12 h
+ once(std::bind(&app::tickNtpUpdate, this), nxtTrig);
+}
+
+
+//-----------------------------------------------------------------------------
+void app::tickCalcSunrise(void) {
+ if (0 == mTimestamp) {
+ once(std::bind(&app::tickCalcSunrise, this), 5); // check again in 5 secs
+ return;
+ }
+ ah::calculateSunriseSunset(mTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
+
+ uint32_t nxtTrig = mTimestamp - ((mTimestamp - 10) % 86400) + 86400; // next midnight, -10 for safety that it is certain next day
+ onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig);
+ if (mConfig->mqtt.broker[0] > 0) {
+ once(std::bind(&PubMqttType::tickerSun, &mMqtt), 1);
+ onceAt(std::bind(&PubMqttType::tickSunrise, &mMqtt), mSunrise);
+ onceAt(std::bind(&PubMqttType::tickSunset, &mMqtt), mSunset);
+ }
+}
+
+//-----------------------------------------------------------------------------
+void app::tickSend(void) {
+ if(!mSys->Radio.isChipConnected()) {
+ DPRINTLN(DBG_WARN, "NRF24 not connected!");
+ return;
+ }
+ if ((mTimestamp > 0) && (!mConfig->sun.disNightCom || (mTimestamp >= mSunrise && mTimestamp <= mSunset))) { // Timestamp is set and (inverter communication only during the day if the option is activated and sunrise/sunset is set)
+ if (!mSys->BufCtrl.empty()) {
+ if (mConfig->serial.debug)
+ DPRINTLN(DBG_DEBUG, F("recbuf not empty! #") + String(mSys->BufCtrl.getFill()));
}
+ int8_t maxLoop = MAX_NUM_INVERTERS;
+ Inverter<> *iv = mSys->getInverterByPos(mSendLastIvId);
+ do {
+ mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1;
+ iv = mSys->getInverterByPos(mSendLastIvId);
+ } while ((NULL == iv) && ((maxLoop--) > 0));
+ if (NULL != iv) {
+ if (!mPayload.isComplete(iv))
+ mPayload.process(false, mConfig->nrf.maxRetransPerPyld, &mStat);
- if (++mSendTicker >= mConfig->nrf.sendInterval) {
- mSendTicker = 0;
+ if (!mPayload.isComplete(iv)) {
+ if (0 == mPayload.getMaxPacketId(iv))
+ mStat.rxFailNoAnser++;
+ else
+ mStat.rxFail++;
- if (mUtcTimestamp > 946684800 && (!mConfig->sun.disNightCom || !mLatestSunTimestamp || (mUtcTimestamp >= mSunrise && mUtcTimestamp <= mSunset))) { // Timestamp is set and (inverter communication only during the day if the option is activated and sunrise/sunset is set)
+ iv->setQueuedCmdFinished(); // command failed
if (mConfig->serial.debug)
- DPRINTLN(DBG_DEBUG, F("Free heap: 0x") + String(ESP.getFreeHeap(), HEX));
-
- if (!mSys->BufCtrl.empty()) {
- if (mConfig->serial.debug)
- DPRINTLN(DBG_DEBUG, F("recbuf not empty! #") + String(mSys->BufCtrl.getFill()));
+ DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout"));
+ if (mConfig->serial.debug) {
+ DPRINT(DBG_INFO, F("(#") + String(iv->id) + ") ");
+ DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload.getRetransmits(iv)) + ")");
}
+ }
+
+ mPayload.reset(iv, mTimestamp);
+ mPayload.request(iv);
- int8_t maxLoop = MAX_NUM_INVERTERS;
- Inverter<> *iv = mSys->getInverterByPos(mSendLastIvId);
- do {
- mSendLastIvId = ((MAX_NUM_INVERTERS - 1) == mSendLastIvId) ? 0 : mSendLastIvId + 1;
- iv = mSys->getInverterByPos(mSendLastIvId);
- } while ((NULL == iv) && ((maxLoop--) > 0));
-
- if (NULL != iv) {
- if (!mPayload.isComplete(iv))
- mPayload.process(false, mConfig->nrf.maxRetransPerPyld, &mStat);
-
- if (!mPayload.isComplete(iv)) {
- if (0 == mPayload.getMaxPacketId(iv))
- mStat.rxFailNoAnser++;
- else
- mStat.rxFail++;
-
- iv->setQueuedCmdFinished(); // command failed
- if (mConfig->serial.debug)
- DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout"));
- if (mConfig->serial.debug) {
- DPRINT(DBG_INFO, F("(#") + String(iv->id) + ") ");
- DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload.getRetransmits(iv)) + ")");
- }
- }
-
- mPayload.reset(iv, mUtcTimestamp);
- mPayload.request(iv);
-
- yield();
- if (mConfig->serial.debug) {
- DPRINTLN(DBG_DEBUG, F("app:loop WiFi WiFi.status ") + String(WiFi.status()));
- DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Requesting Inv SN ") + String(iv->config->serial.u64, HEX));
- }
-
- if (iv->devControlRequest) {
- if (mConfig->serial.debug)
- DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0]));
- mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit);
- mPayload.setTxCmd(iv, iv->devControlCmd);
- iv->clearCmdQueue();
- iv->enqueCommand(SystemConfigPara);
- } else {
- uint8_t cmd = iv->getQueuedCmd();
- DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket"));
- mSys->Radio.sendTimePacket(iv->radioId.u64, cmd, mPayload.getTs(iv), iv->alarmMesIndex);
- mPayload.setTxCmd(iv, cmd);
- mRxTicker = 0;
- }
- }
- } else if (mConfig->serial.debug)
- DPRINTLN(DBG_WARN, F("Time not set or it is night time, therefore no communication to the inverter!"));
yield();
+ if (mConfig->serial.debug) {
+ DPRINTLN(DBG_DEBUG, F("app:loop WiFi WiFi.status ") + String(WiFi.status()));
+ DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Requesting Inv SN ") + String(iv->config->serial.u64, HEX));
+ }
- updateLed();
+ if (iv->devControlRequest) {
+ if (mConfig->serial.debug)
+ DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Devcontrol request ") + String(iv->devControlCmd) + F(" power limit ") + String(iv->powerLimit[0]));
+ mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit);
+ mPayload.setTxCmd(iv, iv->devControlCmd);
+ iv->clearCmdQueue();
+ iv->enqueCommand(SystemConfigPara); // read back power limit
+ } else {
+ uint8_t cmd = iv->getQueuedCmd();
+ DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") sendTimePacket"));
+ mSys->Radio.sendTimePacket(iv->radioId.u64, cmd, mPayload.getTs(iv), iv->alarmMesIndex);
+ mPayload.setTxCmd(iv, cmd);
+ mRxTicker = 0;
+ }
}
+ } else {
+ if (mConfig->serial.debug)
+ DPRINTLN(DBG_WARN, F("Time not set or it is night time, therefore no communication to the inverter!"));
}
+ yield();
+
+ updateLed();
}
//-----------------------------------------------------------------------------
@@ -188,42 +247,23 @@ void app::handleIntr(void) {
mSys->Radio.handleIntr();
}
-//-----------------------------------------------------------------------------
-bool app::getWifiApActive(void) {
- return mWifi->getApActive();
-}
-
-//-----------------------------------------------------------------------------
-void app::scanAvailNetworks(void) {
- mWifi->scanAvailNetworks();
-}
-
-//-----------------------------------------------------------------------------
-void app::getAvailNetworks(JsonObject obj) {
- mWifi->getAvailNetworks(obj);
-}
-
-
//-----------------------------------------------------------------------------
void app::resetSystem(void) {
snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
mShouldReboot = false;
- mUptimeSecs = 0;
mUpdateNtp = false;
mFlagSendDiscoveryConfig = false;
#ifdef AP_ONLY
- mUtcTimestamp = 1;
+ mTimestamp = 1;
#else
- mUtcTimestamp = 0;
+ mTimestamp = 0;
#endif
- mHeapStatCnt = 0;
-
- mSendTicker = 0xffff;
+ mSunrise = 0;
+ mSunset = 0;
- mTicker = 0;
mRxTicker = 0;
mSendLastIvId = 0;
@@ -232,6 +272,11 @@ void app::resetSystem(void) {
memset(&mStat, 0, sizeof(statistics_t));
}
+//-----------------------------------------------------------------------------
+void app::mqttSubRxCb(JsonObject obj) {
+ mApi.ctrlRequest(obj);
+}
+
//-----------------------------------------------------------------------------
void app::setupLed(void) {
/** LED connection diagram
@@ -255,7 +300,7 @@ void app::updateLed(void) {
Inverter<> *iv = mSys->getInverterByPos(0);
if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
- if(iv->isProducing(mUtcTimestamp, rec))
+ if(iv->isProducing(mTimestamp, rec))
digitalWrite(mConfig->led.led0, LOW); // LED on
else
digitalWrite(mConfig->led.led0, HIGH); // LED off
diff --git a/src/app.h b/src/app.h
index 0d5b49cb..ec708310 100644
--- a/src/app.h
+++ b/src/app.h
@@ -6,12 +6,15 @@
#ifndef __APP_H__
#define __APP_H__
+
#include "utils/dbg.h"
#include
#include
#include
#include
+#include "appInterface.h"
+
#include "config/settings.h"
#include "defines.h"
#include "utils/crc.h"
@@ -23,10 +26,12 @@
#include "hm/payload.h"
#include "wifi/ahoywifi.h"
#include "web/web.h"
+#include "web/RestApi.h"
#include "publisher/pubMqtt.h"
#include "publisher/pubSerial.h"
+
// convert degrees and radians for sun calculation
#define SIN(x) (sin(radians(x)))
#define COS(x) (cos(radians(x)))
@@ -35,79 +40,114 @@
typedef HmSystem HmSystemType;
typedef Payload PayloadType;
+typedef Web WebType;
+typedef RestApi RestApiType;
typedef PubMqtt PubMqttType;
typedef PubSerial PubSerialType;
-class ahoywifi;
-class web;
+// PLUGINS
+#if defined(ENA_NOKIA) || defined(ENA_SSD1306)
+ #include "plugins/MonochromeDisplay/MonochromeDisplay.h"
+ typedef MonochromeDisplay MonoDisplayType;
+#endif
+
-class app : public ah::Scheduler {
+class app : public IApp, public ah::Scheduler {
public:
- app() : ah::Scheduler() {}
+ app();
~app() {}
- void setup(uint32_t timeout);
+ void setup(void);
void loop(void);
void handleIntr(void);
void cbMqtt(char* topic, byte* payload, unsigned int length);
void saveValues(void);
void resetPayload(Inverter<>* iv);
bool getWifiApActive(void);
- void scanAvailNetworks(void);
- void getAvailNetworks(JsonObject obj);
- void saveSettings(void) {
- mSettings.saveSettings();
+ uint32_t getUptime() {
+ return Scheduler::getUptime();
+ }
+
+ uint32_t getTimestamp() {
+ return Scheduler::getTimestamp();
+ }
+
+ bool saveSettings() {
+ return mSettings.saveSettings();
}
bool eraseSettings(bool eraseWifi = false) {
return mSettings.eraseSettings(eraseWifi);
}
- uint8_t getIrqPin(void) {
- return mConfig->nrf.pinIrq;
+ statistics_t *getStatistics() {
+ return &mStat;
}
- uint64_t Serial2u64(const char *val) {
- char tmp[3];
- uint64_t ret = 0ULL;
- uint64_t u64;
- memset(tmp, 0, 3);
- for(uint8_t i = 0; i < 6; i++) {
- tmp[0] = val[i*2];
- tmp[1] = val[i*2 + 1];
- if((tmp[0] == '\0') || (tmp[1] == '\0'))
- break;
- u64 = strtol(tmp, NULL, 16);
- ret |= (u64 << ((5-i) << 3));
- }
- return ret;
+ void scanAvailNetworks() {
+ mWifi.scanAvailNetworks();
}
- String getDateTimeStr(time_t t) {
- char str[20];
- if(0 == t)
- sprintf(str, "n/a");
- else
- sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d", year(t), month(t), day(t), hour(t), minute(t), second(t));
- return String(str);
+ void getAvailNetworks(JsonObject obj) {
+ mWifi.getAvailNetworks(obj);
+ }
+
+ void setRebootFlag() {
+ mShouldReboot = true;
+ }
+
+ const char *getVersion() {
+ return mVersion;
+ }
+
+ uint32_t getSunrise() {
+ return mSunrise;
+ }
+
+ uint32_t getSunset() {
+ return mSunset;
+ }
+
+ bool getSettingsValid() {
+ return mSettings.getValid();
+ }
+
+ bool getRebootRequestState() {
+ return mShowRebootRequest;
+ }
+
+ void setMqttDiscoveryFlag() {
+ mFlagSendDiscoveryConfig = true;
+ }
+
+ bool getMqttIsConnected() {
+ return mMqtt.isConnected();
+ }
+
+ uint32_t getMqttTxCnt() {
+ return mMqtt.getTxCnt();
+ }
+
+ uint32_t getMqttRxCnt() {
+ return mMqtt.getRxCnt();
+ }
+
+ uint8_t getIrqPin(void) {
+ return mConfig->nrf.pinIrq;
}
String getTimeStr(uint32_t offset = 0) {
char str[10];
- if(0 == mUtcTimestamp)
+ if(0 == mTimestamp)
sprintf(str, "n/a");
else
- sprintf(str, "%02d:%02d:%02d ", hour(mUtcTimestamp + offset), minute(mUtcTimestamp + offset), second(mUtcTimestamp + offset));
+ sprintf(str, "%02d:%02d:%02d ", hour(mTimestamp + offset), minute(mTimestamp + offset), second(mTimestamp + offset));
return String(str);
}
- inline uint32_t getUptime(void) {
- return mUptimeSecs;
- }
-
- inline uint32_t getTimestamp(void) {
- return mUtcTimestamp;
+ uint32_t getTimezoneOffset() {
+ return mApi.getTimezoneOffset();
}
void setTimestamp(uint32_t newTime) {
@@ -115,66 +155,36 @@ class app : public ah::Scheduler {
if(0 == newTime)
mUpdateNtp = true;
else
- mUtcTimestamp = newTime;
- }
-
- inline uint32_t getSunrise(void) {
- return mSunrise;
+ Scheduler::setTimestamp(newTime);
}
- inline uint32_t getSunset(void) {
- return mSunset;
- }
- inline uint32_t getLatestSunTimestamp(void) {
- return mLatestSunTimestamp;
- }
-
- inline bool mqttIsConnected(void) { return mMqtt.isConnected(); }
- inline bool getSettingsValid(void) { return mSettings.getValid(); }
- inline bool getRebootRequestState(void) { return mShowRebootRequest; }
- inline uint32_t getMqttTxCnt(void) { return mMqtt.getTxCnt(); }
HmSystemType *mSys;
bool mShouldReboot;
- bool mFlagSendDiscoveryConfig;
private:
void resetSystem(void);
- void setupMqtt(void);
+ void mqttSubRxCb(JsonObject obj);
void setupLed(void);
void updateLed(void);
- void uptimeTick(void) {
- mUptimeSecs++;
- if (0 != mUtcTimestamp)
- mUtcTimestamp++;
-
+ void tickSecond(void) {
if (mShouldReboot) {
DPRINTLN(DBG_INFO, F("Rebooting..."));
ESP.restart();
}
-
-
if (mUpdateNtp) {
mUpdateNtp = false;
- mUtcTimestamp = mWifi->getNtpTime();
- DPRINTLN(DBG_INFO, F("[NTP]: ") + getDateTimeStr(mUtcTimestamp) + F(" UTC"));
+ mWifi.getNtpTime();
}
}
- void minuteTick(void) {
- if(0 == mUtcTimestamp) {
- if(!mWifi->getApActive())
- mUpdateNtp = true;
- }
- }
+ void tickNtpUpdate(void);
- void ntpUpdateTick(void) {
- if (!mWifi->getApActive())
- mUpdateNtp = true;
- }
+ void tickCalcSunrise(void);
+ void tickSend(void);
void stats(void) {
DPRINTLN(DBG_VERBOSE, F("main.h:stats"));
@@ -196,16 +206,14 @@ class app : public ah::Scheduler {
DPRINTLN(DBG_VERBOSE, F(" - frag: ") + String(frag));
}
- uint32_t mUptimeSecs;
- uint8_t mHeapStatCnt;
-
- uint32_t mUtcTimestamp;
bool mUpdateNtp;
+ bool mFlagSendDiscoveryConfig;
bool mShowRebootRequest;
- ahoywifi *mWifi;
- web *mWeb;
+ ahoywifi mWifi;
+ WebType mWeb;
+ RestApiType mApi;
PayloadType mPayload;
PubSerialType mPubSerial;
@@ -213,13 +221,11 @@ class app : public ah::Scheduler {
settings mSettings;
settings_t *mConfig;
- uint16_t mSendTicker;
uint8_t mSendLastIvId;
statistics_t mStat;
// timer
- uint32_t mTicker;
uint32_t mRxTicker;
// mqtt
@@ -229,7 +235,11 @@ class app : public ah::Scheduler {
// sun
int32_t mCalculatedTimezoneOffset;
uint32_t mSunrise, mSunset;
- uint32_t mLatestSunTimestamp;
+
+ // plugins
+ #if defined(ENA_NOKIA) || defined(ENA_SSD1306)
+ MonoDisplayType mMonoDisplay;
+ #endif
};
#endif /*__APP_H__*/
diff --git a/src/appInterface.h b/src/appInterface.h
new file mode 100644
index 00000000..c3760de5
--- /dev/null
+++ b/src/appInterface.h
@@ -0,0 +1,41 @@
+//-----------------------------------------------------------------------------
+// 2022 Ahoy, https://ahoydtu.de
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+//-----------------------------------------------------------------------------
+
+#ifndef __IAPP_H__
+#define __IAPP_H__
+
+#include "defines.h"
+
+// abstract interface to App. Make members of App accessible from child class
+// like web or API without forward declaration
+class IApp {
+ public:
+ virtual ~IApp() {}
+ virtual bool saveSettings() = 0;
+ virtual bool eraseSettings(bool eraseWifi) = 0;
+ virtual void setRebootFlag() = 0;
+ virtual const char *getVersion() = 0;
+ virtual statistics_t *getStatistics() = 0;
+ virtual void scanAvailNetworks() = 0;
+ virtual void getAvailNetworks(JsonObject obj) = 0;
+
+ virtual uint32_t getUptime() = 0;
+ virtual uint32_t getTimestamp() = 0;
+ virtual uint32_t getSunrise() = 0;
+ virtual uint32_t getSunset() = 0;
+ virtual void setTimestamp(uint32_t newTime) = 0;
+ virtual String getTimeStr(uint32_t offset) = 0;
+ virtual uint32_t getTimezoneOffset() = 0;
+
+ virtual bool getRebootRequestState() = 0;
+ virtual bool getSettingsValid() = 0;
+ virtual void setMqttDiscoveryFlag() = 0;
+
+ virtual bool getMqttIsConnected() = 0;
+ virtual uint32_t getMqttRxCnt() = 0;
+ virtual uint32_t getMqttTxCnt() = 0;
+};
+
+#endif /*__IAPP_H__*/
diff --git a/src/config/config.h b/src/config/config.h
index 70a36f58..2b5a0688 100644
--- a/src/config/config.h
+++ b/src/config/config.h
@@ -26,7 +26,7 @@
//#define AP_ONLY
// timeout for automatic logoff (20 minutes)
-#define LOGOUT_TIMEOUT (20 * 60 * 60)
+#define LOGOUT_TIMEOUT (20 * 60)
//-------------------------------------
// CONFIGURATION - COMPILE TIME
diff --git a/src/config/settings.h b/src/config/settings.h
index 376dea09..01d2b8c1 100644
--- a/src/config/settings.h
+++ b/src/config/settings.h
@@ -10,6 +10,7 @@
#include
#include
#include "../utils/dbg.h"
+#include "../utils/helper.h"
#include "../defines.h"
/**
@@ -97,6 +98,7 @@ typedef struct {
cfgMqtt_t mqtt;
cfgLed_t led;
cfgInst_t inst;
+ bool valid;
} settings_t;
class settings {
@@ -106,7 +108,7 @@ class settings {
void setup() {
DPRINTLN(DBG_INFO, F("Initializing FS .."));
- mValid = false;
+ mCfg.valid = false;
#if !defined(ESP32)
LittleFSConfig cfg;
cfg.setAutoFormat(false);
@@ -144,7 +146,7 @@ class settings {
}
bool getValid(void) {
- return mValid;
+ return mCfg.valid;
}
void getInfo(uint32_t *used, uint32_t *size) {
@@ -172,7 +174,7 @@ class settings {
DynamicJsonDocument root(4096);
DeserializationError err = deserializeJson(root, fp);
if(!err) {
- mValid = true;
+ mCfg.valid = true;
jsonWifi(root["wifi"]);
jsonNrf(root["nrf"]);
jsonNtp(root["ntp"]);
@@ -223,28 +225,9 @@ class settings {
return saveSettings();
}
- String ip2Str(uint8_t ip[]) {
- return String(ip[0]) + F(".")
- + String(ip[1]) + F(".")
- + String(ip[2]) + F(".")
- + String(ip[3]);
- }
-
- void ip2Arr(uint8_t ip[], const char *ipStr) {
- char *tmp = new char[strlen(ipStr)];
- strncpy(tmp, ipStr, strlen(ipStr));
- char *p = strtok(tmp, ".");
- uint8_t i = 0;
- while(NULL != p) {
- ip[i++] = atoi(p);
- p = strtok(NULL, ".");
- }
- delete[] tmp;
- }
-
private:
void loadDefaults(bool wifi = true) {
- DPRINTLN(DBG_INFO, F("loadDefaults"));
+ DPRINTLN(DBG_VERBOSE, F("loadDefaults"));
if(wifi) {
snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID);
snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD);
@@ -288,25 +271,26 @@ class settings {
void jsonWifi(JsonObject obj, bool set = false) {
if(set) {
+ char buf[16];
obj[F("ssid")] = mCfg.sys.stationSsid;
obj[F("pwd")] = mCfg.sys.stationPwd;
obj[F("dev")] = mCfg.sys.deviceName;
obj[F("adm")] = mCfg.sys.adminPwd;
- obj[F("ip")] = ip2Str(mCfg.sys.ip.ip);
- obj[F("mask")] = ip2Str(mCfg.sys.ip.mask);
- obj[F("dns1")] = ip2Str(mCfg.sys.ip.dns1);
- obj[F("dns2")] = ip2Str(mCfg.sys.ip.dns2);
- obj[F("gtwy")] = ip2Str(mCfg.sys.ip.gateway);
+ ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf);
+ ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf);
+ ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
+ ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf);
+ ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf);
} else {
snprintf(mCfg.sys.stationSsid, SSID_LEN, "%s", obj[F("ssid")].as());
snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as());
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as());
snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as());
- ip2Arr(mCfg.sys.ip.ip, obj[F("ip")]);
- ip2Arr(mCfg.sys.ip.mask, obj[F("mask")]);
- ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")]);
- ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")]);
- ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")]);
+ ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as());
+ ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as());
+ ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as());
+ ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as());
+ ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as());
}
}
@@ -426,7 +410,6 @@ class settings {
}
settings_t mCfg;
- bool mValid;
};
#endif /*__SETTINGS_H__*/
diff --git a/src/defines.h b/src/defines.h
index 16369764..ac80b7a8 100644
--- a/src/defines.h
+++ b/src/defines.h
@@ -13,7 +13,7 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 5
-#define VERSION_PATCH 41
+#define VERSION_PATCH 52
//-------------------------------------
typedef struct {
@@ -85,9 +85,9 @@ union serial_u {
#define DEVNAME_LEN 16
#define NTP_ADDR_LEN 32 // DNS Name
-#define MQTT_ADDR_LEN 32 // DNS Name
-#define MQTT_USER_LEN 16
-#define MQTT_PWD_LEN 32
+#define MQTT_ADDR_LEN 64 // DNS Name
+#define MQTT_USER_LEN 64
+#define MQTT_PWD_LEN 64
#define MQTT_TOPIC_LEN 64
#define MQTT_MAX_PACKET_SIZE 384
diff --git a/src/hm/hmInverter.h b/src/hm/hmInverter.h
index f38a36cb..2494a979 100644
--- a/src/hm/hmInverter.h
+++ b/src/hm/hmInverter.h
@@ -160,17 +160,12 @@ class Inverter {
}
uint8_t getQueuedCmd() {
- if (_commandQueue.empty()){
- // Fill with default commands
- enqueCommand(RealTimeRunData_Debug);
+ if (_commandQueue.empty()) {
if (fwVersion == 0)
- { // info needed maybe after "one night" (=> DC>0 to DC=0 and to DC>0) or reboot
enqueCommand(InverterDevInform_All);
- }
+ enqueCommand(RealTimeRunData_Debug);
if (actPowerLimit == 0xffff)
- { // info needed maybe after "one nigth" (=> DC>0 to DC=0 and to DC>0) or reboot
enqueCommand(SystemConfigPara);
- }
}
return _commandQueue.front().get()->getCmd();
}
diff --git a/src/hm/hmRadio.h b/src/hm/hmRadio.h
index 4e1b749c..90eaa40f 100644
--- a/src/hm/hmRadio.h
+++ b/src/hm/hmRadio.h
@@ -99,7 +99,7 @@ class HmRadio {
DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setup"));
pinMode(irq, INPUT_PULLUP);
mBufCtrl = ctrl;
-
+
uint32_t dtuSn = 0x87654321;
uint32_t chipID = 0; // will be filled with last 3 bytes of MAC
@@ -139,14 +139,15 @@ class HmRadio {
mNrf24.setPALevel(ampPwr & 0x03);
mNrf24.startListening();
- DPRINTLN(DBG_INFO, F("Radio Config:"));
- mNrf24.printPrettyDetails();
mTxCh = setDefaultChannels();
- if(!mNrf24.isChipConnected()) {
- DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
+ if(mNrf24.isChipConnected()) {
+ DPRINTLN(DBG_INFO, F("Radio Config:"));
+ mNrf24.printPrettyDetails();
}
+ else
+ DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
void loop(void) {
@@ -191,7 +192,7 @@ class HmRadio {
uint8_t setDefaultChannels(void) {
//DPRINTLN(DBG_VERBOSE, F("hmRadio.h:setDefaultChannels"));
mTxChIdx = 2; // Start TX with 40
- mRxChIdx = 0; // Start RX with 03
+ mRxChIdx = 0; // Start RX with 03
return mRfChLst[mTxChIdx];
}
@@ -212,7 +213,7 @@ class HmRadio {
uint16_t crc = ah::crc16(&mTxBuf[10], cnt);
mTxBuf[10 + cnt++] = (crc >> 8) & 0xff;
mTxBuf[10 + cnt++] = (crc ) & 0xff;
-
+
// crc over all
mTxBuf[10 + cnt] = ah::crc8(mTxBuf, 10 + cnt);
@@ -295,6 +296,14 @@ class HmRadio {
return mNrf24.isChipConnected();
}
+ uint8_t getDataRate(void) {
+ return mNrf24.getDataRate();
+ }
+
+ bool isPVariant(void) {
+ return mNrf24.isPVariant();
+ }
+
uint32_t mSendCnt;
@@ -360,7 +369,7 @@ class HmRadio {
uint8_t mTxChIdx;
uint8_t mRfChLst[RF_CHANNELS];
-
+
uint8_t mRxChIdx;
uint16_t mRxLoopCnt;
diff --git a/src/hm/hmSystem.h b/src/hm/hmSystem.h
index 53b6e85d..5338d1a0 100644
--- a/src/hm/hmSystem.h
+++ b/src/hm/hmSystem.h
@@ -70,7 +70,7 @@ class HmSystem {
break;
}
}
- else
+ else if(p->config->serial.u64 != 0ULL)
DPRINTLN(DBG_ERROR, F("inverter type can't be detected!"));
p->init();
diff --git a/src/main.cpp b/src/main.cpp
index ea708282..c585d0f2 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -18,7 +18,7 @@ IRAM_ATTR void handleIntr(void) {
//-----------------------------------------------------------------------------
void setup() {
- myApp.setup(WIFI_TRY_CONNECT_TIME);
+ myApp.setup();
// TODO: move to HmRadio
attachInterrupt(digitalPinToInterrupt(myApp.getIrqPin()), handleIntr, FALLING);
diff --git a/src/platformio.ini b/src/platformio.ini
index ac38b9e8..ba6c8be0 100644
--- a/src/platformio.ini
+++ b/src/platformio.ini
@@ -36,7 +36,7 @@ lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24
paulstoffregen/Time
- knolleary/PubSubClient
+ https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson
;esp8266/DNSServer
;esp8266/EEPROM
@@ -89,6 +89,42 @@ monitor_filters =
time ; Add timestamp with milliseconds for each new line
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
+[env:esp8266-nokia5110]
+platform = espressif8266
+board = esp12e
+board_build.f_cpu = 80000000L
+build_flags = -D RELEASE -DU8X8_NO_HW_I2C -DENA_NOKIA
+monitor_filters =
+ ;default ; Remove typical terminal control codes from input
+ time ; Add timestamp with milliseconds for each new line
+ ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
+lib_deps =
+ https://github.com/yubox-node-org/ESPAsyncWebServer
+ nrf24/RF24
+ paulstoffregen/Time
+ https://github.com/bertmelis/espMqttClient#v1.3.3
+ bblanchon/ArduinoJson
+ olikraus/U8g2
+ https://github.com/JChristensen/Timezone
+
+[env:esp8266-ssd1306]
+platform = espressif8266
+board = esp12e
+board_build.f_cpu = 80000000L
+build_flags = -D RELEASE -DENA_SSD1306
+monitor_filters =
+ ;default ; Remove typical terminal control codes from input
+ time ; Add timestamp with milliseconds for each new line
+ ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
+lib_deps =
+ https://github.com/yubox-node-org/ESPAsyncWebServer
+ nrf24/RF24
+ paulstoffregen/Time
+ https://github.com/bertmelis/espMqttClient#v1.3.3
+ bblanchon/ArduinoJson
+ https://github.com/ThingPulse/esp8266-oled-ssd1306.git
+ https://github.com/JChristensen/Timezone
+
[env:esp32-wroom32-release]
platform = espressif32
board = lolin_d32
@@ -109,3 +145,39 @@ monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
+
+[env:esp32-wroom32-nokia5110]
+platform = espressif32
+board = lolin_d32
+build_flags = -D RELEASE -std=gnu++14 -DU8X8_NO_HW_I2C -DENA_NOKIA
+build_unflags = -std=gnu++11
+monitor_filters =
+ ;default ; Remove typical terminal control codes from input
+ time ; Add timestamp with milliseconds for each new line
+ ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
+lib_deps =
+ https://github.com/yubox-node-org/ESPAsyncWebServer
+ nrf24/RF24
+ paulstoffregen/Time
+ https://github.com/bertmelis/espMqttClient#v1.3.3
+ bblanchon/ArduinoJson
+ olikraus/U8g2
+ https://github.com/JChristensen/Timezone
+
+[env:esp32-wroom32-ssd1306]
+platform = espressif32
+board = lolin_d32
+build_flags = -D RELEASE -std=gnu++14 -DENA_SSD1306
+build_unflags = -std=gnu++11
+monitor_filters =
+ ;default ; Remove typical terminal control codes from input
+ time ; Add timestamp with milliseconds for each new line
+ ;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
+lib_deps =
+ https://github.com/yubox-node-org/ESPAsyncWebServer
+ nrf24/RF24
+ paulstoffregen/Time
+ https://github.com/bertmelis/espMqttClient#v1.3.3
+ bblanchon/ArduinoJson
+ https://github.com/ThingPulse/esp8266-oled-ssd1306.git
+ https://github.com/JChristensen/Timezone
diff --git a/src/plugins/MonochromeDisplay/MonochromeDisplay.h b/src/plugins/MonochromeDisplay/MonochromeDisplay.h
new file mode 100644
index 00000000..83236992
--- /dev/null
+++ b/src/plugins/MonochromeDisplay/MonochromeDisplay.h
@@ -0,0 +1,307 @@
+#ifndef __MONOCHROME_DISPLAY__
+#define __MONOCHROME_DISPLAY__
+
+#if defined(ENA_NOKIA) || defined(ENA_SSD1306)
+#ifdef ENA_NOKIA
+ #include
+ #define DISP_PROGMEM U8X8_PROGMEM
+#else // ENA_SSD1306
+ /* esp8266 : SCL = 5, SDA = 4 */
+ /* ewsp32 : SCL = 22, SDA = 21 */
+ #include
+ #include
+ #define DISP_PROGMEM PROGMEM
+#endif
+
+#include
+
+#include "../../utils/helper.h"
+#include "../../hm/hmSystem.h"
+
+static uint8_t bmp_arrow[] DISP_PROGMEM = {
+ B00000000, B00011100, B00011100, B00001110, B00001110, B11111110, B01111111,
+ B01110000, B01110000, B00110000, B00111000, B00011000, B01111111, B00111111,
+ B00011110, B00001110, B00000110, B00000000, B00000000, B00000000, B00000000};
+
+static TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time
+static TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Tim
+
+template
+class MonochromeDisplay {
+ public:
+ #if defined(ENA_NOKIA)
+ MonochromeDisplay() : mDisplay(U8G2_R0, 5, 4, 16), mCE(CEST, CET) {
+ mNewPayload = false;
+ mExtra = 0;
+ }
+ #else // ENA_SSD1306
+ MonochromeDisplay() : mDisplay(0x3c, SDA, SCL), mCE(CEST, CET) {
+ mNewPayload = false;
+ mExtra = 0;
+ mRx = 0;
+ mUp = 1;
+ }
+ #endif
+
+ void setup(HMSYSTEM *sys, uint32_t *utcTs) {
+ mSys = sys;
+ mUtcTs = utcTs;
+ memset( mToday, 0, sizeof(float)*MAX_NUM_INVERTERS );
+ memset( mTotal, 0, sizeof(float)*MAX_NUM_INVERTERS );
+ mLastHour = 25;
+ #if defined(ENA_NOKIA)
+ mDisplay.begin();
+ ShowInfoText("booting...");
+ #else
+ mDisplay.init();
+ mDisplay.flipScreenVertically();
+ mDisplay.setContrast(63);
+ mDisplay.setBrightness(63);
+
+ mDisplay.clear();
+ mDisplay.setFont(ArialMT_Plain_24);
+ mDisplay.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
+
+ mDisplay.drawString(64,22,"Starting...");
+ mDisplay.display();
+ mDisplay.setTextAlignment(TEXT_ALIGN_LEFT);
+ #endif
+ }
+
+ void loop(void) {
+
+ }
+
+ void payloadEventListener(uint8_t cmd) {
+ mNewPayload = true;
+ }
+
+ void tickerSecond() {
+ static int cnt=1;
+ if(mNewPayload || !(cnt % 10)) {
+ cnt=1;
+ mNewPayload = false;
+ DataScreen();
+ }
+ else
+ cnt++;
+ }
+
+ private:
+ #if defined(ENA_NOKIA)
+ void ShowInfoText(const char *txt) {
+ /* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */
+ mDisplay.clear();
+ mDisplay.firstPage();
+ do {
+ const char *e;
+ const char *p = txt;
+ int y=10;
+ mDisplay.setFont(u8g2_font_5x8_tr);
+ while(1) {
+ for(e=p+1; (*e && (*e != '\n')); e++);
+ size_t len=e-p;
+ mDisplay.setCursor(2,y);
+ String res=((String)p).substring(0,len);
+ mDisplay.print(res);
+ if ( !*e )
+ break;
+ p=e+1;
+ y+=12;
+ }
+ mDisplay.sendBuffer();
+ } while( mDisplay.nextPage() );
+ }
+ #endif
+
+ void DataScreen(void) {
+ String timeStr = ah::getDateTimeStr(mCE.toLocal(*mUtcTs)).substring(2, 22);
+ int hr = timeStr.substring(9,2).toInt();
+ IPAddress ip = WiFi.localIP();
+ float totalYield = 0.0, totalYieldToday = 0.0, totalActual = 0.0;
+ char fmtText[32];
+ int ucnt=0, num_inv=0;
+ unsigned int pow_i[ MAX_NUM_INVERTERS ];
+
+ memset( pow_i, 0, sizeof(unsigned int)* MAX_NUM_INVERTERS );
+ if ( hr < mLastHour ) // next day ? reset today-values
+ memset( mToday, 0, sizeof(float)*MAX_NUM_INVERTERS );
+ mLastHour = hr;
+
+ for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
+ Inverter<> *iv = mSys->getInverterByPos(id);
+ if (NULL != iv) {
+ record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
+ uint8_t pos;
+ uint8_t list[] = {FLD_PAC, FLD_YT, FLD_YD};
+
+ for (uint8_t fld = 0; fld < 3; fld++) {
+ pos = iv->getPosByChFld(CH0, list[fld],rec);
+ int isprod = iv->isProducing(*mUtcTs,rec);
+
+ if(fld == 1)
+ {
+ if ( isprod )
+ mTotal[num_inv] = iv->getValue(pos,rec);
+ totalYield += mTotal[num_inv];
+ }
+ if(fld == 2)
+ {
+ if ( isprod )
+ mToday[num_inv] = iv->getValue(pos,rec);
+ totalYieldToday += mToday[num_inv];
+ }
+ if((fld == 0) && isprod )
+ {
+ pow_i[num_inv] = iv->getValue(pos,rec);
+ totalActual += iv->getValue(pos,rec);
+ ucnt++;
+ }
+ }
+ num_inv++;
+ }
+ }
+ /* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */
+ mDisplay.clear();
+ #if defined(ENA_NOKIA)
+ mDisplay.firstPage();
+ do {
+ if(ucnt) {
+ mDisplay.drawXBMP(10,1,8,17,bmp_arrow);
+ mDisplay.setFont(u8g2_font_logisoso16_tr);
+ mDisplay.setCursor(25,17);
+ sprintf(fmtText,"%3.0f",totalActual);
+ mDisplay.print(String(fmtText)+F(" W"));
+ }
+ else
+ {
+ mDisplay.setFont(u8g2_font_logisoso16_tr );
+ mDisplay.setCursor(10,17);
+ mDisplay.print(String(F("offline")));
+ }
+ mDisplay.drawHLine(2,20,78);
+ mDisplay.setFont(u8g2_font_5x8_tr);
+ mDisplay.setCursor(5,29);
+ if (( num_inv < 2 ) || !(mExtra%2))
+ {
+ sprintf(fmtText,"%4.0f",totalYieldToday);
+ mDisplay.print(F("today ")+String(fmtText)+F(" Wh"));
+ mDisplay.setCursor(5,37);
+ sprintf(fmtText,"%.1f",totalYield);
+ mDisplay.print(F("total ")+String(fmtText)+F(" kWh"));
+ }
+ else
+ {
+ int id1=(mExtra/2)%(num_inv-1);
+ if( pow_i[id1] )
+ mDisplay.print(F("#")+String(id1+1)+F(" ")+String(pow_i[id1])+F(" W"));
+ else
+ mDisplay.print(F("#")+String(id1+1)+F(" -----"));
+ mDisplay.setCursor(5,37);
+ if( pow_i[id1+1] )
+ mDisplay.print(F("#")+String(id1+2)+F(" ")+String(pow_i[id1+1])+F(" W"));
+ else
+ mDisplay.print(F("#")+String(id1+2)+F(" -----"));
+ }
+ if ( !(mExtra%10) && ip ) {
+ mDisplay.setCursor(5,47);
+ mDisplay.print(ip.toString());
+ }
+ else {
+ mDisplay.setCursor(0,47);
+ mDisplay.print(timeStr);
+ }
+
+ mDisplay.sendBuffer();
+ } while( mDisplay.nextPage() );
+ mExtra++;
+ #else // ENA_SSD1306
+ if(mUp) {
+ mRx += 2;
+ if(mRx >= 20)
+ mUp = 0;
+ } else {
+ mRx -= 2;
+ if(mRx <= 0)
+ mUp = 1;
+ }
+ int ex = 2*( mExtra % 5 );
+
+ if(ucnt) {
+ mDisplay.setBrightness(63);
+ mDisplay.drawXbm(10+ex,5,8,17,bmp_arrow);
+ mDisplay.setFont(ArialMT_Plain_24);
+ sprintf(fmtText,"%3.0f",totalActual);
+ mDisplay.drawString(25+ex,0,String(fmtText)+F(" W"));
+ }
+ else
+ {
+ mDisplay.setBrightness(1);
+ mDisplay.setFont(ArialMT_Plain_24);
+ mDisplay.drawString(25+ex,0,String(F("offline")));
+ }
+ mDisplay.setFont(ArialMT_Plain_16);
+
+ if (( num_inv < 2 ) || !(mExtra%2))
+ {
+ sprintf(fmtText,"%4.0f",totalYieldToday);
+ mDisplay.drawString(5,22,F("today ")+String(fmtText)+F(" Wh"));
+ sprintf(fmtText,"%.1f",totalYield);
+ mDisplay.drawString(5,35,F("total ")+String(fmtText)+F(" kWh"));
+ }
+ else
+ {
+ int id1=(mExtra/2)%(num_inv-1);
+ if( pow_i[id1] )
+ mDisplay.drawString(15,22,F("#")+String(id1+1)+F(" ")+String(pow_i[id1])+F(" W"));
+ else
+ mDisplay.drawString(15,22,F("#")+String(id1+1)+F(" -----"));
+ if( pow_i[id1+1] )
+ mDisplay.drawString(15,35,F("#")+String(id1+2)+F(" ")+String(pow_i[id1+1])+F(" W"));
+ else
+ mDisplay.drawString(15,35,F("#")+String(id1+2)+F(" -----"));
+ }
+ mDisplay.drawLine(2,23,123,23);
+
+ if ( (!(mExtra%10) && ip )|| (timeStr.length()<16))
+ {
+ mDisplay.drawString(5,49,ip.toString());
+ }
+ else
+ {
+ int w=mDisplay.getStringWidth(timeStr.c_str(),timeStr.length(),0);
+ if ( w>127 )
+ {
+ String tt=timeStr.substring(9,17);
+ w=mDisplay.getStringWidth(tt.c_str(),tt.length(),0);
+ mDisplay.drawString(127-w-mRx,49,tt);
+ }
+ else
+ mDisplay.drawString(0,49,timeStr);
+ }
+
+ mDisplay.display();
+ mExtra++;
+ #endif
+ }
+
+ // private member variables
+ #if defined(ENA_NOKIA)
+ U8G2_PCD8544_84X48_1_4W_HW_SPI mDisplay;
+ #else // ENA_SSD1306
+ SSD1306Wire mDisplay;
+ int mRx;
+ char mUp;
+ #endif
+ int mExtra;
+ bool mNewPayload;
+ float mTotal[ MAX_NUM_INVERTERS ];
+ float mToday[ MAX_NUM_INVERTERS ];
+ uint32_t *mUtcTs;
+ int mLastHour;
+ HMSYSTEM *mSys;
+ Timezone mCE;
+};
+#endif
+
+#endif /*__MONOCHROME_DISPLAY__*/
diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h
index 90f27d45..f94d2206 100644
--- a/src/publisher/pubMqtt.h
+++ b/src/publisher/pubMqtt.h
@@ -3,6 +3,8 @@
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
+// https://bert.emelis.net/espMqttClient/
+
#ifndef __PUB_MQTT_H__
#define __PUB_MQTT_H__
@@ -15,116 +17,161 @@
#include "../utils/dbg.h"
#include "../utils/ahoyTimer.h"
#include "../config/config.h"
-#include
+#include
#include
#include "../defines.h"
#include "../hm/hmSystem.h"
+#define QOS_0 0
+
+typedef std::function subscriptionCb;
+
template
class PubMqtt {
public:
PubMqtt() {
- mClient = new PubSubClient(mEspClient);
- mAddressSet = false;
-
- mLastReconnect = 0;
+ mRxCnt = 0;
mTxCnt = 0;
+ mEnReconnect = false;
+ mSubscriptionCb = NULL;
+ mIsDay = false;
+ mIvAvail = true;
}
~PubMqtt() { }
void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *sunrise, uint32_t *sunset) {
- DPRINTLN(DBG_VERBOSE, F("PubMqtt.h:setup"));
- mAddressSet = true;
-
- mCfg_mqtt = cfg_mqtt;
+ mCfgMqtt = cfg_mqtt;
mDevName = devName;
+ mVersion = version;
mSys = sys;
mUtcTimestamp = utcTs;
mSunrise = sunrise;
mSunset = sunset;
- mClient->setServer(mCfg_mqtt->broker, mCfg_mqtt->port);
- mClient->setBufferSize(MQTT_MAX_PACKET_SIZE);
+ snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic);
- setCallback(std::bind(&PubMqtt::cbMqtt, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
+ #if defined(ESP8266)
+ mHWifiCon = WiFi.onStationModeGotIP(std::bind(&PubMqtt::onWifiConnect, this, std::placeholders::_1));
+ mHWifiDiscon = WiFi.onStationModeDisconnected(std::bind(&PubMqtt::onWifiDisconnect, this, std::placeholders::_1));
+ #else
+ WiFi.onEvent(std::bind(&PubMqtt::onWiFiEvent, this, std::placeholders::_1));
+ #endif
- sendMsg("version", version);
- sendMsg("device", devName);
- sendMsg("uptime", "0");
+ if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0))
+ mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd);
+ mClient.setClientId(mDevName); // TODO: add mac?
+ mClient.setServer(mCfgMqtt->broker, mCfgMqtt->port);
+ mClient.setWill(mLwtTopic, QOS_0, true, mLwtOffline);
+ mClient.onConnect(std::bind(&PubMqtt::onConnect, this, std::placeholders::_1));
+ mClient.onDisconnect(std::bind(&PubMqtt::onDisconnect, this, std::placeholders::_1));
+ mClient.onMessage(std::bind(&PubMqtt::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
}
void loop() {
- if(mAddressSet)
- mClient->loop();
+ #if defined(ESP8266)
+ mClient.loop();
+ #endif
}
void tickerSecond() {
- if(mAddressSet) {
- if(!mClient->connected())
- reconnect();
- }
sendIvData();
}
void tickerMinute() {
- if(mAddressSet) {
- char val[40];
- snprintf(val, 40, "%ld", millis() / 1000);
- sendMsg("uptime", val);
-
- sendMsg("wifi_rssi", String(WiFi.RSSI()).c_str());
+ char val[12];
+ snprintf(val, 12, "%ld", millis() / 1000);
+ publish("uptime", val);
+ publish("wifi_rssi", String(WiFi.RSSI()).c_str());
+ publish("free_heap", String(ESP.getFreeHeap()).c_str());
+
+ if(!mClient.connected()) {
+ if(mEnReconnect)
+ mClient.connect();
}
+ else if(mIvAvail && !mIsDay)
+ tickSunset();
}
- void tickerHour() {
- if(mAddressSet) {
- sendMsg("sunrise", String(*mSunrise).c_str());
- sendMsg("sunset", String(*mSunset).c_str());
- }
+ void tickerSun() {
+ publish("sunrise", String(*mSunrise).c_str(), true);
+ publish("sunset", String(*mSunset).c_str(), true);
}
- void setCallback(MQTT_CALLBACK_SIGNATURE) {
- mClient->setCallback(callback);
+ void tickSunrise() {
+ mIsDay = true;
}
- void sendMsg(const char *topic, const char *msg) {
- //DPRINTLN(DBG_VERBOSE, F("mqtt.h:sendMsg"));
- if(mAddressSet) {
- char top[66];
- snprintf(top, 66, "%s/%s", mCfg_mqtt->topic, topic);
- sendMsg2(top, msg, false);
+ void tickSunset() {
+ mIsDay = false;
+ char topic[MQTT_TOPIC_LEN + 15], val[32];
+ Inverter<> *iv;
+ record_t<> *rec;
+ mIvAvail = false;
+ for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
+ iv = mSys->getInverterByPos(id);
+ if (NULL == iv)
+ continue; // skip to next inverter
+ rec = iv->getRecordStruct(RealTimeRunData_Debug);
+
+ if (!iv->isAvailable(*mUtcTimestamp, rec)) {
+ snprintf(topic, MQTT_TOPIC_LEN + 15, "%s/available_text", iv->config->name);
+ snprintf(val, 32, "not available and not producing");
+ publish(topic, val, true);
+
+ snprintf(topic, MQTT_TOPIC_LEN + 15, "%s/available", iv->config->name);
+ snprintf(val, 32, "%d", MQTT_STATUS_NOT_AVAIL_NOT_PROD);
+ publish(topic, val, true);
+ }
+ else
+ mIvAvail = true;
}
+
+ if(!mIvAvail)
+ publish("status", "offline", true);
}
- void sendMsg2(const char *topic, const char *msg, boolean retained) {
- if(mAddressSet) {
- if(!mClient->connected())
- reconnect();
- if(mClient->connected())
- mClient->publish(topic, msg, retained);
- mTxCnt++;
- }
+ void payloadEventListener(uint8_t cmd) {
+ if(mClient.connected()) // prevent overflow if MQTT broker is not reachable but set
+ mSendList.push(cmd);
}
- bool isConnected(bool doRecon = false) {
- //DPRINTLN(DBG_VERBOSE, F("mqtt.h:isConnected"));
- if(!mAddressSet)
- return false;
- if(doRecon && !mClient->connected())
- reconnect();
- return mClient->connected();
+ void publish(const char *subTopic, const char *payload, bool retained = false, bool addTopic = true) {
+ if(!mClient.connected())
+ return;
+
+ char topic[MQTT_TOPIC_LEN + 2];
+ snprintf(topic, (MQTT_TOPIC_LEN + 2), "%s/%s", mCfgMqtt->topic, subTopic);
+ if(addTopic)
+ mClient.publish(topic, QOS_0, retained, payload);
+ else
+ mClient.publish(subTopic, QOS_0, retained, payload);
+ mTxCnt++;
}
- void payloadEventListener(uint8_t cmd) {
- mSendList.push(cmd);
+ void subscribe(const char *subTopic) {
+ char topic[MQTT_TOPIC_LEN + 20];
+ snprintf(topic, (MQTT_TOPIC_LEN + 20), "%s/%s", mCfgMqtt->topic, subTopic);
+ mClient.subscribe(topic, QOS_0);
}
- uint32_t getTxCnt(void) {
+ void setSubscriptionCb(subscriptionCb cb) {
+ mSubscriptionCb = cb;
+ }
+
+ inline bool isConnected() {
+ return mClient.connected();
+ }
+
+ inline uint32_t getTxCnt(void) {
return mTxCnt;
}
- void sendMqttDiscoveryConfig(const char *topic) {
+ inline uint32_t getRxCnt(void) {
+ return mRxCnt;
+ }
+
+ void sendDiscoveryConfig(void) {
DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig"));
char stateTopic[64], discoveryTopic[64], buffer[512], name[32], uniq_id[32];
@@ -133,11 +180,11 @@ class PubMqtt {
if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
DynamicJsonDocument deviceDoc(128);
- deviceDoc["name"] = iv->config->name;
- deviceDoc["ids"] = String(iv->config->serial.u64, HEX);
- deviceDoc["cu"] = F("http://") + String(WiFi.localIP().toString());
- deviceDoc["mf"] = "Hoymiles";
- deviceDoc["mdl"] = iv->config->name;
+ deviceDoc[F("name")] = iv->config->name;
+ deviceDoc[F("ids")] = String(iv->config->serial.u64, HEX);
+ deviceDoc[F("cu")] = F("http://") + String(WiFi.localIP().toString());
+ deviceDoc[F("mf")] = F("Hoymiles");
+ deviceDoc[F("mdl")] = iv->config->name;
JsonObject deviceObj = deviceDoc.as();
DynamicJsonDocument doc(384);
@@ -147,26 +194,25 @@ class PubMqtt {
} else {
snprintf(name, 32, "%s CH%d %s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
}
- snprintf(stateTopic, 64, "%s/%s/ch%d/%s", topic, iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
+ snprintf(stateTopic, 64, "/ch%d/%s", rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(discoveryTopic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(uniq_id, 32, "ch%d_%s", rec->assign[i].ch, iv->getFieldName(i, rec));
const char *devCls = getFieldDeviceClass(rec->assign[i].fieldId);
const char *stateCls = getFieldStateClass(rec->assign[i].fieldId);
- doc["name"] = name;
- doc["stat_t"] = stateTopic;
- doc["unit_of_meas"] = iv->getUnit(i, rec);
- doc["uniq_id"] = String(iv->config->serial.u64, HEX) + "_" + uniq_id;
- doc["dev"] = deviceObj;
- doc["exp_aft"] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!?
+ doc[F("name")] = name;
+ doc[F("stat_t")] = String(mCfgMqtt->topic) + "/" + String(iv->config->name) + String(stateTopic);
+ doc[F("unit_of_meas")] = iv->getUnit(i, rec);
+ doc[F("uniq_id")] = String(iv->config->serial.u64, HEX) + "_" + uniq_id;
+ doc[F("dev")] = deviceObj;
+ doc[F("exp_aft")] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!?
if (devCls != NULL)
- doc["dev_cla"] = devCls;
+ doc[F("dev_cla")] = devCls;
if (stateCls != NULL)
- doc["stat_cla"] = stateCls;
+ doc[F("stat_cla")] = stateCls;
serializeJson(doc, buffer);
- sendMsg2(discoveryTopic, buffer, true);
- // DPRINTLN(DBG_INFO, F("mqtt sent"));
+ publish(discoveryTopic, buffer, true, false);
doc.clear();
}
@@ -176,40 +222,131 @@ class PubMqtt {
}
private:
- void reconnect(void) {
- DPRINTLN(DBG_DEBUG, F("mqtt.h:reconnect"));
- DPRINTLN(DBG_DEBUG, F("MQTT mClient->_state ") + String(mClient->state()) );
+ #if defined(ESP8266)
+ void onWifiConnect(const WiFiEventStationModeGotIP& event) {
+ DPRINTLN(DBG_VERBOSE, F("MQTT connecting"));
+ mClient.connect();
+ mEnReconnect = true;
+ }
- #ifdef ESP8266
- DPRINTLN(DBG_DEBUG, F("WIFI mEspClient.status ") + String(mEspClient.status()) );
- #endif
+ void onWifiDisconnect(const WiFiEventStationModeDisconnected& event) {
+ mEnReconnect = false;
+ }
+
+ #else
+ void onWiFiEvent(WiFiEvent_t event) {
+ switch(event) {
+ case SYSTEM_EVENT_STA_GOT_IP:
+ DPRINTLN(DBG_VERBOSE, F("MQTT connecting"));
+ mClient.connect();
+ mEnReconnect = true;
+ break;
+
+ case SYSTEM_EVENT_STA_DISCONNECTED:
+ mEnReconnect = false;
+ break;
+
+ default:
+ break;
+ }
+ }
+ #endif
+
+ void onConnect(bool sessionPreset) {
+ DPRINTLN(DBG_INFO, F("MQTT connected"));
+ mEnReconnect = true;
+
+ publish("version", mVersion, true);
+ publish("device", mDevName, true);
+ tickerMinute();
+ publish(mLwtTopic, mLwtOnline, true, false);
- boolean resub = false;
- if(!mClient->connected() && (millis() - mLastReconnect) > MQTT_RECONNECT_DELAY ) {
- mLastReconnect = millis();
- if(strlen(mDevName) > 0) {
- // der Server und der Port mĂĽssen neu gesetzt werden,
- // da ein MQTT_CONNECTION_LOST -3 die Werte zerstört hat.
- mClient->setServer(mCfg_mqtt->broker, mCfg_mqtt->port);
- mClient->setBufferSize(MQTT_MAX_PACKET_SIZE);
-
- char lwt[MQTT_TOPIC_LEN + 7 ]; // "/uptime" --> + 7 byte
- snprintf(lwt, MQTT_TOPIC_LEN + 7, "%s/uptime", mCfg_mqtt->topic);
-
- if((strlen(mCfg_mqtt->user) > 0) && (strlen(mCfg_mqtt->pwd) > 0))
- resub = mClient->connect(mDevName, mCfg_mqtt->user, mCfg_mqtt->pwd, lwt, 0, false, "offline");
- else
- resub = mClient->connect(mDevName, lwt, 0, false, "offline");
- // ein Subscribe ist nur nach einem connect notwendig
- if(resub) {
- char topic[MQTT_TOPIC_LEN + 13 ]; // "/devcontrol/#" --> + 6 byte
- // ToDo: "/devcontrol/#" is hardcoded
- snprintf(topic, MQTT_TOPIC_LEN + 13, "%s/devcontrol/#", mCfg_mqtt->topic);
- DPRINTLN(DBG_INFO, F("subscribe to ") + String(topic));
- mClient->subscribe(topic); // subscribe to mTopic + "/devcontrol/#"
+ subscribe("ctrl/#");
+ subscribe("setup/#");
+ subscribe("status/#");
+ }
+
+ void onDisconnect(espMqttClientTypes::DisconnectReason reason) {
+ DPRINT(DBG_INFO, F("MQTT disconnected, reason: "));
+ switch (reason) {
+ case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED:
+ DBGPRINTLN(F("TCP disconnect"));
+ break;
+ case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
+ DBGPRINTLN(F("wrong protocol version"));
+ break;
+ case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED:
+ DBGPRINTLN(F("identifier rejected"));
+ break;
+ case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE:
+ DBGPRINTLN(F("broker unavailable"));
+ break;
+ case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS:
+ DBGPRINTLN(F("malformed credentials"));
+ break;
+ case espMqttClientTypes::DisconnectReason::MQTT_NOT_AUTHORIZED:
+ DBGPRINTLN(F("not authorized"));
+ break;
+ default:
+ DBGPRINTLN(F("unknown"));
+ }
+ }
+
+ void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
+ DPRINTLN(DBG_VERBOSE, F("MQTT got topic: ") + String(topic));
+ if(NULL == mSubscriptionCb)
+ return;
+
+ char *tpc = new char[strlen(topic) + 1];
+ uint8_t cnt = 0;
+ DynamicJsonDocument json(128);
+ JsonObject root = json.to();
+
+ strncpy(tpc, topic, strlen(topic) + 1);
+ if(len > 0) {
+ char *pyld = new char[len + 1];
+ strncpy(pyld, (const char*)payload, len);
+ pyld[len] = '\0';
+ root["val"] = atoi(pyld);
+ delete[] pyld;
+ }
+
+ char *p = strtok(tpc, "/");
+ p = strtok(NULL, "/"); // remove mCfgMqtt->topic
+ while(NULL != p) {
+ if(0 == cnt) {
+ if(0 == strncmp(p, "ctrl", 4)) {
+ if(NULL != (p = strtok(NULL, "/"))) {
+ root[F("path")] = F("ctrl");
+ root[F("cmd")] = p;
+ }
+ } else if(0 == strncmp(p, "setup", 5)) {
+ if(NULL != (p = strtok(NULL, "/"))) {
+ root[F("path")] = F("setup");
+ root[F("cmd")] = p;
+ }
+ } else if(0 == strncmp(p, "status", 6)) {
+ if(NULL != (p = strtok(NULL, "/"))) {
+ root[F("path")] = F("status");
+ root[F("cmd")] = p;
+ }
}
}
+ else if(1 == cnt) {
+ root[F("id")] = atoi(p);
+ }
+ p = strtok(NULL, "/");
+ cnt++;
}
+ delete[] tpc;
+
+ /*char out[128];
+ serializeJson(root, out, 128);
+ DPRINTLN(DBG_INFO, "json: " + String(out));*/
+ if(NULL != mSubscriptionCb)
+ (mSubscriptionCb)(root);
+
+ mRxCnt++;
}
const char *getFieldDeviceClass(uint8_t fieldId) {
@@ -234,11 +371,12 @@ class PubMqtt {
if(mSendList.empty())
return;
- isConnected(true); // really needed? See comment from HorstG-57 #176
- char topic[32 + MAX_NAME_LENGTH], val[40];
+ char topic[7 + MQTT_TOPIC_LEN], val[40];
float total[4];
bool sendTotal = false;
bool totalIncomplete = false;
+ bool allAvail = true;
+ bool first = true;
while(!mSendList.empty()) {
memset(total, 0, sizeof(float) * 4);
@@ -250,40 +388,58 @@ class PubMqtt {
record_t<> *rec = iv->getRecordStruct(mSendList.front());
if(mSendList.front() == RealTimeRunData_Debug) {
+ if(first)
+ mIvAvail = false;
+ first = false;
+
// inverter status
uint8_t status = MQTT_STATUS_AVAIL_PROD;
if (!iv->isAvailable(*mUtcTimestamp, rec)) {
status = MQTT_STATUS_NOT_AVAIL_NOT_PROD;
totalIncomplete = true;
+ allAvail = false;
}
else if (!iv->isProducing(*mUtcTimestamp, rec)) {
+ mIvAvail = true;
if (MQTT_STATUS_AVAIL_PROD == status)
status = MQTT_STATUS_AVAIL_NOT_PROD;
}
+ else
+ mIvAvail = true;
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->config->name);
snprintf(val, 40, "%s%s%s%s",
- (status == MQTT_STATUS_NOT_AVAIL_NOT_PROD) ? "not yet " : "",
+ (status == MQTT_STATUS_NOT_AVAIL_NOT_PROD) ? "not " : "",
"available and ",
(status == MQTT_STATUS_AVAIL_NOT_PROD) ? "not " : "",
- (status == MQTT_STATUS_NOT_AVAIL_NOT_PROD) ? "" : "producing"
+ "producing"
);
- sendMsg(topic, val);
+ publish(topic, val, true);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
snprintf(val, 40, "%d", status);
- sendMsg(topic, val);
+ publish(topic, val, true);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name);
- snprintf(val, 40, "%i", iv->getLastTs(rec) * 1000);
- sendMsg(topic, val);
+ snprintf(val, 40, "%d", iv->getLastTs(rec));
+ publish(topic, val, true);
}
// data
if(iv->isAvailable(*mUtcTimestamp, rec)) {
for (uint8_t i = 0; i < rec->length; i++) {
+ bool retained = false;
+ if (mSendList.front() == RealTimeRunData_Debug) {
+ switch (rec->assign[i].fieldId) {
+ case FLD_YT:
+ case FLD_YD:
+ retained = true;
+ break;
+ }
+ }
+
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
- snprintf(val, 40, "%.3f", iv->getValue(i, rec));
- sendMsg(topic, val);
+ snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec)));
+ publish(topic, val, retained);
// calculate total values for RealTimeRunData_Debug
if (mSendList.front() == RealTimeRunData_Debug) {
@@ -312,6 +468,9 @@ class PubMqtt {
mSendList.pop(); // remove from list once all inverters were processed
+ snprintf(val, 32, "%s", ((allAvail) ? "online" : ((mIvAvail) ? "partial" : "offline")));
+ publish("status", val, true);
+
if ((true == sendTotal) && (false == totalIncomplete)) {
uint8_t fieldId;
for (uint8_t i = 0; i < 4; i++) {
@@ -331,128 +490,34 @@ class PubMqtt {
break;
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]);
- snprintf(val, 40, "%.3f", total[i]);
- sendMsg(topic, val);
+ snprintf(val, 40, "%g", ah::round3(total[i]));
+ publish(topic, val, true);
}
}
}
}
- void cbMqtt(char *topic, byte *payload, unsigned int length) {
- // callback handling on subscribed devcontrol topic
- DPRINTLN(DBG_INFO, F("cbMqtt"));
- // subcribed topics are mTopic + "/devcontrol/#" where # is /
- // eg. mypvsolar/devcontrol/1/11 with payload "400" --> inverter 1 active power limit 400 Watt
- const char *token = strtok(topic, "/");
- while (token != NULL) {
- if (strcmp(token, "devcontrol") == 0) {
- token = strtok(NULL, "/");
- uint8_t iv_id = std::stoi(token);
-
- if (iv_id >= 0 && iv_id <= MAX_NUM_INVERTERS) {
- Inverter<> *iv = mSys->getInverterByPos(iv_id);
- if (NULL != iv) {
- if (!iv->devControlRequest) { // still pending
- token = strtok(NULL, "/");
-
- switch (std::stoi(token)) {
- // Active Power Control
- case ActivePowerContr:
- token = strtok(NULL, "/"); // get ControlMode aka "PowerPF.Desc" in DTU-Pro Code from topic string
- if (token == NULL) // default via mqtt ommit the LimitControlMode
- iv->powerLimit[1] = AbsolutNonPersistent;
- else
- iv->powerLimit[1] = std::stoi(token);
- if (length <= 5) { // if (std::stoi((char*)payload) > 0) more error handling powerlimit needed?
- if (iv->powerLimit[1] >= AbsolutNonPersistent && iv->powerLimit[1] <= RelativPersistent) {
- iv->devControlCmd = ActivePowerContr;
- iv->powerLimit[0] = std::stoi(std::string((char *)payload, (unsigned int)length)); // THX to @silversurfer
- /*if (iv->powerLimit[1] & 0x0001)
- DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("%"));
- else
- DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W"));*/
-
- DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + String(iv->powerLimit[1] & 0x0001) ? F("%") : F("W"));
- }
- iv->devControlRequest = true;
- } else {
- DPRINTLN(DBG_INFO, F("Invalid mqtt payload recevied: ") + String((char *)payload));
- }
- break;
-
- // Turn On
- case TurnOn:
- iv->devControlCmd = TurnOn;
- DPRINTLN(DBG_INFO, F("Turn on inverter ") + String(iv->id));
- iv->devControlRequest = true;
- break;
-
- // Turn Off
- case TurnOff:
- iv->devControlCmd = TurnOff;
- DPRINTLN(DBG_INFO, F("Turn off inverter ") + String(iv->id));
- iv->devControlRequest = true;
- break;
-
- // Restart
- case Restart:
- iv->devControlCmd = Restart;
- DPRINTLN(DBG_INFO, F("Restart inverter ") + String(iv->id));
- iv->devControlRequest = true;
- break;
-
- // Reactive Power Control
- case ReactivePowerContr:
- iv->devControlCmd = ReactivePowerContr;
- if (true) { // if (std::stoi((char*)payload) > 0) error handling powerlimit needed?
- iv->devControlCmd = ReactivePowerContr;
- iv->powerLimit[0] = std::stoi(std::string((char *)payload, (unsigned int)length));
- iv->powerLimit[1] = 0x0000; // if reactivepower limit is set via external interface --> set it temporay
- DPRINTLN(DBG_DEBUG, F("Reactivepower limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W"));
- iv->devControlRequest = true;
- }
- break;
-
- // Set Power Factor
- case PFSet:
- // iv->devControlCmd = PFSet;
- // uint16_t power_factor = std::stoi(strtok(NULL, "/"));
- DPRINTLN(DBG_INFO, F("Set Power Factor not implemented for inverter ") + String(iv->id));
- break;
-
- // CleanState lock & alarm
- case CleanState_LockAndAlarm:
- iv->devControlCmd = CleanState_LockAndAlarm;
- DPRINTLN(DBG_INFO, F("CleanState lock & alarm for inverter ") + String(iv->id));
- iv->devControlRequest = true;
- break;
-
- default:
- DPRINTLN(DBG_INFO, "Not implemented");
- break;
- }
- }
- }
- }
- break;
- }
- token = strtok(NULL, "/");
- }
- DPRINTLN(DBG_INFO, F("app::cbMqtt finished"));
- }
+ espMqttClient mClient;
+ cfgMqtt_t *mCfgMqtt;
+ #if defined(ESP8266)
+ WiFiEventHandler mHWifiCon, mHWifiDiscon;
+ #endif
uint32_t *mSunrise, *mSunset;
- WiFiClient mEspClient;
- PubSubClient *mClient;
HMSYSTEM *mSys;
uint32_t *mUtcTimestamp;
-
- bool mAddressSet;
- cfgMqtt_t *mCfg_mqtt;
- const char *mDevName;
- uint32_t mLastReconnect;
- uint32_t mTxCnt;
+ uint32_t mRxCnt, mTxCnt;
std::queue mSendList;
+ bool mEnReconnect;
+ subscriptionCb mSubscriptionCb;
+ bool mIsDay;
+ bool mIvAvail; // shows if at least one inverter is available
+
+ // last will topic and payload must be available trough lifetime of 'espMqttClient'
+ char mLwtTopic[MQTT_TOPIC_LEN+5];
+ const char* mLwtOnline = "connected";
+ const char* mLwtOffline = "not connected";
+ const char *mDevName, *mVersion;
};
#endif /*__PUB_MQTT_H__*/
diff --git a/src/publisher/pubSerial.h b/src/publisher/pubSerial.h
index ef4fb88f..d1cc55aa 100644
--- a/src/publisher/pubSerial.h
+++ b/src/publisher/pubSerial.h
@@ -21,28 +21,24 @@ class PubSerial {
mUtcTimestamp = utcTs;
}
- void tickerMinute() {
- DPRINTLN(DBG_INFO, "tickerMinute");
- if(++mTick >= mCfg->serial.interval) {
- mTick = 0;
- if (mCfg->serial.showIv) {
- char topic[30], val[10];
- for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
- Inverter<> *iv = mSys->getInverterByPos(id);
- if (NULL != iv) {
- record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
- if (iv->isAvailable(*mUtcTimestamp, rec)) {
- DPRINTLN(DBG_INFO, F("Inverter: ") + String(id));
- for (uint8_t i = 0; i < rec->length; i++) {
- if (0.0f != iv->getValue(i, rec)) {
- snprintf(topic, 30, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
- snprintf(val, 10, "%.3f %s", iv->getValue(i, rec), iv->getUnit(i, rec));
- DPRINTLN(DBG_INFO, String(topic) + ": " + String(val));
- }
- yield();
+ void tick(void) {
+ if (mCfg->serial.showIv) {
+ char topic[32 + MAX_NAME_LENGTH], val[40];
+ for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
+ Inverter<> *iv = mSys->getInverterByPos(id);
+ if (NULL != iv) {
+ record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
+ if (iv->isAvailable(*mUtcTimestamp, rec)) {
+ DPRINTLN(DBG_INFO, F("Inverter: ") + String(id));
+ for (uint8_t i = 0; i < rec->length; i++) {
+ if (0.0f != iv->getValue(i, rec)) {
+ snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
+ snprintf(val, 40, "%.3f %s", iv->getValue(i, rec), iv->getUnit(i, rec));
+ DPRINTLN(DBG_INFO, String(topic) + ": " + String(val));
}
- DPRINTLN(DBG_INFO, "");
+ yield();
}
+ DPRINTLN(DBG_INFO, "");
}
}
}
@@ -52,7 +48,6 @@ class PubSerial {
private:
settings_t *mCfg;
HMSYSTEM *mSys;
- uint8_t mTick;
uint32_t *mUtcTimestamp;
};
diff --git a/src/utils/helper.cpp b/src/utils/helper.cpp
new file mode 100644
index 00000000..cf0e13d3
--- /dev/null
+++ b/src/utils/helper.cpp
@@ -0,0 +1,58 @@
+//-----------------------------------------------------------------------------
+// 2022 Ahoy, https://github.com/lumpapu/ahoy
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+//-----------------------------------------------------------------------------
+
+#include "helper.h"
+
+namespace ah {
+ void ip2Arr(uint8_t ip[], const char *ipStr) {
+ memset(ip, 0, 4);
+ char *tmp = new char[strlen(ipStr)+1];
+ strncpy(tmp, ipStr, strlen(ipStr)+1);
+ char *p = strtok(tmp, ".");
+ uint8_t i = 0;
+ while(NULL != p) {
+ ip[i++] = atoi(p);
+ p = strtok(NULL, ".");
+ }
+ delete[] tmp;
+ }
+
+ // note: char *str needs to be at least 16 bytes long
+ void ip2Char(uint8_t ip[], char *str) {
+ if(0 == ip[0])
+ str[0] = '\0';
+ else
+ snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
+ }
+
+ double round3(double value) {
+ return (int)(value * 1000 + 0.5) / 1000.0;
+ }
+
+ String getDateTimeStr(time_t t) {
+ char str[20];
+ if(0 == t)
+ sprintf(str, "n/a");
+ else
+ sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d", year(t), month(t), day(t), hour(t), minute(t), second(t));
+ return String(str);
+ }
+
+ uint64_t Serial2u64(const char *val) {
+ char tmp[3];
+ uint64_t ret = 0ULL;
+ uint64_t u64;
+ memset(tmp, 0, 3);
+ for(uint8_t i = 0; i < 6; i++) {
+ tmp[0] = val[i*2];
+ tmp[1] = val[i*2 + 1];
+ if((tmp[0] == '\0') || (tmp[1] == '\0'))
+ break;
+ u64 = strtol(tmp, NULL, 16);
+ ret |= (u64 << ((5-i) << 3));
+ }
+ return ret;
+ }
+}
diff --git a/src/utils/helper.h b/src/utils/helper.h
new file mode 100644
index 00000000..460e4691
--- /dev/null
+++ b/src/utils/helper.h
@@ -0,0 +1,24 @@
+//-----------------------------------------------------------------------------
+// 2022 Ahoy, https://ahoydtu.de
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+//-----------------------------------------------------------------------------
+
+#ifndef __HELPER_H__
+#define __HELPER_H__
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace ah {
+ void ip2Arr(uint8_t ip[], const char *ipStr);
+ void ip2Char(uint8_t ip[], char *str);
+ double round3(double value);
+ String getDateTimeStr(time_t t);
+ uint64_t Serial2u64(const char *val);
+}
+
+#endif /*__HELPER_H__*/
diff --git a/src/utils/llist.h b/src/utils/llist.h
new file mode 100644
index 00000000..dd139c86
--- /dev/null
+++ b/src/utils/llist.h
@@ -0,0 +1,108 @@
+//-----------------------------------------------------------------------------
+// 2022 Ahoy, https://ahoydtu.de
+// Lukas Pusch, lukas@lpusch.de
+// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
+//-----------------------------------------------------------------------------
+#ifndef __LIST_H__
+#define __LIST_H__
+
+template
+struct node_s {
+ typedef T dT;
+ node_s *pre;
+ node_s *nxt;
+ uint32_t id;
+ dT d;
+ node_s() : pre(NULL), nxt(NULL), d() {}
+ node_s(Args... args) : id(0), pre(NULL), nxt(NULL), d(args...) {}
+};
+
+template
+class llist {
+ typedef node_s elmType;
+ typedef T dataType;
+ public:
+ llist() : root(mPool) {
+ root = NULL;
+ elmType *p = mPool;
+ for(uint32_t i = 0; i < MAX_NUM; i++) {
+ p->id = i;
+ p++;
+ }
+ mFill = mMax = 0;
+ }
+
+ elmType *add(Args... args) {
+ elmType *p = root, *t;
+ if(NULL == (t = getFreeNode()))
+ return NULL;
+ if(++mFill > mMax)
+ mMax = mFill;
+
+ if(NULL == root) {
+ p = root = t;
+ p->pre = p;
+ p->nxt = p;
+ }
+ else {
+ p = root->pre;
+ t->pre = p;
+ p->nxt->pre = t;
+ t->nxt = p->nxt;
+ p->nxt = t;
+ }
+ t->d = dataType(args...);
+ return p;
+ }
+
+ elmType *getFront() {
+ return root;
+ }
+
+ elmType *get(elmType *p) {
+ p = p->nxt;
+ return (p == root) ? NULL : p;
+ }
+
+ elmType *rem(elmType *p) {
+ if(NULL == p)
+ return NULL;
+ elmType *t = p->nxt;
+ p->nxt->pre = p->pre;
+ p->pre->nxt = p->nxt;
+ if(root == p)
+ root = NULL;
+ p->nxt = NULL;
+ p->pre = NULL;
+ p = NULL;
+ mFill--;
+ return (NULL == root) ? NULL : ((t == root) ? NULL : t);
+ }
+
+ uint16_t getFill(void) {
+ return mFill;
+ }
+
+ uint16_t getMaxFill(void) {
+ return mMax;
+ }
+
+ protected:
+ elmType *root;
+
+ private:
+ elmType *getFreeNode(void) {
+ elmType *n = mPool;
+ for(uint32_t i = 0; i < MAX_NUM; i++) {
+ if(NULL == n->nxt)
+ return n;
+ n++;
+ }
+ return NULL;
+ }
+
+ elmType mPool[MAX_NUM];
+ uint16_t mFill, mMax;
+};
+
+#endif /*__LIST_H__*/
diff --git a/src/utils/scheduler.h b/src/utils/scheduler.h
index ff732b58..fc684ba9 100644
--- a/src/utils/scheduler.h
+++ b/src/utils/scheduler.h
@@ -7,75 +7,140 @@
#ifndef __SCHEDULER_H__
#define __SCHEDULER_H__
-#include
#include
-#include
-
-enum {EVERY_SEC = 1, EVERY_MIN, EVERY_HR, EVERY_12H, EVERY_DAY};
-typedef std::function SchedulerCb;
+#include "llist.h"
+#include "dbg.h"
namespace ah {
-class Scheduler {
- public:
- Scheduler() {}
-
- void setup() {
- mPrevMillis = 0;
- mSeconds = 0;
- mMinutes = 0;
- mHours = 0;
- }
-
- void loop() {
- if (millis() - mPrevMillis >= 1000) {
- mPrevMillis += 1000;
- notify(&mListSecond);
- if(++mSeconds >= 60) {
- mSeconds = 0;
- notify(&mListMinute);
- if(++mMinutes >= 60) {
- mMinutes = 0;
- notify(&mListHour);
- if(++mHours >= 24) {
- mHours = 0;
- notify(&mListDay);
- notify(&mList12h);
- }
- else if(mHours == 12)
- notify(&mList12h);
+ typedef std::function scdCb;
+
+ enum {SCD_SEC = 1, SCD_MIN = 60, SCD_HOUR = 3600, SCD_12H = 43200, SCD_DAY = 86400};
+
+ struct scdEvry_s {
+ scdCb c;
+ uint32_t timeout;
+ uint32_t reload;
+ scdEvry_s() : c(NULL), timeout(0), reload(0) {}
+ scdEvry_s(scdCb a, uint32_t tmt, uint32_t rl) : c(a), timeout(tmt), reload(rl) {}
+ };
+
+ struct scdAt_s {
+ scdCb c;
+ uint32_t timestamp;
+ scdAt_s() : c(NULL), timestamp(0) {}
+ scdAt_s(scdCb a, uint32_t ts) : c(a), timestamp(ts) {}
+ };
+
+
+ typedef node_s sP;
+ typedef node_s sPAt;
+ class Scheduler {
+ public:
+ Scheduler() {}
+
+ void setup() {
+ mUptime = 0;
+ mTimestamp = 0;
+ mPrevMillis = millis();
+ }
+
+ void loop(void) {
+ mMillis = millis();
+ mDiff = mMillis - mPrevMillis;
+ if (mDiff < 1000)
+ return;
+
+ mDiffSeconds = 1;
+ if (mDiff < 2000)
+ mPrevMillis += 1000;
+ else {
+ if (mMillis < mPrevMillis) { // overflow
+ mDiff = mMillis;
+ if (mDiff < 1000)
+ return;
}
+ mDiffSeconds = mDiff / 1000;
+ mPrevMillis += (mDiffSeconds * 1000);
}
+
+ mUptime += mDiffSeconds;
+ if(0 != mTimestamp)
+ mTimestamp += mDiffSeconds;
+ checkEvery();
+ checkAt();
+
+ }
+
+ void once(scdCb c, uint32_t timeout) { mStack.add(c, timeout, 0); }
+ void every(scdCb c, uint32_t interval) { mStack.add(c, interval, interval); }
+ void onceAt(scdCb c, uint32_t timestamp) { mStackAt.add(c, timestamp); }
+
+ void everySec(scdCb c) { mStack.add(c, SCD_SEC, SCD_SEC); }
+ void everyMin(scdCb c) { mStack.add(c, SCD_MIN, SCD_MIN); }
+ void everyHour(scdCb c) { mStack.add(c, SCD_HOUR, SCD_HOUR); }
+ void every12h(scdCb c) { mStack.add(c, SCD_12H, SCD_12H); }
+ void everyDay(scdCb c) { mStack.add(c, SCD_DAY, SCD_DAY); }
+
+ virtual void setTimestamp(uint32_t ts) {
+ mTimestamp = ts;
+ }
+
+ uint32_t getUptime(void) {
+ return mUptime;
+ }
+
+ uint32_t getTimestamp(void) {
+ return mTimestamp;
+ }
+
+ void stat() {
+ DPRINTLN(DBG_INFO, "max fill every: " + String(mStack.getMaxFill()));
+ DPRINTLN(DBG_INFO, "max fill at: " + String(mStackAt.getMaxFill()));
}
- }
-
- void addListener(uint8_t every, SchedulerCb cb) {
- switch(every) {
- case EVERY_SEC: mListSecond.push_back(cb); break;
- case EVERY_MIN: mListMinute.push_back(cb); break;
- case EVERY_HR: mListHour.push_back(cb); break;
- case EVERY_12H: mList12h.push_back(cb); break;
- case EVERY_DAY: mListDay.push_back(cb); break;
- default: break;
+
+ protected:
+ uint32_t mTimestamp;
+
+ private:
+ inline void checkEvery(void) {
+ sP *p = mStack.getFront();
+ while(NULL != p) {
+ if(mDiffSeconds >= p->d.timeout) { // expired
+ (p->d.c)();
+ yield();
+ if(0 == p->d.reload)
+ p = mStack.rem(p);
+ else {
+ p->d.timeout = p->d.reload - 1;
+ p = mStack.get(p);
+ }
+ }
+ else { // not expired
+ p->d.timeout -= mDiffSeconds;
+ p = mStack.get(p);
+ }
+ }
}
- }
- virtual void notify(std::list *lType) {
- for(std::list::iterator it = lType->begin(); it != lType->end(); ++it) {
- (*it)();
+ inline void checkAt(void) {
+ sPAt *p = mStackAt.getFront();
+ while(NULL != p) {
+ if((p->d.timestamp) <= mTimestamp) {
+ (p->d.c)();
+ yield();
+ p = mStackAt.rem(p);
+ }
+ else
+ p = mStackAt.get(p);
+ }
}
- }
-
- protected:
- std::list mListSecond;
- std::list mListMinute;
- std::list mListHour;
- std::list mList12h;
- std::list mListDay;
-
- private:
- uint32_t mPrevMillis;
- uint8_t mSeconds, mMinutes, mHours;
-};
+
+ llist<25, scdEvry_s, scdCb, uint32_t, uint32_t> mStack;
+ llist<10, scdAt_s, scdCb, uint32_t> mStackAt;
+ uint32_t mMillis, mPrevMillis, mDiff;
+ uint32_t mUptime;
+ uint8_t mDiffSeconds;
+ };
}
#endif /*__SCHEDULER_H__*/
diff --git a/src/utils/sun.h b/src/utils/sun.h
index 476ad0ff..c66149c2 100644
--- a/src/utils/sun.h
+++ b/src/utils/sun.h
@@ -10,8 +10,8 @@ namespace ah {
void calculateSunriseSunset(uint32_t utcTs, uint32_t offset, float lat, float lon, uint32_t *sunrise, uint32_t *sunset) {
// Source: https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth
- // Julian day since 1.1.2000 12:00 + correction 69.12s
- double n_JulianDay = (utcTs + offset) / 86400 - 10957.0 + 0.0008;
+ // Julian day since 1.1.2000 12:00
+ double n_JulianDay = (utcTs + offset) / 86400 - 10957.0;
// Mean solar time
double J = n_JulianDay - lon / 360;
// Solar mean anomaly
@@ -25,7 +25,7 @@ namespace ah {
// Declination of the sun
double delta = ASIN(SIN(lambda) * SIN(23.44));
// Hour angle
- double omega = ACOS(SIN(-0.83) - SIN(lat) * SIN(delta) / COS(lat) * COS(delta));
+ double omega = ACOS((SIN(-0.83) - SIN(lat) * SIN(delta)) / (COS(lat) * COS(delta)));
// Calculate sunrise and sunset
double Jrise = Jtransit - omega / 360;
double Jset = Jtransit + omega / 360;
diff --git a/src/web/RestApi.h b/src/web/RestApi.h
new file mode 100644
index 00000000..e2a7714c
--- /dev/null
+++ b/src/web/RestApi.h
@@ -0,0 +1,538 @@
+#ifndef __WEB_API_H__
+#define __WEB_API_H__
+
+#include "../utils/dbg.h"
+#ifdef ESP32
+ #include "AsyncTCP.h"
+#else
+ #include "ESPAsyncTCP.h"
+#endif
+#include "ESPAsyncWebServer.h"
+#include "AsyncJson.h"
+#include "../hm/hmSystem.h"
+#include "../utils/helper.h"
+
+#include "../appInterface.h"
+
+template
+class RestApi {
+ public:
+ RestApi() {
+ mTimezoneOffset = 0;
+ }
+
+ void setup(IApp *app, HMSYSTEM *sys, AsyncWebServer *srv, settings_t *config) {
+ mApp = app;
+ mSrv = srv;
+ mSys = sys;
+ mConfig = config;
+ mSrv->on("/api", HTTP_GET, std::bind(&RestApi::onApi, this, std::placeholders::_1));
+ mSrv->on("/api", HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1)).onBody(
+ std::bind(&RestApi::onApiPostBody, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5));
+
+ mSrv->on("/get_setup", HTTP_GET, std::bind(&RestApi::onDwnldSetup, this, std::placeholders::_1));
+ }
+
+ uint32_t getTimezoneOffset(void) {
+ return mTimezoneOffset;
+ }
+
+ void ctrlRequest(JsonObject obj) {
+ /*char out[128];
+ serializeJson(obj, out, 128);
+ DPRINTLN(DBG_INFO, "RestApi: " + String(out));*/
+ DynamicJsonDocument json(128);
+ JsonObject dummy = json.to();
+ if(obj[F("path")] == "ctrl")
+ setCtrl(obj, dummy);
+ else if(obj[F("path")] == "setup")
+ setSetup(obj, dummy);
+ }
+
+ private:
+ void onApi(AsyncWebServerRequest *request) {
+ AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192);
+ JsonObject root = response->getRoot();
+
+ Inverter<> *iv = mSys->getInverterByPos(0, false);
+ String path = request->url().substring(5);
+ if(path == "html/system") getHtmlSystem(root);
+ else if(path == "html/logout") getHtmlLogout(root);
+ else if(path == "html/save") getHtmlSave(root);
+ else if(path == "system") getSysInfo(root);
+ else if(path == "reboot") getReboot(root);
+ else if(path == "statistics") getStatistics(root);
+ else if(path == "inverter/list") getInverterList(root);
+ else if(path == "menu") getMenu(root);
+ else if(path == "index") getIndex(root);
+ else if(path == "setup") getSetup(root);
+ else if(path == "setup/networks") getNetworks(root);
+ else if(path == "live") getLive(root);
+ else if(path == "record/info") getRecord(root, iv->getRecordStruct(InverterDevInform_All));
+ else if(path == "record/alarm") getRecord(root, iv->getRecordStruct(AlarmData));
+ else if(path == "record/config") getRecord(root, iv->getRecordStruct(SystemConfigPara));
+ else if(path == "record/live") getRecord(root, iv->getRecordStruct(RealTimeRunData_Debug));
+ else
+ getNotFound(root, F("http://") + request->host() + F("/api/"));
+
+ response->addHeader("Access-Control-Allow-Origin", "*");
+ response->addHeader("Access-Control-Allow-Headers", "content-type");
+ response->setLength();
+ request->send(response);
+ }
+
+ void onApiPost(AsyncWebServerRequest *request) {
+ DPRINTLN(DBG_VERBOSE, "onApiPost");
+ }
+
+ void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
+ DPRINTLN(DBG_VERBOSE, "onApiPostBody");
+ DynamicJsonDocument json(200);
+ AsyncJsonResponse* response = new AsyncJsonResponse(false, 200);
+ JsonObject root = response->getRoot();
+
+ DeserializationError err = deserializeJson(json, (const char *)data, len);
+ JsonObject obj = json.as();
+ root[F("success")] = (err) ? false : true;
+ if(!err) {
+ String path = request->url().substring(5);
+ if(path == "ctrl")
+ root[F("success")] = setCtrl(obj, root);
+ else if(path == "setup")
+ root[F("success")] = setSetup(obj, root);
+ else {
+ root[F("success")] = false;
+ root[F("error")] = "Path not found: " + path;
+ }
+ }
+ else {
+ switch (err.code()) {
+ case DeserializationError::Ok: break;
+ case DeserializationError::InvalidInput: root[F("error")] = F("Invalid input"); break;
+ case DeserializationError::NoMemory: root[F("error")] = F("Not enough memory"); break;
+ default: root[F("error")] = F("Deserialization failed"); break;
+ }
+ }
+
+ response->setLength();
+ request->send(response);
+ }
+
+ void getNotFound(JsonObject obj, String url) {
+ JsonObject ep = obj.createNestedObject("avail_endpoints");
+ ep[F("system")] = url + F("system");
+ ep[F("statistics")] = url + F("statistics");
+ ep[F("inverter/list")] = url + F("inverter/list");
+ ep[F("index")] = url + F("index");
+ ep[F("setup")] = url + F("setup");
+ ep[F("live")] = url + F("live");
+ ep[F("record/info")] = url + F("record/info");
+ ep[F("record/alarm")] = url + F("record/alarm");
+ ep[F("record/config")] = url + F("record/config");
+ ep[F("record/live")] = url + F("record/live");
+ }
+ void onDwnldSetup(AsyncWebServerRequest *request) {
+ AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192);
+ JsonObject root = response->getRoot();
+
+ getSetup(root);
+
+ response->setLength();
+ response->addHeader("Content-Type", "application/octet-stream");
+ response->addHeader("Content-Description", "File Transfer");
+ response->addHeader("Content-Disposition", "attachment; filename=ahoy_setup.json");
+ request->send(response);
+ }
+
+ void getSysInfo(JsonObject obj) {
+ obj[F("ssid")] = mConfig->sys.stationSsid;
+ obj[F("device_name")] = mConfig->sys.deviceName;
+ obj[F("version")] = String(mApp->getVersion());
+ obj[F("build")] = String(AUTO_GIT_HASH);
+
+ obj[F("ts_uptime")] = mApp->getUptime();
+ obj[F("ts_now")] = mApp->getTimestamp();
+ obj[F("ts_sunrise")] = mApp->getSunrise();
+ obj[F("ts_sunset")] = mApp->getSunset();
+ obj[F("wifi_rssi")] = WiFi.RSSI();
+ obj[F("mac")] = WiFi.macAddress();
+ obj[F("hostname")] = WiFi.getHostname();
+ obj[F("pwd_set")] = (strlen(mConfig->sys.adminPwd) > 0);
+
+ obj[F("sdk")] = ESP.getSdkVersion();
+ obj[F("cpu_freq")] = ESP.getCpuFreqMHz();
+ obj[F("heap_free")] = ESP.getFreeHeap();
+ obj[F("sketch_total")] = ESP.getFreeSketchSpace();
+ obj[F("sketch_used")] = ESP.getSketchSize() / 1024; // in kb
+
+
+ getRadio(obj.createNestedObject(F("radio")));
+
+ #if defined(ESP32)
+ obj[F("heap_total")] = ESP.getHeapSize();
+ obj[F("chip_revision")] = ESP.getChipRevision();
+ obj[F("chip_model")] = ESP.getChipModel();
+ obj[F("chip_cores")] = ESP.getChipCores();
+ //obj[F("core_version")] = F("n/a");
+ //obj[F("flash_size")] = F("n/a");
+ //obj[F("heap_frag")] = F("n/a");
+ //obj[F("max_free_blk")] = F("n/a");
+ //obj[F("reboot_reason")] = F("n/a");
+ #else
+ //obj[F("heap_total")] = F("n/a");
+ //obj[F("chip_revision")] = F("n/a");
+ //obj[F("chip_model")] = F("n/a");
+ //obj[F("chip_cores")] = F("n/a");
+ obj[F("core_version")] = ESP.getCoreVersion();
+ obj[F("flash_size")] = ESP.getFlashChipRealSize() / 1024; // in kb
+ obj[F("heap_frag")] = ESP.getHeapFragmentation();
+ obj[F("max_free_blk")] = ESP.getMaxFreeBlockSize();
+ obj[F("reboot_reason")] = ESP.getResetReason();
+ #endif
+ //obj[F("littlefs_total")] = LittleFS.totalBytes();
+ //obj[F("littlefs_used")] = LittleFS.usedBytes();
+
+ #if defined(ESP32)
+ obj[F("esp_type")] = F("ESP32");
+ #else
+ obj[F("esp_type")] = F("ESP8266");
+ #endif
+ }
+
+ void getHtmlSystem(JsonObject obj) {
+ getMenu(obj.createNestedObject(F("menu")));
+ getSysInfo(obj.createNestedObject(F("system")));
+ obj[F("html")] = F("Factory Reset
Reboot");
+ }
+
+ void getHtmlLogout(JsonObject obj) {
+ getMenu(obj.createNestedObject(F("menu")));
+ getSysInfo(obj.createNestedObject(F("system")));
+ obj[F("refresh")] = 3;
+ obj[F("refresh_url")] = "/";
+ obj[F("html")] = F("succesfully logged out");
+ }
+
+ void getHtmlSave(JsonObject obj) {
+ getMenu(obj.createNestedObject(F("menu")));
+ getSysInfo(obj.createNestedObject(F("system")));
+ obj[F("refresh")] = 2;
+ obj[F("refresh_url")] = "/setup";
+ obj[F("html")] = F("settings succesfully save");
+ }
+
+ void getReboot(JsonObject obj) {
+ getMenu(obj.createNestedObject(F("menu")));
+ getSysInfo(obj.createNestedObject(F("system")));
+ obj[F("refresh")] = 10;
+ obj[F("refresh_url")] = "/";
+ obj[F("html")] = F("reboot. Autoreload after 10 seconds");
+ }
+
+ void getStatistics(JsonObject obj) {
+ statistics_t *stat = mApp->getStatistics();
+ obj[F("rx_success")] = stat->rxSuccess;
+ obj[F("rx_fail")] = stat->rxFail;
+ obj[F("rx_fail_answer")] = stat->rxFailNoAnser;
+ obj[F("frame_cnt")] = stat->frmCnt;
+ obj[F("tx_cnt")] = mSys->Radio.mSendCnt;
+ }
+
+ void getInverterList(JsonObject obj) {
+ JsonArray invArr = obj.createNestedArray(F("inverter"));
+
+ Inverter<> *iv;
+ for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
+ iv = mSys->getInverterByPos(i);
+ if(NULL != iv) {
+ JsonObject obj2 = invArr.createNestedObject();
+ obj2[F("id")] = i;
+ obj2[F("name")] = String(iv->config->name);
+ obj2[F("serial")] = String(iv->config->serial.u64, HEX);
+ obj2[F("channels")] = iv->channels;
+ obj2[F("version")] = String(iv->fwVersion);
+
+ for(uint8_t j = 0; j < iv->channels; j ++) {
+ obj2[F("ch_max_power")][j] = iv->config->chMaxPwr[j];
+ obj2[F("ch_name")][j] = iv->config->chName[j];
+ }
+ }
+ }
+ obj[F("interval")] = String(mConfig->nrf.sendInterval);
+ obj[F("retries")] = String(mConfig->nrf.maxRetransPerPyld);
+ obj[F("max_num_inverters")] = MAX_NUM_INVERTERS;
+ }
+
+ void getMqtt(JsonObject obj) {
+ obj[F("broker")] = String(mConfig->mqtt.broker);
+ obj[F("port")] = String(mConfig->mqtt.port);
+ obj[F("user")] = String(mConfig->mqtt.user);
+ obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String("");
+ obj[F("topic")] = String(mConfig->mqtt.topic);
+ }
+
+ void getNtp(JsonObject obj) {
+ obj[F("addr")] = String(mConfig->ntp.addr);
+ obj[F("port")] = String(mConfig->ntp.port);
+ }
+
+ void getSun(JsonObject obj) {
+ obj[F("lat")] = mConfig->sun.lat ? String(mConfig->sun.lat, 5) : "";
+ obj[F("lon")] = mConfig->sun.lat ? String(mConfig->sun.lon, 5) : "";
+ obj[F("disnightcom")] = mConfig->sun.disNightCom;
+ }
+
+ void getPinout(JsonObject obj) {
+ obj[F("cs")] = mConfig->nrf.pinCs;
+ obj[F("ce")] = mConfig->nrf.pinCe;
+ obj[F("irq")] = mConfig->nrf.pinIrq;
+ obj[F("led0")] = mConfig->led.led0;
+ obj[F("led1")] = mConfig->led.led1;
+ }
+
+ void getRadio(JsonObject obj) {
+ obj[F("power_level")] = mConfig->nrf.amplifierPower;
+ obj[F("isconnected")] = mSys->Radio.isChipConnected();
+ obj[F("DataRate")] = mSys->Radio.getDataRate();
+ obj[F("isPVariant")] = mSys->Radio.isPVariant();
+ }
+
+ void getSerial(JsonObject obj) {
+ obj[F("interval")] = (uint16_t)mConfig->serial.interval;
+ obj[F("show_live_data")] = mConfig->serial.showIv;
+ obj[F("debug")] = mConfig->serial.debug;
+ }
+
+ void getStaticIp(JsonObject obj) {
+ char buf[16];
+ ah::ip2Char(mConfig->sys.ip.ip, buf); obj[F("ip")] = String(buf);
+ ah::ip2Char(mConfig->sys.ip.mask, buf); obj[F("mask")] = String(buf);
+ ah::ip2Char(mConfig->sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
+ ah::ip2Char(mConfig->sys.ip.dns2, buf); obj[F("dns2")] = String(buf);
+ ah::ip2Char(mConfig->sys.ip.gateway, buf); obj[F("gateway")] = String(buf);
+ }
+
+ void getMenu(JsonObject obj) {
+ obj["name"][0] = "Live";
+ obj["link"][0] = "/live";
+ obj["name"][1] = "Serial / Control";
+ obj["link"][1] = "/serial";
+ obj["name"][2] = "Settings";
+ obj["link"][2] = "/setup";
+ obj["name"][3] = "-";
+ obj["name"][4] = "REST API";
+ obj["link"][4] = "/api";
+ obj["trgt"][4] = "_blank";
+ obj["name"][5] = "-";
+ obj["name"][6] = "Update";
+ obj["link"][6] = "/update";
+ obj["name"][7] = "System";
+ obj["link"][7] = "/system";
+ obj["name"][8] = "-";
+ obj["name"][9] = "Documentation";
+ obj["link"][9] = "https://ahoydtu.de";
+ obj["trgt"][9] = "_blank";
+ if(strlen(mConfig->sys.adminPwd) > 0) {
+ obj["name"][10] = "-";
+ obj["name"][11] = "Logout";
+ obj["link"][11] = "/logout";
+ }
+ }
+
+ void getIndex(JsonObject obj) {
+ getMenu(obj.createNestedObject(F("menu")));
+ getSysInfo(obj.createNestedObject(F("system")));
+ getRadio(obj.createNestedObject(F("radio")));
+ getStatistics(obj.createNestedObject(F("statistics")));
+ obj["refresh_interval"] = mConfig->nrf.sendInterval;
+
+ JsonArray inv = obj.createNestedArray(F("inverter"));
+ Inverter<> *iv;
+ for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
+ iv = mSys->getInverterByPos(i);
+ if(NULL != iv) {
+ record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
+ JsonObject invObj = inv.createNestedObject();
+ invObj[F("id")] = i;
+ invObj[F("name")] = String(iv->config->name);
+ invObj[F("version")] = String(iv->fwVersion);
+ invObj[F("is_avail")] = iv->isAvailable(mApp->getTimestamp(), rec);
+ invObj[F("is_producing")] = iv->isProducing(mApp->getTimestamp(), rec);
+ invObj[F("ts_last_success")] = iv->getLastTs(rec);
+ }
+ }
+
+ JsonArray warn = obj.createNestedArray(F("warnings"));
+ if(!mSys->Radio.isChipConnected())
+ warn.add(F("your NRF24 module can't be reached, check the wiring and pinout"));
+ else if(!mSys->Radio.isPVariant())
+ warn.add(F("your NRF24 module isn't a plus version(+), maybe incompatible!"));
+
+ if((!mApp->getMqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0))
+ warn.add(F("MQTT is not connected"));
+
+ JsonArray info = obj.createNestedArray(F("infos"));
+ if(mApp->getRebootRequestState())
+ info.add(F("reboot your ESP to apply all your configuration changes!"));
+ if(!mApp->getSettingsValid())
+ info.add(F("your settings are invalid"));
+ if(mApp->getMqttIsConnected())
+ info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent, ") + String(mApp->getMqttRxCnt()) + F(" packets received"));
+ }
+
+ void getSetup(JsonObject obj) {
+ getMenu(obj.createNestedObject(F("menu")));
+ getSysInfo(obj.createNestedObject(F("system")));
+ getInverterList(obj.createNestedObject(F("inverter")));
+ getMqtt(obj.createNestedObject(F("mqtt")));
+ getNtp(obj.createNestedObject(F("ntp")));
+ getSun(obj.createNestedObject(F("sun")));
+ getPinout(obj.createNestedObject(F("pinout")));
+ getRadio(obj.createNestedObject(F("radio")));
+ getSerial(obj.createNestedObject(F("serial")));
+ getStaticIp(obj.createNestedObject(F("static_ip")));
+ }
+
+ void getNetworks(JsonObject obj) {
+ mApp->getAvailNetworks(obj);
+ }
+
+ void getLive(JsonObject obj) {
+ getMenu(obj.createNestedObject(F("menu")));
+ getSysInfo(obj.createNestedObject(F("system")));
+ JsonArray invArr = obj.createNestedArray(F("inverter"));
+ obj["refresh_interval"] = mConfig->nrf.sendInterval;
+
+ uint8_t list[] = {FLD_UAC, FLD_IAC, FLD_PAC, FLD_F, FLD_PF, FLD_T, FLD_YT, FLD_YD, FLD_PDC, FLD_EFF, FLD_Q};
+
+ Inverter<> *iv;
+ uint8_t pos;
+ for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
+ iv = mSys->getInverterByPos(i);
+ if(NULL != iv) {
+ record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
+ JsonObject obj2 = invArr.createNestedObject();
+ obj2[F("name")] = String(iv->config->name);
+ obj2[F("channels")] = iv->channels;
+ obj2[F("power_limit_read")] = ah::round3(iv->actPowerLimit);
+ obj2[F("last_alarm")] = String(iv->lastAlarmMsg);
+ obj2[F("ts_last_success")] = rec->ts;
+
+ JsonArray ch = obj2.createNestedArray("ch");
+ JsonArray ch0 = ch.createNestedArray();
+ obj2[F("ch_names")][0] = "AC";
+ for (uint8_t fld = 0; fld < sizeof(list); fld++) {
+ pos = (iv->getPosByChFld(CH0, list[fld], rec));
+ ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
+ obj[F("ch0_fld_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail;
+ obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
+ }
+
+ for(uint8_t j = 1; j <= iv->channels; j ++) {
+ obj2[F("ch_names")][j] = String(iv->config->chName[j-1]);
+ JsonArray cur = ch.createNestedArray();
+ for (uint8_t k = 0; k < 6; k++) {
+ switch(k) {
+ default: pos = (iv->getPosByChFld(j, FLD_UDC, rec)); break;
+ case 1: pos = (iv->getPosByChFld(j, FLD_IDC, rec)); break;
+ case 2: pos = (iv->getPosByChFld(j, FLD_PDC, rec)); break;
+ case 3: pos = (iv->getPosByChFld(j, FLD_YD, rec)); break;
+ case 4: pos = (iv->getPosByChFld(j, FLD_YT, rec)); break;
+ case 5: pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break;
+ }
+ cur[k] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
+ if(1 == j) {
+ obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail;
+ obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ void getRecord(JsonObject obj, record_t<> *rec) {
+ JsonArray invArr = obj.createNestedArray(F("inverter"));
+
+ Inverter<> *iv;
+ uint8_t pos;
+ for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
+ iv = mSys->getInverterByPos(i);
+ if(NULL != iv) {
+ JsonArray obj2 = invArr.createNestedArray();
+ for(uint8_t j = 0; j < rec->length; j++) {
+ byteAssign_t *assign = iv->getByteAssign(j, rec);
+ pos = (iv->getPosByChFld(assign->ch, assign->fieldId, rec));
+ obj2[j]["fld"] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
+ obj2[j]["unit"] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail;
+ obj2[j]["val"] = (0xff != pos) ? String(iv->getValue(pos, rec)) : notAvail;
+ }
+ }
+ }
+ }
+
+ bool setCtrl(JsonObject jsonIn, JsonObject jsonOut) {
+ Inverter<> *iv = mSys->getInverterByPos(jsonIn[F("id")]);
+ if(NULL == iv) {
+ jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as();
+ return false;
+ }
+
+ if(F("power") == jsonIn[F("cmd")]) {
+ iv->devControlCmd = (jsonIn[F("val")] == 1) ? TurnOn : TurnOff;
+ iv->devControlRequest = true;
+ } else if(F("restart") == jsonIn[F("restart")]) {
+ iv->devControlCmd = Restart;
+ iv->devControlRequest = true;
+ }
+ else if(0 == strncmp("limit_", jsonIn[F("cmd")].as(), 6)) {
+ iv->powerLimit[0] = jsonIn["val"];
+ if(F("limit_persistent_relative") == jsonIn[F("cmd")])
+ iv->powerLimit[1] = RelativPersistent;
+ else if(F("limit_persistent_absolute") == jsonIn[F("cmd")])
+ iv->powerLimit[1] = AbsolutPersistent;
+ else if(F("limit_nonpersistent_relative") == jsonIn[F("cmd")])
+ iv->powerLimit[1] = RelativNonPersistent;
+ else if(F("limit_nonpersistent_absolute") == jsonIn[F("cmd")])
+ iv->powerLimit[1] = AbsolutNonPersistent;
+ iv->devControlCmd = ActivePowerContr;
+ iv->devControlRequest = true;
+ }
+ else {
+ jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as() + "'";
+ return false;
+ }
+
+ return true;
+ }
+
+ bool setSetup(JsonObject jsonIn, JsonObject jsonOut) {
+ if(F("scan_wifi") == jsonIn[F("cmd")]) {
+ mApp->scanAvailNetworks();
+ }
+ else if(F("set_time") == jsonIn[F("cmd")])
+ mApp->setTimestamp(jsonIn[F("val")]);
+ else if(F("sync_ntp") == jsonIn[F("cmd")])
+ mApp->setTimestamp(0); // 0: update ntp flag
+ else if(F("serial_utc_offset") == jsonIn[F("cmd")])
+ mTimezoneOffset = jsonIn[F("val")];
+ else if(F("discovery_cfg") == jsonIn[F("cmd")]) {
+ mApp->setMqttDiscoveryFlag(); // for homeassistant
+ }
+ else {
+ jsonOut[F("error")] = F("unknown cmd");
+ return false;
+ }
+
+ return true;
+ }
+
+ IApp *mApp;
+ HMSYSTEM *mSys;
+ AsyncWebServer *mSrv;
+ settings_t *mConfig;
+
+ uint32_t mTimezoneOffset;
+};
+
+#endif /*__WEB_API_H__*/
diff --git a/src/web/html/index.html b/src/web/html/index.html
index c2d25038..22d7650b 100644
--- a/src/web/html/index.html
+++ b/src/web/html/index.html
@@ -39,18 +39,14 @@
WiFi RSSI: dBm
- Statistics:
+ System Infos:
- Every seconds the values are updated