Browse Source

Merge branch 'development03' into nrf_info

pull/470/head
DanielR92 2 years ago
committed by GitHub
parent
commit
58fede748d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/compile_development.yml
  2. 261
      User_Manual.md
  3. 10
      scripts/getVersion.py
  4. 22
      src/app.cpp
  5. 5
      src/app.h
  6. 2
      src/config/config.h
  7. 41
      src/config/settings.h
  8. 2
      src/defines.h
  9. 2
      src/hm/hmSystem.h
  10. 3
      src/platformio.ini
  11. 454
      src/publisher/pubMqtt.h
  12. 33
      src/utils/helper.cpp
  13. 20
      src/utils/helper.h
  14. 4
      src/utils/sun.h
  15. 6
      src/web/html/index.html
  16. 49
      src/web/html/serial.html
  17. 8
      src/web/html/setup.html
  18. 6
      src/web/html/update.html
  19. 56
      src/web/web.cpp
  20. 11
      src/web/web.h
  21. 136
      src/web/webApi.cpp
  22. 14
      src/web/webApi.h
  23. 14
      src/wifi/ahoywifi.cpp
  24. 4
      src/wifi/ahoywifi.h

2
.github/workflows/compile_development.yml

@ -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

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_relative/<INVERTER_ID>
```
with a payload `[0 .. 65535]`
Example:
```mqtt
inverter/ctrl/limit_persistent_relative/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_relative/<INVERTER_ID>
```
with a payload `[0 .. 65535]`
Example:
```mqtt
inverter/ctrl/limit_nonpersistent_relative/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 | | |
| ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------- | --------- | | ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------- | --------- |

10
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")
@ -48,16 +55,19 @@ def readVersion(path, infile):
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)
gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_esp8266_1m_" + sha + ".bin" versionout = version[:-1] + "_esp8266_1m_" + 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)
gzip_bin(dst, dst + ".gz")
# other ESP32 bin files # other ESP32 bin files
src = path + ".pio/build/esp32-wroom32-release/" src = path + ".pio/build/esp32-wroom32-release/"

22
src/app.cpp

@ -12,6 +12,12 @@
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "utils/sun.h" #include "utils/sun.h"
//-----------------------------------------------------------------------------
app::app() : ah::Scheduler() {
mWeb = NULL;
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::setup(uint32_t timeout) { void app::setup(uint32_t timeout) {
Serial.begin(115200); Serial.begin(115200);
@ -28,12 +34,12 @@ void app::setup(uint32_t timeout) {
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());
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);
mWifi = new ahoywifi(mConfig);
mWifi->setup(timeout, mSettings.getValid());
if(mSys->Radio.isChipConnected()) if(mSys->Radio.isChipConnected())
{ {
@ -54,6 +60,7 @@ void app::setup(uint32_t timeout) {
addListener(EVERY_SEC, std::bind(&PubMqttType::tickerSecond, &mMqtt)); addListener(EVERY_SEC, std::bind(&PubMqttType::tickerSecond, &mMqtt));
addListener(EVERY_MIN, std::bind(&PubMqttType::tickerMinute, &mMqtt)); addListener(EVERY_MIN, std::bind(&PubMqttType::tickerMinute, &mMqtt));
addListener(EVERY_HR, std::bind(&PubMqttType::tickerHour, &mMqtt)); addListener(EVERY_HR, std::bind(&PubMqttType::tickerHour, &mMqtt));
mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
} }
setupLed(); setupLed();
@ -229,7 +236,8 @@ void app::resetSystem(void) {
mUtcTimestamp = 0; mUtcTimestamp = 0;
#endif #endif
mHeapStatCnt = 0; mSunrise = 0;
mSunset = 0;
mSendTicker = 0xffff; mSendTicker = 0xffff;
@ -242,6 +250,12 @@ void app::resetSystem(void) {
memset(&mStat, 0, sizeof(statistics_t)); memset(&mStat, 0, sizeof(statistics_t));
} }
//-----------------------------------------------------------------------------
void app::mqttSubRxCb(JsonObject obj) {
if(NULL != mWeb)
mWeb->apiCtrlRequest(obj);
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void app::setupLed(void) { void app::setupLed(void) {
/** LED connection diagram /** LED connection diagram

5
src/app.h

@ -43,7 +43,7 @@ class web;
class app : public ah::Scheduler { class app : public ah::Scheduler {
public: public:
app() : ah::Scheduler() {} app();
~app() {} ~app() {}
void setup(uint32_t timeout); void setup(uint32_t timeout);
@ -140,7 +140,7 @@ class app : public ah::Scheduler {
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);
@ -197,7 +197,6 @@ class app : public ah::Scheduler {
} }
uint32_t mUptimeSecs; uint32_t mUptimeSecs;
uint8_t mHeapStatCnt;
uint32_t mUtcTimestamp; uint32_t mUtcTimestamp;
bool mUpdateNtp; bool mUpdateNtp;

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

41
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"
/** /**
@ -223,25 +224,6 @@ 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_INFO, F("loadDefaults"));
@ -288,25 +270,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*>());
} }
} }

2
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 46
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {

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();

3
src/platformio.ini

@ -36,7 +36,8 @@ 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
;knolleary/PubSubClient
bblanchon/ArduinoJson bblanchon/ArduinoJson
;esp8266/DNSServer ;esp8266/DNSServer
;esp8266/EEPROM ;esp8266/EEPROM

454
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,115 +17,120 @@
#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;
} }
~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();
} }
} }
void tickerHour() { void tickerHour() {
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 publish(const char *subTopic, const char *payload, bool retained = false, bool addTopic = true) {
mClient->setCallback(callback); char topic[MQTT_TOPIC_LEN + 2];
snprintf(topic, (MQTT_TOPIC_LEN + 2), "%s/%s", mCfgMqtt->topic, subTopic);
if(addTopic)
mClient.publish(topic, QOS_0, retained, payload);
else
mClient.publish(subTopic, QOS_0, retained, payload);
mTxCnt++;
} }
void sendMsg(const char *topic, const char *msg) { void subscribe(const char *subTopic) {
//DPRINTLN(DBG_VERBOSE, F("mqtt.h:sendMsg")); char topic[MQTT_TOPIC_LEN + 20];
if(mAddressSet) { snprintf(topic, (MQTT_TOPIC_LEN + 20), "%s/%s", mCfgMqtt->topic, subTopic);
char top[66]; mClient.subscribe(topic, QOS_0);
snprintf(top, 66, "%s/%s", mCfg_mqtt->topic, topic);
sendMsg2(top, msg, false);
}
} }
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 setSubscriptionCb(subscriptionCb cb) {
//DPRINTLN(DBG_VERBOSE, F("mqtt.h:isConnected")); mSubscriptionCb = cb;
if(!mAddressSet)
return false;
if(doRecon && !mClient->connected())
reconnect();
return mClient->connected();
} }
void payloadEventListener(uint8_t cmd) { inline bool isConnected() {
mSendList.push(cmd); return mClient.connected();
} }
uint32_t getTxCnt(void) { inline uint32_t getTxCnt(void) {
return mTxCnt; return mTxCnt;
} }
inline uint32_t getRxCnt(void) {
return mRxCnt;
}
void sendMqttDiscoveryConfig(const char *topic) { void sendMqttDiscoveryConfig(const char *topic) {
DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig")); DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig"));
@ -133,11 +140,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);
@ -153,20 +160,19 @@ class PubMqtt {
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")] = 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 +182,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);
publish("uptime", "0");
publish(mLwtTopic, mLwtOnline, true, false);
subscribe("ctrl/#");
subscribe("setup/#");
subscribe("status/#");
}
void onDisconnect(espMqttClientTypes::DisconnectReason reason) {
DBGPRINT(F("MQTT disconnected, reason: "));
switch (reason) {
case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED:
DBGPRINTLN(F("TCP disconnect"));
break;
case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
DBGPRINTLN(F("wrong protocol version"));
break;
case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED:
DBGPRINTLN(F("identifier rejected"));
break;
case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE:
DBGPRINTLN(F("broker unavailable"));
break;
case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS:
DBGPRINTLN(F("malformed credentials"));
break;
case espMqttClientTypes::DisconnectReason::MQTT_NOT_AUTHORIZED:
DBGPRINTLN(F("not authorized"));
break;
default:
DBGPRINTLN(F("unknown"));
}
}
boolean resub = false; void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
if(!mClient->connected() && (millis() - mLastReconnect) > MQTT_RECONNECT_DELAY ) { DPRINTLN(DBG_VERBOSE, F("MQTT got topic: ") + String(topic));
mLastReconnect = millis(); if(NULL == mSubscriptionCb)
if(strlen(mDevName) > 0) { return;
// der Server und der Port müssen neu gesetzt werden,
// da ein MQTT_CONNECTION_LOST -3 die Werte zerstört hat. char *tpc = new char[strlen(topic) + 1];
mClient->setServer(mCfg_mqtt->broker, mCfg_mqtt->port); uint8_t cnt = 0;
mClient->setBufferSize(MQTT_MAX_PACKET_SIZE); DynamicJsonDocument json(128);
JsonObject root = json.to<JsonObject>();
char lwt[MQTT_TOPIC_LEN + 7 ]; // "/uptime" --> + 7 byte
snprintf(lwt, MQTT_TOPIC_LEN + 7, "%s/uptime", mCfg_mqtt->topic); strncpy(tpc, topic, strlen(topic) + 1);
if(len > 0) {
if((strlen(mCfg_mqtt->user) > 0) && (strlen(mCfg_mqtt->pwd) > 0)) char *pyld = new char[len + 1];
resub = mClient->connect(mDevName, mCfg_mqtt->user, mCfg_mqtt->pwd, lwt, 0, false, "offline"); strncpy(pyld, (const char*)payload, len);
else pyld[len] = '\0';
resub = mClient->connect(mDevName, lwt, 0, false, "offline"); root["val"] = atoi(pyld);
// ein Subscribe ist nur nach einem connect notwendig delete[] pyld;
if(resub) { }
char topic[MQTT_TOPIC_LEN + 13 ]; // "/devcontrol/#" --> + 6 byte
// ToDo: "/devcontrol/#" is hardcoded char *p = strtok(tpc, "/");
snprintf(topic, MQTT_TOPIC_LEN + 13, "%s/devcontrol/#", mCfg_mqtt->topic); p = strtok(NULL, "/"); // remove mCfgMqtt->topic
DPRINTLN(DBG_INFO, F("subscribe to ") + String(topic)); while(NULL != p) {
mClient->subscribe(topic); // subscribe to mTopic + "/devcontrol/#" 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,7 +331,6 @@ class PubMqtt {
if(mSendList.empty()) if(mSendList.empty())
return; return;
isConnected(true); // really needed? See comment from HorstG-57 #176
char topic[32 + MAX_NAME_LENGTH], val[40]; char topic[32 + MAX_NAME_LENGTH], val[40];
float total[4]; float total[4];
bool sendTotal = false; bool sendTotal = false;
@ -262,28 +358,38 @@ class PubMqtt {
} }
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, "%i", iv->getLastTs(rec) * 1000);
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) {
@ -331,128 +437,32 @@ 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;
// 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__*/

33
src/utils/helper.cpp

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

20
src/utils/helper.h

@ -0,0 +1,20 @@
//-----------------------------------------------------------------------------
// 2022 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
#ifndef __HELPER_H__
#define __HELPER_H__
#include <cstdint>
#include <cstring>
#include <stdio.h>
#include <stdlib.h>
namespace ah {
void ip2Arr(uint8_t ip[], const char *ipStr);
void ip2Char(uint8_t ip[], char *str);
double round3(double value);
}
#endif /*__HELPER_H__*/

4
src/utils/sun.h

@ -11,7 +11,7 @@ namespace ah {
// 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 + correction 69.12s
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;

6
src/web/html/index.html

@ -97,7 +97,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));
} }
@ -196,9 +196,9 @@
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"]; document.getElementById("refresh").innerHTML = 10;
if(exeOnce) { if(exeOnce) {
window.setInterval("getAjax('/api/index', parse)", obj["refresh_interval"] * 1000); window.setInterval("getAjax('/api/index', parse)", 10000);
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));
}); });

8
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"/>
@ -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() {

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>

56
src/web/web.cpp

@ -11,6 +11,7 @@
#include "web.h" #include "web.h"
#include "../utils/ahoyTimer.h" #include "../utils/ahoyTimer.h"
#include "../utils/helper.h"
#include "html/h/index_html.h" #include "html/h/index_html.h"
#include "html/h/login_html.h" #include "html/h/login_html.h"
@ -107,8 +108,10 @@ void web::loop(void) {
void web::tickSecond() { void web::tickSecond() {
if(0 != mLogoutTimeout) { if(0 != mLogoutTimeout) {
mLogoutTimeout -= 1; mLogoutTimeout -= 1;
if(0 == mLogoutTimeout) if(0 == mLogoutTimeout) {
mProtected = true; if(strlen(mConfig->sys.adminPwd) > 0)
mProtected = true;
}
DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout)); DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout));
} }
@ -341,28 +344,16 @@ void web::showSave(AsyncWebServerRequest *request) {
// static ip // static ip
if(request->arg("ipAddr") != "") { request->arg("ipAddr").toCharArray(buf, 20);
request->arg("ipAddr").toCharArray(buf, SSID_LEN); ah::ip2Arr(mConfig->sys.ip.ip, buf);
ip2Arr(mConfig->sys.ip.ip, buf); request->arg("ipMask").toCharArray(buf, 20);
if(request->arg("ipMask") != "") { ah::ip2Arr(mConfig->sys.ip.mask, buf);
request->arg("ipMask").toCharArray(buf, SSID_LEN); request->arg("ipDns1").toCharArray(buf, 20);
ip2Arr(mConfig->sys.ip.mask, buf); ah::ip2Arr(mConfig->sys.ip.dns1, buf);
} request->arg("ipDns2").toCharArray(buf, 20);
if(request->arg("ipDns1") != "") { ah::ip2Arr(mConfig->sys.ip.dns2, buf);
request->arg("ipDns1").toCharArray(buf, SSID_LEN); request->arg("ipGateway").toCharArray(buf, 20);
ip2Arr(mConfig->sys.ip.dns1, buf); ah::ip2Arr(mConfig->sys.ip.gateway, 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
@ -435,12 +426,14 @@ void web::showSave(AsyncWebServerRequest *request) {
String addr = request->arg("mqttAddr"); String addr = request->arg("mqttAddr");
addr.trim(); addr.trim();
addr.toCharArray(mConfig->mqtt.broker, MQTT_ADDR_LEN); 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();
} }
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 // serial console
if(request->arg("serIntvl") != "") { if(request->arg("serIntvl") != "") {
@ -646,9 +639,12 @@ void web::serialCb(String msg) {
mSerialBufFill = 0; mSerialBufFill = 0;
mEvts->send("webSerial, buffer overflow!", "serial", millis()); mEvts->send("webSerial, buffer overflow!", "serial", millis());
} }
} }
//-----------------------------------------------------------------------------
void web::apiCtrlRequest(JsonObject obj) {
mApi->ctrlRequest(obj);
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifdef ENABLE_JSON_EP #ifdef ENABLE_JSON_EP

11
src/web/web.h

@ -40,6 +40,8 @@ class web {
void serialCb(String msg); void serialCb(String msg);
void apiCtrlRequest(JsonObject obj);
private: private:
void onConnect(AsyncEventSourceClient *client); void onConnect(AsyncEventSourceClient *client);
@ -62,15 +64,6 @@ class web {
void onSerial(AsyncWebServerRequest *request); void onSerial(AsyncWebServerRequest *request);
void onSystem(AsyncWebServerRequest *request); void onSystem(AsyncWebServerRequest *request);
void ip2Arr(uint8_t ip[], char *ipStr) {
char *p = strtok(ipStr, ".");
uint8_t i = 0;
while(NULL != p) {
ip[i++] = atoi(p);
p = strtok(NULL, ".");
}
}
#ifdef ENABLE_JSON_EP #ifdef ENABLE_JSON_EP
void showJson(void); void showJson(void);
#endif #endif

136
src/web/webApi.cpp

@ -27,13 +27,26 @@ void webApi::setup(void) {
mSrv->on("/api", HTTP_POST, std::bind(&webApi::onApiPost, this, std::placeholders::_1)).onBody( 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)); 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)); mSrv->on("/get_setup", HTTP_GET, std::bind(&webApi::onDwnldSetup, this, std::placeholders::_1));
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void webApi::loop(void) { void webApi::loop(void) {
} }
//-----------------------------------------------------------------------------
void webApi::ctrlRequest(JsonObject obj) {
/*char out[128];
serializeJson(obj, out, 128);
DPRINTLN(DBG_INFO, "webApi: " + 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);
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void webApi::onApi(AsyncWebServerRequest *request) { void webApi::onApi(AsyncWebServerRequest *request) {
@ -325,13 +338,12 @@ void webApi::getSerial(JsonObject obj) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void webApi::getStaticIp(JsonObject obj) { void webApi::getStaticIp(JsonObject obj) {
if(mConfig->sys.ip.ip[0] != 0) { char buf[16];
obj[F("ip")] = ip2String(mConfig->sys.ip.ip); ah::ip2Char(mConfig->sys.ip.ip, buf); obj[F("ip")] = String(buf);
obj[F("mask")] = ip2String(mConfig->sys.ip.mask); ah::ip2Char(mConfig->sys.ip.mask, buf); obj[F("mask")] = String(buf);
obj[F("dns1")] = ip2String(mConfig->sys.ip.dns1); ah::ip2Char(mConfig->sys.ip.dns1, buf); obj[F("dns1")] = String(buf);
obj[F("dns2")] = ip2String(mConfig->sys.ip.dns2); ah::ip2Char(mConfig->sys.ip.dns2, buf); obj[F("dns2")] = String(buf);
obj[F("gateway")] = ip2String(mConfig->sys.ip.gateway); ah::ip2Char(mConfig->sys.ip.gateway, buf); obj[F("gateway")] = String(buf);
}
} }
@ -339,7 +351,7 @@ void webApi::getStaticIp(JsonObject obj) {
void webApi::getMenu(JsonObject obj) { void webApi::getMenu(JsonObject obj) {
obj["name"][0] = "Live"; obj["name"][0] = "Live";
obj["link"][0] = "/live"; obj["link"][0] = "/live";
obj["name"][1] = "Serial Console"; obj["name"][1] = "Serial / Control";
obj["link"][1] = "/serial"; obj["link"][1] = "/serial";
obj["name"][2] = "Settings"; obj["name"][2] = "Settings";
obj["link"][2] = "/setup"; obj["link"][2] = "/setup";
@ -352,10 +364,14 @@ void webApi::getMenu(JsonObject obj) {
obj["link"][6] = "/update"; obj["link"][6] = "/update";
obj["name"][7] = "System"; obj["name"][7] = "System";
obj["link"][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) { if(strlen(mConfig->sys.adminPwd) > 0) {
obj["name"][8] = "-"; obj["name"][10] = "-";
obj["name"][9] = "Logout"; obj["name"][11] = "Logout";
obj["link"][9] = "/logout"; obj["link"][11] = "/logout";
} }
} }
@ -390,7 +406,7 @@ void webApi::getIndex(JsonObject obj) {
else if(!mApp->mSys->Radio.isPVariant()) else if(!mApp->mSys->Radio.isPVariant())
warn.add(F("your NRF24 module have not a plus(+), please check!")); warn.add(F("your NRF24 module have not a plus(+), please check!"));
if(!mApp->mqttIsConnected()) if((!mApp->mqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0))
warn.add(F("MQTT is not connected")); warn.add(F("MQTT is not connected"));
JsonArray info = obj.createNestedArray(F("infos")); JsonArray info = obj.createNestedArray(F("infos"));
@ -442,7 +458,7 @@ void webApi::getLive(JsonObject obj) {
JsonObject obj2 = invArr.createNestedObject(); JsonObject obj2 = invArr.createNestedObject();
obj2[F("name")] = String(iv->config->name); obj2[F("name")] = String(iv->config->name);
obj2[F("channels")] = iv->channels; obj2[F("channels")] = iv->channels;
obj2[F("power_limit_read")] = round3(iv->actPowerLimit); obj2[F("power_limit_read")] = ah::round3(iv->actPowerLimit);
obj2[F("last_alarm")] = String(iv->lastAlarmMsg); obj2[F("last_alarm")] = String(iv->lastAlarmMsg);
obj2[F("ts_last_success")] = rec->ts; obj2[F("ts_last_success")] = rec->ts;
@ -451,7 +467,7 @@ void webApi::getLive(JsonObject obj) {
obj2[F("ch_names")][0] = "AC"; obj2[F("ch_names")][0] = "AC";
for (uint8_t fld = 0; fld < sizeof(list); fld++) { for (uint8_t fld = 0; fld < sizeof(list); fld++) {
pos = (iv->getPosByChFld(CH0, list[fld], rec)); pos = (iv->getPosByChFld(CH0, list[fld], rec));
ch0[fld] = (0xff != pos) ? round3(iv->getValue(pos, rec)) : 0.0; 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_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail;
obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail; obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
} }
@ -468,7 +484,7 @@ void webApi::getLive(JsonObject obj) {
case 4: pos = (iv->getPosByChFld(j, FLD_YT, rec)); break; case 4: pos = (iv->getPosByChFld(j, FLD_YT, rec)); break;
case 5: pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break; case 5: pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break;
} }
cur[k] = (0xff != pos) ? round3(iv->getValue(pos, rec)) : 0.0; cur[k] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
if(1 == j) { if(1 == j) {
obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail; 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; obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
@ -504,72 +520,50 @@ void webApi::getRecord(JsonObject obj, record_t<> *rec) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
bool webApi::setCtrl(JsonObject jsonIn, JsonObject jsonOut) { bool webApi::setCtrl(JsonObject jsonIn, JsonObject jsonOut) {
uint8_t cmd = jsonIn[F("cmd")]; Inverter<> *iv = mApp->mSys->getInverterByPos(jsonIn[F("id")]);
if(NULL == iv) {
// Todo: num is the inverter number 0-3. For better display in DPRINTLN jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as<String>();
uint8_t num = jsonIn[F("inverter")]; return false;
uint8_t tx_request = jsonIn[F("tx_request")]; }
if(TX_REQ_DEVCONTROL == tx_request) if(F("power") == jsonIn[F("cmd")]) {
{ iv->devControlCmd = (jsonIn[F("val")] == 1) ? TurnOn : TurnOff;
DPRINTLN(DBG_INFO, F("devcontrol [") + String(num) + F("], cmd: 0x") + String(cmd, HEX)); iv->devControlRequest = true;
} else if(F("restart") == jsonIn[F("restart")]) {
Inverter<> *iv = getInverter(jsonIn, jsonOut); iv->devControlCmd = Restart;
JsonArray payload = jsonIn[F("payload")].as<JsonArray>(); iv->devControlRequest = true;
}
if(NULL != iv) else if(0 == strncmp("limit_", jsonIn[F("cmd")].as<const char*>(), 6)) {
{ iv->powerLimit[0] = jsonIn["val"];
switch (cmd) if(F("limit_persistent_relative") == jsonIn[F("cmd")])
{ iv->powerLimit[1] = RelativPersistent;
case TurnOn: else if(F("limit_persistent_absolute") == jsonIn[F("cmd")])
iv->devControlCmd = TurnOn; iv->powerLimit[1] = AbsolutPersistent;
iv->devControlRequest = true; else if(F("limit_nonpersistent_relative") == jsonIn[F("cmd")])
break; iv->powerLimit[1] = RelativNonPersistent;
case TurnOff: else if(F("limit_nonpersistent_absolute") == jsonIn[F("cmd")])
iv->devControlCmd = TurnOff; iv->powerLimit[1] = AbsolutNonPersistent;
iv->devControlRequest = true; iv->devControlCmd = ActivePowerContr;
break; iv->devControlRequest = true;
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 { else {
jsonOut[F("error")] = F("unknown 'tx_request'"); jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as<String>() + "'";
return false; return false;
} }
return true; return true;
} }
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
bool webApi::setSetup(JsonObject jsonIn, JsonObject jsonOut) { bool webApi::setSetup(JsonObject jsonIn, JsonObject jsonOut) {
if(F("scan_wifi") == jsonIn[F("cmd")]) if(F("scan_wifi") == jsonIn[F("cmd")])
mApp->scanAvailNetworks(); mApp->scanAvailNetworks();
else if(F("set_time") == jsonIn[F("cmd")]) else if(F("set_time") == jsonIn[F("cmd")])
mApp->setTimestamp(jsonIn[F("ts")]); mApp->setTimestamp(jsonIn[F("val")]);
else if(F("sync_ntp") == jsonIn[F("cmd")]) else if(F("sync_ntp") == jsonIn[F("cmd")])
mApp->setTimestamp(0); // 0: update ntp flag mApp->setTimestamp(0); // 0: update ntp flag
else if(F("serial_utc_offset") == jsonIn[F("cmd")]) else if(F("serial_utc_offset") == jsonIn[F("cmd")])
mTimezoneOffset = jsonIn[F("ts")]; mTimezoneOffset = jsonIn[F("val")];
else if(F("discovery_cfg") == jsonIn[F("cmd")]) else if(F("discovery_cfg") == jsonIn[F("cmd")])
mApp->mFlagSendDiscoveryConfig = true; // for homeassistant mApp->mFlagSendDiscoveryConfig = true; // for homeassistant
else { else {
@ -579,13 +573,3 @@ bool webApi::setSetup(JsonObject jsonIn, JsonObject jsonOut) {
return true; 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;
}

14
src/web/webApi.h

@ -25,6 +25,8 @@ class webApi {
return mTimezoneOffset; return mTimezoneOffset;
} }
void ctrlRequest(JsonObject obj);
private: private:
void onApi(AsyncWebServerRequest *request); void onApi(AsyncWebServerRequest *request);
void onApiPost(AsyncWebServerRequest *request); void onApiPost(AsyncWebServerRequest *request);
@ -57,18 +59,6 @@ class webApi {
bool setCtrl(JsonObject jsonIn, JsonObject jsonOut); bool setCtrl(JsonObject jsonIn, JsonObject jsonOut);
bool setSetup(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; AsyncWebServer *mSrv;
app *mApp; app *mApp;

14
src/wifi/ahoywifi.cpp

@ -32,6 +32,8 @@ ahoywifi::ahoywifi(settings_t *config) {
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
void ahoywifi::setup(uint32_t timeout, bool settingValid) { void ahoywifi::setup(uint32_t timeout, bool settingValid) {
//wifiConnectHandler = WiFi.onStationModeGotIP(std::bind(&ahoywifi::onConnect, this, std::placeholders::_1));
//wifiDisconnectHandler = WiFi.onStationModeDisconnected(std::bind(&ahoywifi::onDisconnect, this, std::placeholders::_1));
#ifdef FB_WIFI_OVERRIDDEN #ifdef FB_WIFI_OVERRIDDEN
mStationWifiIsDef = false; mStationWifiIsDef = false;
@ -287,3 +289,15 @@ void ahoywifi::sendNTPpacket(IPAddress& address) {
mUdp->write(buf, NTP_PACKET_SIZE); mUdp->write(buf, NTP_PACKET_SIZE);
mUdp->endPacket(); mUdp->endPacket();
} }
//-----------------------------------------------------------------------------
/*void ahoywifi::onConnect(const WiFiEventStationModeGotIP& event) {
Serial.println("Connected to Wi-Fi.");
}
//-----------------------------------------------------------------------------
void ahoywifi::onDisconnect(const WiFiEventStationModeDisconnected& event) {
Serial.println("Disconnected from Wi-Fi.");
}*/

4
src/wifi/ahoywifi.h

@ -33,11 +33,15 @@ class ahoywifi {
private: private:
void sendNTPpacket(IPAddress& address); void sendNTPpacket(IPAddress& address);
//void onConnect(const WiFiEventStationModeGotIP& event);
//void onDisconnect(const WiFiEventStationModeDisconnected& event);
settings_t *mConfig; settings_t *mConfig;
DNSServer *mDns; DNSServer *mDns;
WiFiUDP *mUdp; // for time server WiFiUDP *mUdp; // for time server
//WiFiEventHandler wifiConnectHandler;
//WiFiEventHandler wifiDisconnectHandler;
uint32_t mWifiStationTimeout; uint32_t mWifiStationTimeout;
uint32_t mNextTryTs; uint32_t mNextTryTs;

Loading…
Cancel
Save