Browse Source

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

pull/498/head
lumapu 2 years ago
parent
commit
105e4c6657
  1. 4
      .github/workflows/compile_development.yml
  2. 2
      .github/workflows/compile_release.yml
  3. 255
      User_Manual.md
  4. 37
      scripts/getVersion.py
  5. 126
      src/app.cpp
  6. 76
      src/app.h
  7. 2
      src/config/config.h
  8. 51
      src/config/settings.h
  9. 2
      src/defines.h
  10. 7
      src/hm/hmInverter.h
  11. 9
      src/hm/hmRadio.h
  12. 2
      src/hm/hmSystem.h
  13. 2
      src/main.cpp
  14. 72
      src/platformio.ini
  15. 285
      src/plugins/MonochromeDisplay/MonochromeDisplay.h
  16. 464
      src/publisher/pubMqtt.h
  17. 13
      src/publisher/pubSerial.h
  18. 42
      src/utils/helper.cpp
  19. 23
      src/utils/helper.h
  20. 108
      src/utils/llist.h
  21. 150
      src/utils/scheduler.h
  22. 6
      src/utils/sun.h
  23. 35
      src/web/html/index.html
  24. 49
      src/web/html/serial.html
  25. 8
      src/web/html/setup.html
  26. 6
      src/web/html/update.html
  27. 50
      src/web/web.cpp
  28. 11
      src/web/web.h
  29. 123
      src/web/webApi.cpp
  30. 14
      src/web/webApi.h
  31. 267
      src/wifi/ahoywifi.cpp
  32. 41
      src/wifi/ahoywifi.h

4
.github/workflows/compile_development.yml

@ -47,7 +47,7 @@ jobs:
run: python convert.py
- name: Run PlatformIO
run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp32-wroom32-release
run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp8266-nokia5110 --environment esp8266-ssd1306 --environment esp32-wroom32-release --environment esp32-wroom32-nokia5110 --environment esp32-wroom32-ssd1306
- name: Rename Binary files
id: rename-binary-files
@ -68,7 +68,7 @@ jobs:
- name: Create Artifact
uses: actions/upload-artifact@v3
with:
name: ${{ steps.rename-binary-files.outputs.name }}_dev_build
name: ahoydtu_dev
path: |
src/firmware/*
src/User_Manual.md

2
.github/workflows/compile_release.yml

@ -51,7 +51,7 @@ jobs:
run: python convert.py
- name: Run PlatformIO
run: pio run -d src --environment esp8266-release --environment esp8285-release --environment esp32-wroom32-release
run: pio run -d tools/esp8266 --environment esp8266-release --environment esp8285-release --environment esp8266-nokia5110 --environment esp8266-ssd1306 --environment esp32-wroom32-release --environment esp32-wroom32-nokia5110 --environment esp32-wroom32-ssd1306
- name: Rename Binary files
id: rename-binary-files

255
User_Manual.md

@ -10,8 +10,8 @@ In the initial case or after click "erase settings" the fields for the inverter
Set at least the serial number and a name for each inverter, check "reboot after save" and click the "Save" button.
## MQTT Output
The ahoy dtu will publish on the following topics
`<CHOOSEN_TOPIC_FROM_SETUP>/<INVERTER_NAME_FROM_SETUP>/ch0/#`
The AhoyDTU will publish on the following topics
`<TOPIC>/<INVERTER_NAME_FROM_SETUP>/ch0/#`
| Topic | Example Value | Remarks |
|---|---|---|
@ -34,7 +34,7 @@ The ahoy dtu will publish on the following topics
|PowerLimit | 80.000|actual set point for power limit control AC active power in percent|
|LastAlarmCode | 1.000| Last Alarm Code eg. "inverter start"|
`<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
@ -47,7 +47,8 @@ The ahoy dtu will publish on the following topics
|YieldTotal | 110.819 | Energy converted to AC since reset Watt hours per module/channel (measured on DC) |
|Irradiation |5.65 | ratio DC Power over set maximum power per module/channel in percent |
## Active Power Limit via Setup Page
## Active Power Limit via Serial / Control Page
URL: `/serial`
If you leave the field "Active Power Limit" empty during the setup and reboot the ahoy-dtu will set a value of 65535 in the setup.
That is the value you have to fill in case you want to operate the inverter without a active power limit.
If the value is 65535 or -1 after another reboot the value will be set automatically to "100" and in the drop-down menu "relative in percent persistent" will be set. Of course you can do this also by your self.
@ -68,117 +69,162 @@ after a power cycle of the inverter (P_DC=0 and P_AC=0 for at least 10 seconds)
The user has to ensure correct settings. Remember that for the inverters of 3rd generation the relative active power limit is in the range of 2% up to 100%.
Also an absolute active power limit below approx. 30 Watt seems to be not meanful because of the control capabilities and reactive power load.
## Active Power Limit via MQTT
The ahoy-dtu subscribes on the topic `<CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/#` if the mqtt broker is set-up correctly. The default topic is `inverter/devcontrol/#`.
## Control via MQTT
To set the active power limit (controled value is the AC Power of the inverter) you have four options. (Only single phase inverters are actually in focus).
### Generic Information
| topic | payload | active power limit in | Condition |
| --------------------------------------------------------------- | ----------- | -------------------------------------------- | -------------- |
| <CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/<INVERTER_ID>/11 OR <CHOOSEN_TOPIC_FROM_SETUP>/devcontrol/<INVERTER_ID>/11/0 | [0..65535] | Watt | not persistent |
| <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 |
The AhoyDTU subscribes on three topics `<TOPIC>/ctrl/#`, `<TOPIC>/setup` and `<TOPIC>/status`.
👆 `<TOPIC>` can be set on setup page, default is `inverter`.
👆 `<INVERTER_ID>` is the number of the specific inverter in the setup page.
* First inverter --> `<INVERTER_ID>` = 0
* Second inverter --> `<INVERTER_ID>` = 1
* ...
### 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;
### Inverter Power (On / Off)
```mqtt
<TOPIC>/ctrl/power/<INVERTER_ID>
```
with payload `1` = `ON` and `0` = `OFF`
Example:
```mqtt
inverter/ctrl/power/0 1
```
### Inverter restart
```mqtt
<TOPIC>/ctrl/restart/<INVERTER_ID>
```
Example:
```mqtt
inverter/ctrl/restart/0
```
### Power Limit relative persistent [%]
```mqtt
<TOPIC>/ctrl/limit_persistent_relative/<INVERTER_ID>
```
with a payload `[2 .. 100]`
Example:
```mqtt
inverter/ctrl/limit_persistent_relative/0 70
```
### 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
```
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.
See here the actual implementation to set the send buffer bytes.
```C
void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data) {
sendCmdPacket(invId, TX_REQ_DEVCONTROL, ALL_FRAMES, false);
int cnt = 0;
// cmd --> 0x0b => Type_ActivePowerContr, 0 on, 1 off, 2 restart, 12 reactive power, 13 power factor
mTxBuf[10] = cmd;
mTxBuf[10 + (++cnt)] = 0x00;
if (cmd >= ActivePowerContr && cmd <= PFSet){
mTxBuf[10 + (++cnt)] = ((data[0] * 10) >> 8) & 0xff; // power limit || high byte from MQTT payload
mTxBuf[10 + (++cnt)] = ((data[0] * 10) ) & 0xff; // power limit || low byte from MQTT payload
mTxBuf[10 + (++cnt)] = ((data[1] ) >> 8) & 0xff; // high byte from MQTT topic value <DATA2>
mTxBuf[10 + (++cnt)] = ((data[1] ) ) & 0xff; // low byte from MQTT topic value <DATA2>
### 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)
```json
{
"id": <INVERTER_ID>,
"cmd": "power",
"val": <VALUE>
}
// crc control data
uint16_t crc = Hoymiles::crc16(&mTxBuf[10], cnt+1);
mTxBuf[10 + (++cnt)] = (crc >> 8) & 0xff;
mTxBuf[10 + (++cnt)] = (crc ) & 0xff;
// crc over all
cnt +=1;
mTxBuf[10 + cnt] = Hoymiles::crc8(mTxBuf, 10 + cnt);
sendPacket(invId, mTxBuf, 10 + (++cnt), true);
```
The `<VALUE>` should be set to `1` = `ON` and `0` = `OFF`
### Inverter restart
```json
{
"id": <INVERTER_ID>,
"cmd": "restart"
}
```
So as example sending any payload on `inverter/devcontrol/0/1` will switch off the inverter.
## 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
### Power Limit relative persistent [%]
```json
{
"inverter":<INVERTER_ID>,
"tx_request": <TX_REQUEST_BYTE>,
"cmd": <SUB_CMD_BYTE>,
"payload": <PAYLOAD_INTEGER_TWO_BYTES>,
"payload2": <PAYLOAD_INTEGER_TWO_BYTES>
"id": <INVERTER_ID>,
"cmd": "limit_persistent_relative",
"val": <VALUE>
}
```
With the following value ranges
The `VALUE` represents a percent number in a range of `[2 .. 100]`
| Value | range | note |
| --------------------------- | ----------- | ------------------------------- |
| <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 |
### Power Limit absolute persistent [Watts]
Example to set the active power limit non persistent to 10%
```json
{
"inverter":0,
"tx_request": 81,
"cmd": 11,
"payload": 10,
"payload2": 1
"id": <INVERTER_ID>,
"cmd": "limit_persistent_absolute",
"val": <VALUE>
}
```
Example to set the active power limit persistent to 600Watt
The `VALUE` represents watts in a range of `[0 .. 65535]`
### Power Limit relative non persistent [%]
```json
{
"inverter":0,
"tx_request": 81,
"cmd": 11,
"payload": 600,
"payload2": 256
"id": <INVERTER_ID>,
"cmd": "limit_nonpersistent_relative",
"val": <VALUE>
}
```
The `VALUE` represents a percent number in a range of `[2 .. 100]`
### Power Limit absolute non persistent [Watts]
```json
{
"id": <INVERTER_ID>,
"cmd": "limit_nonpersistent_absolute",
"val": <VALUE>
}
```
The `VALUE` represents watts in a range of `[0 .. 65535]`
### Developer Information REST API
### Developer Information REST API (obsolete)
In the same approach as for MQTT any other SubCmd and also MainCmd can be applied and the response payload can be observed in the serial logs. Eg. request the Alarm-Data from the Alarm-Index 5 from inverter 0 will look like this:
```json
{
@ -190,38 +236,15 @@ In the same approach as for MQTT any other SubCmd and also MainCmd can be applie
}
```
## 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
## 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
## Issues and Debuging for active power limit settings
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.
In case of issues please report:
1. Version of firmware
2. The output of the serial debug esp. the TX messages starting with "0x51" and the RX messages starting with "0xD1" or "0xF1"
3. Which case you have tried: Setup-Page, MQTT, REST API and at what was shown on the "Visualization Page" at the Location "Limit"
4. The setting means payload, relative, absolute, persistent, not persistent (see tables above)
**Developer Information General for Active Power Limit**
⚡The following was verified by field tests and feedback from users
Internally this values will be set for the second two bytes for MainCmd: 0x51 SubCmd: 0x0b --> DevControl set ActivePowerLimit
```C
typedef enum {
AbsolutNonPersistent = 0x0000, // 0
RelativNonPersistent = 0x0001, // 1
AbsolutPersistent = 0x0100, // 256
RelativPersistent = 0x0101 // 257
} PowerLimitControlType;
```
## Firmware Version collection
Gather user inverter information here to understand what differs between some inverters.
To get the information open the URL `/api/record/info` on your AhoyDTU. The information will only be present once the AhoyDTU was able to communicate with an inverter.
| Name | Inverter Typ | Bootloader V. | FWVersion | FWBuild [YYYY] | FWBuild [MM-DD] | HWPartId | | |
| ---------- | ------------ | ------------- | --------- | -------------- | --------------- | --------- | -------- | --------- |

37
scripts/getVersion.py

@ -1,4 +1,6 @@
import os
import shutil
import gzip
from datetime import date
def genOtaBin(path):
@ -24,6 +26,11 @@ def genOtaBin(path):
with open(path + "ota.bin", "wb") as f:
f.write(bytearray(arr))
# write gzip firmware file
def gzip_bin(bin_file, gzip_file):
with open(bin_file,"rb") as fp:
with gzip.open(gzip_file, "wb", compresslevel = 9) as f:
shutil.copyfileobj(fp, f)
def readVersion(path, infile):
f = open(path + infile, "r")
@ -44,20 +51,48 @@ def readVersion(path, infile):
os.mkdir(path + "firmware/")
sha = os.getenv("SHA",default="sha")
versionout = version[:-1] + "_esp8266_" + sha + ".bin"
src = path + ".pio/build/esp8266-release/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_esp8266_nokia5110_" + sha + ".bin"
src = path + ".pio/build/esp8266-nokia5110/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_esp8266_ssd1306_" + sha + ".bin"
src = path + ".pio/build/esp8266-ssd1306/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_esp8266_1m_" + sha + ".bin"
versionout = version[:-1] + "_esp8285_" + sha + ".bin"
src = path + ".pio/build/esp8285-release/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_esp32_" + sha + ".bin"
src = path + ".pio/build/esp32-wroom32-release/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_esp32_nokia5110_" + sha + ".bin"
src = path + ".pio/build/esp32-wroom32-nokia5110/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
gzip_bin(dst, dst + ".gz")
versionout = version[:-1] + "_esp32_ssd1306_" + sha + ".bin"
src = path + ".pio/build/esp32-wroom32-ssd1306/firmware.bin"
dst = path + "firmware/" + versionout
os.rename(src, dst)
gzip_bin(dst, dst + ".gz")
# other ESP32 bin files
src = path + ".pio/build/esp32-wroom32-release/"

126
src/app.cpp

@ -13,37 +13,55 @@
#include "utils/sun.h"
//-----------------------------------------------------------------------------
void app::setup(uint32_t timeout) {
app::app() : ah::Scheduler() {
mWeb = NULL;
}
//-----------------------------------------------------------------------------
void app::setup() {
Serial.begin(115200);
while (!Serial)
yield();
addListener(EVERY_SEC, std::bind(&app::uptimeTick, this));
addListener(EVERY_MIN, std::bind(&app::minuteTick, this));
addListener(EVERY_12H, std::bind(&app::ntpUpdateTick, this));
ah::Scheduler::setup();
resetSystem();
mSettings.setup();
mSettings.getPtr(mConfig);
DPRINTLN(DBG_INFO, F("Settings valid: ") + String((mSettings.getValid()) ? F("true") : F("false")));
mWifi = new ahoywifi(mConfig);
mWifi->setup(timeout, mSettings.getValid());
everySec(std::bind(&app::tickSecond, this));
everyMin(std::bind(&app::tickMinute, this));
every12h(std::bind(&app::tickNtpUpdate, this));
every(std::bind(&app::tickSend, this), mConfig->nrf.sendInterval);
#if !defined(AP_ONLY)
if((mConfig->sun.lat) && (mConfig->sun.lon)) {
mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
once(std::bind(&app::tickCalcSunrise, this), 5);
}
#endif
mSys = new HmSystemType();
mSys->enableDebug();
mSys->setup(mConfig->nrf.amplifierPower, mConfig->nrf.pinIrq, mConfig->nrf.pinCe, mConfig->nrf.pinCs);
mSys->addInverters(&mConfig->inst);
#if !defined(AP_ONLY)
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, mSys, &mTimestamp, &mSunrise, &mSunset);
#endif
mWifi.setup(mConfig, &mTimestamp);
mPayload.setup(mSys);
mPayload.enableSerialDebug(mConfig->serial.debug);
#if !defined(AP_ONLY)
if (mConfig->mqtt.broker[0] > 0) {
mMqtt.setup(&mConfig->mqtt, mConfig->sys.deviceName, mVersion, mSys, &mUtcTimestamp, &mSunrise, &mSunset);
mPayload.addListener(std::bind(&PubMqttType::payloadEventListener, &mMqtt, std::placeholders::_1));
addListener(EVERY_SEC, std::bind(&PubMqttType::tickerSecond, &mMqtt));
addListener(EVERY_MIN, std::bind(&PubMqttType::tickerMinute, &mMqtt));
addListener(EVERY_HR, std::bind(&PubMqttType::tickerHour, &mMqtt));
everySec(std::bind(&PubMqttType::tickerSecond, &mMqtt));
everyMin(std::bind(&PubMqttType::tickerMinute, &mMqtt));
mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
}
#endif
setupLed();
@ -51,9 +69,17 @@ void app::setup(uint32_t timeout) {
mWeb = new web(this, mConfig, &mStat, mVersion);
mWeb->setup();
mWeb->setProtection(strlen(mConfig->sys.adminPwd) != 0);
addListener(EVERY_SEC, std::bind(&web::tickSecond, mWeb));
everySec(std::bind(&web::tickSecond, mWeb));
// 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
//addListener(EVERY_MIN, std::bind(&PubSerialType::tickerMinute, &mPubSerial));
mPubSerial.setup(mConfig, mSys, &mTimestamp);
every(std::bind(&PubSerialType::tick, &mPubSerial), mConfig->serial.interval);
}
//-----------------------------------------------------------------------------
@ -62,6 +88,10 @@ void app::loop(void) {
ah::Scheduler::loop();
#if !defined(AP_ONLY)
mWifi.loop();
#endif
mWeb->loop();
if (mFlagSendDiscoveryConfig) {
@ -99,25 +129,31 @@ void app::loop(void) {
}
mMqtt.loop();
if (ah::checkTicker(&mTicker, 1000)) {
if (mUtcTimestamp > 946684800 && mConfig->sun.lat && mConfig->sun.lon && (mUtcTimestamp + mCalculatedTimezoneOffset) / 86400 != (mLatestSunTimestamp + mCalculatedTimezoneOffset) / 86400) { // update on reboot or midnight
if (!mLatestSunTimestamp) { // first call: calculate time zone from longitude to refresh at local midnight
mCalculatedTimezoneOffset = (int8_t)((mConfig->sun.lon >= 0 ? mConfig->sun.lon + 7.5 : mConfig->sun.lon - 7.5) / 15) * 3600;
}
ah::calculateSunriseSunset(mUtcTimestamp, mCalculatedTimezoneOffset, mConfig->sun.lat, mConfig->sun.lon, &mSunrise, &mSunset);
mLatestSunTimestamp = mUtcTimestamp;
}
//-----------------------------------------------------------------------------
void app::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);
if (++mSendTicker >= mConfig->nrf.sendInterval) {
mSendTicker = 0;
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)
if (mConfig->serial.debug)
DPRINTLN(DBG_DEBUG, F("Free heap: 0x") + String(ESP.getFreeHeap(), HEX));
uint32_t nxtTrig = mTimestamp - (mTimestamp % 86400) + 86400; // next midnight
onceAt(std::bind(&app::tickCalcSunrise, this), nxtTrig);
if (mConfig->mqtt.broker[0] > 0) {
once(std::bind(&PubMqttType::tickerSun, &mMqtt), 1);
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()));
@ -149,7 +185,7 @@ void app::loop(void) {
}
}
mPayload.reset(iv, mUtcTimestamp);
mPayload.reset(iv, mTimestamp);
mPayload.request(iv);
yield();
@ -173,14 +209,14 @@ void app::loop(void) {
mRxTicker = 0;
}
}
} else if (mConfig->serial.debug)
} 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();
}
}
}
//-----------------------------------------------------------------------------
void app::handleIntr(void) {
@ -188,19 +224,14 @@ void app::handleIntr(void) {
mSys->Radio.handleIntr();
}
//-----------------------------------------------------------------------------
bool app::getWifiApActive(void) {
return mWifi->getApActive();
}
//-----------------------------------------------------------------------------
void app::scanAvailNetworks(void) {
mWifi->scanAvailNetworks();
mWifi.scanAvailNetworks();
}
//-----------------------------------------------------------------------------
void app::getAvailNetworks(JsonObject obj) {
mWifi->getAvailNetworks(obj);
mWifi.getAvailNetworks(obj);
}
@ -209,21 +240,18 @@ void app::resetSystem(void) {
snprintf(mVersion, 12, "%d.%d.%d", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
mShouldReboot = false;
mUptimeSecs = 0;
mUpdateNtp = false;
mFlagSendDiscoveryConfig = false;
#ifdef AP_ONLY
mUtcTimestamp = 1;
mTimestamp = 1;
#else
mUtcTimestamp = 0;
mTimestamp = 0;
#endif
mHeapStatCnt = 0;
mSendTicker = 0xffff;
mSunrise = 0;
mSunset = 0;
mTicker = 0;
mRxTicker = 0;
mSendLastIvId = 0;
@ -232,6 +260,12 @@ void app::resetSystem(void) {
memset(&mStat, 0, sizeof(statistics_t));
}
//-----------------------------------------------------------------------------
void app::mqttSubRxCb(JsonObject obj) {
if(NULL != mWeb)
mWeb->apiCtrlRequest(obj);
}
//-----------------------------------------------------------------------------
void app::setupLed(void) {
/** LED connection diagram
@ -255,7 +289,7 @@ void app::updateLed(void) {
Inverter<> *iv = mSys->getInverterByPos(0);
if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if(iv->isProducing(mUtcTimestamp, rec))
if(iv->isProducing(mTimestamp, rec))
digitalWrite(mConfig->led.led0, LOW); // LED on
else
digitalWrite(mConfig->led.led0, HIGH); // LED off

76
src/app.h

@ -27,6 +27,7 @@
#include "publisher/pubMqtt.h"
#include "publisher/pubSerial.h"
// convert degrees and radians for sun calculation
#define SIN(x) (sin(radians(x)))
#define COS(x) (cos(radians(x)))
@ -38,15 +39,20 @@ typedef Payload<HmSystemType> PayloadType;
typedef PubMqtt<HmSystemType> PubMqttType;
typedef PubSerial<HmSystemType> PubSerialType;
class ahoywifi;
// PLUGINS
#if defined(ENA_NOKIA) || defined(ENA_SSD1306)
#include "plugins/MonochromeDisplay/MonochromeDisplay.h"
typedef MonochromeDisplay<HmSystemType> MonoDisplayType;
#endif
class web;
class app : public ah::Scheduler {
public:
app() : ah::Scheduler() {}
app();
~app() {}
void setup(uint32_t timeout);
void setup(void);
void loop(void);
void handleIntr(void);
void cbMqtt(char* topic, byte* payload, unsigned int length);
@ -84,38 +90,21 @@ class app : public ah::Scheduler {
return ret;
}
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);
}
String getTimeStr(uint32_t offset = 0) {
char str[10];
if(0 == mUtcTimestamp)
if(0 == mTimestamp)
sprintf(str, "n/a");
else
sprintf(str, "%02d:%02d:%02d ", hour(mUtcTimestamp + offset), minute(mUtcTimestamp + offset), second(mUtcTimestamp + offset));
sprintf(str, "%02d:%02d:%02d ", hour(mTimestamp + offset), minute(mTimestamp + offset), second(mTimestamp + offset));
return String(str);
}
inline uint32_t getUptime(void) {
return mUptimeSecs;
}
inline uint32_t getTimestamp(void) {
return mUtcTimestamp;
}
void setTimestamp(uint32_t newTime) {
DPRINTLN(DBG_DEBUG, F("setTimestamp: ") + String(newTime));
if(0 == newTime)
mUpdateNtp = true;
else
mUtcTimestamp = newTime;
Scheduler::setTimestamp(newTime);
}
inline uint32_t getSunrise(void) {
@ -124,9 +113,6 @@ class app : public ah::Scheduler {
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(); }
@ -140,42 +126,36 @@ class app : public ah::Scheduler {
private:
void resetSystem(void);
void setupMqtt(void);
void mqttSubRxCb(JsonObject obj);
void setupLed(void);
void updateLed(void);
void uptimeTick(void) {
mUptimeSecs++;
if (0 != mUtcTimestamp)
mUtcTimestamp++;
void tickSecond(void) {
if (mShouldReboot) {
DPRINTLN(DBG_INFO, F("Rebooting..."));
ESP.restart();
}
if (mUpdateNtp) {
mUpdateNtp = false;
mUtcTimestamp = mWifi->getNtpTime();
DPRINTLN(DBG_INFO, F("[NTP]: ") + getDateTimeStr(mUtcTimestamp) + F(" UTC"));
mWifi.getNtpTime();
}
}
void minuteTick(void) {
if(0 == mUtcTimestamp) {
if(!mWifi->getApActive())
void tickMinute(void) {
if(0 == mTimestamp) {
mUpdateNtp = true;
}
}
void ntpUpdateTick(void) {
if (!mWifi->getApActive())
void tickNtpUpdate(void) {
mUpdateNtp = true;
}
void tickCalcSunrise(void);
void tickSend(void);
void stats(void) {
DPRINTLN(DBG_VERBOSE, F("main.h:stats"));
#ifdef ESP8266
@ -196,15 +176,11 @@ class app : public ah::Scheduler {
DPRINTLN(DBG_VERBOSE, F(" - frag: ") + String(frag));
}
uint32_t mUptimeSecs;
uint8_t mHeapStatCnt;
uint32_t mUtcTimestamp;
bool mUpdateNtp;
bool mShowRebootRequest;
ahoywifi *mWifi;
ahoywifi mWifi;
web *mWeb;
PayloadType mPayload;
PubSerialType mPubSerial;
@ -213,13 +189,11 @@ class app : public ah::Scheduler {
settings mSettings;
settings_t *mConfig;
uint16_t mSendTicker;
uint8_t mSendLastIvId;
statistics_t mStat;
// timer
uint32_t mTicker;
uint32_t mRxTicker;
// mqtt
@ -229,7 +203,11 @@ class app : public ah::Scheduler {
// sun
int32_t mCalculatedTimezoneOffset;
uint32_t mSunrise, mSunset;
uint32_t mLatestSunTimestamp;
// plugins
#if defined(ENA_NOKIA) || defined(ENA_SSD1306)
MonoDisplayType mMonoDisplay;
#endif
};
#endif /*__APP_H__*/

2
src/config/config.h

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

51
src/config/settings.h

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

2
src/defines.h

@ -13,7 +13,7 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 5
#define VERSION_PATCH 41
#define VERSION_PATCH 50
//-------------------------------------
typedef struct {

7
src/hm/hmInverter.h

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

9
src/hm/hmRadio.h

@ -139,14 +139,15 @@ class HmRadio {
mNrf24.setPALevel(ampPwr & 0x03);
mNrf24.startListening();
DPRINTLN(DBG_INFO, F("Radio Config:"));
mNrf24.printPrettyDetails();
mTxCh = setDefaultChannels();
if(!mNrf24.isChipConnected()) {
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
if(mNrf24.isChipConnected()) {
DPRINTLN(DBG_INFO, F("Radio Config:"));
mNrf24.printPrettyDetails();
}
else
DPRINTLN(DBG_WARN, F("WARNING! your NRF24 module can't be reached, check the wiring"));
}
void loop(void) {

2
src/hm/hmSystem.h

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

2
src/main.cpp

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

72
src/platformio.ini

@ -36,7 +36,7 @@ lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24
paulstoffregen/Time
knolleary/PubSubClient
https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson
;esp8266/DNSServer
;esp8266/EEPROM
@ -89,6 +89,42 @@ monitor_filters =
time ; Add timestamp with milliseconds for each new line
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
[env:esp8266-nokia5110]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = -D RELEASE -DU8X8_NO_HW_I2C -DENA_NOKIA
monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24
paulstoffregen/Time
https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson
olikraus/U8g2
https://github.com/JChristensen/Timezone
[env:esp8266-ssd1306]
platform = espressif8266
board = esp12e
board_build.f_cpu = 80000000L
build_flags = -D RELEASE -DENA_SSD1306
monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24
paulstoffregen/Time
https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson
https://github.com/ThingPulse/esp8266-oled-ssd1306.git
https://github.com/JChristensen/Timezone
[env:esp32-wroom32-release]
platform = espressif32
board = lolin_d32
@ -109,3 +145,37 @@ monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line
log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
[env:esp32-wroom32-nokia5110]
platform = espressif32
board = lolin_d32
build_flags = -D RELEASE -std=gnu++14 -DU8X8_NO_HW_I2C -DENA_NOKIA
build_unflags = -std=gnu++11
monitor_filters =
;default ; Remove typical terminal control codes from input
time ; Add timestamp with milliseconds for each new line
;log2file ; Log data to a file “platformio-device-monitor-*.log” located in the current working directory
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24
paulstoffregen/Time
https://github.com/bertmelis/espMqttClient#v1.3.3
bblanchon/ArduinoJson
olikraus/U8g2
[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

285
src/plugins/MonochromeDisplay/MonochromeDisplay.h

@ -0,0 +1,285 @@
#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} ;
template<class HMSYSTEM>
class MonochromeDisplay {
public:
#if defined(ENA_NOKIA)
MonochromeDisplay() : mDisplay(U8G2_R0,5,4,16) {
mNewPayload = false;
mExtra = 0;
}
#else // ENA_SSD1306
MonochromeDisplay() : mDisplay(0x3c, SDA, SCL) {
mNewPayload = false;
mExtra = 0;
mRx = 0;
mUp = 1;
}
#endif
void setup(HMSYSTEM *sys, uint32_t *utcTs) {
mSys = sys;
mUtcTs = utcTs;
#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() {
if(mNewPayload) {
mNewPayload = false;
DataScreen();
}
}
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) {
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Time
Timezone CE(CEST, CET);
String timeStr = ah::getDateTimeStr(CE.toLocal(*mUtcTs)).substring(2, 22);
IPAddress ip = WiFi.localIP();
float totalYield = 0.000, totalYieldToday = 0.000, 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 );
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};
num_inv++;
if ( !iv->isProducing(*mUtcTs,rec) )
continue;
for (uint8_t fld = 0; fld < 3; fld++) {
pos = iv->getPosByChFld(CH0, list[fld],rec);
if(fld == 1)
totalYield += iv->getValue(pos,rec);
if(fld == 2)
totalYieldToday += iv->getValue(pos,rec);
if(fld == 0)
{
pow_i[num_inv-1] = iv->getValue(pos,rec);
totalActual += iv->getValue(pos,rec);
}
}
ucnt++;
}
}
/* u8g2_font_open_iconic_embedded_2x_t 'D' + 'G' + 'J' */
mDisplay.clear();
#if defined(ENA_NOKIA)
mDisplay.firstPage();
do {
if(ucnt) {
mDisplay.drawXBMP(10,0,8,17,bmp_arrow);
mDisplay.setFont(u8g2_font_logisoso16_tr);
mDisplay.setCursor(25,16);
sprintf(fmtText,"%3.0f",totalActual);
mDisplay.print(String(fmtText)+F(" W"));
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
{
if( pow_i[0] )
mDisplay.print(F("#1 ")+String(pow_i[0])+F(" W"));
else
mDisplay.print(F("#1 -----"));
mDisplay.setCursor(5,37);
if( pow_i[1] )
mDisplay.print(F("#2 ")+String(pow_i[1])+F(" W"));
else
mDisplay.print(F("#2 -----"));
}
}
else {
mDisplay.setFont(u8g2_font_logisoso16_tr);
mDisplay.setCursor(30,30);
mDisplay.print(F("off"));
mDisplay.setFont(u8g2_font_5x8_tr);
}
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"));
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
{
if( pow_i[0] )
mDisplay.drawString(15,22,F("#1 ")+String(pow_i[0])+F(" W"));
else
mDisplay.drawString(15,22,F("#1 -----"));
if( pow_i[1] )
mDisplay.drawString(15,35,F("#2 ")+String(pow_i[1])+F(" W"));
else
mDisplay.drawString(15,35,F("#2 -----"));
}
mDisplay.drawLine(2,23,123,23);
}
else {
mDisplay.setBrightness(1);
mDisplay.setFont(ArialMT_Plain_24);
mDisplay.drawString(mRx+50, 10, F("off"));
mDisplay.setFont(ArialMT_Plain_16);
}
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;
uint32_t *mUtcTs;
HMSYSTEM *mSys;
};
#endif
#endif /*__MONOCHROME_DISPLAY__*/

464
src/publisher/pubMqtt.h

@ -3,6 +3,8 @@
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/
//-----------------------------------------------------------------------------
// https://bert.emelis.net/espMqttClient/
#ifndef __PUB_MQTT_H__
#define __PUB_MQTT_H__
@ -15,115 +17,138 @@
#include "../utils/dbg.h"
#include "../utils/ahoyTimer.h"
#include "../config/config.h"
#include <PubSubClient.h>
#include <espMqttClient.h>
#include <ArduinoJson.h>
#include "../defines.h"
#include "../hm/hmSystem.h"
#define QOS_0 0
typedef std::function<void(JsonObject)> subscriptionCb;
template<class HMSYSTEM>
class PubMqtt {
public:
PubMqtt() {
mClient = new PubSubClient(mEspClient);
mAddressSet = false;
mLastReconnect = 0;
mRxCnt = 0;
mTxCnt = 0;
mEnReconnect = false;
mSubscriptionCb = NULL;
}
~PubMqtt() { }
void setup(cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *sunrise, uint32_t *sunset) {
DPRINTLN(DBG_VERBOSE, F("PubMqtt.h:setup"));
mAddressSet = true;
mCfg_mqtt = cfg_mqtt;
mCfgMqtt = cfg_mqtt;
mDevName = devName;
mVersion = version;
mSys = sys;
mUtcTimestamp = utcTs;
mSunrise = sunrise;
mSunset = sunset;
mClient->setServer(mCfg_mqtt->broker, mCfg_mqtt->port);
mClient->setBufferSize(MQTT_MAX_PACKET_SIZE);
snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic);
setCallback(std::bind(&PubMqtt<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);
sendMsg("device", devName);
sendMsg("uptime", "0");
if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0))
mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd);
mClient.setClientId(mDevName); // TODO: add mac?
mClient.setServer(mCfgMqtt->broker, mCfgMqtt->port);
mClient.setWill(mLwtTopic, QOS_0, true, mLwtOffline);
mClient.onConnect(std::bind(&PubMqtt::onConnect, this, std::placeholders::_1));
mClient.onDisconnect(std::bind(&PubMqtt::onDisconnect, this, std::placeholders::_1));
mClient.onMessage(std::bind(&PubMqtt::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6));
}
void loop() {
if(mAddressSet)
mClient->loop();
#if defined(ESP8266)
mClient.loop();
#endif
}
void tickerSecond() {
if(mAddressSet) {
if(!mClient->connected())
reconnect();
}
sendIvData();
}
void tickerMinute() {
if(mAddressSet) {
char val[40];
snprintf(val, 40, "%ld", millis() / 1000);
sendMsg("uptime", val);
char val[12];
snprintf(val, 12, "%ld", millis() / 1000);
publish("uptime", val);
publish("wifi_rssi", String(WiFi.RSSI()).c_str());
publish("free_heap", String(ESP.getFreeHeap()).c_str());
sendMsg("wifi_rssi", String(WiFi.RSSI()).c_str());
if(!mClient.connected()) {
if(mEnReconnect)
mClient.connect();
}
}
void tickerHour() {
if(mAddressSet) {
sendMsg("sunrise", String(*mSunrise).c_str());
sendMsg("sunset", String(*mSunset).c_str());
}
void tickerSun() {
publish("sunrise", String(*mSunrise).c_str(), true);
publish("sunset", String(*mSunset).c_str(), true);
}
void setCallback(MQTT_CALLBACK_SIGNATURE) {
mClient->setCallback(callback);
}
void tickSunset() {
printf("tickSunset\n");
char topic[MAX_NAME_LENGTH + 15], val[32];
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL == iv)
continue; // skip to next inverter
snprintf(topic, MAX_NAME_LENGTH + 15, "%s/available_text", iv->config->name);
snprintf(val, 32, "not available and not producing");
publish(topic, val, true);
void sendMsg(const char *topic, const char *msg) {
//DPRINTLN(DBG_VERBOSE, F("mqtt.h:sendMsg"));
if(mAddressSet) {
char top[66];
snprintf(top, 66, "%s/%s", mCfg_mqtt->topic, topic);
sendMsg2(top, msg, false);
snprintf(topic, MAX_NAME_LENGTH + 15, "%s/available", iv->config->name);
snprintf(val, 32, "%d", MQTT_STATUS_NOT_AVAIL_NOT_PROD);
publish(topic, val, true);
}
}
void sendMsg2(const char *topic, const char *msg, boolean retained) {
if(mAddressSet) {
if(!mClient->connected())
reconnect();
if(mClient->connected())
mClient->publish(topic, msg, retained);
void payloadEventListener(uint8_t cmd) {
if(mClient.connected()) // prevent overflow if MQTT broker is not reachable but set
mSendList.push(cmd);
}
void publish(const char *subTopic, const char *payload, bool retained = false, bool addTopic = true) {
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 subscribe(const char *subTopic) {
char topic[MQTT_TOPIC_LEN + 20];
snprintf(topic, (MQTT_TOPIC_LEN + 20), "%s/%s", mCfgMqtt->topic, subTopic);
mClient.subscribe(topic, QOS_0);
}
bool isConnected(bool doRecon = false) {
//DPRINTLN(DBG_VERBOSE, F("mqtt.h:isConnected"));
if(!mAddressSet)
return false;
if(doRecon && !mClient->connected())
reconnect();
return mClient->connected();
void setSubscriptionCb(subscriptionCb cb) {
mSubscriptionCb = cb;
}
void payloadEventListener(uint8_t cmd) {
mSendList.push(cmd);
inline bool isConnected() {
return mClient.connected();
}
uint32_t getTxCnt(void) {
inline uint32_t getTxCnt(void) {
return mTxCnt;
}
inline uint32_t getRxCnt(void) {
return mRxCnt;
}
void sendMqttDiscoveryConfig(const char *topic) {
DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig"));
@ -133,11 +158,11 @@ class PubMqtt {
if (NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
DynamicJsonDocument deviceDoc(128);
deviceDoc["name"] = iv->config->name;
deviceDoc["ids"] = String(iv->config->serial.u64, HEX);
deviceDoc["cu"] = F("http://") + String(WiFi.localIP().toString());
deviceDoc["mf"] = "Hoymiles";
deviceDoc["mdl"] = iv->config->name;
deviceDoc[F("name")] = iv->config->name;
deviceDoc[F("ids")] = String(iv->config->serial.u64, HEX);
deviceDoc[F("cu")] = F("http://") + String(WiFi.localIP().toString());
deviceDoc[F("mf")] = F("Hoymiles");
deviceDoc[F("mdl")] = iv->config->name;
JsonObject deviceObj = deviceDoc.as<JsonObject>();
DynamicJsonDocument doc(384);
@ -153,20 +178,19 @@ class PubMqtt {
const char *devCls = getFieldDeviceClass(rec->assign[i].fieldId);
const char *stateCls = getFieldStateClass(rec->assign[i].fieldId);
doc["name"] = name;
doc["stat_t"] = stateTopic;
doc["unit_of_meas"] = iv->getUnit(i, rec);
doc["uniq_id"] = String(iv->config->serial.u64, HEX) + "_" + uniq_id;
doc["dev"] = deviceObj;
doc["exp_aft"] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!?
doc[F("name")] = name;
doc[F("stat_t")] = stateTopic;
doc[F("unit_of_meas")] = iv->getUnit(i, rec);
doc[F("uniq_id")] = String(iv->config->serial.u64, HEX) + "_" + uniq_id;
doc[F("dev")] = deviceObj;
doc[F("exp_aft")] = MQTT_INTERVAL + 5; // add 5 sec if connection is bad or ESP too slow @TODO: stimmt das wirklich als expire!?
if (devCls != NULL)
doc["dev_cla"] = devCls;
doc[F("dev_cla")] = devCls;
if (stateCls != NULL)
doc["stat_cla"] = stateCls;
doc[F("stat_cla")] = stateCls;
serializeJson(doc, buffer);
sendMsg2(discoveryTopic, buffer, true);
// DPRINTLN(DBG_INFO, F("mqtt sent"));
publish(discoveryTopic, buffer, true, false);
doc.clear();
}
@ -176,41 +200,132 @@ class PubMqtt {
}
private:
void reconnect(void) {
DPRINTLN(DBG_DEBUG, F("mqtt.h:reconnect"));
DPRINTLN(DBG_DEBUG, F("MQTT mClient->_state ") + String(mClient->state()) );
#if defined(ESP8266)
void onWifiConnect(const WiFiEventStationModeGotIP& event) {
DPRINTLN(DBG_VERBOSE, F("MQTT connecting"));
mClient.connect();
mEnReconnect = true;
}
#ifdef ESP8266
DPRINTLN(DBG_DEBUG, F("WIFI mEspClient.status ") + String(mEspClient.status()) );
void onWifiDisconnect(const WiFiEventStationModeDisconnected& event) {
mEnReconnect = false;
}
#else
void onWiFiEvent(WiFiEvent_t event) {
switch(event) {
case SYSTEM_EVENT_STA_GOT_IP:
DPRINTLN(DBG_VERBOSE, F("MQTT connecting"));
mClient.connect();
mEnReconnect = true;
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
mEnReconnect = false;
break;
default:
break;
}
}
#endif
boolean resub = false;
if(!mClient->connected() && (millis() - mLastReconnect) > MQTT_RECONNECT_DELAY ) {
mLastReconnect = millis();
if(strlen(mDevName) > 0) {
// der Server und der Port müssen neu gesetzt werden,
// da ein MQTT_CONNECTION_LOST -3 die Werte zerstört hat.
mClient->setServer(mCfg_mqtt->broker, mCfg_mqtt->port);
mClient->setBufferSize(MQTT_MAX_PACKET_SIZE);
void onConnect(bool sessionPreset) {
DPRINTLN(DBG_INFO, F("MQTT connected"));
mEnReconnect = true;
char lwt[MQTT_TOPIC_LEN + 7 ]; // "/uptime" --> + 7 byte
snprintf(lwt, MQTT_TOPIC_LEN + 7, "%s/uptime", mCfg_mqtt->topic);
publish("version", mVersion, true);
publish("device", mDevName, true);
tickerMinute();
publish(mLwtTopic, mLwtOnline, true, false);
if((strlen(mCfg_mqtt->user) > 0) && (strlen(mCfg_mqtt->pwd) > 0))
resub = mClient->connect(mDevName, mCfg_mqtt->user, mCfg_mqtt->pwd, lwt, 0, false, "offline");
else
resub = mClient->connect(mDevName, lwt, 0, false, "offline");
// ein Subscribe ist nur nach einem connect notwendig
if(resub) {
char topic[MQTT_TOPIC_LEN + 13 ]; // "/devcontrol/#" --> + 6 byte
// ToDo: "/devcontrol/#" is hardcoded
snprintf(topic, MQTT_TOPIC_LEN + 13, "%s/devcontrol/#", mCfg_mqtt->topic);
DPRINTLN(DBG_INFO, F("subscribe to ") + String(topic));
mClient->subscribe(topic); // subscribe to mTopic + "/devcontrol/#"
subscribe("ctrl/#");
subscribe("setup/#");
subscribe("status/#");
}
void onDisconnect(espMqttClientTypes::DisconnectReason reason) {
DPRINT(DBG_INFO, F("MQTT disconnected, reason: "));
switch (reason) {
case espMqttClientTypes::DisconnectReason::TCP_DISCONNECTED:
DBGPRINTLN(F("TCP disconnect"));
break;
case espMqttClientTypes::DisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION:
DBGPRINTLN(F("wrong protocol version"));
break;
case espMqttClientTypes::DisconnectReason::MQTT_IDENTIFIER_REJECTED:
DBGPRINTLN(F("identifier rejected"));
break;
case espMqttClientTypes::DisconnectReason::MQTT_SERVER_UNAVAILABLE:
DBGPRINTLN(F("broker unavailable"));
break;
case espMqttClientTypes::DisconnectReason::MQTT_MALFORMED_CREDENTIALS:
DBGPRINTLN(F("malformed credentials"));
break;
case espMqttClientTypes::DisconnectReason::MQTT_NOT_AUTHORIZED:
DBGPRINTLN(F("not authorized"));
break;
default:
DBGPRINTLN(F("unknown"));
}
}
void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
DPRINTLN(DBG_VERBOSE, F("MQTT got topic: ") + String(topic));
if(NULL == mSubscriptionCb)
return;
char *tpc = new char[strlen(topic) + 1];
uint8_t cnt = 0;
DynamicJsonDocument json(128);
JsonObject root = json.to<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) {
uint8_t pos = 0;
@ -234,7 +349,6 @@ class PubMqtt {
if(mSendList.empty())
return;
isConnected(true); // really needed? See comment from HorstG-57 #176
char topic[32 + MAX_NAME_LENGTH], val[40];
float total[4];
bool sendTotal = false;
@ -262,28 +376,38 @@ class PubMqtt {
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available_text", iv->config->name);
snprintf(val, 40, "%s%s%s%s",
(status == MQTT_STATUS_NOT_AVAIL_NOT_PROD) ? "not yet " : "",
(status == MQTT_STATUS_NOT_AVAIL_NOT_PROD) ? "not " : "",
"available and ",
(status == MQTT_STATUS_AVAIL_NOT_PROD) ? "not " : "",
(status == MQTT_STATUS_NOT_AVAIL_NOT_PROD) ? "" : "producing"
"producing"
);
sendMsg(topic, val);
publish(topic, val, true);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/available", iv->config->name);
snprintf(val, 40, "%d", status);
sendMsg(topic, val);
publish(topic, val, true);
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/last_success", iv->config->name);
snprintf(val, 40, "%i", iv->getLastTs(rec) * 1000);
sendMsg(topic, val);
snprintf(val, 40, "%d", iv->getLastTs(rec));
publish(topic, val, true);
}
// data
if(iv->isAvailable(*mUtcTimestamp, rec)) {
for (uint8_t i = 0; i < rec->length; i++) {
bool retained = false;
if (mSendList.front() == RealTimeRunData_Debug) {
switch (rec->assign[i].fieldId) {
case FLD_YT:
case FLD_YD:
retained = true;
break;
}
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, fields[rec->assign[i].fieldId]);
snprintf(val, 40, "%.3f", iv->getValue(i, rec));
sendMsg(topic, val);
snprintf(val, 40, "%g", ah::round3(iv->getValue(i, rec)));
publish(topic, val, retained);
// calculate total values for RealTimeRunData_Debug
if (mSendList.front() == RealTimeRunData_Debug) {
@ -331,128 +455,32 @@ class PubMqtt {
break;
}
snprintf(topic, 32 + MAX_NAME_LENGTH, "total/%s", fields[fieldId]);
snprintf(val, 40, "%.3f", total[i]);
sendMsg(topic, val);
}
}
}
snprintf(val, 40, "%g", ah::round3(total[i]));
publish(topic, val, true);
}
void cbMqtt(char *topic, byte *payload, unsigned int length) {
// callback handling on subscribed devcontrol topic
DPRINTLN(DBG_INFO, F("cbMqtt"));
// subcribed topics are mTopic + "/devcontrol/#" where # is <inverter_id>/<subcmd in dec>
// eg. mypvsolar/devcontrol/1/11 with payload "400" --> inverter 1 active power limit 400 Watt
const char *token = strtok(topic, "/");
while (token != NULL) {
if (strcmp(token, "devcontrol") == 0) {
token = strtok(NULL, "/");
uint8_t iv_id = std::stoi(token);
if (iv_id >= 0 && iv_id <= MAX_NUM_INVERTERS) {
Inverter<> *iv = mSys->getInverterByPos(iv_id);
if (NULL != iv) {
if (!iv->devControlRequest) { // still pending
token = strtok(NULL, "/");
switch (std::stoi(token)) {
// Active Power Control
case ActivePowerContr:
token = strtok(NULL, "/"); // get ControlMode aka "PowerPF.Desc" in DTU-Pro Code from topic string
if (token == NULL) // default via mqtt ommit the LimitControlMode
iv->powerLimit[1] = AbsolutNonPersistent;
else
iv->powerLimit[1] = std::stoi(token);
if (length <= 5) { // if (std::stoi((char*)payload) > 0) more error handling powerlimit needed?
if (iv->powerLimit[1] >= AbsolutNonPersistent && iv->powerLimit[1] <= RelativPersistent) {
iv->devControlCmd = ActivePowerContr;
iv->powerLimit[0] = std::stoi(std::string((char *)payload, (unsigned int)length)); // THX to @silversurfer
/*if (iv->powerLimit[1] & 0x0001)
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("%"));
else
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W"));*/
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + String(iv->powerLimit[1] & 0x0001) ? F("%") : F("W"));
}
iv->devControlRequest = true;
} else {
DPRINTLN(DBG_INFO, F("Invalid mqtt payload recevied: ") + String((char *)payload));
}
break;
// Turn On
case TurnOn:
iv->devControlCmd = TurnOn;
DPRINTLN(DBG_INFO, F("Turn on inverter ") + String(iv->id));
iv->devControlRequest = true;
break;
// Turn Off
case TurnOff:
iv->devControlCmd = TurnOff;
DPRINTLN(DBG_INFO, F("Turn off inverter ") + String(iv->id));
iv->devControlRequest = true;
break;
// Restart
case Restart:
iv->devControlCmd = Restart;
DPRINTLN(DBG_INFO, F("Restart inverter ") + String(iv->id));
iv->devControlRequest = true;
break;
// Reactive Power Control
case ReactivePowerContr:
iv->devControlCmd = ReactivePowerContr;
if (true) { // if (std::stoi((char*)payload) > 0) error handling powerlimit needed?
iv->devControlCmd = ReactivePowerContr;
iv->powerLimit[0] = std::stoi(std::string((char *)payload, (unsigned int)length));
iv->powerLimit[1] = 0x0000; // if reactivepower limit is set via external interface --> set it temporay
DPRINTLN(DBG_DEBUG, F("Reactivepower limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W"));
iv->devControlRequest = true;
}
break;
// Set Power Factor
case PFSet:
// iv->devControlCmd = PFSet;
// uint16_t power_factor = std::stoi(strtok(NULL, "/"));
DPRINTLN(DBG_INFO, F("Set Power Factor not implemented for inverter ") + String(iv->id));
break;
// CleanState lock & alarm
case CleanState_LockAndAlarm:
iv->devControlCmd = CleanState_LockAndAlarm;
DPRINTLN(DBG_INFO, F("CleanState lock & alarm for inverter ") + String(iv->id));
iv->devControlRequest = true;
break;
default:
DPRINTLN(DBG_INFO, "Not implemented");
break;
}
}
}
}
break;
}
token = strtok(NULL, "/");
}
DPRINTLN(DBG_INFO, F("app::cbMqtt finished"));
}
espMqttClient mClient;
cfgMqtt_t *mCfgMqtt;
#if defined(ESP8266)
WiFiEventHandler mHWifiCon, mHWifiDiscon;
#endif
uint32_t *mSunrise, *mSunset;
WiFiClient mEspClient;
PubSubClient *mClient;
HMSYSTEM *mSys;
uint32_t *mUtcTimestamp;
bool mAddressSet;
cfgMqtt_t *mCfg_mqtt;
const char *mDevName;
uint32_t mLastReconnect;
uint32_t mTxCnt;
uint32_t mRxCnt, mTxCnt;
std::queue<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__*/

13
src/publisher/pubSerial.h

@ -21,12 +21,9 @@ class PubSerial {
mUtcTimestamp = utcTs;
}
void tickerMinute() {
DPRINTLN(DBG_INFO, "tickerMinute");
if(++mTick >= mCfg->serial.interval) {
mTick = 0;
void tick(void) {
if (mCfg->serial.showIv) {
char topic[30], val[10];
char topic[32 + MAX_NAME_LENGTH], val[40];
for (uint8_t id = 0; id < mSys->getNumInverters(); id++) {
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL != iv) {
@ -35,8 +32,8 @@ class PubSerial {
DPRINTLN(DBG_INFO, F("Inverter: ") + String(id));
for (uint8_t i = 0; i < rec->length; i++) {
if (0.0f != iv->getValue(i, rec)) {
snprintf(topic, 30, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(val, 10, "%.3f %s", iv->getValue(i, rec), iv->getUnit(i, rec));
snprintf(topic, 32 + MAX_NAME_LENGTH, "%s/ch%d/%s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(val, 40, "%.3f %s", iv->getValue(i, rec), iv->getUnit(i, rec));
DPRINTLN(DBG_INFO, String(topic) + ": " + String(val));
}
yield();
@ -47,12 +44,10 @@ class PubSerial {
}
}
}
}
private:
settings_t *mCfg;
HMSYSTEM *mSys;
uint8_t mTick;
uint32_t *mUtcTimestamp;
};

42
src/utils/helper.cpp

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

23
src/utils/helper.h

@ -0,0 +1,23 @@
//-----------------------------------------------------------------------------
// 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);
}
#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__*/

150
src/utils/scheduler.h

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

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

35
src/web/html/index.html

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

49
src/web/html/serial.html

@ -45,15 +45,15 @@
<br/>
<br/>
<br/>
<label>Send Power Limit: </label>
<label for="pwrlimval">Power Limit Value</label>
<input type="number" class="text" name="pwrlimval" maxlength="4"/>
<label> </label>
<select name="pwrlimcntrl" id="pwrlimcntrl">
<label for="pwrlimctrl">Power Limit Command</label>
<select name="pwrlimctrl">
<option value="" selected disabled hidden>select the unit and persistence</option>
<option value="0">absolute in Watt non persistent</option>
<option value="1">relative in percent non persistent</option>
<option value="256">absolute in Watt persistent</option>
<option value="257">relative in percent persistent</option>
<option value="limit_nonpersistent_absolute">absolute non persistent [W]</option>
<option value="limit_nonpersistent_relative">relative non persistent [%]</option>
<option value="limit_persistent_absolute">absolute persistent [W]</option>
<option value="limit_persistent_relative">relative persistent [%]</option>
</select>
<br/>
<input type="button" value="Send Power Limit" class="btn" id="sendpwrlim"/>
@ -120,7 +120,7 @@
// set time offset for serial console
var obj = new Object();
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));
}
@ -152,7 +152,6 @@
}
// only for test
function ctrlCb(obj) {
var e = document.getElementById("result");
if(obj["success"])
@ -169,44 +168,36 @@
const wrapper = document.getElementById('power');
wrapper.addEventListener('click', (event) => {
var power = event.target.value;
var obj = new Object();
obj.id = get_selected_iv();
obj.cmd = "power";
switch (power)
{
switch (event.target.value) {
default:
case "Turn On":
obj.cmd = 0;
obj.val = 1;
break;
case "Turn Off":
obj.cmd = 1;
obj.val = 0;
break;
default:
obj.cmd = 2;
}
obj.inverter = get_selected_iv();
obj.tx_request = 81;
getAjax("/api/ctrl", ctrlCb, "POST", JSON.stringify(obj));
});
document.getElementById("sendpwrlim").addEventListener("click", function() {
var val = parseInt(document.getElementsByName('pwrlimval')[0].value);
var ctrl = parseInt(document.getElementsByName('pwrlimcntrl')[0].value);
if((ctrl == 1 || ctrl == 257) && val < 2) val = 2;
var cmd = document.getElementsByName('pwrlimctrl')[0].value;
if(isNaN(val) || isNaN(ctrl))
{
var tmp = (isNaN(val)) ? "Value" : "Unit";
document.getElementById("result").textContent = tmp + " is missing";
if(isNaN(val)) {
document.getElementById("result").textContent = "value is missing";
return;
}
var obj = new Object();
obj.inverter = get_selected_iv();
obj.cmd = 11;
obj.tx_request = 81;
obj.payload = [val, ctrl];
obj.id = get_selected_iv();
obj.cmd = cmd;
obj.val = val;
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/>
<label for="networks">Avail Networks</label>
<select name="networks" id="networks" onChange="selNet()">
<option value="-1">not scanned</option>
<option value="-1" selected disabled hidden>not scanned</option>
</select>
<label for="ssid">SSID</label>
<input type="text" name="ssid" class="text"/>
@ -163,7 +163,7 @@
</div>
<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/>
<br/>
<a href="/get_setup" target="_blank">Download your settings (JSON file)</a> (only saved values)
@ -226,7 +226,7 @@
var date = new Date();
var obj = new Object();
obj.cmd = "set_time";
obj.ts = parseInt(date.getTime() / 1000);
obj.val = parseInt(date.getTime() / 1000);
getAjax("/api/setup", apiCbNtp, "POST", JSON.stringify(obj));
}
@ -234,7 +234,7 @@
var obj = new Object();
obj.cmd = "scan_wifi";
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() {

6
src/web/html/update.html

@ -18,12 +18,6 @@
</div>
<div id="wrapper">
<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">
<input type="file" name="update"><input type="submit" value="Update">
</form>

50
src/web/web.cpp

@ -11,6 +11,7 @@
#include "web.h"
#include "../utils/ahoyTimer.h"
#include "../utils/helper.h"
#include "html/h/index_html.h"
#include "html/h/login_html.h"
@ -107,8 +108,10 @@ void web::loop(void) {
void web::tickSecond() {
if(0 != mLogoutTimeout) {
mLogoutTimeout -= 1;
if(0 == mLogoutTimeout)
if(0 == mLogoutTimeout) {
if(strlen(mConfig->sys.adminPwd) > 0)
mProtected = true;
}
DPRINTLN(DBG_DEBUG, "auto logout in " + String(mLogoutTimeout));
}
@ -341,28 +344,16 @@ void web::showSave(AsyncWebServerRequest *request) {
// 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);
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
@ -435,12 +426,14 @@ void web::showSave(AsyncWebServerRequest *request) {
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") != "") {
@ -646,9 +639,12 @@ void web::serialCb(String msg) {
mSerialBufFill = 0;
mEvts->send("webSerial, buffer overflow!", "serial", millis());
}
}
//-----------------------------------------------------------------------------
void web::apiCtrlRequest(JsonObject obj) {
mApi->ctrlRequest(obj);
}
//-----------------------------------------------------------------------------
#ifdef ENABLE_JSON_EP
@ -668,10 +664,10 @@ void web::showJson(void) {
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("\t\"last_msg\": \"") + ah::getDateTimeStr(iv->ts) + F("\"\n\t},\n");
}
}
modJson += F("\"json_ts\": \"") + String(mMain->getDateTimeStr(mMain->mTimestamp)) + F("\"\n}\n");
modJson += F("\"json_ts\": \"") + String(ah::getDateTimeStr(mMain->mTimestamp)) + F("\"\n}\n");
mWeb->send(200, F("application/json"), modJson);
}

11
src/web/web.h

@ -40,6 +40,8 @@ class web {
void serialCb(String msg);
void apiCtrlRequest(JsonObject obj);
private:
void onConnect(AsyncEventSourceClient *client);
@ -62,15 +64,6 @@ class web {
void onSerial(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
void showJson(void);
#endif

123
src/web/webApi.cpp

@ -34,6 +34,19 @@ void webApi::setup(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) {
@ -150,7 +163,6 @@ void webApi::getSysInfo(JsonObject obj) {
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();
@ -319,13 +331,12 @@ void webApi::getSerial(JsonObject obj) {
//-----------------------------------------------------------------------------
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);
}
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);
}
@ -333,7 +344,7 @@ void webApi::getStaticIp(JsonObject obj) {
void webApi::getMenu(JsonObject obj) {
obj["name"][0] = "Live";
obj["link"][0] = "/live";
obj["name"][1] = "Serial Console";
obj["name"][1] = "Serial / Control";
obj["link"][1] = "/serial";
obj["name"][2] = "Settings";
obj["link"][2] = "/setup";
@ -346,10 +357,14 @@ void webApi::getMenu(JsonObject obj) {
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";
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";
}
}
@ -380,7 +395,7 @@ void webApi::getIndex(JsonObject obj) {
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())
if((!mApp->mqttIsConnected()) && (String(mConfig->mqtt.broker).length() > 0))
warn.add(F("MQTT is not connected"));
JsonArray info = obj.createNestedArray(F("infos"));
@ -432,7 +447,7 @@ void webApi::getLive(JsonObject obj) {
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("power_limit_read")] = ah::round3(iv->actPowerLimit);
obj2[F("last_alarm")] = String(iv->lastAlarmMsg);
obj2[F("ts_last_success")] = rec->ts;
@ -441,7 +456,7 @@ void webApi::getLive(JsonObject obj) {
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;
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;
}
@ -458,7 +473,7 @@ void webApi::getLive(JsonObject obj) {
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;
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;
@ -494,72 +509,50 @@ void webApi::getRecord(JsonObject obj, record_t<> *rec) {
//-----------------------------------------------------------------------------
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>();
Inverter<> *iv = mApp->mSys->getInverterByPos(jsonIn[F("id")]);
if(NULL == iv) {
jsonOut[F("error")] = F("inverter index invalid: ") + jsonIn[F("id")].as<String>();
return false;
}
if(NULL != iv)
{
switch (cmd)
{
case TurnOn:
iv->devControlCmd = TurnOn;
iv->devControlRequest = true;
break;
case TurnOff:
iv->devControlCmd = TurnOff;
if(F("power") == jsonIn[F("cmd")]) {
iv->devControlCmd = (jsonIn[F("val")] == 1) ? TurnOn : TurnOff;
iv->devControlRequest = true;
break;
case CleanState_LockAndAlarm:
iv->devControlCmd = CleanState_LockAndAlarm;
iv->devControlRequest = true;
break;
case Restart:
} else if(F("restart") == jsonIn[F("restart")]) {
iv->devControlCmd = Restart;
iv->devControlRequest = true;
break;
case ActivePowerContr:
}
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;
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'");
jsonOut[F("error")] = F("unknown cmd: '") + jsonIn["cmd"].as<String>() + "'";
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")]);
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("ts")];
mTimezoneOffset = jsonIn[F("val")];
else if(F("discovery_cfg") == jsonIn[F("cmd")])
mApp->mFlagSendDiscoveryConfig = true; // for homeassistant
else {
@ -569,13 +562,3 @@ bool webApi::setSetup(JsonObject jsonIn, JsonObject jsonOut) {
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;
}
void ctrlRequest(JsonObject obj);
private:
void onApi(AsyncWebServerRequest *request);
void onApiPost(AsyncWebServerRequest *request);
@ -57,18 +59,6 @@ class webApi {
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;

267
src/wifi/ahoywifi.cpp

@ -8,150 +8,93 @@
#define F(sl) (sl)
#endif
#include "ahoywifi.h"
#include "../utils/ahoyTimer.h"
// NTP CONFIG
#define NTP_PACKET_SIZE 48
//-----------------------------------------------------------------------------
ahoywifi::ahoywifi(settings_t *config) {
mConfig = config;
mDns = new DNSServer();
mUdp = new WiFiUDP();
mWifiStationTimeout = 10;
wifiWasEstablished = false;
mNextTryTs = 0;
mApLastTick = 0;
mApActive = false;
ahoywifi::ahoywifi() {
mCnt = 0;
mConnected = false;
mInitNtp = true;
}
//-----------------------------------------------------------------------------
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
mStationWifiIsDef = false;
#else
mStationWifiIsDef = (strncmp(mConfig->sys.stationSsid, FB_WIFI_SSID, 14) == 0);
#if !defined(FB_WIFI_OVERRIDDEN)
if(strncmp(mConfig->sys.stationSsid, FB_WIFI_SSID, 14) == 0)
setupAp();
#endif
mWifiStationTimeout = timeout;
#ifndef AP_ONLY
if(false == mApActive)
mApActive = (mStationWifiIsDef) ? true : setupStation(mWifiStationTimeout);
#if !defined(AP_ONLY)
if(mConfig->valid)
setupStation();
#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
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) {
if(mApActive) {
mDns->processNextRequest();
#ifndef AP_ONLY
if(ah::checkTicker(&mNextTryTs, (WIFI_AP_ACTIVE_TIME * 1000))) {
mApActive = (mStationWifiIsDef) ? true : setupStation(mWifiStationTimeout);
if(mApActive) {
if(strlen(WIFI_AP_PWD) < 8)
DPRINTLN(DBG_ERROR, F("password must be at least 8 characters long"));
mApLastTick = millis();
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"));
}
void ahoywifi::loop() {
#if !defined(AP_ONLY)
if(!mConnected) {
delay(100);
mCnt++;
if((mCnt % 50) == 0)
WiFi.disconnect();
else if((mCnt % 60) == 0) {
WiFi.reconnect();
mCnt = 0;
}
} else if(mInitNtp) {
getNtpTime();
mInitNtp = false;
}
mCnt = 0;
#endif
}
if((WiFi.status() != WL_CONNECTED) && wifiWasEstablished) {
if(!mApActive) {
DPRINTLN(DBG_INFO, "[WiFi]: Connection Lost");
mApActive = (mStationWifiIsDef) ? true : setupStation(mWifiStationTimeout);
}
}
return mApActive;
}
//-----------------------------------------------------------------------------
void ahoywifi::setupAp(const char *ssid, const char *pwd) {
DPRINTLN(DBG_VERBOSE, F("app::setupAp"));
void ahoywifi::setupAp(void) {
DPRINTLN(DBG_VERBOSE, F("wifi::setupAp"));
IPAddress apIp(192, 168, 4, 1);
DBGPRINTLN(F("\n---------\nAhoy Info:"));
DBGPRINTLN(F("\n---------\nAhoyDTU Info:"));
DBGPRINT(F("Version: "));
DBGPRINTLN(String(VERSION_MAJOR) + F(".") + String(VERSION_MINOR) + F(".") + String(VERSION_PATCH));
DBGPRINT(F("Github Hash: "));
DBGPRINTLN(String(AUTO_GIT_HASH));
DBGPRINT(F("\n---------\nAP MODE\nSSID: "));
DBGPRINTLN(ssid);
DBGPRINTLN(WIFI_AP_SSID);
DBGPRINT(F("PWD: "));
DBGPRINTLN(pwd);
DBGPRINT(F("\nActive for: "));
DBGPRINT(String(WIFI_AP_ACTIVE_TIME));
DBGPRINTLN(F(" seconds"));
DBGPRINTLN(WIFI_AP_PWD);
DBGPRINTLN("IP Address: http://" + apIp.toString());
DBGPRINTLN(F("---------\n"));
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.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) {
DPRINTLN(DBG_VERBOSE, F("app::setupStation"));
int32_t cnt;
bool startAp = false;
if(timeout >= 3)
cnt = (timeout - 3) / 2 * 10;
else {
timeout = 1;
cnt = 1;
}
WiFi.mode(WIFI_STA);
void ahoywifi::setupStation(void) {
DPRINTLN(DBG_VERBOSE, F("wifi::setupStation"));
if(mConfig->sys.ip.ip[0] != 0) {
IPAddress ip(mConfig->sys.ip.ip);
IPAddress mask(mConfig->sys.ip.mask);
@ -165,45 +108,14 @@ bool ahoywifi::setupStation(uint32_t timeout) {
if(String(mConfig->sys.deviceName) != "")
WiFi.hostname(mConfig->sys.deviceName);
delay(2000);
DBGPRINT(F("connect to network '"));
DBGPRINT(mConfig->sys.stationSsid);
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) {
return mApActive;
}
//-----------------------------------------------------------------------------
time_t ahoywifi::getNtpTime(void) {
void ahoywifi::getNtpTime(void) {
//DPRINTLN(DBG_VERBOSE, F("wifi::getNtpTime"));
time_t date = 0;
IPAddress timeServer;
@ -211,16 +123,16 @@ time_t ahoywifi::getNtpTime(void) {
uint8_t retry = 0;
WiFi.hostByName(mConfig->ntp.addr, timeServer);
mUdp->begin(mConfig->ntp.port);
mUdp.begin(mConfig->ntp.port);
sendNTPpacket(timeServer);
while(retry++ < 5) {
int wait = 150;
while(--wait) {
if(NTP_PACKET_SIZE <= mUdp->parsePacket()) {
if(NTP_PACKET_SIZE <= mUdp.parsePacket()) {
uint64_t secsSince1900;
mUdp->read(buf, NTP_PACKET_SIZE);
mUdp.read(buf, NTP_PACKET_SIZE);
secsSince1900 = (buf[40] << 24);
secsSince1900 |= (buf[41] << 16);
secsSince1900 |= (buf[42] << 8);
@ -228,13 +140,14 @@ time_t ahoywifi::getNtpTime(void) {
date = secsSince1900 - 2208988800UL; // UTC time
break;
}
else
} else
delay(10);
}
}
return date;
*mUtcTimestamp = date;
DPRINTLN(DBG_INFO, "[NTP]: " + ah::getDateTimeStr(*mUtcTimestamp) + " UTC");
}
@ -283,7 +196,73 @@ void ahoywifi::sendNTPpacket(IPAddress& address) {
buf[14] = 49;
buf[15] = 52;
mUdp->beginPacket(address, 123); // NTP request, port 123
mUdp->write(buf, NTP_PACKET_SIZE);
mUdp->endPacket();
mUdp.beginPacket(address, 123); // NTP request, port 123
mUdp.write(buf, NTP_PACKET_SIZE);
mUdp.endPacket();
}
//-----------------------------------------------------------------------------
#if defined(ESP8266)
void ahoywifi::onConnect(const WiFiEventStationModeGotIP& event) {
if(!mConnected) {
mConnected = true;
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;
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 <Arduino.h>
#include <WiFiUdp.h>
#include <TimeLib.h>
#include <DNSServer.h>
#include "ESPAsyncWebServer.h"
@ -19,32 +18,38 @@ class app;
class ahoywifi {
public:
ahoywifi(settings_t *config);
~ahoywifi() {}
void setup(uint32_t timeout, bool settingValid);
bool loop(void);
void setupAp(const char *ssid, const char *pwd);
bool setupStation(uint32_t timeout);
bool getApActive(void);
time_t getNtpTime(void);
ahoywifi();
void setup(settings_t *config, uint32_t *utcTimestamp);
void loop(void);
void getNtpTime(void);
void scanAvailNetworks(void);
void getAvailNetworks(JsonObject obj);
private:
void setupAp(void);
void setupStation(void);
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;
DNSServer *mDns;
WiFiUDP *mUdp; // for time server
DNSServer mDns;
WiFiUDP mUdp; // for time server
#if defined(ESP8266)
WiFiEventHandler wifiConnectHandler;
WiFiEventHandler wifiDisconnectHandler;
#endif
uint32_t mWifiStationTimeout;
uint32_t mNextTryTs;
uint32_t mApLastTick;
bool mApActive;
bool wifiWasEstablished;
bool mStationWifiIsDef;
bool mConnected, mInitNtp;
uint8_t mCnt;
uint32_t *mUtcTimestamp;
};
#endif /*__AHOYWIFI_H__*/

Loading…
Cancel
Save