Browse Source

Merge branch 'monodpy3' of https://github.com/gh-fx2/ahoy into gh-fx2-monodpy3

pull/558/head
lumapu 2 years ago
parent
commit
943b2e0405
  1. 4
      .github/workflows/compile_development.yml
  2. 2
      .github/workflows/compile_release.yml
  3. 1
      Getting_Started.md
  4. 261
      User_Manual.md
  5. 31
      scripts/getVersion.py
  6. 51
      src/.vscode/settings.json
  7. 80
      src/CHANGES.md
  8. 275
      src/app.cpp
  9. 182
      src/app.h
  10. 41
      src/appInterface.h
  11. 2
      src/config/config.h
  12. 51
      src/config/settings.h
  13. 8
      src/defines.h
  14. 9
      src/hm/hmInverter.h
  15. 17
      src/hm/hmRadio.h
  16. 2
      src/hm/hmSystem.h
  17. 2
      src/main.cpp
  18. 74
      src/platformio.ini
  19. 307
      src/plugins/MonochromeDisplay/MonochromeDisplay.h
  20. 517
      src/publisher/pubMqtt.h
  21. 37
      src/publisher/pubSerial.h
  22. 58
      src/utils/helper.cpp
  23. 24
      src/utils/helper.h
  24. 108
      src/utils/llist.h
  25. 185
      src/utils/scheduler.h
  26. 6
      src/utils/sun.h
  27. 538
      src/web/RestApi.h
  28. 37
      src/web/html/index.html
  29. 49
      src/web/html/serial.html
  30. 10
      src/web/html/setup.html
  31. 24
      src/web/html/system.html
  32. 6
      src/web/html/update.html
  33. 30
      src/web/html/visualization.html
  34. 726
      src/web/web.cpp
  35. 683
      src/web/web.h
  36. 581
      src/web/webApi.cpp
  37. 82
      src/web/webApi.h
  38. 275
      src/wifi/ahoywifi.cpp
  39. 41
      src/wifi/ahoywifi.h

4
.github/workflows/compile_development.yml

@ -47,7 +47,7 @@ jobs:
run: python convert.py run: python convert.py
- name: Run PlatformIO - 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 - name: Rename Binary files
id: rename-binary-files id: rename-binary-files
@ -68,7 +68,7 @@ jobs:
- name: Create Artifact - name: Create Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: ${{ steps.rename-binary-files.outputs.name }}_dev_build name: ahoydtu_dev
path: | path: |
src/firmware/* src/firmware/*
src/User_Manual.md src/User_Manual.md

2
.github/workflows/compile_release.yml

@ -51,7 +51,7 @@ jobs:
run: python convert.py run: python convert.py
- name: Run PlatformIO - 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 - name: Rename Binary files
id: rename-binary-files id: rename-binary-files

1
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 | | `RF24` | 1.4.5 | GPL-2.0 |
| `PubSubClient` | 2.8 | MIT | | `PubSubClient` | 2.8 | MIT |
| `ArduinoJson` | 6.19.4 | MIT | | `ArduinoJson` | 6.19.4 | MIT |
| `ESP Async WebServer` | 4.3.0 | ? |
## Contact ## Contact

261
User_Manual.md

@ -1,4 +1,4 @@
# User Manual Ahoy DTU (on ESP8266) # User Manual AhoyDTU (on ESP8266)
Version #{VERSION}# Version #{VERSION}#
## Introduction ## Introduction
See the repository [README.md](Getting_Started.md) 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. Set at least the serial number and a name for each inverter, check "reboot after save" and click the "Save" button.
## MQTT Output ## MQTT Output
The ahoy dtu will publish on the following topics The AhoyDTU will publish on the following topics
`<CHOOSEN_TOPIC_FROM_SETUP>/<INVERTER_NAME_FROM_SETUP>/ch0/#` `<TOPIC>/<INVERTER_NAME_FROM_SETUP>/ch0/#`
| Topic | Example Value | Remarks | | 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| |PowerLimit | 80.000|actual set point for power limit control AC active power in percent|
|LastAlarmCode | 1.000| Last Alarm Code eg. "inverter start"| |LastAlarmCode | 1.000| Last Alarm Code eg. "inverter start"|
`<CHOOSEN_TOPIC_FROM_SETUP>/<INVERTER_NAME_FROM_SETUP>/ch<CHANNEL_NUMBER>/#` `<TOPIC>/<INVERTER_NAME_FROM_SETUP>/ch<CHANNEL_NUMBER>/#`
`<CHANNEL_NUMBER>` is in the range 1 to 4 depending on the inverter type `<CHANNEL_NUMBER>` 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) | |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 | |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. 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. 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. 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%. 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. 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 ## Control via MQTT
The ahoy-dtu subscribes on the topic `<CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/#` if the mqtt broker is set-up correctly. The default topic is `inverter/devcontrol/#`.
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 | The AhoyDTU subscribes on three topics `<TOPIC>/ctrl/#`, `<TOPIC>/setup` and `<TOPIC>/status`.
| --------------------------------------------------------------- | ----------- | -------------------------------------------- | -------------- |
| <CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/<INVERTER_ID>/11 OR <CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/<INVERTER_ID>/11/0 | [0..65535] | Watt | not persistent | 👆 `<TOPIC>` can be set on setup page, default is `inverter`.
| <CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/<INVERTER_ID>/11/256 | [0..65535] | Watt | persistent |
| <CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/<INVERTER_ID>/11/1 | [2..100] | % | not persistent |
| <CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/<INVERTER_ID>/11/257 | [2..100] | % | persistent |
👆 `<INVERTER_ID>` is the number of the specific inverter in the setup page. 👆 `<INVERTER_ID>` is the number of the specific inverter in the setup page.
* First inverter --> `<INVERTER_ID>` = 0
* Second inverter --> `<INVERTER_ID>` = 1 ### Inverter Power (On / Off)
* ... ```mqtt
<TOPIC>/ctrl/power/<INVERTER_ID>
### Developer Information MQTT Interface
`<CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/<INVERTER_ID>/<DevControlCmdType>/<DATA2>`
The implementation allows to set any of the available `<DevControlCmdType>` 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;
``` ```
The MQTT payload will be set on first to bytes and `<DATA2>`, which is taken from the topic path will be set on the second two bytes if the corresponding DevControlCmdType supports 4 byte data. with payload `1` = `ON` and `0` = `OFF`
See here the actual implementation to set the send buffer bytes.
```C Example:
void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data) { ```mqtt
sendCmdPacket(invId, TX_REQ_DEVCONTROL, ALL_FRAMES, false); inverter/ctrl/power/0 1
int cnt = 0; ```
// cmd --> 0x0b => Type_ActivePowerContr, 0 on, 1 off, 2 restart, 12 reactive power, 13 power factor
mTxBuf[10] = cmd; ### Inverter restart
mTxBuf[10 + (++cnt)] = 0x00; ```mqtt
if (cmd >= ActivePowerContr && cmd <= PFSet){ <TOPIC>/ctrl/restart/<INVERTER_ID>
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 Example:
mTxBuf[10 + (++cnt)] = ((data[1] ) >> 8) & 0xff; // high byte from MQTT topic value <DATA2> ```mqtt
mTxBuf[10 + (++cnt)] = ((data[1] ) ) & 0xff; // low byte from MQTT topic value <DATA2> inverter/ctrl/restart/0
} ```
// crc control data
uint16_t crc = Hoymiles::crc16(&mTxBuf[10], cnt+1); ### Power Limit relative persistent [%]
mTxBuf[10 + (++cnt)] = (crc >> 8) & 0xff;
mTxBuf[10 + (++cnt)] = (crc ) & 0xff; ```mqtt
// crc over all <TOPIC>/ctrl/limit_persistent_relative/<INVERTER_ID>
cnt +=1; ```
mTxBuf[10 + cnt] = Hoymiles::crc8(mTxBuf, 10 + cnt); with a payload `[2 .. 100]`
sendPacket(invId, mTxBuf, 10 + (++cnt), true); 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
<TOPIC>/ctrl/limit_persistent_absolute/<INVERTER_ID>
```
with a payload `[0 .. 65535]`
Example:
```mqtt
inverter/ctrl/limit_persistent_absolute/0 600
```
### Power Limit relative non persistent [%]
```mqtt
<TOPIC>/ctrl/limit_nonpersistent_relative/<INVERTER_ID>
```
with a payload `[2 .. 100]`
Example:
```mqtt
inverter/ctrl/limit_nonpersistent_relative/0 70
```
### Power Limit absolute non persistent [Watts]
```mqtt
<TOPIC>/ctrl/limit_nonpersistent_absolute/<INVERTER_ID>
```
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.
👆 `<INVERTER_ID>` 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 ```json
{ {
"inverter":<INVERTER_ID>, "id": <INVERTER_ID>,
"tx_request": <TX_REQUEST_BYTE>, "cmd": "power",
"cmd": <SUB_CMD_BYTE>, "val": <VALUE>
"payload": <PAYLOAD_INTEGER_TWO_BYTES>,
"payload2": <PAYLOAD_INTEGER_TWO_BYTES>
} }
``` ```
With the following value ranges The `<VALUE>` should be set to `1` = `ON` and `0` = `OFF`
| Value | range | note | ### Inverter restart
| --------------------------- | ----------- | ------------------------------- |
| <TX_REQUEST_BYTE> | 81 or 21 | integer uint8, (0x15 or 0x51) |
| <SUB_CMD_BYTE> | [0...255] | integer uint8, subcmds eg. 0x0b |
| <PAYLOAD_INTEGER_TWO_BYTES> | [0...65535] | uint16 |
| <INVERTER_ID> | [0...3] | integer uint8 |
Example to set the active power limit non persistent to 10%
```json ```json
{ {
"inverter":0, "id": <INVERTER_ID>,
"tx_request": 81, "cmd": "restart"
"cmd": 11,
"payload": 10,
"payload2": 1
} }
``` ```
Example to set the active power limit persistent to 600Watt
### Power Limit relative persistent [%]
```json ```json
{ {
"inverter":0, "id": <INVERTER_ID>,
"tx_request": 81, "cmd": "limit_persistent_relative",
"cmd": 11, "val": <VALUE>
"payload": 600,
"payload2": 256
} }
``` ```
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 ```json
{ {
"inverter":0, "id": <INVERTER_ID>,
"tx_request": 21, "cmd": "limit_persistent_absolute",
"cmd": 17, "val": <VALUE>
"payload": 5,
"payload2": 0
} }
``` ```
The `VALUE` represents watts in a range of `[0 .. 65535]`
## Zero Export Control
* You can use the mqtt topic `<CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/<INVERTER_ID>/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 `<CHOOSEN_TOPIC_FROM_SETUP>/<INVERTER_NAME_FROM_SETUP>/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 `<CHOOSEN_TOPIC_FROM_SETUP>/<INVERTER_NAME_FROM_SETUP>/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": <INVERTER_ID>,
"cmd": "limit_nonpersistent_relative",
"val": <VALUE>
}
```
The `VALUE` represents a percent number in a range of `[2 .. 100]`
In case of issues please report:
1. Version of firmware ### Power Limit absolute non persistent [Watts]
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" ```json
4. The setting means payload, relative, absolute, persistent, not persistent (see tables above) {
"id": <INVERTER_ID>,
"cmd": "limit_nonpersistent_absolute",
"val": <VALUE>
}
```
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 ### Developer Information REST API (obsolete)
```C 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:
typedef enum { ```json
AbsolutNonPersistent = 0x0000, // 0 {
RelativNonPersistent = 0x0001, // 1 "inverter":0,
AbsolutPersistent = 0x0100, // 256 "tx_request": 21,
RelativPersistent = 0x0101 // 257 "cmd": 17,
} PowerLimitControlType; "payload": 5,
"payload2": 0
}
``` ```
## Zero Export Control (needs rework)
* You can use the mqtt topic `<TOPIC>/devcontrol/<INVERTER_ID>/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 `<TOPIC>/<INVERTER_NAME_FROM_SETUP>/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 `<TOPIC>/<INVERTER_NAME_FROM_SETUP>/ch0/P_AC` 👆 This value is ALWAYS in Watt
## Firmware Version collection ## Firmware Version collection
Gather user inverter information here to understand what differs between some inverters. 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 | | | | Name | Inverter Typ | Bootloader V. | FWVersion | FWBuild [YYYY] | FWBuild [MM-DD] | HWPartId | | |
| ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------- | --------- | | ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------- | --------- |

31
scripts/getVersion.py

@ -1,4 +1,6 @@
import os import os
import shutil
import gzip
from datetime import date from datetime import date
def genOtaBin(path): def genOtaBin(path):
@ -24,6 +26,11 @@ def genOtaBin(path):
with open(path + "ota.bin", "wb") as f: with open(path + "ota.bin", "wb") as f:
f.write(bytearray(arr)) 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): def readVersion(path, infile):
f = open(path + infile, "r") f = open(path + infile, "r")
@ -44,21 +51,43 @@ def readVersion(path, infile):
os.mkdir(path + "firmware/") os.mkdir(path + "firmware/")
sha = os.getenv("SHA",default="sha") sha = os.getenv("SHA",default="sha")
versionout = version[:-1] + "_esp8266_" + sha + ".bin" versionout = version[:-1] + "_esp8266_" + sha + ".bin"
src = path + ".pio/build/esp8266-release/firmware.bin" src = path + ".pio/build/esp8266-release/firmware.bin"
dst = path + "firmware/" + versionout dst = path + "firmware/" + versionout
os.rename(src, dst) os.rename(src, dst)
versionout = version[:-1] + "_esp8266_1m_" + sha + ".bin" 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] + "_esp8285_" + sha + ".bin"
src = path + ".pio/build/esp8285-release/firmware.bin" src = path + ".pio/build/esp8285-release/firmware.bin"
dst = path + "firmware/" + versionout dst = path + "firmware/" + versionout
os.rename(src, dst) os.rename(src, dst)
gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_esp32_" + sha + ".bin" versionout = version[:-1] + "_esp32_" + sha + ".bin"
src = path + ".pio/build/esp32-wroom32-release/firmware.bin" src = path + ".pio/build/esp32-wroom32-release/firmware.bin"
dst = path + "firmware/" + versionout dst = path + "firmware/" + versionout
os.rename(src, dst) 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 # other ESP32 bin files
src = path + ".pio/build/esp32-wroom32-release/" src = path + ".pio/build/esp32-wroom32-release/"
dst = path + "firmware/" dst = path + "firmware/"

51
src/.vscode/settings.json

@ -24,6 +24,55 @@
"typeinfo": "cpp", "typeinfo": "cpp",
"string": "cpp", "string": "cpp",
"istream": "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"
}, },
} }

80
src/CHANGES.md

@ -1,13 +1,71 @@
# Changelog # Changelog
* fix browser sync NTP button ## 0.5.52
* added login feature (protect web ui) * improved ahoyWifi class
* added static IP option * added interface class for app
* improved initial boot - don't connect to `YOUR_WIFI_SSID` any more, directly boot into AP mode * refactored web and webApi -> RestApi
* added status LED support * fix calcSunrise was not called every day
* improved MQTT handling (boot, periodic updates, no zero values any more) * added MQTT RX counter to index.html
* replaced deprecated workflow functions * all values are displayed on /live even if they are 0
* refactored code to make it more clearly * added MQTT <TOPIC>/status to show status over all inverters
* 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) ## 0.5.51
* added a lot of system infos to `System` page for support * 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

275
src/app.cpp

@ -13,47 +13,86 @@
#include "utils/sun.h" #include "utils/sun.h"
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::setup(uint32_t timeout) { app::app() : ah::Scheduler() {}
//-----------------------------------------------------------------------------
void app::setup() {
Serial.begin(115200); Serial.begin(115200);
while (!Serial) while (!Serial)
yield(); yield();
addListener(EVERY_SEC, std::bind(&app::uptimeTick, this)); ah::Scheduler::setup();
addListener(EVERY_MIN, std::bind(&app::minuteTick, this));
addListener(EVERY_12H, std::bind(&app::ntpUpdateTick, this));
resetSystem(); resetSystem();
mSettings.setup(); mSettings.setup();
mSettings.getPtr(mConfig); mSettings.getPtr(mConfig);
DPRINTLN(DBG_INFO, F("Settings valid: ") + String((mSettings.getValid()) ? F("true") : F("false"))); 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 = new HmSystemType();
mSys->enableDebug(); mSys->enableDebug();
mSys->setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs); mSys->setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs);
mSys->addInverters(&mConfig->inst);
mPayload.setup(mSys); #if !defined(AP_ONLY)
mPayload.enableSerialDebug(mConfig->serial.debug); mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, mSys, &mTimestamp, &mSunrise, &mSunset);
#if !defined(AP_ONLY) #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) { 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)); mPayload.addListener(std::bind(&PubMqttType::payloadEventListener, &mMqtt, std::placeholders::_1));
addListener(EVERY_SEC, std::bind(&PubMqttType::tickerSecond, &mMqtt)); everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt));
addListener(EVERY_MIN, std::bind(&PubMqttType::tickerMinute, &mMqtt)); everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt));
addListener(EVERY_HR, std::bind(&PubMqttType::tickerHour, &mMqtt)); mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
} }
#endif #endif
setupLed(); setupLed();
mWeb = new web(this, mConfig, &mStat, mVersion); mWeb.setup(this, mSys, mConfig);
mWeb->setup(); mWeb.setProtection(strlen(mConfig->sys.adminPwd) != 0);
mWeb->setProtection(strlen(mConfig->sys.adminPwd) != 0); everySec(std::bind(&WebType::tickSecond, &mWeb));
addListener(EVERY_SEC, std::bind(&web::tickSecond, mWeb));
mApi.setup(this, mSys, mWeb.getWebSrvPtr(), mConfig);
//addListener(EVERY_MIN, std::bind(&PubSerialType::tickerMinute, &mPubSerial)); /*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(); ah::Scheduler::loop();
mWeb->loop(); #if !defined(AP_ONLY)
mWifi.loop();
#endif
mWeb.loop();
if (mFlagSendDiscoveryConfig) { if (mFlagSendDiscoveryConfig) {
mFlagSendDiscoveryConfig = false; mFlagSendDiscoveryConfig = false;
mMqtt.sendMqttDiscoveryConfig(mConfig->mqtt.topic); mMqtt.sendDiscoveryConfig();
} }
mSys->Radio.loop(); mSys->Radio.loop();
@ -99,87 +142,103 @@ void app::loop(void) {
} }
mMqtt.loop(); 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 void app::tickNtpUpdate(void) {
if (!mLatestSunTimestamp) { // first call: calculate time zone from longitude to refresh at local midnight uint32_t nxtTrig = 5; // default: check again in 5 sec
mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600; if (mWifi.getNtpTime())
} nxtTrig = 43200; // check again in 12 h
ah::calculateSunriseSunset(mUtcTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset); once(std::bind(&app::tickNtpUpdate, this), nxtTrig);
mLatestSunTimestamp = mUtcTimestamp; }
//-----------------------------------------------------------------------------
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) { if (!mPayload.isComplete(iv)) {
mSendTicker = 0; 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) if (mConfig->serial.debug)
DPRINTLN(DBG_DEBUG, F("Free heap: 0x") + String(ESP.getFreeHeap(), HEX)); DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout"));
if (mConfig->serial.debug) {
if (!mSys->BufCtrl.empty()) { DPRINT(DBG_INFO, F("(#") + String(iv->id) + ") ");
if (mConfig->serial.debug) DPRINTLN(DBG_INFO, F("no Payload received! (retransmits: ") + String(mPayload.getRetransmits(iv)) + ")");
DPRINTLN(DBG_DEBUG, F("recbuf not empty! #") + String(mSys->BufCtrl.getFill()));
} }
}
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<InfoCommand>(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(); 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<InfoCommand>(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(); 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) { void app::resetSystem(void) {
snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
mShouldReboot = false; mShouldReboot = false;
mUptimeSecs = 0;
mUpdateNtp = false; mUpdateNtp = false;
mFlagSendDiscoveryConfig = false; mFlagSendDiscoveryConfig = false;
#ifdef AP_ONLY #ifdef AP_ONLY
mUtcTimestamp = 1; mTimestamp = 1;
#else #else
mUtcTimestamp = 0; mTimestamp = 0;
#endif #endif
mHeapStatCnt = 0; mSunrise = 0;
mSunset = 0;
mSendTicker = 0xffff;
mTicker = 0;
mRxTicker = 0; mRxTicker = 0;
mSendLastIvId = 0; mSendLastIvId = 0;
@ -232,6 +272,11 @@ void app::resetSystem(void) {
memset(&mStat, 0, sizeof(statistics_t)); memset(&mStat, 0, sizeof(statistics_t));
} }
//-----------------------------------------------------------------------------
void app::mqttSubRxCb(JsonObject obj) {
mApi.ctrlRequest(obj);
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::setupLed(void) { void app::setupLed(void) {
/** LED connection diagram /** LED connection diagram
@ -255,7 +300,7 @@ void app::updateLed(void) {
Inverter<> *iv = mSys->getInverterByPos(0); Inverter<> *iv = mSys->getInverterByPos(0);
if (NULL != iv) { if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if(iv->isProducing(mUtcTimestamp, rec)) if(iv->isProducing(mTimestamp, rec))
digitalWrite(mConfig->led.led0, LOW); // LED on digitalWrite(mConfig->led.led0, LOW); // LED on
else else
digitalWrite(mConfig->led.led0, HIGH); // LED off digitalWrite(mConfig->led.led0, HIGH); // LED off

182
src/app.h

@ -6,12 +6,15 @@
#ifndef __APP_H__ #ifndef __APP_H__
#define __APP_H__ #define __APP_H__
#include "utils/dbg.h" #include "utils/dbg.h"
#include <Arduino.h> #include <Arduino.h>
#include <RF24.h> #include <RF24.h>
#include <RF24_config.h> #include <RF24_config.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "appInterface.h"
#include "config/settings.h" #include "config/settings.h"
#include "defines.h" #include "defines.h"
#include "utils/crc.h" #include "utils/crc.h"
@ -23,10 +26,12 @@
#include "hm/payload.h" #include "hm/payload.h"
#include "wifi/ahoywifi.h" #include "wifi/ahoywifi.h"
#include "web/web.h" #include "web/web.h"
#include "web/RestApi.h"
#include "publisher/pubMqtt.h" #include "publisher/pubMqtt.h"
#include "publisher/pubSerial.h" #include "publisher/pubSerial.h"
// convert degrees and radians for sun calculation // convert degrees and radians for sun calculation
#define SIN(x) (sin(radians(x))) #define SIN(x) (sin(radians(x)))
#define COS(x) (cos(radians(x))) #define COS(x) (cos(radians(x)))
@ -35,79 +40,114 @@
typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType; typedef HmSystem<MAX_NUM_INVERTERS> HmSystemType;
typedef Payload<HmSystemType> PayloadType; typedef Payload<HmSystemType> PayloadType;
typedef Web<HmSystemType> WebType;
typedef RestApi<HmSystemType> RestApiType;
typedef PubMqtt<HmSystemType> PubMqttType; typedef PubMqtt<HmSystemType> PubMqttType;
typedef PubSerial<HmSystemType> PubSerialType; typedef PubSerial<HmSystemType> PubSerialType;
class ahoywifi; // PLUGINS
class web; #if defined(ENA_NOKIA) || defined(ENA_SSD1306)
#include "plugins/MonochromeDisplay/MonochromeDisplay.h"
typedef MonochromeDisplay<HmSystemType> MonoDisplayType;
#endif
class app : public ah::Scheduler { class app : public IApp, public ah::Scheduler {
public: public:
app() : ah::Scheduler() {} app();
~app() {} ~app() {}
void setup(uint32_t timeout); void setup(void);
void loop(void); void loop(void);
void handleIntr(void); void handleIntr(void);
void cbMqtt(char* topic, byte* payload, unsigned int length); void cbMqtt(char* topic, byte* payload, unsigned int length);
void saveValues(void); void saveValues(void);
void resetPayload(Inverter<>* iv); void resetPayload(Inverter<>* iv);
bool getWifiApActive(void); bool getWifiApActive(void);
void scanAvailNetworks(void);
void getAvailNetworks(JsonObject obj);
void saveSettings(void) { uint32_t getUptime() {
mSettings.saveSettings(); return Scheduler::getUptime();
}
uint32_t getTimestamp() {
return Scheduler::getTimestamp();
}
bool saveSettings() {
return mSettings.saveSettings();
} }
bool eraseSettings(bool eraseWifi = false) { bool eraseSettings(bool eraseWifi = false) {
return mSettings.eraseSettings(eraseWifi); return mSettings.eraseSettings(eraseWifi);
} }
uint8_t getIrqPin(void) { statistics_t *getStatistics() {
return mConfig->nrf.pinIrq; return &mStat;
} }
uint64_t Serial2u64(const char *val) { void scanAvailNetworks() {
char tmp[3]; mWifi.scanAvailNetworks();
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;
} }
String getDateTimeStr(time_t t) { void getAvailNetworks(JsonObject obj) {
char str[20]; mWifi.getAvailNetworks(obj);
if(0 == t) }
sprintf(str, "n/a");
else void setRebootFlag() {
sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d", year(t), month(t), day(t), hour(t), minute(t), second(t)); mShouldReboot = true;
return String(str); }
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) { String getTimeStr(uint32_t offset = 0) {
char str[10]; char str[10];
if(0 == mUtcTimestamp) if(0 == mTimestamp)
sprintf(str, "n/a"); sprintf(str, "n/a");
else 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); return String(str);
} }
inline uint32_t getUptime(void) { uint32_t getTimezoneOffset() {
return mUptimeSecs; return mApi.getTimezoneOffset();
}
inline uint32_t getTimestamp(void) {
return mUtcTimestamp;
} }
void setTimestamp(uint32_t newTime) { void setTimestamp(uint32_t newTime) {
@ -115,66 +155,36 @@ class app : public ah::Scheduler {
if(0 == newTime) if(0 == newTime)
mUpdateNtp = true; mUpdateNtp = true;
else else
mUtcTimestamp = newTime; Scheduler::setTimestamp(newTime);
}
inline uint32_t getSunrise(void) {
return mSunrise;
} }
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; HmSystemType *mSys;
bool mShouldReboot; bool mShouldReboot;
bool mFlagSendDiscoveryConfig;
private: private:
void resetSystem(void); void resetSystem(void);
void setupMqtt(void); void mqttSubRxCb(JsonObject obj);
void setupLed(void); void setupLed(void);
void updateLed(void); void updateLed(void);
void uptimeTick(void) { void tickSecond(void) {
mUptimeSecs++;
if (0 != mUtcTimestamp)
mUtcTimestamp++;
if (mShouldReboot) { if (mShouldReboot) {
DPRINTLN(DBG_INFO, F("Rebooting...")); DPRINTLN(DBG_INFO, F("Rebooting..."));
ESP.restart(); ESP.restart();
} }
if (mUpdateNtp) { if (mUpdateNtp) {
mUpdateNtp = false; mUpdateNtp = false;
mUtcTimestamp = mWifi->getNtpTime(); mWifi.getNtpTime();
DPRINTLN(DBG_INFO, F("[NTP]: ") + getDateTimeStr(mUtcTimestamp) + F(" UTC"));
} }
} }
void minuteTick(void) { void tickNtpUpdate(void);
if(0 == mUtcTimestamp) {
if(!mWifi->getApActive())
mUpdateNtp = true;
}
}
void ntpUpdateTick(void) { void tickCalcSunrise(void);
if (!mWifi->getApActive()) void tickSend(void);
mUpdateNtp = true;
}
void stats(void) { void stats(void) {
DPRINTLN(DBG_VERBOSE, F("main.h:stats")); DPRINTLN(DBG_VERBOSE, F("main.h:stats"));
@ -196,16 +206,14 @@ class app : public ah::Scheduler {
DPRINTLN(DBG_VERBOSE, F(" - frag: ") + String(frag)); DPRINTLN(DBG_VERBOSE, F(" - frag: ") + String(frag));
} }
uint32_t mUptimeSecs;
uint8_t mHeapStatCnt;
uint32_t mUtcTimestamp;
bool mUpdateNtp; bool mUpdateNtp;
bool mFlagSendDiscoveryConfig;
bool mShowRebootRequest; bool mShowRebootRequest;
ahoywifi *mWifi; ahoywifi mWifi;
web *mWeb; WebType mWeb;
RestApiType mApi;
PayloadType mPayload; PayloadType mPayload;
PubSerialType mPubSerial; PubSerialType mPubSerial;
@ -213,13 +221,11 @@ class app : public ah::Scheduler {
settings mSettings; settings mSettings;
settings_t *mConfig; settings_t *mConfig;
uint16_t mSendTicker;
uint8_t mSendLastIvId; uint8_t mSendLastIvId;
statistics_t mStat; statistics_t mStat;
// timer // timer
uint32_t mTicker;
uint32_t mRxTicker; uint32_t mRxTicker;
// mqtt // mqtt
@ -229,7 +235,11 @@ class app : public ah::Scheduler {
// sun // sun
int32_t mCalculatedTimezoneOffset; int32_t mCalculatedTimezoneOffset;
uint32_t mSunrise, mSunset; uint32_t mSunrise, mSunset;
uint32_t mLatestSunTimestamp;
// plugins
#if defined(ENA_NOKIA) || defined(ENA_SSD1306)
MonoDisplayType mMonoDisplay;
#endif
}; };
#endif /*__APP_H__*/ #endif /*__APP_H__*/

41
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__*/

2
src/config/config.h

@ -26,7 +26,7 @@
//#define AP_ONLY //#define AP_ONLY
// timeout for automatic logoff (20 minutes) // timeout for automatic logoff (20 minutes)
#define LOGOUT_TIMEOUT (20 * 60 * 60) #define LOGOUT_TIMEOUT (20 * 60)
//------------------------------------- //-------------------------------------
// CONFIGURATION - COMPILE TIME // CONFIGURATION - COMPILE TIME

51
src/config/settings.h

@ -10,6 +10,7 @@
#include <LittleFS.h> #include <LittleFS.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "../utils/dbg.h" #include "../utils/dbg.h"
#include "../utils/helper.h"
#include "../defines.h" #include "../defines.h"
/** /**
@ -97,6 +98,7 @@ typedef struct {
cfgMqtt_t mqtt; cfgMqtt_t mqtt;
cfgLed_t led; cfgLed_t led;
cfgInst_t inst; cfgInst_t inst;
bool valid;
} settings_t; } settings_t;
class settings { class settings {
@ -106,7 +108,7 @@ class settings {
void setup() { void setup() {
DPRINTLN(DBG_INFO, F("Initializing FS ..")); DPRINTLN(DBG_INFO, F("Initializing FS .."));
mValid = false; mCfg.valid = false;
#if !defined(ESP32) #if !defined(ESP32)
LittleFSConfig cfg; LittleFSConfig cfg;
cfg.setAutoFormat(false); cfg.setAutoFormat(false);
@ -144,7 +146,7 @@ class settings {
} }
bool getValid(void) { bool getValid(void) {
return mValid; return mCfg.valid;
} }
void getInfo(uint32_t *used, uint32_t *size) { void getInfo(uint32_t *used, uint32_t *size) {
@ -172,7 +174,7 @@ class settings {
DynamicJsonDocument root(4096); DynamicJsonDocument root(4096);
DeserializationError err = deserializeJson(root, fp); DeserializationError err = deserializeJson(root, fp);
if(!err) { if(!err) {
mValid = true; mCfg.valid = true;
jsonWifi(root["wifi"]); jsonWifi(root["wifi"]);
jsonNrf(root["nrf"]); jsonNrf(root["nrf"]);
jsonNtp(root["ntp"]); jsonNtp(root["ntp"]);
@ -223,28 +225,9 @@ class settings {
return saveSettings(); 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: private:
void loadDefaults(bool wifi = true) { void loadDefaults(bool wifi = true) {
DPRINTLN(DBG_INFO, F("loadDefaults")); DPRINTLN(DBG_VERBOSE, F("loadDefaults"));
if(wifi) { if(wifi) {
snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID); snprintf(mCfg.sys.stationSsid, SSID_LEN, FB_WIFI_SSID);
snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD); snprintf(mCfg.sys.stationPwd, PWD_LEN, FB_WIFI_PWD);
@ -288,25 +271,26 @@ class settings {
void jsonWifi(JsonObject obj, bool set = false) { void jsonWifi(JsonObject obj, bool set = false) {
if(set) { if(set) {
char buf[16];
obj[F("ssid")] = mCfg.sys.stationSsid; obj[F("ssid")] = mCfg.sys.stationSsid;
obj[F("pwd")] = mCfg.sys.stationPwd; obj[F("pwd")] = mCfg.sys.stationPwd;
obj[F("dev")] = mCfg.sys.deviceName; obj[F("dev")] = mCfg.sys.deviceName;
obj[F("adm")] = mCfg.sys.adminPwd; obj[F("adm")] = mCfg.sys.adminPwd;
obj[F("ip")] = ip2Str(mCfg.sys.ip.ip); ah::ip2Char(mCfg.sys.ip.ip, buf); obj[F("ip")] = String(buf);
obj[F("mask")] = ip2Str(mCfg.sys.ip.mask); ah::ip2Char(mCfg.sys.ip.mask, buf); obj[F("mask")] = String(buf);
obj[F("dns1")] = ip2Str(mCfg.sys.ip.dns1); ah::ip2Char(mCfg.sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
obj[F("dns2")] = ip2Str(mCfg.sys.ip.dns2); ah::ip2Char(mCfg.sys.ip.dns2, buf); obj[F("dns2")] = String(buf);
obj[F("gtwy")] = ip2Str(mCfg.sys.ip.gateway); ah::ip2Char(mCfg.sys.ip.gateway, buf); obj[F("gtwy")] = String(buf);
} else { } else {
snprintf(mCfg.sys.stationSsid, SSID_LEN, "%s", obj[F("ssid")].as<const char*>()); snprintf(mCfg.sys.stationSsid, SSID_LEN, "%s", obj[F("ssid")].as<const char*>());
snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as<const char*>()); snprintf(mCfg.sys.stationPwd, PWD_LEN, "%s", obj[F("pwd")].as<const char*>());
snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as<const char*>()); snprintf(mCfg.sys.deviceName, DEVNAME_LEN, "%s", obj[F("dev")].as<const char*>());
snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as<const char*>()); snprintf(mCfg.sys.adminPwd, PWD_LEN, "%s", obj[F("adm")].as<const char*>());
ip2Arr(mCfg.sys.ip.ip, obj[F("ip")]); ah::ip2Arr(mCfg.sys.ip.ip, obj[F("ip")].as<const char*>());
ip2Arr(mCfg.sys.ip.mask, obj[F("mask")]); ah::ip2Arr(mCfg.sys.ip.mask, obj[F("mask")].as<const char*>());
ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")]); ah::ip2Arr(mCfg.sys.ip.dns1, obj[F("dns1")].as<const char*>());
ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")]); ah::ip2Arr(mCfg.sys.ip.dns2, obj[F("dns2")].as<const char*>());
ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")]); ah::ip2Arr(mCfg.sys.ip.gateway, obj[F("gtwy")].as<const char*>());
} }
} }
@ -426,7 +410,6 @@ class settings {
} }
settings_t mCfg; settings_t mCfg;
bool mValid;
}; };
#endif /*__SETTINGS_H__*/ #endif /*__SETTINGS_H__*/

8
src/defines.h

@ -13,7 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 5 #define VERSION_MINOR 5
#define VERSION_PATCH 41 #define VERSION_PATCH 52
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {
@ -85,9 +85,9 @@ union serial_u {
#define DEVNAME_LEN 16 #define DEVNAME_LEN 16
#define NTP_ADDR_LEN 32 // DNS Name #define NTP_ADDR_LEN 32 // DNS Name
#define MQTT_ADDR_LEN 32 // DNS Name #define MQTT_ADDR_LEN 64 // DNS Name
#define MQTT_USER_LEN 16 #define MQTT_USER_LEN 64
#define MQTT_PWD_LEN 32 #define MQTT_PWD_LEN 64
#define MQTT_TOPIC_LEN 64 #define MQTT_TOPIC_LEN 64
#define MQTT_MAX_PACKET_SIZE 384 #define MQTT_MAX_PACKET_SIZE 384

9
src/hm/hmInverter.h

@ -160,17 +160,12 @@ class Inverter {
} }
uint8_t getQueuedCmd() { uint8_t getQueuedCmd() {
if (_commandQueue.empty()){ if (_commandQueue.empty()) {
// Fill with default commands
enqueCommand<InfoCommand>(RealTimeRunData_Debug);
if (fwVersion == 0) if (fwVersion == 0)
{ // info needed maybe after "one night" (=> DC>0 to DC=0 and to DC>0) or reboot
enqueCommand<InfoCommand>(InverterDevInform_All); enqueCommand<InfoCommand>(InverterDevInform_All);
} enqueCommand<InfoCommand>(RealTimeRunData_Debug);
if (actPowerLimit == 0xffff) if (actPowerLimit == 0xffff)
{ // info needed maybe after "one nigth" (=> DC>0 to DC=0 and to DC>0) or reboot
enqueCommand<InfoCommand>(SystemConfigPara); enqueCommand<InfoCommand>(SystemConfigPara);
}
} }
return _commandQueue.front().get()->getCmd(); return _commandQueue.front().get()->getCmd();
} }

17
src/hm/hmRadio.h

@ -139,14 +139,15 @@ class HmRadio {
mNrf24.setPALevel(ampPwr & 0x03); mNrf24.setPALevel(ampPwr & 0x03);
mNrf24.startListening(); mNrf24.startListening();
DPRINTLN(DBG_INFO, F("Radio Config:"));
mNrf24.printPrettyDetails();
mTxCh = setDefaultChannels(); mTxCh = setDefaultChannels();
if(!mNrf24.isChipConnected()) { if(mNrf24.isChipConnected()) {
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring")); 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) { void loop(void) {
@ -295,6 +296,14 @@ class HmRadio {
return mNrf24.isChipConnected(); return mNrf24.isChipConnected();
} }
uint8_t getDataRate(void) {
return mNrf24.getDataRate();
}
bool isPVariant(void) {
return mNrf24.isPVariant();
}
uint32_t mSendCnt; uint32_t mSendCnt;

2
src/hm/hmSystem.h

@ -70,7 +70,7 @@ class HmSystem {
break; break;
} }
} }
else else if(p->config->serial.u64 != 0ULL)
DPRINTLN(DBG_ERROR, F("inverter type can't be detected!")); DPRINTLN(DBG_ERROR, F("inverter type can't be detected!"));
p->init(); p->init();

2
src/main.cpp

@ -18,7 +18,7 @@ IRAM_ATTR void handleIntr(void) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void setup() { void setup() {
myApp.setup(WIFI_TRY_CONNECT_TIME); myApp.setup();
// TODO: move to HmRadio // TODO: move to HmRadio
attachInterrupt(digitalPinToInterrupt(myApp.getIrqPin()), handleIntr, FALLING); attachInterrupt(digitalPinToInterrupt(myApp.getIrqPin()), handleIntr, FALLING);

74
src/platformio.ini

@ -36,7 +36,7 @@ lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24 nrf24/RF24
paulstoffregen/Time paulstoffregen/Time
knolleary/PubSubClient https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson bblanchon/ArduinoJson
;esp8266/DNSServer ;esp8266/DNSServer
;esp8266/EEPROM ;esp8266/EEPROM
@ -89,6 +89,42 @@ monitor_filters =
time ; Add timestamp with milliseconds for each new line 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 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] [env:esp32-wroom32-release]
platform = espressif32 platform = espressif32
board = lolin_d32 board = lolin_d32
@ -109,3 +145,39 @@ monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line 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 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

307
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 <U8g2lib.h>
#define DISP_PROGMEM U8X8_PROGMEM
#else // ENA_SSD1306
/* esp8266 : SCL = 5, SDA = 4 */
/* ewsp32 : SCL = 22, SDA = 21 */
#include <Wire.h>
#include <SSD1306Wire.h>
#define DISP_PROGMEM PROGMEM
#endif
#include <Timezone.h>
#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 HMSYSTEM>
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__*/

517
src/publisher/pubMqtt.h

@ -3,6 +3,8 @@
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// https://bert.emelis.net/espMqttClient/
#ifndef __PUB_MQTT_H__ #ifndef __PUB_MQTT_H__
#define __PUB_MQTT_H__ #define __PUB_MQTT_H__
@ -15,116 +17,161 @@
#include "../utils/dbg.h" #include "../utils/dbg.h"
#include "../utils/ahoyTimer.h" #include "../utils/ahoyTimer.h"
#include "../config/config.h" #include "../config/config.h"
#include <PubSubClient.h> #include <espMqttClient.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "../defines.h" #include "../defines.h"
#include "../hm/hmSystem.h" #include "../hm/hmSystem.h"
#define QOS_0 0
typedef std::function<void(JsonObject)> subscriptionCb;
template<class HMSYSTEM> template<class HMSYSTEM>
class PubMqtt { class PubMqtt {
public: public:
PubMqtt() { PubMqtt() {
mClient = new PubSubClient(mEspClient); mRxCnt = 0;
mAddressSet = false;
mLastReconnect = 0;
mTxCnt = 0; mTxCnt = 0;
mEnReconnect = false;
mSubscriptionCb = NULL;
mIsDay = false;
mIvAvail = true;
} }
~PubMqtt() { } ~PubMqtt() { }
void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *sunrise, uint32_t *sunset) { 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")); mCfgMqtt = cfg_mqtt;
mAddressSet = true;
mCfg_mqtt = cfg_mqtt;
mDevName = devName; mDevName = devName;
mVersion = version;
mSys = sys; mSys = sys;
mUtcTimestamp = utcTs; mUtcTimestamp = utcTs;
mSunrise = sunrise; mSunrise = sunrise;
mSunset = sunset; mSunset = sunset;
mClient->setServer(mCfg_mqtt->broker, mCfg_mqtt->port); snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic);
mClient->setBufferSize(MQTT_MAX_PACKET_SIZE);
setCallback(std::bind(&PubMqtt<HMSYSTEM>::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); if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0))
sendMsg("device", devName); mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd);
sendMsg("uptime", "0"); 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() { void loop() {
if(mAddressSet) #if defined(ESP8266)
mClient->loop(); mClient.loop();
#endif
} }
void tickerSecond() { void tickerSecond() {
if(mAddressSet) {
if(!mClient->connected())
reconnect();
}
sendIvData(); sendIvData();
} }
void tickerMinute() { void tickerMinute() {
if(mAddressSet) { char val[12];
char val[40]; snprintf(val, 12, "%ld", millis() / 1000);
snprintf(val, 40, "%ld", millis() / 1000); publish("uptime", val);
sendMsg("uptime", val); publish("wifi_rssi", String(WiFi.RSSI()).c_str());
publish("free_heap", String(ESP.getFreeHeap()).c_str());
sendMsg("wifi_rssi", String(WiFi.RSSI()).c_str());
if(!mClient.connected()) {
if(mEnReconnect)
mClient.connect();
} }
else if(mIvAvail && !mIsDay)
tickSunset();
} }
void tickerHour() { void tickerSun() {
if(mAddressSet) { publish("sunrise", String(*mSunrise).c_str(), true);
sendMsg("sunrise", String(*mSunrise).c_str()); publish("sunset", String(*mSunset).c_str(), true);
sendMsg("sunset", String(*mSunset).c_str());
}
} }
void setCallback(MQTT_CALLBACK_SIGNATURE) { void tickSunrise() {
mClient->setCallback(callback); mIsDay = true;
} }
void sendMsg(const char *topic, const char *msg) { void tickSunset() {
//DPRINTLN(DBG_VERBOSE, F("mqtt.h:sendMsg")); mIsDay = false;
if(mAddressSet) { char topic[MQTT_TOPIC_LEN + 15], val[32];
char top[66]; Inverter<> *iv;
snprintf(top, 66, "%s/%s", mCfg_mqtt->topic, topic); record_t<> *rec;
sendMsg2(top, msg, false); 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) { void payloadEventListener(uint8_t cmd) {
if(mAddressSet) { if(mClient.connected()) // prevent overflow if MQTT broker is not reachable but set
if(!mClient->connected()) mSendList.push(cmd);
reconnect();
if(mClient->connected())
mClient->publish(topic, msg, retained);
mTxCnt++;
}
} }
bool isConnected(bool doRecon = false) { void publish(const char *subTopic, const char *payload, bool retained = false, bool addTopic = true) {
//DPRINTLN(DBG_VERBOSE, F("mqtt.h:isConnected")); if(!mClient.connected())
if(!mAddressSet) return;
return false;
if(doRecon && !mClient->connected()) char topic[MQTT_TOPIC_LEN + 2];
reconnect(); snprintf(topic, (MQTT_TOPIC_LEN + 2), "%s/%s", mCfgMqtt->topic, subTopic);
return mClient->connected(); if(addTopic)
mClient.publish(topic, QOS_0, retained, payload);
else
mClient.publish(subTopic, QOS_0, retained, payload);
mTxCnt++;
} }
void payloadEventListener(uint8_t cmd) { void subscribe(const char *subTopic) {
mSendList.push(cmd); 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; return mTxCnt;
} }
void sendMqttDiscoveryConfig(const char *topic) { inline uint32_t getRxCnt(void) {
return mRxCnt;
}
void sendDiscoveryConfig(void) {
DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig")); DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig"));
char stateTopic[64], discoveryTopic[64], buffer[512], name[32], uniq_id[32]; char stateTopic[64], discoveryTopic[64], buffer[512], name[32], uniq_id[32];
@ -133,11 +180,11 @@ class PubMqtt {
if (NULL != iv) { if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
DynamicJsonDocument deviceDoc(128); DynamicJsonDocument deviceDoc(128);
deviceDoc["name"] = iv->config->name; deviceDoc[F("name")] = iv->config->name;
deviceDoc["ids"] = String(iv->config->serial.u64, HEX); deviceDoc[F("ids")] = String(iv->config->serial.u64, HEX);
deviceDoc["cu"] = F("http://") + String(WiFi.localIP().toString()); deviceDoc[F("cu")] = F("http://") + String(WiFi.localIP().toString());
deviceDoc["mf"] = "Hoymiles"; deviceDoc[F("mf")] = F("Hoymiles");
deviceDoc["mdl"] = iv->config->name; deviceDoc[F("mdl")] = iv->config->name;
JsonObject deviceObj = deviceDoc.as<JsonObject>(); JsonObject deviceObj = deviceDoc.as<JsonObject>();
DynamicJsonDocument doc(384); DynamicJsonDocument doc(384);
@ -147,26 +194,25 @@ class PubMqtt {
} else { } else {
snprintf(name, 32, "%s CH%d %s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec)); 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(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)); 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 *devCls = getFieldDeviceClass(rec->assign[i].fieldId);
const char *stateCls = getFieldStateClass(rec->assign[i].fieldId); const char *stateCls = getFieldStateClass(rec->assign[i].fieldId);
doc["name"] = name; doc[F("name")] = name;
doc["stat_t"] = stateTopic; doc[F("stat_t")] = String(mCfgMqtt->topic) + "/" + String(iv->config->name) + String(stateTopic);
doc["unit_of_meas"] = iv->getUnit(i, rec); doc[F("unit_of_meas")] = iv->getUnit(i, rec);
doc["uniq_id"] = String(iv->config->serial.u64, HEX) + "_" + uniq_id; doc[F("uniq_id")] = String(iv->config->serial.u64, HEX) + "_" + uniq_id;
doc["dev"] = deviceObj; doc[F("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("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) if (devCls != NULL)
doc["dev_cla"] = devCls; doc[F("dev_cla")] = devCls;
if (stateCls != NULL) if (stateCls != NULL)
doc["stat_cla"] = stateCls; doc[F("stat_cla")] = stateCls;
serializeJson(doc, buffer); serializeJson(doc, buffer);
sendMsg2(discoveryTopic, buffer, true); publish(discoveryTopic, buffer, true, false);
// DPRINTLN(DBG_INFO, F("mqtt sent"));
doc.clear(); doc.clear();
} }
@ -176,40 +222,131 @@ class PubMqtt {
} }
private: private:
void reconnect(void) { #if defined(ESP8266)
DPRINTLN(DBG_DEBUG, F("mqtt.h:reconnect")); void onWifiConnect(const WiFiEventStationModeGotIP& event) {
DPRINTLN(DBG_DEBUG, F("MQTT mClient->_state ") + String(mClient->state()) ); DPRINTLN(DBG_VERBOSE, F("MQTT connecting"));
mClient.connect();
mEnReconnect = true;
}
#ifdef ESP8266 void onWifiDisconnect(const WiFiEventStationModeDisconnected& event) {
DPRINTLN(DBG_DEBUG, F("WIFI mEspClient.status ") + String(mEspClient.status()) ); mEnReconnect = false;
#endif }
#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; subscribe("ctrl/#");
if(!mClient->connected() && (millis() - mLastReconnect) > MQTT_RECONNECT_DELAY ) { subscribe("setup/#");
mLastReconnect = millis(); subscribe("status/#");
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. void onDisconnect(espMqttClientTypes::DisconnectReason reason) {
mClient->setServer(mCfg_mqtt->broker, mCfg_mqtt->port); DPRINT(DBG_INFO, F("MQTT disconnected, reason: "));
mClient->setBufferSize(MQTT_MAX_PACKET_SIZE); switch (reason) {
case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED:
char lwt[MQTT_TOPIC_LEN + 7 ]; // "/uptime" --> + 7 byte DBGPRINTLN(F("TCP disconnect"));
snprintf(lwt, MQTT_TOPIC_LEN + 7, "%s/uptime", mCfg_mqtt->topic); break;
case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
if((strlen(mCfg_mqtt->user) > 0) && (strlen(mCfg_mqtt->pwd) > 0)) DBGPRINTLN(F("wrong protocol version"));
resub = mClient->connect(mDevName, mCfg_mqtt->user, mCfg_mqtt->pwd, lwt, 0, false, "offline"); break;
else case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED:
resub = mClient->connect(mDevName, lwt, 0, false, "offline"); DBGPRINTLN(F("identifier rejected"));
// ein Subscribe ist nur nach einem connect notwendig break;
if(resub) { case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE:
char topic[MQTT_TOPIC_LEN + 13 ]; // "/devcontrol/#" --> + 6 byte DBGPRINTLN(F("broker unavailable"));
// ToDo: "/devcontrol/#" is hardcoded break;
snprintf(topic, MQTT_TOPIC_LEN + 13, "%s/devcontrol/#", mCfg_mqtt->topic); case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS:
DPRINTLN(DBG_INFO, F("subscribe to ") + String(topic)); DBGPRINTLN(F("malformed credentials"));
mClient->subscribe(topic); // subscribe to mTopic + "/devcontrol/#" 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<JsonObject>();
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) { const char *getFieldDeviceClass(uint8_t fieldId) {
@ -234,11 +371,12 @@ class PubMqtt {
if(mSendList.empty()) if(mSendList.empty())
return; return;
isConnected(true); // really needed? See comment from HorstG-57 #176 char topic[7 + MQTT_TOPIC_LEN], val[40];
char topic[32 + MAX_NAME_LENGTH], val[40];
float total[4]; float total[4];
bool sendTotal = false; bool sendTotal = false;
bool totalIncomplete = false; bool totalIncomplete = false;
bool allAvail = true;
bool first = true;
while(!mSendList.empty()) { while(!mSendList.empty()) {
memset(total, 0, sizeof(float) * 4); memset(total, 0, sizeof(float) * 4);
@ -250,40 +388,58 @@ class PubMqtt {
record_t<> *rec = iv->getRecordStruct(mSendList.front()); record_t<> *rec = iv->getRecordStruct(mSendList.front());
if(mSendList.front() == RealTimeRunData_Debug) { if(mSendList.front() == RealTimeRunData_Debug) {
if(first)
mIvAvail = false;
first = false;
// inverter status // inverter status
uint8_t status = MQTT_STATUS_AVAIL_PROD; uint8_t status = MQTT_STATUS_AVAIL_PROD;
if (!iv->isAvailable(*mUtcTimestamp, rec)) { if (!iv->isAvailable(*mUtcTimestamp, rec)) {
status = MQTT_STATUS_NOT_AVAIL_NOT_PROD; status = MQTT_STATUS_NOT_AVAIL_NOT_PROD;
totalIncomplete = true; totalIncomplete = true;
allAvail = false;
} }
else if (!iv->isProducing(*mUtcTimestamp, rec)) { else if (!iv->isProducing(*mUtcTimestamp, rec)) {
mIvAvail = true;
if (MQTT_STATUS_AVAIL_PROD == status) if (MQTT_STATUS_AVAIL_PROD == status)
status = MQTT_STATUS_AVAIL_NOT_PROD; status = MQTT_STATUS_AVAIL_NOT_PROD;
} }
else
mIvAvail = true;
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->config->name); snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->config->name);
snprintf(val, 40, "%s%s%s%s", 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 ", "available and ",
(status == MQTT_STATUS_AVAIL_NOT_PROD) ? "not " : "", (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(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
snprintf(val, 40, "%d", status); 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(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name);
snprintf(val, 40, "%i", iv->getLastTs(rec) * 1000); snprintf(val, 40, "%d", iv->getLastTs(rec));
sendMsg(topic, val); publish(topic, val, true);
} }
// data // data
if(iv->isAvailable(*mUtcTimestamp, rec)) { if(iv->isAvailable(*mUtcTimestamp, rec)) {
for (uint8_t i = 0; i < rec->length; i++) { 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(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)); snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec)));
sendMsg(topic, val); publish(topic, val, retained);
// calculate total values for RealTimeRunData_Debug // calculate total values for RealTimeRunData_Debug
if (mSendList.front() == RealTimeRunData_Debug) { if (mSendList.front() == RealTimeRunData_Debug) {
@ -312,6 +468,9 @@ class PubMqtt {
mSendList.pop(); // remove from list once all inverters were processed 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)) { if ((true == sendTotal) && (false == totalIncomplete)) {
uint8_t fieldId; uint8_t fieldId;
for (uint8_t i = 0; i < 4; i++) { for (uint8_t i = 0; i < 4; i++) {
@ -331,128 +490,34 @@ class PubMqtt {
break; break;
} }
snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]); snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]);
snprintf(val, 40, "%.3f", total[i]); snprintf(val, 40, "%g", ah::round3(total[i]));
sendMsg(topic, val); publish(topic, val, true);
} }
} }
} }
} }
void cbMqtt(char *topic, byte *payload, unsigned int length) { espMqttClient mClient;
// callback handling on subscribed devcontrol topic cfgMqtt_t *mCfgMqtt;
DPRINTLN(DBG_INFO, F("cbMqtt")); #if defined(ESP8266)
// subcribed topics are mTopic + "/devcontrol/#" where # is <inverter_id>/<subcmd in dec> WiFiEventHandler mHWifiCon, mHWifiDiscon;
// eg. mypvsolar/devcontrol/1/11 with payload "400" --> inverter 1 active power limit 400 Watt #endif
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"));
}
uint32_t *mSunrise, *mSunset; uint32_t *mSunrise, *mSunset;
WiFiClient mEspClient;
PubSubClient *mClient;
HMSYSTEM *mSys; HMSYSTEM *mSys;
uint32_t *mUtcTimestamp; uint32_t *mUtcTimestamp;
uint32_t mRxCnt, mTxCnt;
bool mAddressSet;
cfgMqtt_t *mCfg_mqtt;
const char *mDevName;
uint32_t mLastReconnect;
uint32_t mTxCnt;
std::queue<uint8_t> mSendList; std::queue<uint8_t> 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__*/ #endif /*__PUB_MQTT_H__*/

37
src/publisher/pubSerial.h

@ -21,28 +21,24 @@ class PubSerial {
mUtcTimestamp = utcTs; mUtcTimestamp = utcTs;
} }
void tickerMinute() { void tick(void) {
DPRINTLN(DBG_INFO, "tickerMinute"); if (mCfg->serial.showIv) {
if(++mTick >= mCfg->serial.interval) { char topic[32 + MAX_NAME_LENGTH], val[40];
mTick = 0; for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
if (mCfg->serial.showIv) { Inverter<> *iv = mSys->getInverterByPos(id);
char topic[30], val[10]; if (NULL != iv) {
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) { record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
Inverter<> *iv = mSys->getInverterByPos(id); if (iv->isAvailable(*mUtcTimestamp, rec)) {
if (NULL != iv) { DPRINTLN(DBG_INFO, F("Inverter: ") + String(id));
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); for (uint8_t i = 0; i < rec->length; i++) {
if (iv->isAvailable(*mUtcTimestamp, rec)) { if (0.0f != iv->getValue(i, rec)) {
DPRINTLN(DBG_INFO, F("Inverter: ") + String(id)); snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
for (uint8_t i = 0; i < rec->length; i++) { snprintf(val, 40, "%.3f %s", iv->getValue(i, rec), iv->getUnit(i, rec));
if (0.0f != iv->getValue(i, rec)) { DPRINTLN(DBG_INFO, String(topic) + ": " + String(val));
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();
} }
DPRINTLN(DBG_INFO, ""); yield();
} }
DPRINTLN(DBG_INFO, "");
} }
} }
} }
@ -52,7 +48,6 @@ class PubSerial {
private: private:
settings_t *mCfg; settings_t *mCfg;
HMSYSTEM *mSys; HMSYSTEM *mSys;
uint8_t mTick;
uint32_t *mUtcTimestamp; uint32_t *mUtcTimestamp;
}; };

58
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;
}
}

24
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 <Arduino.h>
#include <cstdint>
#include <cstring>
#include <stdio.h>
#include <stdlib.h>
#include <TimeLib.h>
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__*/

108
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<class T, class... Args>
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<int MAX_NUM, class T, class... Args>
class llist {
typedef node_s<T, Args...> 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__*/

185
src/utils/scheduler.h

@ -7,75 +7,140 @@
#ifndef __SCHEDULER_H__ #ifndef __SCHEDULER_H__
#define __SCHEDULER_H__ #define __SCHEDULER_H__
#include <memory>
#include <functional> #include <functional>
#include <list> #include "llist.h"
#include "dbg.h"
enum {EVERY_SEC = 1, EVERY_MIN, EVERY_HR, EVERY_12H, EVERY_DAY};
typedef std::function<void()> SchedulerCb;
namespace ah { namespace ah {
class Scheduler { typedef std::function<void()> scdCb;
public:
Scheduler() {} enum {SCD_SEC = 1, SCD_MIN = 60, SCD_HOUR = 3600, SCD_12H = 43200, SCD_DAY = 86400};
void setup() { struct scdEvry_s {
mPrevMillis = 0; scdCb c;
mSeconds = 0; uint32_t timeout;
mMinutes = 0; uint32_t reload;
mHours = 0; scdEvry_s() : c(NULL), timeout(0), reload(0) {}
} scdEvry_s(scdCb a, uint32_t tmt, uint32_t rl) : c(a), timeout(tmt), reload(rl) {}
};
void loop() {
if (millis() - mPrevMillis >= 1000) { struct scdAt_s {
mPrevMillis += 1000; scdCb c;
notify(&mListSecond); uint32_t timestamp;
if(++mSeconds >= 60) { scdAt_s() : c(NULL), timestamp(0) {}
mSeconds = 0; scdAt_s(scdCb a, uint32_t ts) : c(a), timestamp(ts) {}
notify(&mListMinute); };
if(++mMinutes >= 60) {
mMinutes = 0;
notify(&mListHour); typedef node_s<scdEvry_s, scdCb, uint32_t, uint32_t> sP;
if(++mHours >= 24) { typedef node_s<scdAt_s, scdCb, uint32_t> sPAt;
mHours = 0; class Scheduler {
notify(&mListDay); public:
notify(&mList12h); Scheduler() {}
}
else if(mHours == 12) void setup() {
notify(&mList12h); 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()));
} }
}
protected:
void addListener(uint8_t every, SchedulerCb cb) { uint32_t mTimestamp;
switch(every) {
case EVERY_SEC: mListSecond.push_back(cb); break; private:
case EVERY_MIN: mListMinute.push_back(cb); break; inline void checkEvery(void) {
case EVERY_HR: mListHour.push_back(cb); break; sP *p = mStack.getFront();
case EVERY_12H: mList12h.push_back(cb); break; while(NULL != p) {
case EVERY_DAY: mListDay.push_back(cb); break; if(mDiffSeconds >= p->d.timeout) { // expired
default: break; (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<SchedulerCb> *lType) { inline void checkAt(void) {
for(std::list<SchedulerCb>::iterator it = lType->begin(); it != lType->end(); ++it) { sPAt *p = mStackAt.getFront();
(*it)(); while(NULL != p) {
if((p->d.timestamp) <= mTimestamp) {
(p->d.c)();
yield();
p = mStackAt.rem(p);
}
else
p = mStackAt.get(p);
}
} }
}
llist<25, scdEvry_s, scdCb, uint32_t, uint32_t> mStack;
protected: llist<10, scdAt_s, scdCb, uint32_t> mStackAt;
std::list<SchedulerCb> mListSecond; uint32_t mMillis, mPrevMillis, mDiff;
std::list<SchedulerCb> mListMinute; uint32_t mUptime;
std::list<SchedulerCb> mListHour; uint8_t mDiffSeconds;
std::list<SchedulerCb> mList12h; };
std::list<SchedulerCb> mListDay;
private:
uint32_t mPrevMillis;
uint8_t mSeconds, mMinutes, mHours;
};
} }
#endif /*__SCHEDULER_H__*/ #endif /*__SCHEDULER_H__*/

6
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) { 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 // Source: https://en.wikipedia.org/wiki/Sunrise_equation#Complete_calculation_on_Earth
// Julian day since 1.1.2000 12:00 + correction 69.12s // Julian day since 1.1.2000 12:00
double n_JulianDay = (utcTs + offset) / 86400 - 10957.0 + 0.0008; double n_JulianDay = (utcTs + offset) / 86400 - 10957.0;
// Mean solar time // Mean solar time
double J = n_JulianDay - lon / 360; double J = n_JulianDay - lon / 360;
// Solar mean anomaly // Solar mean anomaly
@ -25,7 +25,7 @@ namespace ah {
// Declination of the sun // Declination of the sun
double delta = ASIN(SIN(lambda) * SIN(23.44)); double delta = ASIN(SIN(lambda) * SIN(23.44));
// Hour angle // 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 // Calculate sunrise and sunset
double Jrise = Jtransit - omega / 360; double Jrise = Jtransit - omega / 360;
double Jset = Jtransit + omega / 360; double Jset = Jtransit + omega / 360;

538
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 HMSYSTEM>
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<JsonObject>();
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<JsonObject>();
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("<a href=\"/factory\" class=\"btn\">Factory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>");
}
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<String>();
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<const char*>(), 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<String>() + "'";
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__*/

37
src/web/html/index.html

@ -39,18 +39,14 @@
</div> </div>
<p><span class="des">WiFi RSSI: </span><span id="wifi_rssi"></span> dBm</p> <p><span class="des">WiFi RSSI: </span><span id="wifi_rssi"></span> dBm</p>
<p> <p>
<span class="des">Statistics: </span> <span class="des">System Infos: </span>
<pre id="stat"></pre> <pre id="stat"></pre>
<pre id="iv"></pre> <pre id="iv"></pre>
<pre id="warn_info"></pre> <pre id="warn_info"></pre>
</p> </p>
<p>Every <span id="refresh"></span> seconds the values are updated</p>
<div id="note"> <div id="note">
Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a><br/> Discuss with us on <a href="https://discord.gg/WzhxEY62mB">Discord</a><br/>
<h3>Documentation</h3>
<a href="https://ahoydtu.de" target="_blank">ahoydtu.de</a>
<h3>Support this project:</h3> <h3>Support this project:</h3>
<ul> <ul>
<li>Report <a href="https://github.com/lumapu/ahoy/issues" target="_blank">issues</a></li> <li>Report <a href="https://github.com/lumapu/ahoy/issues" target="_blank">issues</a></li>
@ -82,6 +78,8 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
var exeOnce = true; var exeOnce = true;
var tickCnt = 0;
var ts = 0;
function apiCb(obj) { function apiCb(obj) {
var e = document.getElementById("apiResult"); var e = document.getElementById("apiResult");
@ -97,7 +95,7 @@
var date = new Date(); var date = new Date();
var obj = new Object(); var obj = new Object();
obj.cmd = "set_time"; obj.cmd = "set_time";
obj.ts = parseInt(date.getTime() / 1000); obj.val = parseInt(date.getTime() / 1000);
getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj)); getAjax("/api/setup", apiCb, "POST", JSON.stringify(obj));
} }
@ -110,6 +108,7 @@
} }
document.getElementById("wifi_rssi").innerHTML = obj["wifi_rssi"]; document.getElementById("wifi_rssi").innerHTML = obj["wifi_rssi"];
ts = obj["ts_now"];
var date = new Date(obj["ts_now"] * 1000); var date = new Date(obj["ts_now"] * 1000);
var up = obj["ts_uptime"]; var up = obj["ts_uptime"];
var days = parseInt(up / 86400) % 365; var days = parseInt(up / 86400) % 365;
@ -118,10 +117,13 @@
var sec = up % 60; var sec = up % 60;
var sunrise = new Date(obj["ts_sunrise"] * 1000); var sunrise = new Date(obj["ts_sunrise"] * 1000);
var sunset = new Date(obj["ts_sunset"] * 1000); var sunset = new Date(obj["ts_sunset"] * 1000);
document.getElementById("uptime").innerHTML = days + " Days, " var e = document.getElementById("uptime");
+ ("0"+hrs).substr(-2) + ":" e.innerHTML = days + " Day";
+ ("0"+min).substr(-2) + ":" if(1 != days)
+ ("0"+sec).substr(-2); e.innerHTML += "s";
e.innerHTML += ", " + ("0"+hrs).substr(-2) + ":"
+ ("0"+min).substr(-2) + ":"
+ ("0"+sec).substr(-2);
var dSpan = document.getElementById("date"); var dSpan = document.getElementById("date");
if(0 != obj["ts_now"]) if(0 != obj["ts_now"])
dSpan.innerHTML = date.toLocaleString('de-DE'); dSpan.innerHTML = date.toLocaleString('de-DE');
@ -134,7 +136,7 @@
e.addEventListener("click", setTime); e.addEventListener("click", setTime);
} }
if(!obj["ts_sun_upd"]) { if(0 == obj["ts_sunrise"]) {
var e = document.getElementById("sun"); var e = document.getElementById("sun");
if(null != e) if(null != e)
e.parentNode.removeChild(e); e.parentNode.removeChild(e);
@ -188,6 +190,16 @@
document.getElementById("warn_info").innerHTML = html; document.getElementById("warn_info").innerHTML = html;
} }
function tick() {
++tickCnt;
if(0 != ts)
document.getElementById("date").innerHTML = (new Date((ts+tickCnt) * 1000)).toLocaleString('de-DE');
if(tickCnt >= 10) {
tickCnt = 0;
getAjax('/api/index', parse);
}
}
function parse(obj) { function parse(obj) {
if(null != obj) { if(null != obj) {
if(exeOnce) if(exeOnce)
@ -196,9 +208,8 @@
parseStat(obj["statistics"]); parseStat(obj["statistics"]);
parseIv(obj["inverter"]); parseIv(obj["inverter"]);
parseWarnInfo(obj["warnings"], obj["infos"]); parseWarnInfo(obj["warnings"], obj["infos"]);
document.getElementById("refresh").innerHTML = obj["refresh_interval"];
if(exeOnce) { if(exeOnce) {
window.setInterval("getAjax('/api/index', parse)", obj["refresh_interval"] * 1000); window.setInterval("tick()", 1000);
exeOnce = false; exeOnce = false;
} }
} }

49
src/web/html/serial.html

@ -45,15 +45,15 @@
<br/> <br/>
<br/> <br/>
<br/> <br/>
<label>Send Power Limit: </label> <label for="pwrlimval">Power Limit Value</label>
<input type="number" class="text" name="pwrlimval" maxlength="4"/> <input type="number" class="text" name="pwrlimval" maxlength="4"/>
<label> </label> <label for="pwrlimctrl">Power Limit Command</label>
<select name="pwrlimcntrl" id="pwrlimcntrl"> <select name="pwrlimctrl">
<option value="" selected disabled hidden>select the unit and persistence</option> <option value="" selected disabled hidden>select the unit and persistence</option>
<option value="0">absolute in Watt non persistent</option> <option value="limit_nonpersistent_absolute">absolute non persistent [W]</option>
<option value="1">relative in percent non persistent</option> <option value="limit_nonpersistent_relative">relative non persistent [%]</option>
<option value="256">absolute in Watt persistent</option> <option value="limit_persistent_absolute">absolute persistent [W]</option>
<option value="257">relative in percent persistent</option> <option value="limit_persistent_relative">relative persistent [%]</option>
</select> </select>
<br/> <br/>
<input type="button" value="Send Power Limit" class="btn" id="sendpwrlim"/> <input type="button" value="Send Power Limit" class="btn" id="sendpwrlim"/>
@ -120,7 +120,7 @@
// set time offset for serial console // set time offset for serial console
var obj = new Object(); var obj = new Object();
obj.cmd = "serial_utc_offset"; obj.cmd = "serial_utc_offset";
obj.ts = new Date().getTimezoneOffset() * -60; obj.val = new Date().getTimezoneOffset() * -60;
getAjax("/api/setup", null, "POST", JSON.stringify(obj)); getAjax("/api/setup", null, "POST", JSON.stringify(obj));
} }
@ -152,7 +152,6 @@
} }
// only for test
function ctrlCb(obj) { function ctrlCb(obj) {
var e = document.getElementById("result"); var e = document.getElementById("result");
if(obj["success"]) if(obj["success"])
@ -169,44 +168,36 @@
const wrapper = document.getElementById('power'); const wrapper = document.getElementById('power');
wrapper.addEventListener('click', (event) => { wrapper.addEventListener('click', (event) => {
var power = event.target.value;
var obj = new Object(); var obj = new Object();
obj.id = get_selected_iv();
obj.cmd = "power";
switch (power) switch (event.target.value) {
{ default:
case "Turn On": case "Turn On":
obj.cmd = 0; obj.val = 1;
break; break;
case "Turn Off": case "Turn Off":
obj.cmd = 1; obj.val = 0;
break; break;
default:
obj.cmd = 2;
} }
obj.inverter = get_selected_iv();
obj.tx_request = 81;
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj)); getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
}); });
document.getElementById("sendpwrlim").addEventListener("click", function() { document.getElementById("sendpwrlim").addEventListener("click", function() {
var val = parseInt(document.getElementsByName('pwrlimval')[0].value); var val = parseInt(document.getElementsByName('pwrlimval')[0].value);
var ctrl = parseInt(document.getElementsByName('pwrlimcntrl')[0].value); var cmd = document.getElementsByName('pwrlimctrl')[0].value;
if((ctrl == 1 || ctrl == 257) && val < 2) val = 2;
if(isNaN(val) || isNaN(ctrl)) if(isNaN(val)) {
{ document.getElementById("result").textContent = "value is missing";
var tmp = (isNaN(val)) ? "Value" : "Unit";
document.getElementById("result").textContent = tmp + " is missing";
return; return;
} }
var obj = new Object(); var obj = new Object();
obj.inverter = get_selected_iv(); obj.id = get_selected_iv();
obj.cmd = 11; obj.cmd = cmd;
obj.tx_request = 81; obj.val = val;
obj.payload = [val, ctrl];
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj)); getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
}); });

10
src/web/html/setup.html

@ -50,7 +50,7 @@
<input type="button" name="scanbtn" id="scanbtn" class="btn" value="scan" onclick="scan()"/><br/> <input type="button" name="scanbtn" id="scanbtn" class="btn" value="scan" onclick="scan()"/><br/>
<label for="networks">Avail Networks</label> <label for="networks">Avail Networks</label>
<select name="networks" id="networks" onChange="selNet()"> <select name="networks" id="networks" onChange="selNet()">
<option value="-1">not scanned</option> <option value="-1" selected disabled hidden>not scanned</option>
</select> </select>
<label for="ssid">SSID</label> <label for="ssid">SSID</label>
<input type="text" name="ssid" class="text"/> <input type="text" name="ssid" class="text"/>
@ -127,7 +127,7 @@
<fieldset> <fieldset>
<legend class="des">MQTT</legend> <legend class="des">MQTT</legend>
<label for="mqttAddr">Broker / Server IP</label> <label for="mqttAddr">Broker / Server IP</label>
<input type="text" class="text" name="mqttAddr" maxlength="32" /> <input type="text" class="text" name="mqttAddr"/>
<label for="mqttPort">Port</label> <label for="mqttPort">Port</label>
<input type="text" class="text" name="mqttPort"/> <input type="text" class="text" name="mqttPort"/>
<label for="mqttUser">Username (optional)</label> <label for="mqttUser">Username (optional)</label>
@ -163,7 +163,7 @@
</div> </div>
<label for="reboot">Reboot device after successful save</label> <label for="reboot">Reboot device after successful save</label>
<input type="checkbox" class="cb" name="reboot"/> <input type="checkbox" class="cb" name="reboot" checked />
<input type="submit" value="save" class="btn right"/><br/> <input type="submit" value="save" class="btn right"/><br/>
<br/> <br/>
<a href="/get_setup" target="_blank">Download your settings (JSON file)</a> (only saved values) <a href="/get_setup" target="_blank">Download your settings (JSON file)</a> (only saved values)
@ -226,7 +226,7 @@
var date = new Date(); var date = new Date();
var obj = new Object(); var obj = new Object();
obj.cmd = "set_time"; obj.cmd = "set_time";
obj.ts = parseInt(date.getTime() / 1000); obj.val = parseInt(date.getTime() / 1000);
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj)); getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj));
} }
@ -234,7 +234,7 @@
var obj = new Object(); var obj = new Object();
obj.cmd = "scan_wifi"; obj.cmd = "scan_wifi";
getAjax("/api/setup", apiCbWifi, "POST", JSON.stringify(obj)); getAjax("/api/setup", apiCbWifi, "POST", JSON.stringify(obj));
setTimeout(function() {getAjax('/api/setup/networks', listNetworks)}, 7000); setTimeout(function() {getAjax('/api/setup/networks', listNetworks)}, 5000);
} }
function syncTime() { function syncTime() {

24
src/web/html/system.html

@ -19,6 +19,7 @@
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<div><ul id="info"></ul></div> <div><ul id="info"></ul></div>
<div><ul id="radio"></ul></div>
<div id="system"></div> <div id="system"></div>
</div> </div>
</div> </div>
@ -59,11 +60,34 @@
} }
} }
function parseRadio(obj) {
const pa = ["MIN", "LOW", "HIGH", "MAX"];
const datarate = ["1 MBps", "2 MBps", "250 kbps"];
var ul = document.getElementById("radio");
let data;
var li = document.createElement("li");
li.appendChild(document.createTextNode("nrf24l01" + (obj["isPVariant"] ? "+ " : "") + (obj["isconnected"] ? "is connected " : "is not connected ")));
ul.appendChild(li);
if(obj["isconnected"]) {
var li = document.createElement("li");
li.appendChild(document.createTextNode("Datarate: " + datarate[obj["DataRate"]]));
ul.appendChild(li);
var li = document.createElement("li");
li.appendChild(document.createTextNode("Power Level: " + pa[obj["power_level"]]));
ul.appendChild(li);
}
}
function parse(obj) { function parse(obj) {
if(null != obj) { if(null != obj) {
parseMenu(obj["menu"]); parseMenu(obj["menu"]);
parseSys(obj["system"]); parseSys(obj["system"]);
parseSysInfo(obj["system"]); parseSysInfo(obj["system"]);
parseRadio(obj["system"]["radio"]);
var e = document.getElementById("system"); var e = document.getElementById("system");
e.innerHTML = obj["html"]; e.innerHTML = obj["html"];

6
src/web/html/update.html

@ -18,12 +18,6 @@
</div> </div>
<div id="wrapper"> <div id="wrapper">
<div id="content"> <div id="content">
<div>
Make sure that you have noted all your settings before starting an update. New versions may have changed their memory layout which can break your existing settings.<br/>
<br/>
<a href="/get_setup" target="_blank">Download your settings (JSON file)</a>
</div>
<br/><br/>
<form method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8"> <form method="POST" action="/update" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="update"><input type="submit" value="Update"> <input type="file" name="update"><input type="submit" value="Update">
</form> </form>

30
src/web/html/visualization.html

@ -68,19 +68,17 @@
for(var j = 0; j < root.ch0_fld_names.length; j++) { for(var j = 0; j < root.ch0_fld_names.length; j++) {
var val = Math.round(iv["ch"][0][j] * 100) / 100; var val = Math.round(iv["ch"][0][j] * 100) / 100;
if(val > 0) { var sub = div(["subgrp"]);
var sub = div(["subgrp"]); sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"]));
sub.appendChild(span(val + " " + span(root["ch0_fld_units"][j], ["unit"]).innerHTML, ["value"])); sub.appendChild(span(root["ch0_fld_names"][j], ["info"]));
sub.appendChild(span(root["ch0_fld_names"][j], ["info"])); ch0.appendChild(sub);
ch0.appendChild(sub);
switch(j) {
switch(j) { case 2: total[j] += val; break; // P_AC
case 2: total[j] += val; break; // P_AC case 6: total[j] += val; break; // YieldTotal
case 6: total[j] += val; break; // YieldTotal case 7: total[j] += val; break; // YieldDay
case 7: total[j] += val; break; // YieldDay case 8: total[j] += val; break; // P_DC
case 8: total[j] += val; break; // P_DC case 10: total[j] += val; break; // Q_AC
case 10: total[j] += val; break; // Q_AC
}
} }
} }
main.appendChild(ch0); main.appendChild(ch0);
@ -92,10 +90,8 @@
for(var j = 0; j < root.fld_names.length; j++) { for(var j = 0; j < root.fld_names.length; j++) {
var val = Math.round(iv["ch"][i][j] * 100) / 100; var val = Math.round(iv["ch"][i][j] * 100) / 100;
if(val > 0) { ch.appendChild(span(val + " " + span(root["fld_units"][j], ["unit"]).innerHTML, ["value"]));
ch.appendChild(span(val + " " + span(root["fld_units"][j], ["unit"]).innerHTML, ["value"])); ch.appendChild(span(root["fld_names"][j], ["info"]));
ch.appendChild(span(root["fld_names"][j], ["info"]));
}
} }
main.appendChild(ch); main.appendChild(ch);
} }

726
src/web/web.cpp

@ -1,726 +0,0 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#if defined(ESP32) && defined(F)
#undef F
#define F(sl) (sl)
#endif
#include "web.h"
#include "../utils/ahoyTimer.h"
#include "html/h/index_html.h"
#include "html/h/login_html.h"
#include "html/h/style_css.h"
#include "html/h/api_js.h"
#include "html/h/favicon_ico.h"
#include "html/h/setup_html.h"
#include "html/h/visualization_html.h"
#include "html/h/update_html.h"
#include "html/h/serial_html.h"
#include "html/h/system_html.h"
const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinLed0", "pinLed1"};
//-----------------------------------------------------------------------------
web::web(app *main, settings_t *config, statistics_t *stat, char version[]) {
mMain = main;
mConfig = config;
mStat = stat;
mVersion = version;
mWeb = new AsyncWebServer(80);
mEvts = new AsyncEventSource("/events");
mApi = new webApi(mWeb, main, config, stat, version);
mProtected = true;
mLogoutTimeout = 0;
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0;
mWebSerialTicker = 0;
mWebSerialInterval = 1000; // [ms]
mSerialAddTime = true;
}
//-----------------------------------------------------------------------------
void web::setup(void) {
DPRINTLN(DBG_VERBOSE, F("app::setup-begin"));
mWeb->begin();
DPRINTLN(DBG_VERBOSE, F("app::setup-on"));
mWeb->on("/", HTTP_GET, std::bind(&web::onIndex, this, std::placeholders::_1));
mWeb->on("/login", HTTP_ANY, std::bind(&web::onLogin, this, std::placeholders::_1));
mWeb->on("/logout", HTTP_GET, std::bind(&web::onLogout, this, std::placeholders::_1));
mWeb->on("/style.css", HTTP_GET, std::bind(&web::onCss, this, std::placeholders::_1));
mWeb->on("/api.js", HTTP_GET, std::bind(&web::onApiJs, this, std::placeholders::_1));
mWeb->on("/favicon.ico", HTTP_GET, std::bind(&web::onFavicon, this, std::placeholders::_1));
mWeb->onNotFound ( std::bind(&web::showNotFound, this, std::placeholders::_1));
mWeb->on("/reboot", HTTP_ANY, std::bind(&web::onReboot, this, std::placeholders::_1));
mWeb->on("/system", HTTP_ANY, std::bind(&web::onSystem, this, std::placeholders::_1));
mWeb->on("/erase", HTTP_ANY, std::bind(&web::showErase, this, std::placeholders::_1));
mWeb->on("/factory", HTTP_ANY, std::bind(&web::showFactoryRst, this, std::placeholders::_1));
mWeb->on("/setup", HTTP_GET, std::bind(&web::onSetup, this, std::placeholders::_1));
mWeb->on("/save", HTTP_ANY, std::bind(&web::showSave, this, std::placeholders::_1));
mWeb->on("/live", HTTP_ANY, std::bind(&web::onLive, this, std::placeholders::_1));
mWeb->on("/api1", HTTP_POST, std::bind(&web::showWebApi, this, std::placeholders::_1));
#ifdef ENABLE_JSON_EP
mWeb->on("/json", HTTP_ANY, std::bind(&web::showJson, this, std::placeholders::_1));
#endif
#ifdef ENABLE_PROMETHEUS_EP
mWeb->on("/metrics", HTTP_ANY, std::bind(&web::showMetrics, this, std::placeholders::_1));
#endif
mWeb->on("/update", HTTP_GET, std::bind(&web::onUpdate, this, std::placeholders::_1));
mWeb->on("/update", HTTP_POST, std::bind(&web::showUpdate, this, std::placeholders::_1),
std::bind(&web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
mWeb->on("/serial", HTTP_GET, std::bind(&web::onSerial, this, std::placeholders::_1));
mEvts->onConnect(std::bind(&web::onConnect, this, std::placeholders::_1));
mWeb->addHandler(mEvts);
mApi->setup();
registerDebugCb(std::bind(&web::serialCb, this, std::placeholders::_1));
}
//-----------------------------------------------------------------------------
void web::loop(void) {
mApi->loop();
if(ah::checkTicker(&mWebSerialTicker, mWebSerialInterval)) {
if(mSerialBufFill > 0) {
mEvts->send(mSerialBuf, "serial", millis());
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0;
}
}
}
//-----------------------------------------------------------------------------
void web::tickSecond() {
if(0 != mLogoutTimeout) {
mLogoutTimeout -= 1;
if(0 == mLogoutTimeout)
mProtected = true;
DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout));
}
}
//-----------------------------------------------------------------------------
void web::setProtection(bool protect) {
mProtected = protect;
}
//-----------------------------------------------------------------------------
void web::onConnect(AsyncEventSourceClient *client) {
DPRINTLN(DBG_VERBOSE, "onConnect");
if(client->lastId())
DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId()));
client->send("hello!", NULL, millis(), 1000);
}
//-----------------------------------------------------------------------------
void web::onIndex(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onIndex"));
if(mProtected) {
request->redirect("/login");
return;
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), index_html, index_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::onLogin(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLogin"));
if(request->args() > 0) {
if(String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) {
mProtected = false;
request->redirect("/");
}
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), login_html, login_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::onCss(AsyncWebServerRequest *request) {
mLogoutTimeout = LOGOUT_TIMEOUT;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::onApiJs(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onApiJs"));
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), api_js, api_js_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::onFavicon(AsyncWebServerRequest *request) {
static const char favicon_type[] PROGMEM = "image/x-icon";
AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::showNotFound(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showNotFound - ") + request->url());
String msg = F("File Not Found\n\nURL: ");
msg += request->url();
msg += F("\nMethod: ");
msg += ( request->method() == HTTP_GET ) ? "GET" : "POST";
msg += F("\nArguments: ");
msg += request->args();
msg += "\n";
for(uint8_t i = 0; i < request->args(); i++ ) {
msg += " " + request->argName(i) + ": " + request->arg(i) + "\n";
}
request->send(404, F("text/plain"), msg);
}
//-----------------------------------------------------------------------------
void web::onReboot(AsyncWebServerRequest *request) {
mMain->mShouldReboot = true;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::onSystem(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSystem"));
if(mProtected) {
request->redirect("/login");
return;
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::onLogout(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLogout"));
if(mProtected) {
request->redirect("/login");
return;
}
mProtected = true;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::showErase(AsyncWebServerRequest *request) {
if(mProtected) {
request->redirect("/login");
return;
}
DPRINTLN(DBG_VERBOSE, F("showErase"));
mMain->eraseSettings(false);
onReboot(request);
}
//-----------------------------------------------------------------------------
void web::showFactoryRst(AsyncWebServerRequest *request) {
if(mProtected) {
request->redirect("/login");
return;
}
DPRINTLN(DBG_VERBOSE, F("showFactoryRst"));
String content = "";
int refresh = 3;
if(request->args() > 0) {
if(request->arg("reset").toInt() == 1) {
refresh = 10;
if(mMain->eraseSettings(true))
content = F("factory reset: success\n\nrebooting ... ");
else
content = F("factory reset: failed\n\nrebooting ... ");
}
else {
content = F("factory reset: aborted");
refresh = 3;
}
}
else {
content = F("<h1>Factory Reset</h1>"
"<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>");
refresh = 120;
}
request->send(200, F("text/html"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>"));
if(refresh == 10) {
delay(1000);
ESP.restart();
}
}
//-----------------------------------------------------------------------------
void web::onSetup(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSetup"));
if(mProtected) {
request->redirect("/login");
return;
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), setup_html, setup_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::showSave(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showSave"));
if(mProtected) {
request->redirect("/login");
return;
}
if(request->args() > 0) {
char buf[20] = {0};
// general
if(request->arg("ssid") != "")
request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN);
if(request->arg("pwd") != "{PWD}")
request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN);
if(request->arg("device") != "")
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
if(request->arg("adminpwd") != "{PWD}") {
request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN);
mProtected = (strlen(mConfig->sys.adminPwd) > 0);
}
// static ip
if(request->arg("ipAddr") != "") {
request->arg("ipAddr").toCharArray(buf, SSID_LEN);
ip2Arr(mConfig->sys.ip.ip, buf);
if(request->arg("ipMask") != "") {
request->arg("ipMask").toCharArray(buf, SSID_LEN);
ip2Arr(mConfig->sys.ip.mask, buf);
}
if(request->arg("ipDns1") != "") {
request->arg("ipDns1").toCharArray(buf, SSID_LEN);
ip2Arr(mConfig->sys.ip.dns1, buf);
}
if(request->arg("ipDns2") != "") {
request->arg("ipDns2").toCharArray(buf, SSID_LEN);
ip2Arr(mConfig->sys.ip.dns2, buf);
}
if(request->arg("ipGateway") != "") {
request->arg("ipGateway").toCharArray(buf, SSID_LEN);
ip2Arr(mConfig->sys.ip.gateway, buf);
}
}
else
memset(&mConfig->sys.ip.ip, 0, 4);
// inverter
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mMain->mSys->getInverterByPos(i, false);
// address
request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20);
if(strlen(buf) == 0)
memset(buf, 0, 20);
iv->config->serial.u64 = mMain->Serial2u64(buf);
switch(iv->config->serial.b[4]) {
case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break;
case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break;
case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break;
default: break;
}
// name
request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH);
// max channel power / name
for(uint8_t j = 0; j < 4; j++) {
iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff;
request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH);
}
iv->initialized = true;
}
if(request->arg("invInterval") != "")
mConfig->nrf.sendInterval = request->arg("invInterval").toInt();
if(request->arg("invRetry") != "")
mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt();
// pinout
uint8_t pin;
for(uint8_t i = 0; i < 5; i ++) {
pin = request->arg(String(pinArgNames[i])).toInt();
switch(i) {
default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break;
case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_CE_PIN); break;
case 2: mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break;
case 3: mConfig->led.led0 = pin; break;
case 4: mConfig->led.led1 = pin; break;
}
}
// nrf24 amplifier power
mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03;
// ntp
if(request->arg("ntpAddr") != "") {
request->arg("ntpAddr").toCharArray(mConfig->ntp.addr, NTP_ADDR_LEN);
mConfig->ntp.port = request->arg("ntpPort").toInt() & 0xffff;
}
// sun
if(request->arg("sunLat") == "" || (request->arg("sunLon") == "")) {
mConfig->sun.lat = 0.0;
mConfig->sun.lon = 0.0;
mConfig->sun.disNightCom = false;
} else {
mConfig->sun.lat = request->arg("sunLat").toFloat();
mConfig->sun.lon = request->arg("sunLon").toFloat();
mConfig->sun.disNightCom = (request->arg("sunDisNightCom") == "on");
}
// mqtt
if(request->arg("mqttAddr") != "") {
String addr = request->arg("mqttAddr");
addr.trim();
addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN);
request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN);
if(request->arg("mqttPwd") != "{PWD}")
request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN);
request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN);
mConfig->mqtt.port = request->arg("mqttPort").toInt();
}
// serial console
if(request->arg("serIntvl") != "") {
mConfig->serial.interval = request->arg("serIntvl").toInt() & 0xffff;
mConfig->serial.debug = (request->arg("serDbg") == "on");
mConfig->serial.showIv = (request->arg("serEn") == "on");
// Needed to log TX buffers to serial console
mMain->mSys->Radio.mSerialDebug = mConfig->serial.debug;
}
mMain->saveSettings();
if(request->arg("reboot") == "on")
onReboot(request);
else {
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
}
}
//-----------------------------------------------------------------------------
void web::onLive(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLive"));
if(mProtected) {
request->redirect("/login");
return;
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), visualization_html, visualization_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::showWebApi(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("web::showWebApi"));
DPRINTLN(DBG_DEBUG, request->arg("plain"));
const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity.
DynamicJsonDocument response(capacity);
// Parse JSON object
deserializeJson(response, request->arg("plain"));
// ToDo: error handling for payload
uint8_t iv_id = response["inverter"];
uint8_t cmd = response["cmd"];
Inverter<> *iv = mMain->mSys->getInverterByPos(iv_id);
if (NULL != iv) {
if (response["tx_request"] == (uint8_t)TX_REQ_INFO) {
// if the AlarmData is requested set the Alarm Index to the requested one
if (cmd == AlarmData || cmd == AlarmUpdate) {
// set the AlarmMesIndex for the request from user input
iv->alarmMesIndex = response["payload"];
}
DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String((uint16_t) response["payload"]));
// process payload from web request corresponding to the cmd
iv->enqueCommand<InfoCommand>(cmd);
}
if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) {
if (response["cmd"] == (uint8_t)ActivePowerContr) {
uint16_t webapiPayload = response["payload"];
uint16_t webapiPayload2 = response["payload2"];
if (webapiPayload > 0 && webapiPayload < 10000) {
iv->devControlCmd = ActivePowerContr;
iv->powerLimit[0] = webapiPayload;
if (webapiPayload2 > 0)
iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check
else // if not set, set it to 0x0000 default
iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporary in Watt absolut
if (iv->powerLimit[1] & 0x0001)
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API"));
else
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API"));
iv->devControlRequest = true; // queue it in the request loop
}
}
if (response["cmd"] == (uint8_t)TurnOff) {
iv->devControlCmd = TurnOff;
iv->devControlRequest = true; // queue it in the request loop
}
if (response["cmd"] == (uint8_t)TurnOn) {
iv->devControlCmd = TurnOn;
iv->devControlRequest = true; // queue it in the request loop
}
if (response["cmd"] == (uint8_t)CleanState_LockAndAlarm) {
iv->devControlCmd = CleanState_LockAndAlarm;
iv->devControlRequest = true; // queue it in the request loop
}
if (response["cmd"] == (uint8_t)Restart) {
iv->devControlCmd = Restart;
iv->devControlRequest = true; // queue it in the request loop
}
}
}
request->send(200, "text/json", "{success:true}");
}
//-----------------------------------------------------------------------------
void web::onUpdate(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onUpdate"));
/*if(mProtected) {
request->redirect("/login");
return;
}*/
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), update_html, update_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::showUpdate(AsyncWebServerRequest *request) {
bool reboot = !Update.hasError();
String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Update: ");
if(reboot)
html += "success";
else
html += "failed";
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html);
response->addHeader("Connection", "close");
request->send(response);
mMain->mShouldReboot = reboot;
}
//-----------------------------------------------------------------------------
void web::showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if(!index) {
Serial.printf("Update Start: %s\n", filename.c_str());
#ifndef ESP32
Update.runAsync(true);
#endif
if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
Update.printError(Serial);
}
}
if(!Update.hasError()) {
if(Update.write(data, len) != len){
Update.printError(Serial);
}
}
if(final) {
if(Update.end(true)) {
Serial.printf("Update Success: %uB\n", index+len);
} else {
Update.printError(Serial);
}
}
}
//-----------------------------------------------------------------------------
void web::onSerial(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSerial"));
if(mProtected) {
request->redirect("/login");
return;
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), serial_html, serial_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
//-----------------------------------------------------------------------------
void web::serialCb(String msg) {
msg.replace("\r\n", "<rn>");
if(mSerialAddTime) {
if((9 + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) {
strncpy(&mSerialBuf[mSerialBufFill], mMain->getTimeStr(mApi->getTimezoneOffset()).c_str(), 9);
mSerialBufFill += 9;
}
else {
mSerialBufFill = 0;
mEvts->send("webSerial, buffer overflow!", "serial", millis());
}
mSerialAddTime = false;
}
if(msg.endsWith("<rn>"))
mSerialAddTime = true;
uint16_t length = msg.length();
if((length + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) {
strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length);
mSerialBufFill += length;
}
else {
mSerialBufFill = 0;
mEvts->send("webSerial, buffer overflow!", "serial", millis());
}
}
//-----------------------------------------------------------------------------
#ifdef ENABLE_JSON_EP
void web::showJson(void) {
DPRINTLN(DBG_VERBOSE, F("web::showJson"));
String modJson;
modJson = F("{\n");
for(uint8_t id = 0; id < mMain->mSys->getNumInverters(); id++) {
Inverter<> *iv = mMain->mSys->getInverterByPos(id);
if(NULL != iv) {
char topic[40], val[25];
snprintf(topic, 30, "\"%s\": {\n", iv->name);
modJson += String(topic);
for(uint8_t i = 0; i < iv->listLen; i++) {
snprintf(topic, 40, "\t\"ch%d/%s\"", iv->assign[i].ch, iv->getFieldName(i));
snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i), iv->getUnit(i));
modJson += String(topic) + ": " + String(val) + F(",\n");
}
modJson += F("\t\"last_msg\": \"") + mMain->getDateTimeStr(iv->ts) + F("\"\n\t},\n");
}
}
modJson += F("\"json_ts\": \"") + String(mMain->getDateTimeStr(mMain->mTimestamp)) + F("\"\n}\n");
mWeb->send(200, F("application/json"), modJson);
}
#endif
//-----------------------------------------------------------------------------
#ifdef ENABLE_PROMETHEUS_EP
std::pair<String, String> web::convertToPromUnits(String shortUnit) {
if(shortUnit == "A") return {"ampere", "gauge"};
if(shortUnit == "V") return {"volt", "gauge"};
if(shortUnit == "%") return {"ratio", "gauge"};
if(shortUnit == "W") return {"watt", "gauge"};
if(shortUnit == "Wh") return {"watt_daily", "counter"};
if(shortUnit == "kWh") return {"watt_total", "counter"};
if(shortUnit == "°C") return {"celsius", "gauge"};
return {"", "gauge"};
}
//-----------------------------------------------------------------------------
void web::showMetrics(void) {
DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));
String metrics;
char headline[80];
snprintf(headline, 80, "ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1", mVersion, mconfig->sys.deviceName);
metrics += "# TYPE ahoy_solar_info gauge\n" + String(headline) + "\n";
for(uint8_t id = 0; id < mMain->mSys->getNumInverters(); id++) {
Inverter<> *iv = mMain->mSys->getInverterByPos(id);
if(NULL != iv) {
char type[60], topic[60], val[25];
for(uint8_t i = 0; i < iv->listLen; i++) {
uint8_t channel = iv->assign[i].ch;
if(channel == 0) {
String promUnit, promType;
std::tie(promUnit, promType) = convertToPromUnits( iv->getUnit(i) );
snprintf(type, 60, "# TYPE ahoy_solar_%s_%s %s", iv->getFieldName(i), promUnit.c_str(), promType.c_str());
snprintf(topic, 60, "ahoy_solar_%s_%s{inverter=\"%s\"}", iv->getFieldName(i), promUnit.c_str(), iv->name);
snprintf(val, 25, "%.3f", iv->getValue(i));
metrics += String(type) + "\n" + String(topic) + " " + String(val) + "\n";
}
}
}
}
mWeb->send(200, F("text/plain"), metrics);
}
#endif

683
src/web/web.h

@ -14,82 +14,675 @@
#include "ESPAsyncTCP.h" #include "ESPAsyncTCP.h"
#endif #endif
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
#include "../app.h"
#include "webApi.h" #include "../appInterface.h"
#include "../hm/hmSystem.h"
#include "../utils/ahoyTimer.h"
#include "../utils/helper.h"
#include "html/h/index_html.h"
#include "html/h/login_html.h"
#include "html/h/style_css.h"
#include "html/h/api_js.h"
#include "html/h/favicon_ico.h"
#include "html/h/setup_html.h"
#include "html/h/visualization_html.h"
#include "html/h/update_html.h"
#include "html/h/serial_html.h"
#include "html/h/system_html.h"
#define WEB_SERIAL_BUF_SIZE 2048 #define WEB_SERIAL_BUF_SIZE 2048
class app; const char* const pinArgNames[] = {"pinCs", "pinCe", "pinIrq", "pinLed0", "pinLed1"};
class webApi;
class web { template<class HMSYSTEM>
class Web {
public: public:
web(app *main, settings_t *config, statistics_t *stat, char version[]); Web(void) {
~web() {} mProtected = true;
mLogoutTimeout = 0;
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0;
mWebSerialTicker = 0;
mWebSerialInterval = 1000; // [ms]
mSerialAddTime = true;
}
void setup(IApp *app, HMSYSTEM *sys, settings_t *config) {
mApp = app;
mSys = sys;
mConfig = config;
mWeb = new AsyncWebServer(80);
mEvts = new AsyncEventSource("/events");
DPRINTLN(DBG_VERBOSE, F("app::setup-begin"));
mWeb->begin();
DPRINTLN(DBG_VERBOSE, F("app::setup-on"));
mWeb->on("/", HTTP_GET, std::bind(&Web::onIndex, this, std::placeholders::_1));
mWeb->on("/login", HTTP_ANY, std::bind(&Web::onLogin, this, std::placeholders::_1));
mWeb->on("/logout", HTTP_GET, std::bind(&Web::onLogout, this, std::placeholders::_1));
mWeb->on("/style.css", HTTP_GET, std::bind(&Web::onCss, this, std::placeholders::_1));
mWeb->on("/api.js", HTTP_GET, std::bind(&Web::onApiJs, this, std::placeholders::_1));
mWeb->on("/favicon.ico", HTTP_GET, std::bind(&Web::onFavicon, this, std::placeholders::_1));
mWeb->onNotFound ( std::bind(&Web::showNotFound, this, std::placeholders::_1));
mWeb->on("/reboot", HTTP_ANY, std::bind(&Web::onReboot, this, std::placeholders::_1));
mWeb->on("/system", HTTP_ANY, std::bind(&Web::onSystem, this, std::placeholders::_1));
mWeb->on("/erase", HTTP_ANY, std::bind(&Web::showErase, this, std::placeholders::_1));
mWeb->on("/factory", HTTP_ANY, std::bind(&Web::showFactoryRst, this, std::placeholders::_1));
mWeb->on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1));
mWeb->on("/save", HTTP_ANY, std::bind(&Web::showSave, this, std::placeholders::_1));
mWeb->on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1));
mWeb->on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1));
void setup(void); #ifdef ENABLE_JSON_EP
void loop(void); mWeb->on("/json", HTTP_ANY, std::bind(&Web::showJson, this, std::placeholders::_1));
void tickSecond(); #endif
#ifdef ENABLE_PROMETHEUS_EP
mWeb->on("/metrics", HTTP_ANY, std::bind(&Web::showMetrics, this, std::placeholders::_1));
#endif
void setProtection(bool protect); mWeb->on("/update", HTTP_GET, std::bind(&Web::onUpdate, this, std::placeholders::_1));
mWeb->on("/update", HTTP_POST, std::bind(&Web::showUpdate, this, std::placeholders::_1),
std::bind(&Web::showUpdate2, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
mWeb->on("/serial", HTTP_GET, std::bind(&Web::onSerial, this, std::placeholders::_1));
void onUpdate(AsyncWebServerRequest *request); mEvts->onConnect(std::bind(&Web::onConnect, this, std::placeholders::_1));
void showUpdate(AsyncWebServerRequest *request); mWeb->addHandler(mEvts);
void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
void serialCb(String msg); registerDebugCb(std::bind(&Web::serialCb, this, std::placeholders::_1)); // dbg.h
}
void loop(void) {
if(ah::checkTicker(&mWebSerialTicker, mWebSerialInterval)) {
if(mSerialBufFill > 0) {
mEvts->send(mSerialBuf, "serial", millis());
memset(mSerialBuf, 0, WEB_SERIAL_BUF_SIZE);
mSerialBufFill = 0;
}
}
}
void tickSecond() {
if(0 != mLogoutTimeout) {
mLogoutTimeout -= 1;
if(0 == mLogoutTimeout) {
if(strlen(mConfig->sys.adminPwd) > 0)
mProtected = true;
}
DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout));
}
}
AsyncWebServer *getWebSrvPtr(void) {
return mWeb;
}
void setProtection(bool protect) {
mProtected = protect;
}
void showUpdate2(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if(!index) {
Serial.printf("Update Start: %s\n", filename.c_str());
#ifndef ESP32
Update.runAsync(true);
#endif
if(!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
Update.printError(Serial);
}
}
if(!Update.hasError()) {
if(Update.write(data, len) != len){
Update.printError(Serial);
}
}
if(final) {
if(Update.end(true)) {
Serial.printf("Update Success: %uB\n", index+len);
} else {
Update.printError(Serial);
}
}
}
void serialCb(String msg) {
msg.replace("\r\n", "<rn>");
if(mSerialAddTime) {
if((9 + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) {
strncpy(&mSerialBuf[mSerialBufFill], mApp->getTimeStr(mApp->getTimezoneOffset()).c_str(), 9);
mSerialBufFill += 9;
}
else {
mSerialBufFill = 0;
mEvts->send("webSerial, buffer overflow!", "serial", millis());
}
mSerialAddTime = false;
}
if(msg.endsWith("<rn>"))
mSerialAddTime = true;
uint16_t length = msg.length();
if((length + mSerialBufFill) <= WEB_SERIAL_BUF_SIZE) {
strncpy(&mSerialBuf[mSerialBufFill], msg.c_str(), length);
mSerialBufFill += length;
}
else {
mSerialBufFill = 0;
mEvts->send("webSerial, buffer overflow!", "serial", millis());
}
}
private: private:
void onConnect(AsyncEventSourceClient *client); void onUpdate(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onUpdate"));
/*if(mProtected) {
request->redirect("/login");
return;
}*/
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), update_html, update_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void showUpdate(AsyncWebServerRequest *request) {
bool reboot = !Update.hasError();
String html = F("<!doctype html><html><head><title>Update</title><meta http-equiv=\"refresh\" content=\"20; URL=/\"></head><body>Update: ");
if(reboot)
html += "success";
else
html += "failed";
html += F("<br/><br/>rebooting ... auto reload after 20s</body></html>");
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html"), html);
response->addHeader("Connection", "close");
request->send(response);
if(reboot)
mApp->setRebootFlag();
}
void onConnect(AsyncEventSourceClient *client) {
DPRINTLN(DBG_VERBOSE, "onConnect");
if(client->lastId())
DPRINTLN(DBG_VERBOSE, "Client reconnected! Last message ID that it got is: " + String(client->lastId()));
client->send("hello!", NULL, millis(), 1000);
}
void onIndex(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onIndex"));
if(mProtected) {
request->redirect("/login");
return;
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), index_html, index_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void onLogin(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLogin"));
if(request->args() > 0) {
if(String(request->arg("pwd")) == String(mConfig->sys.adminPwd)) {
mProtected = false;
request->redirect("/");
}
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), login_html, login_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void onLogout(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLogout"));
if(mProtected) {
request->redirect("/login");
return;
}
mProtected = true;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void onCss(AsyncWebServerRequest *request) {
mLogoutTimeout = LOGOUT_TIMEOUT;
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/css"), style_css, style_css_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void onApiJs(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onApiJs"));
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/javascript"), api_js, api_js_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void onFavicon(AsyncWebServerRequest *request) {
static const char favicon_type[] PROGMEM = "image/x-icon";
AsyncWebServerResponse *response = request->beginResponse_P(200, favicon_type, favicon_ico, favicon_ico_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void showNotFound(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showNotFound - ") + request->url());
String msg = F("File Not Found\n\nURL: ");
msg += request->url();
msg += F("\nMethod: ");
msg += ( request->method() == HTTP_GET ) ? "GET" : "POST";
msg += F("\nArguments: ");
msg += request->args();
msg += "\n";
for(uint8_t i = 0; i < request->args(); i++ ) {
msg += " " + request->argName(i) + ": " + request->arg(i) + "\n";
}
void onIndex(AsyncWebServerRequest *request); request->send(404, F("text/plain"), msg);
void onLogin(AsyncWebServerRequest *request); }
void onLogout(AsyncWebServerRequest *request);
void onCss(AsyncWebServerRequest *request); void onReboot(AsyncWebServerRequest *request) {
void onApiJs(AsyncWebServerRequest *request); mApp->setRebootFlag();
void onFavicon(AsyncWebServerRequest *request); AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len);
void showNotFound(AsyncWebServerRequest *request); response->addHeader(F("Content-Encoding"), "gzip");
void onReboot(AsyncWebServerRequest *request); request->send(response);
void showErase(AsyncWebServerRequest *request); }
void showFactoryRst(AsyncWebServerRequest *request);
void onSetup(AsyncWebServerRequest *request); void showErase(AsyncWebServerRequest *request) {
void showSave(AsyncWebServerRequest *request); if(mProtected) {
request->redirect("/login");
return;
}
DPRINTLN(DBG_VERBOSE, F("showErase"));
mApp->eraseSettings(false);
onReboot(request);
}
void showFactoryRst(AsyncWebServerRequest *request) {
if(mProtected) {
request->redirect("/login");
return;
}
void onLive(AsyncWebServerRequest *request); DPRINTLN(DBG_VERBOSE, F("showFactoryRst"));
void showWebApi(AsyncWebServerRequest *request); String content = "";
int refresh = 3;
if(request->args() > 0) {
if(request->arg("reset").toInt() == 1) {
refresh = 10;
if(mApp->eraseSettings(true))
content = F("factory reset: success\n\nrebooting ... ");
else
content = F("factory reset: failed\n\nrebooting ... ");
}
else {
content = F("factory reset: aborted");
refresh = 3;
}
}
else {
content = F("<h1>Factory Reset</h1>"
"<p><a href=\"/factory?reset=1\">RESET</a><br/><br/><a href=\"/factory?reset=0\">CANCEL</a><br/></p>");
refresh = 120;
}
request->send(200, F("text/html"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>"));
if(refresh == 10) {
delay(1000);
ESP.restart();
}
}
void onSerial(AsyncWebServerRequest *request); void onSetup(AsyncWebServerRequest *request) {
void onSystem(AsyncWebServerRequest *request); DPRINTLN(DBG_VERBOSE, F("onSetup"));
void ip2Arr(uint8_t ip[], char *ipStr) { if(mProtected) {
char *p = strtok(ipStr, "."); request->redirect("/login");
uint8_t i = 0; return;
while(NULL != p) {
ip[i++] = atoi(p);
p = strtok(NULL, ".");
} }
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), setup_html, setup_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void showSave(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("showSave"));
if(mProtected) {
request->redirect("/login");
return;
}
if(request->args() > 0) {
char buf[20] = {0};
// general
if(request->arg("ssid") != "")
request->arg("ssid").toCharArray(mConfig->sys.stationSsid, SSID_LEN);
if(request->arg("pwd") != "{PWD}")
request->arg("pwd").toCharArray(mConfig->sys.stationPwd, PWD_LEN);
if(request->arg("device") != "")
request->arg("device").toCharArray(mConfig->sys.deviceName, DEVNAME_LEN);
if(request->arg("adminpwd") != "{PWD}") {
request->arg("adminpwd").toCharArray(mConfig->sys.adminPwd, PWD_LEN);
mProtected = (strlen(mConfig->sys.adminPwd) > 0);
}
// static ip
request->arg("ipAddr").toCharArray(buf, 20);
ah::ip2Arr(mConfig->sys.ip.ip, buf);
request->arg("ipMask").toCharArray(buf, 20);
ah::ip2Arr(mConfig->sys.ip.mask, buf);
request->arg("ipDns1").toCharArray(buf, 20);
ah::ip2Arr(mConfig->sys.ip.dns1, buf);
request->arg("ipDns2").toCharArray(buf, 20);
ah::ip2Arr(mConfig->sys.ip.dns2, buf);
request->arg("ipGateway").toCharArray(buf, 20);
ah::ip2Arr(mConfig->sys.ip.gateway, buf);
// inverter
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i, false);
// address
request->arg("inv" + String(i) + "Addr").toCharArray(buf, 20);
if(strlen(buf) == 0)
memset(buf, 0, 20);
iv->config->serial.u64 = ah::Serial2u64(buf);
switch(iv->config->serial.b[4]) {
case 0x21: iv->type = INV_TYPE_1CH; iv->channels = 1; break;
case 0x41: iv->type = INV_TYPE_2CH; iv->channels = 2; break;
case 0x61: iv->type = INV_TYPE_4CH; iv->channels = 4; break;
default: break;
}
// name
request->arg("inv" + String(i) + "Name").toCharArray(iv->config->name, MAX_NAME_LENGTH);
// max channel power / name
for(uint8_t j = 0; j < 4; j++) {
iv->config->chMaxPwr[j] = request->arg("inv" + String(i) + "ModPwr" + String(j)).toInt() & 0xffff;
request->arg("inv" + String(i) + "ModName" + String(j)).toCharArray(iv->config->chName[j], MAX_NAME_LENGTH);
}
iv->initialized = true;
}
if(request->arg("invInterval") != "")
mConfig->nrf.sendInterval = request->arg("invInterval").toInt();
if(request->arg("invRetry") != "")
mConfig->nrf.maxRetransPerPyld = request->arg("invRetry").toInt();
// pinout
uint8_t pin;
for(uint8_t i = 0; i < 5; i ++) {
pin = request->arg(String(pinArgNames[i])).toInt();
switch(i) {
default: mConfig->nrf.pinCs = ((pin != 0xff) ? pin : DEF_CS_PIN); break;
case 1: mConfig->nrf.pinCe = ((pin != 0xff) ? pin : DEF_CE_PIN); break;
case 2: mConfig->nrf.pinIrq = ((pin != 0xff) ? pin : DEF_IRQ_PIN); break;
case 3: mConfig->led.led0 = pin; break;
case 4: mConfig->led.led1 = pin; break;
}
}
// nrf24 amplifier power
mConfig->nrf.amplifierPower = request->arg("rf24Power").toInt() & 0x03;
// ntp
if(request->arg("ntpAddr") != "") {
request->arg("ntpAddr").toCharArray(mConfig->ntp.addr, NTP_ADDR_LEN);
mConfig->ntp.port = request->arg("ntpPort").toInt() & 0xffff;
}
// sun
if(request->arg("sunLat") == "" || (request->arg("sunLon") == "")) {
mConfig->sun.lat = 0.0;
mConfig->sun.lon = 0.0;
mConfig->sun.disNightCom = false;
} else {
mConfig->sun.lat = request->arg("sunLat").toFloat();
mConfig->sun.lon = request->arg("sunLon").toFloat();
mConfig->sun.disNightCom = (request->arg("sunDisNightCom") == "on");
}
// mqtt
if(request->arg("mqttAddr") != "") {
String addr = request->arg("mqttAddr");
addr.trim();
addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN);
}
else
mConfig->mqtt.broker[0] = '\0';
request->arg("mqttUser").toCharArray(mConfig->mqtt.user, MQTT_USER_LEN);
if(request->arg("mqttPwd") != "{PWD}")
request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN);
request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN);
mConfig->mqtt.port = request->arg("mqttPort").toInt();
// serial console
if(request->arg("serIntvl") != "") {
mConfig->serial.interval = request->arg("serIntvl").toInt() & 0xffff;
mConfig->serial.debug = (request->arg("serDbg") == "on");
mConfig->serial.showIv = (request->arg("serEn") == "on");
// Needed to log TX buffers to serial console
mSys->Radio.mSerialDebug = mConfig->serial.debug;
}
mApp->saveSettings();
if(request->arg("reboot") == "on")
onReboot(request);
else {
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
}
}
void onLive(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLive"));
if(mProtected) {
request->redirect("/login");
return;
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), visualization_html, visualization_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void showWebApi(AsyncWebServerRequest *request) {
// TODO: remove
DPRINTLN(DBG_VERBOSE, F("web::showWebApi"));
DPRINTLN(DBG_DEBUG, request->arg("plain"));
const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity.
DynamicJsonDocument response(capacity);
// Parse JSON object
deserializeJson(response, request->arg("plain"));
// ToDo: error handling for payload
uint8_t iv_id = response["inverter"];
uint8_t cmd = response["cmd"];
Inverter<> *iv = mSys->getInverterByPos(iv_id);
if (NULL != iv) {
if (response["tx_request"] == (uint8_t)TX_REQ_INFO) {
// if the AlarmData is requested set the Alarm Index to the requested one
if (cmd == AlarmData || cmd == AlarmUpdate) {
// set the AlarmMesIndex for the request from user input
iv->alarmMesIndex = response["payload"];
}
DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String((uint16_t) response["payload"]));
// process payload from web request corresponding to the cmd
iv->enqueCommand<InfoCommand>(cmd);
}
if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) {
if (response["cmd"] == (uint8_t)ActivePowerContr) {
uint16_t webapiPayload = response["payload"];
uint16_t webapiPayload2 = response["payload2"];
if (webapiPayload > 0 && webapiPayload < 10000) {
iv->devControlCmd = ActivePowerContr;
iv->powerLimit[0] = webapiPayload;
if (webapiPayload2 > 0)
iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check
else // if not set, set it to 0x0000 default
iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporary in Watt absolut
if (iv->powerLimit[1] & 0x0001)
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API"));
else
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API"));
iv->devControlRequest = true; // queue it in the request loop
}
}
if (response["cmd"] == (uint8_t)TurnOff) {
iv->devControlCmd = TurnOff;
iv->devControlRequest = true; // queue it in the request loop
}
if (response["cmd"] == (uint8_t)TurnOn) {
iv->devControlCmd = TurnOn;
iv->devControlRequest = true; // queue it in the request loop
}
if (response["cmd"] == (uint8_t)CleanState_LockAndAlarm) {
iv->devControlCmd = CleanState_LockAndAlarm;
iv->devControlRequest = true; // queue it in the request loop
}
if (response["cmd"] == (uint8_t)Restart) {
iv->devControlCmd = Restart;
iv->devControlRequest = true; // queue it in the request loop
}
}
}
request->send(200, "text/json", "{success:true}");
}
void onSerial(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSerial"));
if(mProtected) {
request->redirect("/login");
return;
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), serial_html, serial_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
}
void onSystem(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onSystem"));
if(mProtected) {
request->redirect("/login");
return;
}
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip");
request->send(response);
} }
#ifdef ENABLE_JSON_EP #ifdef ENABLE_JSON_EP
void showJson(void); void showJson(void) {
DPRINTLN(DBG_VERBOSE, F("web::showJson"));
String modJson;
modJson = F("{\n");
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
char topic[40], val[25];
snprintf(topic, 30, "\"%s\": {\n", iv->name);
modJson += String(topic);
for(uint8_t i = 0; i < iv->listLen; i++) {
snprintf(topic, 40, "\t\"ch%d/%s\"", iv->assign[i].ch, iv->getFieldName(i));
snprintf(val, 25, "[%.3f, \"%s\"]", iv->getValue(i), iv->getUnit(i));
modJson += String(topic) + ": " + String(val) + F(",\n");
}
modJson += F("\t\"last_msg\": \"") + ah::getDateTimeStr(iv->ts) + F("\"\n\t},\n");
}
}
modJson += F("\"json_ts\": \"") + String(ah::getDateTimeStr(mMain->mTimestamp)) + F("\"\n}\n");
mWeb->send(200, F("application/json"), modJson);
}
#endif #endif
#ifdef ENABLE_PROMETHEUS_EP #ifdef ENABLE_PROMETHEUS_EP
void showMetrics(void); void showMetrics(void) {
std::pair<String, String> convertToPromUnits(String shortUnit); DPRINTLN(DBG_VERBOSE, F("web::showMetrics"));
String metrics;
char headline[80];
snprintf(headline, 80, "ahoy_solar_info{version=\"%s\",image=\"\",devicename=\"%s\"} 1", mApp->getVersion(), mconfig->sys.deviceName);
metrics += "# TYPE ahoy_solar_info gauge\n" + String(headline) + "\n";
for(uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if(NULL != iv) {
char type[60], topic[60], val[25];
for(uint8_t i = 0; i < iv->listLen; i++) {
uint8_t channel = iv->assign[i].ch;
if(channel == 0) {
String promUnit, promType;
std::tie(promUnit, promType) = convertToPromUnits( iv->getUnit(i) );
snprintf(type, 60, "# TYPE ahoy_solar_%s_%s %s", iv->getFieldName(i), promUnit.c_str(), promType.c_str());
snprintf(topic, 60, "ahoy_solar_%s_%s{inverter=\"%s\"}", iv->getFieldName(i), promUnit.c_str(), iv->name);
snprintf(val, 25, "%.3f", iv->getValue(i));
metrics += String(type) + "\n" + String(topic) + " " + String(val) + "\n";
}
}
}
}
mWeb->send(200, F("text/plain"), metrics);
}
std::pair<String, String> convertToPromUnits(String shortUnit) {
if(shortUnit == "A") return {"ampere", "gauge"};
if(shortUnit == "V") return {"volt", "gauge"};
if(shortUnit == "%") return {"ratio", "gauge"};
if(shortUnit == "W") return {"watt", "gauge"};
if(shortUnit == "Wh") return {"watt_daily", "counter"};
if(shortUnit == "kWh") return {"watt_total", "counter"};
if(shortUnit == "°C") return {"celsius", "gauge"};
return {"", "gauge"};
}
#endif #endif
AsyncWebServer *mWeb; AsyncWebServer *mWeb;
AsyncEventSource *mEvts; AsyncEventSource *mEvts;
bool mProtected; bool mProtected;
uint32_t mLogoutTimeout; uint32_t mLogoutTimeout;
IApp *mApp;
HMSYSTEM *mSys;
settings_t *mConfig; settings_t *mConfig;
statistics_t *mStat;
char *mVersion;
app *mMain;
webApi *mApi;
bool mSerialAddTime; bool mSerialAddTime;
char mSerialBuf[WEB_SERIAL_BUF_SIZE]; char mSerialBuf[WEB_SERIAL_BUF_SIZE];

581
src/web/webApi.cpp

@ -1,581 +0,0 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#if defined(ESP32) && defined(F)
#undef F
#define F(sl) (sl)
#endif
#include "webApi.h"
//-----------------------------------------------------------------------------
webApi::webApi(AsyncWebServer *srv, app *app, settings_t *config, statistics_t *stat, char version[]) {
mSrv = srv;
mApp = app;
mConfig = config;
mStat = stat;
mVersion = version;
mTimezoneOffset = 0;
}
//-----------------------------------------------------------------------------
void webApi::setup(void) {
mSrv->on("/api", HTTP_GET, std::bind(&webApi::onApi, this, std::placeholders::_1));
mSrv->on("/api", HTTP_POST, std::bind(&webApi::onApiPost, this, std::placeholders::_1)).onBody(
std::bind(&webApi::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(&webApi::onDwnldSetup, this, std::placeholders::_1));
}
//-----------------------------------------------------------------------------
void webApi::loop(void) {
}
//-----------------------------------------------------------------------------
void webApi::onApi(AsyncWebServerRequest *request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192);
JsonObject root = response->getRoot();
Inverter<> *iv = mApp->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 webApi::onApiPost(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, "onApiPost");
}
//-----------------------------------------------------------------------------
void webApi::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<JsonObject>();
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 webApi::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 webApi::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 webApi::getSysInfo(JsonObject obj) {
obj[F("ssid")] = mConfig->sys.stationSsid;
obj[F("device_name")] = mConfig->sys.deviceName;
obj[F("version")] = String(mVersion);
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("ts_sun_upd")] = mApp->getLatestSunTimestamp();
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
#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 webApi::getHtmlSystem(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getSysInfo(obj.createNestedObject(F("system")));
obj[F("html")] = F("<a href=\"/factory\" class=\"btn\">Factory Reset</a><br/><br/><a href=\"/reboot\" class=\"btn\">Reboot</a>");
}
//-----------------------------------------------------------------------------
void webApi::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 webApi::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 webApi::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 webApi::getStatistics(JsonObject obj) {
obj[F("rx_success")] = mStat->rxSuccess;
obj[F("rx_fail")] = mStat->rxFail;
obj[F("rx_fail_answer")] = mStat->rxFailNoAnser;
obj[F("frame_cnt")] = mStat->frmCnt;
obj[F("tx_cnt")] = mApp->mSys->Radio.mSendCnt;
}
//-----------------------------------------------------------------------------
void webApi::getInverterList(JsonObject obj) {
JsonArray invArr = obj.createNestedArray(F("inverter"));
Inverter<> *iv;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mApp->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 webApi::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 webApi::getNtp(JsonObject obj) {
obj[F("addr")] = String(mConfig->ntp.addr);
obj[F("port")] = String(mConfig->ntp.port);
}
//-----------------------------------------------------------------------------
void webApi::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 webApi::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 webApi::getRadio(JsonObject obj) {
obj[F("power_level")] = mConfig->nrf.amplifierPower;
}
//-----------------------------------------------------------------------------
void webApi::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 webApi::getStaticIp(JsonObject obj) {
if(mConfig->sys.ip.ip[0] != 0) {
obj[F("ip")] = ip2String(mConfig->sys.ip.ip);
obj[F("mask")] = ip2String(mConfig->sys.ip.mask);
obj[F("dns1")] = ip2String(mConfig->sys.ip.dns1);
obj[F("dns2")] = ip2String(mConfig->sys.ip.dns2);
obj[F("gateway")] = ip2String(mConfig->sys.ip.gateway);
}
}
//-----------------------------------------------------------------------------
void webApi::getMenu(JsonObject obj) {
obj["name"][0] = "Live";
obj["link"][0] = "/live";
obj["name"][1] = "Serial Console";
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";
if(strlen(mConfig->sys.adminPwd) > 0) {
obj["name"][8] = "-";
obj["name"][9] = "Logout";
obj["link"][9] = "/logout";
}
}
//-----------------------------------------------------------------------------
void webApi::getIndex(JsonObject obj) {
getMenu(obj.createNestedObject(F("menu")));
getSysInfo(obj.createNestedObject(F("system")));
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 = mApp->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(!mApp->mSys->Radio.isChipConnected())
warn.add(F("your NRF24 module can't be reached, check the wiring and pinout"));
if(!mApp->mqttIsConnected())
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->mqttIsConnected())
info.add(F("MQTT is connected, ") + String(mApp->getMqttTxCnt()) + F(" packets sent"));
}
//-----------------------------------------------------------------------------
void webApi::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 webApi::getNetworks(JsonObject obj) {
mApp->getAvailNetworks(obj);
}
//-----------------------------------------------------------------------------
void webApi::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 = mApp->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")] = 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) ? 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) ? 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 webApi::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 = mApp->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 webApi::setCtrl(JsonObject jsonIn, JsonObject jsonOut) {
uint8_t cmd = jsonIn[F("cmd")];
// Todo: num is the inverter number 0-3. For better display in DPRINTLN
uint8_t num = jsonIn[F("inverter")];
uint8_t tx_request = jsonIn[F("tx_request")];
if(TX_REQ_DEVCONTROL == tx_request)
{
DPRINTLN(DBG_INFO, F("devcontrol [") + String(num) + F("], cmd: 0x") + String(cmd, HEX));
Inverter<> *iv = getInverter(jsonIn, jsonOut);
JsonArray payload = jsonIn[F("payload")].as<JsonArray>();
if(NULL != iv)
{
switch (cmd)
{
case TurnOn:
iv->devControlCmd = TurnOn;
iv->devControlRequest = true;
break;
case TurnOff:
iv->devControlCmd = TurnOff;
iv->devControlRequest = true;
break;
case CleanState_LockAndAlarm:
iv->devControlCmd = CleanState_LockAndAlarm;
iv->devControlRequest = true;
break;
case Restart:
iv->devControlCmd = Restart;
iv->devControlRequest = true;
break;
case ActivePowerContr:
iv->devControlCmd = ActivePowerContr;
iv->devControlRequest = true;
iv->powerLimit[0] = payload[0];
iv->powerLimit[1] = payload[1];
break;
default:
jsonOut["error"] = "unknown 'cmd' = " + String(cmd);
return false;
}
} else {
return false;
}
}
else {
jsonOut[F("error")] = F("unknown 'tx_request'");
return false;
}
return true;
}
//-----------------------------------------------------------------------------
bool webApi::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("ts")]);
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("ts")];
else if(F("discovery_cfg") == jsonIn[F("cmd")])
mApp->mFlagSendDiscoveryConfig = true; // for homeassistant
else {
jsonOut[F("error")] = F("unknown cmd");
return false;
}
return true;
}
//-----------------------------------------------------------------------------
Inverter<> *webApi::getInverter(JsonObject jsonIn, JsonObject jsonOut) {
uint8_t id = jsonIn[F("inverter")];
Inverter<> *iv = mApp->mSys->getInverterByPos(id);
if(NULL == iv)
jsonOut[F("error")] = F("inverter index to high: ") + String(id);
return iv;
}

82
src/web/webApi.h

@ -1,82 +0,0 @@
#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 "../app.h"
class app;
class webApi {
public:
webApi(AsyncWebServer *srv, app *app, settings_t *config, statistics_t *stat, char version[]);
void setup(void);
void loop(void);
uint32_t getTimezoneOffset() {
return mTimezoneOffset;
}
private:
void onApi(AsyncWebServerRequest *request);
void onApiPost(AsyncWebServerRequest *request);
void onApiPostBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total);
void getNotFound(JsonObject obj, String url);
void onDwnldSetup(AsyncWebServerRequest *request);
void getSysInfo(JsonObject obj);
void getHtmlSystem(JsonObject obj);
void getHtmlLogout(JsonObject obj);
void getHtmlSave(JsonObject obj);
void getReboot(JsonObject obj);
void getStatistics(JsonObject obj);
void getInverterList(JsonObject obj);
void getMqtt(JsonObject obj);
void getNtp(JsonObject obj);
void getSun(JsonObject obj);
void getPinout(JsonObject obj);
void getRadio(JsonObject obj);
void getSerial(JsonObject obj);
void getStaticIp(JsonObject obj);
void getMenu(JsonObject obj);
void getIndex(JsonObject obj);
void getSetup(JsonObject obj);
void getNetworks(JsonObject obj);
void getLive(JsonObject obj);
void getRecord(JsonObject obj, record_t<> *rec);
bool setCtrl(JsonObject jsonIn, JsonObject jsonOut);
bool setSetup(JsonObject jsonIn, JsonObject jsonOut);
Inverter<> *getInverter(JsonObject jsonIn, JsonObject jsonOut);
double round3(double value) {
return (int)(value * 1000 + 0.5) / 1000.0;
}
String ip2String(uint8_t ip[]) {
char str[16];
snprintf(str, 16, "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
return String(str);
}
AsyncWebServer *mSrv;
app *mApp;
settings_t *mConfig;
statistics_t *mStat;
char *mVersion;
uint32_t mTimezoneOffset;
};
#endif /*__WEB_API_H__*/

275
src/wifi/ahoywifi.cpp

@ -8,150 +8,89 @@
#define F(sl) (sl) #define F(sl) (sl)
#endif #endif
#include "ahoywifi.h" #include "ahoywifi.h"
#include "../utils/ahoyTimer.h"
// NTP CONFIG // NTP CONFIG
#define NTP_PACKET_SIZE 48 #define NTP_PACKET_SIZE 48
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
ahoywifi::ahoywifi(settings_t *config) { ahoywifi::ahoywifi() {
mConfig = config; mCnt = 0;
mConnected = false;
mDns = new DNSServer(); mReconnect = false;
mUdp = new WiFiUDP();
mWifiStationTimeout = 10;
wifiWasEstablished = false;
mNextTryTs = 0;
mApLastTick = 0;
mApActive = false;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void ahoywifi::setup(uint32_t timeout, bool settingValid) { void ahoywifi::setup(settings_t *config, uint32_t *utcTimestamp) {
mConfig = config;
mUtcTimestamp = utcTimestamp;
#ifdef FB_WIFI_OVERRIDDEN #if !defined(FB_WIFI_OVERRIDDEN)
mStationWifiIsDef = false; if(strncmp(mConfig->sys.stationSsid, FB_WIFI_SSID, 14) == 0)
#else setupAp();
mStationWifiIsDef = (strncmp(mConfig->sys.stationSsid, FB_WIFI_SSID, 14) == 0);
#endif #endif
mWifiStationTimeout = timeout; #if !defined(AP_ONLY)
#ifndef AP_ONLY if(mConfig->valid)
if(false == mApActive) setupStation();
mApActive = (mStationWifiIsDef) ? true : setupStation(mWifiStationTimeout); #endif
#if defined(ESP8266)
wifiConnectHandler = WiFi.onStationModeGotIP(std::bind(&ahoywifi::onConnect, this, std::placeholders::_1));
wifiDisconnectHandler = WiFi.onStationModeDisconnected(std::bind(&ahoywifi::onDisconnect, this, std::placeholders::_1));
#else
WiFi.onEvent(std::bind(&ahoywifi::onWiFiEvent, this, std::placeholders::_1));
#endif #endif
if(!settingValid) {
DPRINTLN(DBG_WARN, F("your settings are not valid! check [IP]/setup"));
mApActive = true;
mApLastTick = millis();
mNextTryTs = (millis() + (WIFI_AP_ACTIVE_TIME * 1000));
setupAp(WIFI_AP_SSID, WIFI_AP_PWD);
}
else {
DPRINTLN(DBG_INFO, F("\n\n----------------------------------------"));
DPRINTLN(DBG_INFO, F("Welcome to AHOY!"));
DPRINT(DBG_INFO, F("\npoint your browser to http://"));
if(mApActive)
DBGPRINTLN(F("192.168.4.1"));
else
DBGPRINTLN(WiFi.localIP().toString());
DPRINTLN(DBG_INFO, F("to configure your device"));
DPRINTLN(DBG_INFO, F("----------------------------------------\n"));
}
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
bool ahoywifi::loop(void) { void ahoywifi::loop() {
if(mApActive) { #if !defined(AP_ONLY)
mDns->processNextRequest(); if(mReconnect) {
#ifndef AP_ONLY delay(100);
if(ah::checkTicker(&mNextTryTs, (WIFI_AP_ACTIVE_TIME * 1000))) { mCnt++;
mApActive = (mStationWifiIsDef) ? true : setupStation(mWifiStationTimeout); if((mCnt % 50) == 0)
if(mApActive) { WiFi.disconnect();
if(strlen(WIFI_AP_PWD) < 8) else if((mCnt % 60) == 0) {
DPRINTLN(DBG_ERROR, F("password must be at least 8 characters long")); WiFi.reconnect();
mApLastTick = millis(); mCnt = 0;
mNextTryTs = (millis() + (WIFI_AP_ACTIVE_TIME * 1000));
setupAp(WIFI_AP_SSID, WIFI_AP_PWD);
}
}
else {
if(millis() - mApLastTick > 10000) {
mApLastTick = millis();
uint8_t cnt = WiFi.softAPgetStationNum();
if(cnt > 0) {
DPRINTLN(DBG_INFO, String(cnt) + F(" client connected (no timeout)"));
mNextTryTs = (millis() + (WIFI_AP_ACTIVE_TIME * 1000));
}
else {
DBGPRINT(F("AP will be closed in "));
DBGPRINT(String((mNextTryTs - mApLastTick) / 1000));
DBGPRINTLN(F(" seconds"));
}
}
}
#endif
}
if((WiFi.status() != WL_CONNECTED) && wifiWasEstablished) {
if(!mApActive) {
DPRINTLN(DBG_INFO, "[WiFi]: Connection Lost");
mApActive = (mStationWifiIsDef) ? true : setupStation(mWifiStationTimeout);
} }
} }
#endif
return mApActive;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void ahoywifi::setupAp(const char *ssid, const char *pwd) { void ahoywifi::setupAp(void) {
DPRINTLN(DBG_VERBOSE, F("app::setupAp")); DPRINTLN(DBG_VERBOSE, F("wifi::setupAp"));
IPAddress apIp(192, 168, 4, 1); IPAddress apIp(192, 168, 4, 1);
DBGPRINTLN(F("\n---------\nAhoy Info:")); DBGPRINTLN(F("\n---------\nAhoyDTU Info:"));
DBGPRINT(F("Version: ")); DBGPRINT(F("Version: "));
DBGPRINTLN(String(VERSION_MAJOR) + F(".") + String(VERSION_MINOR) + F(".") + String(VERSION_PATCH)); DBGPRINTLN(String(VERSION_MAJOR) + F(".") + String(VERSION_MINOR) + F(".") + String(VERSION_PATCH));
DBGPRINT(F("Github Hash: ")); DBGPRINT(F("Github Hash: "));
DBGPRINTLN(String(AUTO_GIT_HASH)); DBGPRINTLN(String(AUTO_GIT_HASH));
DBGPRINT(F("\n---------\nAP MODE\nSSID: ")); DBGPRINT(F("\n---------\nAP MODE\nSSID: "));
DBGPRINTLN(ssid); DBGPRINTLN(WIFI_AP_SSID);
DBGPRINT(F("PWD: ")); DBGPRINT(F("PWD: "));
DBGPRINTLN(pwd); DBGPRINTLN(WIFI_AP_PWD);
DBGPRINT(F("\nActive for: ")); DBGPRINTLN("IP Address: http://" + apIp.toString());
DBGPRINT(String(WIFI_AP_ACTIVE_TIME)); DBGPRINTLN(F("---------\n"));
DBGPRINTLN(F(" seconds"));
DBGPRINTLN("\nIp Address: " + apIp[0] + apIp[1] + apIp[2] + apIp[3]);
DBGPRINTLN(F("\n---------\n"));
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP_STA);
WiFi.softAPConfig(apIp, apIp, IPAddress(255, 255, 255, 0)); WiFi.softAPConfig(apIp, apIp, IPAddress(255, 255, 255, 0));
WiFi.softAP(ssid, pwd); WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PWD);
mDns->start(53, "*", apIp); mDns.start(53, "*", apIp);
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
bool ahoywifi::setupStation(uint32_t timeout) { void ahoywifi::setupStation(void) {
DPRINTLN(DBG_VERBOSE, F("app::setupStation")); DPRINTLN(DBG_VERBOSE, F("wifi::setupStation"));
int32_t cnt;
bool startAp = false;
if(timeout >= 3)
cnt = (timeout - 3) / 2 * 10;
else {
timeout = 1;
cnt = 1;
}
WiFi.mode(WIFI_STA);
if(mConfig->sys.ip.ip[0] != 0) { if(mConfig->sys.ip.ip[0] != 0) {
IPAddress ip(mConfig->sys.ip.ip); IPAddress ip(mConfig->sys.ip.ip);
IPAddress mask(mConfig->sys.ip.mask); IPAddress mask(mConfig->sys.ip.mask);
@ -165,76 +104,48 @@ bool ahoywifi::setupStation(uint32_t timeout) {
if(String(mConfig->sys.deviceName) != "") if(String(mConfig->sys.deviceName) != "")
WiFi.hostname(mConfig->sys.deviceName); WiFi.hostname(mConfig->sys.deviceName);
delay(2000);
DBGPRINT(F("connect to network '")); DBGPRINT(F("connect to network '"));
DBGPRINT(mConfig->sys.stationSsid); DBGPRINT(mConfig->sys.stationSsid);
DBGPRINTLN(F("' ...")); DBGPRINTLN(F("' ..."));
while (WiFi.status() != WL_CONNECTED) {
delay(100);
if(cnt % 40 == 0)
DBGPRINTLN(".");
else
DBGPRINT(".");
if(timeout > 0) { // limit == 0 -> no limit
if(--cnt <= 0) {
if(WiFi.status() != WL_CONNECTED) {
startAp = true;
WiFi.disconnect();
}
delay(100);
break;
}
}
}
Serial.println(".");
if(false == startAp)
wifiWasEstablished = true;
delay(1000);
return startAp;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
bool ahoywifi::getApActive(void) { bool ahoywifi::getNtpTime(void) {
return mApActive;
}
//-----------------------------------------------------------------------------
time_t ahoywifi::getNtpTime(void) {
//DPRINTLN(DBG_VERBOSE, F("wifi::getNtpTime")); //DPRINTLN(DBG_VERBOSE, F("wifi::getNtpTime"));
time_t date = 0; if(!mConnected)
return false;
IPAddress timeServer; IPAddress timeServer;
uint8_t buf[NTP_PACKET_SIZE]; uint8_t buf[NTP_PACKET_SIZE];
uint8_t retry = 0; uint8_t retry = 0;
WiFi.hostByName(mConfig->ntp.addr, timeServer); WiFi.hostByName(mConfig->ntp.addr, timeServer);
mUdp->begin(mConfig->ntp.port); mUdp.begin(mConfig->ntp.port);
sendNTPpacket(timeServer); sendNTPpacket(timeServer);
while(retry++ < 5) { while(retry++ < 5) {
int wait = 150; int wait = 150;
while(--wait) { while(--wait) {
if(NTP_PACKET_SIZE <= mUdp->parsePacket()) { if(NTP_PACKET_SIZE <= mUdp.parsePacket()) {
uint64_t secsSince1900; uint64_t secsSince1900;
mUdp->read(buf, NTP_PACKET_SIZE); mUdp.read(buf, NTP_PACKET_SIZE);
secsSince1900 = (buf[40] << 24); secsSince1900 = (buf[40] << 24);
secsSince1900 |= (buf[41] << 16); secsSince1900 |= (buf[41] << 16);
secsSince1900 |= (buf[42] << 8); secsSince1900 |= (buf[42] << 8);
secsSince1900 |= (buf[43] ); secsSince1900 |= (buf[43] );
date = secsSince1900 - 2208988800UL; // UTC time *mUtcTimestamp = secsSince1900 - 2208988800UL; // UTC time
break; DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC");
} return true;
else } else
delay(10); delay(10);
} }
} }
return date; DPRINTLN(DBG_INFO, F("[NTP]: getNtpTime failed"));
return false;
} }
@ -283,7 +194,75 @@ void ahoywifi::sendNTPpacket(IPAddress& address) {
buf[14] = 49; buf[14] = 49;
buf[15] = 52; buf[15] = 52;
mUdp->beginPacket(address, 123); // NTP request, port 123 mUdp.beginPacket(address, 123); // NTP request, port 123
mUdp->write(buf, NTP_PACKET_SIZE); mUdp.write(buf, NTP_PACKET_SIZE);
mUdp->endPacket(); mUdp.endPacket();
}
//-----------------------------------------------------------------------------
#if defined(ESP8266)
void ahoywifi::onConnect(const WiFiEventStationModeGotIP& event) {
if(!mConnected) {
mConnected = true;
mReconnect = false;
DBGPRINTLN(F("\n[WiFi] Connected"));
WiFi.mode(WIFI_STA);
DBGPRINTLN(F("[WiFi] AP disabled"));
mDns.stop();
welcome(WiFi.localIP().toString() + F(" (Station)"));
}
}
//-------------------------------------------------------------------------
void ahoywifi::onDisconnect(const WiFiEventStationModeDisconnected& event) {
if(mConnected) {
mConnected = false;
mReconnect = true;
DPRINTLN(DBG_INFO, "[WiFi] Connection Lost");
}
}
#else
//-------------------------------------------------------------------------
void ahoywifi::onWiFiEvent(WiFiEvent_t event) {
switch(event) {
case SYSTEM_EVENT_STA_GOT_IP:
if(!mConnected) {
delay(1000);
mConnected = true;
DBGPRINTLN(F("\n[WiFi] Connected"));
welcome(WiFi.localIP().toString() + F(" (Station)"));
WiFi.mode(WIFI_STA);
WiFi.begin();
DBGPRINTLN(F("[WiFi] AP disabled"));
mDns.stop();
}
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
if(mConnected) {
mConnected = false;
DPRINTLN(DBG_INFO, "[WiFi] Connection Lost");
}
break;
default:
break;
}
}
#endif
//-----------------------------------------------------------------------------
void ahoywifi::welcome(String msg) {
DBGPRINTLN(F("\n\n--------------------------------"));
DBGPRINTLN(F("Welcome to AHOY!"));
DBGPRINT(F("\npoint your browser to http://"));
DBGPRINTLN(msg);
DBGPRINTLN(F("to configure your device"));
DBGPRINTLN(F("--------------------------------\n"));
} }

41
src/wifi/ahoywifi.h

@ -9,7 +9,6 @@
#include "../utils/dbg.h" #include "../utils/dbg.h"
#include <Arduino.h> #include <Arduino.h>
#include <WiFiUdp.h> #include <WiFiUdp.h>
#include <TimeLib.h>
#include <DNSServer.h> #include <DNSServer.h>
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
@ -19,32 +18,38 @@ class app;
class ahoywifi { class ahoywifi {
public: public:
ahoywifi(settings_t *config); ahoywifi();
~ahoywifi() {}
void setup(settings_t *config, uint32_t *utcTimestamp);
void setup(uint32_t timeout, bool settingValid); void loop(void);
bool loop(void); bool getNtpTime(void);
void setupAp(const char *ssid, const char *pwd);
bool setupStation(uint32_t timeout);
bool getApActive(void);
time_t getNtpTime(void);
void scanAvailNetworks(void); void scanAvailNetworks(void);
void getAvailNetworks(JsonObject obj); void getAvailNetworks(JsonObject obj);
private: private:
void setupAp(void);
void setupStation(void);
void sendNTPpacket(IPAddress& address); void sendNTPpacket(IPAddress& address);
#if defined(ESP8266)
void onConnect(const WiFiEventStationModeGotIP& event);
void onDisconnect(const WiFiEventStationModeDisconnected& event);
#else
void onWiFiEvent(WiFiEvent_t event);
#endif
void welcome(String msg);
settings_t *mConfig; settings_t *mConfig;
DNSServer *mDns; DNSServer mDns;
WiFiUDP *mUdp; // for time server WiFiUDP mUdp; // for time server
#if defined(ESP8266)
WiFiEventHandler wifiConnectHandler;
WiFiEventHandler wifiDisconnectHandler;
#endif
uint32_t mWifiStationTimeout; bool mConnected, mReconnect;
uint32_t mNextTryTs; uint8_t mCnt;
uint32_t mApLastTick; uint32_t *mUtcTimestamp;
bool mApActive;
bool wifiWasEstablished;
bool mStationWifiIsDef;
}; };
#endif /*__AHOYWIFI_H__*/ #endif /*__AHOYWIFI_H__*/

Loading…
Cancel
Save