Browse Source

Merge remote-tracking branch 'refs/remotes/origin/development03' into development03

pull/861/head
Frank 2 years ago
parent
commit
7718411ee6
  1. 4
      Getting_Started.md
  2. 2
      README.md
  3. 67
      User_Manual.md
  4. 2
      doc/prometheus_ep_description.md
  5. 1
      src/.vscode/settings.json
  6. 61
      src/CHANGES.md
  7. 7
      src/LICENSE
  8. 6
      src/app.cpp
  9. 31
      src/app.h
  10. 6
      src/appInterface.h
  11. 65
      src/config/settings.h
  12. 4
      src/defines.h
  13. 10
      src/hm/hmInverter.h
  14. 61
      src/hm/hmPayload.h
  15. 31
      src/hm/hmRadio.h
  16. 415
      src/hm/miPayload.h
  17. 4
      src/main.cpp
  18. 19
      src/platformio.ini
  19. 10
      src/plugins/Display/Display.h
  20. 6
      src/plugins/Display/Display_Mono.cpp
  21. 2
      src/plugins/Display/Display_ePaper.cpp
  22. 245
      src/publisher/pubMqtt.h
  23. 18
      src/publisher/pubMqttDefs.h
  24. 10
      src/utils/dbg.h
  25. 68
      src/web/RestApi.h
  26. 3
      src/web/html/convert.py
  27. 2
      src/web/html/includes/footer.html
  28. 4
      src/web/html/index.html
  29. 51
      src/web/html/save.html
  30. 69
      src/web/html/setup.html
  31. 115
      src/web/web.h
  32. 12
      src/wifi/ahoywifi.cpp
  33. 15
      tools/rpi/Dockerfile
  34. 18
      tools/rpi/README.md
  35. 42
      tools/rpi/ahoy_system.service

4
Getting_Started.md

@ -53,6 +53,10 @@ Hoymiles Inverters
- HM1000? - HM1000?
- HM1200 - HM1200
- HM1500 - HM1500
- MI-300* [For MI inverters see remarks here](User_Manual.md#mi-inverters)
- MI-600*
- MI-700*
- MI-1500* (2nd gen. still untested)
TSUN Inverters: TSUN Inverters:

2
README.md

@ -48,4 +48,4 @@ Please try to describe your issues as precise as possible and think about if thi
- [OpenDTU](https://github.com/tbnobody/OpenDTU) - [OpenDTU](https://github.com/tbnobody/OpenDTU)
<- Our sister project for Hoymiles HM-300, HM-600, HM-1200 (for ESP32 only!) <- Our sister project for Hoymiles HM-300, HM-600, HM-1200 (for ESP32 only!)
- [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles) - [DTU Simulator](https://github.com/Ziyatoe/DTUsimMI1x00-Hoymiles)
<- Go here for Hoymiles MI-300, MI-600, MI-1200 Software <- Go here for Hoymiles MI-300, MI-600, MI-1200 Software (single inverter only)

67
User_Manual.md

@ -91,9 +91,6 @@ The AhoyDTU will publish on the following topics
## Active Power Limit via Serial / Control Page ## Active Power Limit via Serial / Control Page
URL: `/serial` 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.
You can change the setting in the following manner. You can change the setting in the following manner.
Decide if you want to set Decide if you want to set
@ -115,24 +112,17 @@ Also an absolute active power limit below approx. 30 Watt seems to be not meanfu
### Generic Information ### Generic Information
The AhoyDTU subscribes on three topics `<TOPIC>/ctrl/#`, `<TOPIC>/setup` and `<TOPIC>/status`. The AhoyDTU subscribes on following topics:
- `<TOPIC>/ctrl/limit/<INVERTER_ID>`
- `<TOPIC>/ctrl/restart/<INVERTER_ID>`
- `<TOPIC>/setup/set_time`
👆 `<TOPIC>` can be set on setup page, default is `inverter`. 👆 `<TOPIC>` can be set on setup page, default is `inverter`.
👆 `<INVERTER_ID>` is the number of the specific inverter in the setup page. 👆 `<INVERTER_ID>` is the number of the specific inverter in the setup page.
### 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 ### Inverter restart
```mqtt ```mqtt
<TOPIC>/ctrl/restart/<INVERTER_ID> <TOPIC>/ctrl/restart/<INVERTER_ID>
@ -142,50 +132,35 @@ Example:
inverter/ctrl/restart/0 inverter/ctrl/restart/0
``` ```
### Power Limit relative persistent [%] ### Power Limit relative (non persistent) [%]
```mqtt ```mqtt
<TOPIC>/ctrl/limit_persistent_relative/<INVERTER_ID> <TOPIC>/ctrl/limit/<INVERTER_ID>
``` ```
with a payload `[2 .. 100]` with a payload `[2 .. 100]`
**NOTE: optional a `%` can be sent as last character**
Example: Example:
```mqtt ```mqtt
inverter/ctrl/limit_persistent_relative/0 70 inverter/ctrl/limit/0 70
``` ```
### Power Limit absolute persistent [Watts] ### Power Limit absolute (non persistent) [Watts]
```mqtt ```mqtt
<TOPIC>/ctrl/limit_persistent_absolute/<INVERTER_ID> <TOPIC>/ctrl/limit/<INVERTER_ID>
``` ```
with a payload `[0 .. 65535]` with a payload `[0 .. 65535]`
Example: **NOTE: the unit `W` is necessary to determine an absolute limit**
```mqtt
inverter/ctrl/limit_persistent_absolute/0 600
```
### Power Limit relative non persistent [%]
```mqtt
<TOPIC>/ctrl/limit_nonpersistent_relative/<INVERTER_ID>
```
with a payload `[2 .. 100]`
Example: Example:
```mqtt ```mqtt
inverter/ctrl/limit_nonpersistent_relative/0 70 inverter/ctrl/limit/0 600W
```
### Power Limit absolute non persistent [Watts]
```mqtt
<TOPIC>/ctrl/limit_nonpersistent_absolute/<INVERTER_ID>
``` ```
with a payload `[0 .. 65535]`
Example: ### Power Limit persistent
```mqtt This feature was removed. The persisten limit should not be modified cyclic by a script because of potential wearout of the flash inside the inverter.
inverter/ctrl/limit_nonpersistent_absolute/0 600
```
## Control via REST API ## Control via REST API
@ -310,6 +285,8 @@ To get the information open the URL `/api/record/info` on your AhoyDTU. The info
| tomquist | TSOL-M1600 | | 1.0.12 | 2020 | 06-24 | 100 | | | | tomquist | TSOL-M1600 | | 1.0.12 | 2020 | 06-24 | 100 | | |
| rejoe2 | MI-600 | | 236 | 2018 | 11-27 | 17 | | | | rejoe2 | MI-600 | | 236 | 2018 | 11-27 | 17 | | |
| rejoe2 | MI-1500 | | 1.0.12 | 2020 | 06-24 | 100 | | | | rejoe2 | MI-1500 | | 1.0.12 | 2020 | 06-24 | 100 | | |
| dragricola | HM-1200 | | 1.0.16 | 2021 | 10-12 | 100 | | |
| dragricola | MI-300 | | 230 | 2017 | 08-08 | 1 | | |
| | | | | | | | | | | | | | | | | | | |
## Developer Information about Command Queue ## Developer Information about Command Queue
@ -344,3 +321,11 @@ Send Power Limit:
- A persistent limit is only needed if you want to throttle your inverter permanently or you can use it to set a start value on the battery, which is then always the switch-on limit when switching on, otherwise it would ramp up to 100% without regulation, which is continuous load is not healthy. - A persistent limit is only needed if you want to throttle your inverter permanently or you can use it to set a start value on the battery, which is then always the switch-on limit when switching on, otherwise it would ramp up to 100% without regulation, which is continuous load is not healthy.
- You can set a new limit in the turn-off state, which is then used for on (switching on again), otherwise the last limit from before the turn-off is used, but of course this only applies if DC voltage is applied the whole time. - You can set a new limit in the turn-off state, which is then used for on (switching on again), otherwise the last limit from before the turn-off is used, but of course this only applies if DC voltage is applied the whole time.
- If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit. - If the DC voltage is missing for a few seconds, the microcontroller in the inverter goes off and forgets everything that was temporary/non-persistent in the RAM: YieldDay, error memory, non-persistent limit.
## Additional Notes
### MI Inverters
- AhoyDTU supports MI type inverters as well, since dev. version 0.5.70.
- MI inverters are known to be delivered with two different generations of firmwares: inverters with serial numbers 10x2 already use the 3rd generation protocol and behave just like the newer HM models, *the follwoing remarks do not apply to these*.
- Older MI inverters (#sn 10x1) use a different rf protocol and thus do not deliver exactly the same data. E.g. the AC power value will therefore be calculated by AhoyDTU itself, while other values might not be available at all.
- Single and dual channel 2nd gen. devices seem not to accept power limiting commands at all, the lower limit for 4-channel MI is 10% (instead of 2% for newer models)
- 4-channel MI type inverters might work, but code still is untested.

2
doc/prometheus_ep_description.md

@ -18,6 +18,8 @@ Prometheus metrics provided at `/metrics`.
| Metric name | Type | Description | Labels | | Metric name | Type | Description | Labels |
|----------------------------------------|---------|--------------------------------------------------------|--------------| |----------------------------------------|---------|--------------------------------------------------------|--------------|
| `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename | | `ahoy_solar_info` | Gauge | Information about the AhoyDTU device | version, image, devicename |
| `ahoy_solar_uptime` | Counter | Seconds since boot of the AhoyDTU device | devicename |
| `ahoy_solar_rssi_db` | Gauge | Quality of the Wifi STA connection | devicename |
| `ahoy_solar_inverter_info` | Gauge | Information about the configured inverter(s) | name, serial | | `ahoy_solar_inverter_info` | Gauge | Information about the configured inverter(s) | name, serial |
| `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter | | `ahoy_solar_inverter_enabled` | Gauge | Is the inverter enabled? | inverter |
| `ahoy_solar_inverter_is_available` | Gauge | is the inverter available? | inverter | | `ahoy_solar_inverter_is_available` | Gauge | is the inverter available? | inverter |

1
src/.vscode/settings.json

@ -85,4 +85,5 @@
"stop_token": "cpp", "stop_token": "cpp",
"thread": "cpp" "thread": "cpp"
}, },
"cmake.configureOnOpen": false,
} }

61
src/CHANGES.md

@ -2,6 +2,67 @@
(starting from release version `0.5.66`) (starting from release version `0.5.66`)
## 0.5.107
* fix: show save message
* fix: removed serial newline for `enqueueCmd`
* Merged improved Prometheus #808
## 0.5.106
* merged MI and debug message changes #804
* fixed MQTT autodiscover #794, #632
## 0.5.105
* merged MI, thx @rejoe2 #788
* fixed reboot message #793
## 0.5.104
* further improved save settings
* removed `#` character from ePaper
* fixed saving pinout for `Nokia-Display`
* removed `Reset` Pin for monochrome displays
* improved wifi connection #652
## 0.5.103
* merged MI improvements, thx @rejoe2 #778
* changed display inverter online message
* merged heap improvements #772
## 0.5.102
* Warning: old exports are not compatible any more!
* fix JSON import #775
* fix save settings, at least already stored settings are not lost #771
* further save settings improvements (only store inverters which are existing)
* improved display of settings save return value
* made save settings asynchronous (more heap memory is free)
## 0.5.101
* fix SSD1306
* update documentation
* Update miPayload.h
* Update README.md
* MI - remarks to user manual
* MI - fix AC calc
* MI - fix status msg. analysis
## 0.5.100
* fix add inverter `setup.html` #766
* fix MQTT retained flag for total values #726
* renamed buttons for import and export `setup.html`
* added serial message `settings saved`
## 0.5.99
* fix limit in [User_Manual.md](../User_Manual.md)
* changed `contrast` to `luminance` in `setup.html`
* try to fix SSD1306 display #759
* only show necessary display pins depending on setting
## 0.5.98
* fix SH1106 rotation and turn off during night #756
* removed MQTT subscription `sync_ntp`, `set_time` with a value of `0` does the same #696
* simplified MQTT subscription for `limit`. Check [User_Manual.md](../User_Manual.md) for new syntax #696, #713
* repaired inverter wise limit control
* fix upload settings #686
## 0.5.97 ## 0.5.97
* Attention: re-ordered display types, check your settings! #746 * Attention: re-ordered display types, check your settings! #746
* improved saving settings of display #747, #746 * improved saving settings of display #747, #746

7
src/LICENSE

@ -1,7 +0,0 @@
License
CC-CY-NC-SA 3.0
https://creativecommons.org/licenses/by-nc-sa/3.0/de
This project is for non-commercial use only!

6
src/app.cpp

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de // 2023 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#include "app.h" #include "app.h"
@ -78,7 +78,7 @@ void app::setup() {
// Plugins // Plugins
if (mConfig->plugin.display.type != 0) if (mConfig->plugin.display.type != 0)
mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, 0xff, mVersion); mDisplay.setup(&mConfig->plugin.display, &mSys, &mTimestamp, mVersion);
mPubSerial.setup(mConfig, &mSys, &mTimestamp); mPubSerial.setup(mConfig, &mSys, &mTimestamp);
@ -374,6 +374,8 @@ void app::resetSystem(void) {
mSendLastIvId = 0; mSendLastIvId = 0;
mShowRebootRequest = false; mShowRebootRequest = false;
mIVCommunicationOn = true; mIVCommunicationOn = true;
mSavePending = false;
mSaveReboot = false;
memset(&mStat, 0, sizeof(statistics_t)); memset(&mStat, 0, sizeof(statistics_t));
} }

31
src/app.h

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de // 2023 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef __APP_H__ #ifndef __APP_H__
@ -68,9 +68,12 @@ class app : public IApp, public ah::Scheduler {
return Scheduler::getTimestamp(); return Scheduler::getTimestamp();
} }
bool saveSettings() { bool saveSettings(bool reboot) {
mShowRebootRequest = true; mShowRebootRequest = true; // only message on index, no reboot
return mSettings.saveSettings(); mSavePending = true;
mSaveReboot = reboot;
once(std::bind(&app::tickSave, this), 3, "save");
return true;
} }
bool readSettings(const char *path) { bool readSettings(const char *path) {
@ -81,6 +84,14 @@ class app : public IApp, public ah::Scheduler {
return mSettings.eraseSettings(eraseWifi); return mSettings.eraseSettings(eraseWifi);
} }
bool getSavePending() {
return mSavePending;
}
bool getLastSaveSucceed() {
return mSettings.getLastSaveSucceed();
}
statistics_t *getStatistics() { statistics_t *getStatistics() {
return &mStat; return &mStat;
} }
@ -210,9 +221,19 @@ class app : public IApp, public ah::Scheduler {
onWifi(false); onWifi(false);
ah::Scheduler::resetTicker(); ah::Scheduler::resetTicker();
WiFi.disconnect(); WiFi.disconnect();
delay(200);
ESP.restart(); ESP.restart();
} }
void tickSave(void) {
if(!mSettings.saveSettings())
mSaveReboot = false;
mSavePending = false;
if(mSaveReboot)
setRebootFlag();
}
void tickNtpUpdate(void); void tickNtpUpdate(void);
void tickCalcSunrise(void); void tickCalcSunrise(void);
void tickIVCommunication(void); void tickIVCommunication(void);
@ -252,6 +273,8 @@ class app : public IApp, public ah::Scheduler {
char mVersion[12]; char mVersion[12];
settings mSettings; settings mSettings;
settings_t *mConfig; settings_t *mConfig;
bool mSavePending;
bool mSaveReboot;
uint8_t mSendLastIvId; uint8_t mSendLastIvId;
bool mSendFirst; bool mSendFirst;

6
src/appInterface.h

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2022 Ahoy, https://ahoydtu.de // 2022 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef __IAPP_H__ #ifndef __IAPP_H__
@ -14,9 +14,11 @@
class IApp { class IApp {
public: public:
virtual ~IApp() {} virtual ~IApp() {}
virtual bool saveSettings() = 0; virtual bool saveSettings(bool stopFs) = 0;
virtual bool readSettings(const char *path) = 0; virtual bool readSettings(const char *path) = 0;
virtual bool eraseSettings(bool eraseWifi) = 0; virtual bool eraseSettings(bool eraseWifi) = 0;
virtual bool getSavePending() = 0;
virtual bool getLastSaveSucceed() = 0;
virtual void setOnUpdate() = 0; virtual void setOnUpdate() = 0;
virtual void setRebootFlag() = 0; virtual void setRebootFlag() = 0;
virtual const char *getVersion() = 0; virtual const char *getVersion() = 0;

65
src/config/settings.h

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de // 2023 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef __SETTINGS_H__ #ifndef __SETTINGS_H__
@ -14,6 +14,12 @@
#include "../utils/dbg.h" #include "../utils/dbg.h"
#include "../utils/helper.h" #include "../utils/helper.h"
#if defined(ESP32)
#define MAX_ALLOWED_BUF_SIZE ESP.getMaxAllocHeap() - 1024
#else
#define MAX_ALLOWED_BUF_SIZE ESP.getMaxFreeBlockSize() - 1024
#endif
/** /**
* More info: * More info:
* https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#flash-layout
@ -155,7 +161,9 @@ typedef struct {
class settings { class settings {
public: public:
settings() {} settings() {
mLastSaveSucceed = false;
}
void setup() { void setup() {
DPRINTLN(DBG_INFO, F("Initializing FS ..")); DPRINTLN(DBG_INFO, F("Initializing FS .."));
@ -202,6 +210,10 @@ class settings {
return mCfg.valid; return mCfg.valid;
} }
inline bool getLastSaveSucceed() {
return mLastSaveSucceed;
}
void getInfo(uint32_t *used, uint32_t *size) { void getInfo(uint32_t *used, uint32_t *size) {
#if !defined(ESP32) #if !defined(ESP32)
FSInfo info; FSInfo info;
@ -224,8 +236,9 @@ class settings {
else { else {
//DPRINTLN(DBG_INFO, fp.readString()); //DPRINTLN(DBG_INFO, fp.readString());
//fp.seek(0, SeekSet); //fp.seek(0, SeekSet);
DynamicJsonDocument root(5500); DynamicJsonDocument root(MAX_ALLOWED_BUF_SIZE);
DeserializationError err = deserializeJson(root, fp); DeserializationError err = deserializeJson(root, fp);
root.shrinkToFit();
if(!err && (root.size() > 0)) { if(!err && (root.size() > 0)) {
mCfg.valid = true; mCfg.valid = true;
jsonWifi(root[F("wifi")]); jsonWifi(root[F("wifi")]);
@ -247,15 +260,10 @@ class settings {
return mCfg.valid; return mCfg.valid;
} }
bool saveSettings(void) { bool saveSettings() {
DPRINTLN(DBG_DEBUG, F("save settings")); DPRINTLN(DBG_DEBUG, F("save settings"));
File fp = LittleFS.open("/settings.json", "w");
if(!fp) {
DPRINTLN(DBG_ERROR, F("can't open settings file!"));
return false;
}
DynamicJsonDocument json(6500); DynamicJsonDocument json(MAX_ALLOWED_BUF_SIZE);
JsonObject root = json.to<JsonObject>(); JsonObject root = json.to<JsonObject>();
jsonWifi(root.createNestedObject(F("wifi")), true); jsonWifi(root.createNestedObject(F("wifi")), true);
jsonNrf(root.createNestedObject(F("nrf")), true); jsonNrf(root.createNestedObject(F("nrf")), true);
@ -267,12 +275,35 @@ class settings {
jsonPlugin(root.createNestedObject(F("plugin")), true); jsonPlugin(root.createNestedObject(F("plugin")), true);
jsonInst(root.createNestedObject(F("inst")), true); jsonInst(root.createNestedObject(F("inst")), true);
DPRINT(DBG_INFO, F("memory usage: "));
DBGPRINTLN(String(json.memoryUsage()));
DPRINT(DBG_INFO, F("capacity: "));
DBGPRINTLN(String(json.capacity()));
DPRINT(DBG_INFO, F("max alloc: "));
DBGPRINTLN(String(MAX_ALLOWED_BUF_SIZE));
if(json.overflowed()) {
DPRINTLN(DBG_ERROR, F("buffer too small!"));
mLastSaveSucceed = false;
return false;
}
File fp = LittleFS.open("/settings.json", "w");
if(!fp) {
DPRINTLN(DBG_ERROR, F("can't open settings file!"));
mLastSaveSucceed = false;
return false;
}
if(0 == serializeJson(root, fp)) { if(0 == serializeJson(root, fp)) {
DPRINTLN(DBG_ERROR, F("can't write settings file!")); DPRINTLN(DBG_ERROR, F("can't write settings file!"));
mLastSaveSucceed = false;
return false; return false;
} }
fp.close(); fp.close();
DPRINTLN(DBG_INFO, F("settings saved"));
mLastSaveSucceed = true;
return true; return true;
} }
@ -403,6 +434,11 @@ class settings {
mCfg.nrf.pinCe = obj[F("ce")]; mCfg.nrf.pinCe = obj[F("ce")];
mCfg.nrf.pinIrq = obj[F("irq")]; mCfg.nrf.pinIrq = obj[F("irq")];
mCfg.nrf.amplifierPower = obj[F("pwr")]; mCfg.nrf.amplifierPower = obj[F("pwr")];
if((obj[F("cs")] == obj[F("ce")])) {
mCfg.nrf.pinCs = DEF_CS_PIN;
mCfg.nrf.pinCe = DEF_CE_PIN;
mCfg.nrf.pinIrq = DEF_IRQ_PIN;
}
} }
} }
@ -523,12 +559,16 @@ class settings {
if(set) if(set)
ivArr = obj.createNestedArray(F("iv")); ivArr = obj.createNestedArray(F("iv"));
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
if(set) if(set) {
if(mCfg.inst.iv[i].serial.u64 != 0ULL)
jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true); jsonIv(ivArr.createNestedObject(), &mCfg.inst.iv[i], true);
else }
else {
if(!obj[F("iv")][i].isNull())
jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]); jsonIv(obj[F("iv")][i], &mCfg.inst.iv[i]);
} }
} }
}
void jsonIv(JsonObject obj, cfgIv_t *cfg, bool set = false) { void jsonIv(JsonObject obj, cfgIv_t *cfg, bool set = false) {
if(set) { if(set) {
@ -553,6 +593,7 @@ class settings {
} }
settings_t mCfg; settings_t mCfg;
bool mLastSaveSucceed;
}; };
#endif /*__SETTINGS_H__*/ #endif /*__SETTINGS_H__*/

4
src/defines.h

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://www.mikrocontroller.net/topic/525778 // 2023 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef __DEFINES_H__ #ifndef __DEFINES_H__
@ -13,7 +13,7 @@
//------------------------------------- //-------------------------------------
#define VERSION_MAJOR 0 #define VERSION_MAJOR 0
#define VERSION_MINOR 5 #define VERSION_MINOR 5
#define VERSION_PATCH 97 #define VERSION_PATCH 107
//------------------------------------- //-------------------------------------
typedef struct { typedef struct {

10
src/hm/hmInverter.h

@ -143,7 +143,9 @@ class Inverter {
template <typename T> template <typename T>
void enqueCommand(uint8_t cmd) { void enqueCommand(uint8_t cmd) {
_commandQueue.push(std::make_shared<T>(cmd)); _commandQueue.push(std::make_shared<T>(cmd));
DPRINTLN(DBG_INFO, F("(#") + String(id) + F(") enqueuedCmd: 0x") + String(cmd, HEX)); DPRINT_IVID(DBG_INFO, id);
DBGPRINT(F("enqueCommand: 0x"));
DBGHEXLN(cmd);
} }
void setQueuedCmdFinished() { void setQueuedCmdFinished() {
@ -286,7 +288,8 @@ class Inverter {
alarmMesIndex = rec->record[pos]; alarmMesIndex = rec->record[pos];
//enqueCommand<InfoCommand>(AlarmUpdate); // What is the function of AlarmUpdate? //enqueCommand<InfoCommand>(AlarmUpdate); // What is the function of AlarmUpdate?
DPRINTLN(DBG_INFO, "alarm ID incremented to " + String(alarmMesIndex)); DPRINT(DBG_INFO, "alarm ID incremented to ");
DBGPRINTLN(String(alarmMesIndex));
enqueCommand<InfoCommand>(AlarmData); enqueCommand<InfoCommand>(AlarmData);
} }
} }
@ -300,7 +303,8 @@ class Inverter {
DPRINTLN(DBG_DEBUG, "add config"); DPRINTLN(DBG_DEBUG, "add config");
if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){ if (getPosByChFld(0, FLD_ACT_ACTIVE_PWR_LIMIT, rec) == pos){
actPowerLimit = rec->record[pos]; actPowerLimit = rec->record[pos];
DPRINT(DBG_DEBUG, F("Inverter actual power limit: ") + String(actPowerLimit, 1)); DPRINT(DBG_DEBUG, F("Inverter actual power limit: "));
DBGPRINTLN(String(actPowerLimit, 1));
} }
} }
else if (rec->assign == AlarmDataAssignment) { else if (rec->assign == AlarmDataAssignment) {

61
src/hm/hmPayload.h

@ -116,12 +116,10 @@ class HmPayload {
mStat->rxFail++; // got fragments but not complete response mStat->rxFail++; // got fragments but not complete response
iv->setQueuedCmdFinished(); // command failed iv->setQueuedCmdFinished(); // command failed
if (mSerialDebug)
DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout"));
if (mSerialDebug) { if (mSerialDebug) {
DPRINT(DBG_INFO, F("(#")); DPRINTLN(DBG_INFO, F("enqueued cmd failed/timeout"));
DBGPRINT(String(iv->id)); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F(") no Payload received! (retransmits: ")); DBGPRINT(F("no Payload received! (retransmits: "));
DBGPRINT(String(mPayload[iv->id].retransmits)); DBGPRINT(String(mPayload[iv->id].retransmits));
DBGPRINTLN(F(")")); DBGPRINTLN(F(")"));
} }
@ -134,17 +132,15 @@ class HmPayload {
yield(); yield();
if (mSerialDebug) { if (mSerialDebug) {
DPRINT(DBG_INFO, F("(#")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(String(iv->id)); DBGPRINT(F("Requesting Inv SN "));
DBGPRINT(F(") Requesting Inv SN "));
DBGPRINTLN(String(iv->config->serial.u64, HEX)); DBGPRINTLN(String(iv->config->serial.u64, HEX));
} }
if (iv->getDevControlRequest()) { if (iv->getDevControlRequest()) {
if (mSerialDebug) { if (mSerialDebug) {
DPRINT(DBG_INFO, F("(#")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(String(iv->id)); DBGPRINT(F("Devcontrol request 0x"));
DBGPRINT(F(") Devcontrol request 0x"));
DBGPRINT(String(iv->devControlCmd, HEX)); DBGPRINT(String(iv->devControlCmd, HEX));
DBGPRINT(F(" power limit ")); DBGPRINT(F(" power limit "));
DBGPRINTLN(String(iv->powerLimit[0])); DBGPRINTLN(String(iv->powerLimit[0]));
@ -155,10 +151,9 @@ class HmPayload {
//iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit //iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
} else { } else {
uint8_t cmd = iv->getQueuedCmd(); uint8_t cmd = iv->getQueuedCmd();
DPRINT(DBG_INFO, F("(#")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(String(iv->id)); DBGPRINT(F("prepareDevInformCmd 0x"));
DBGPRINT(F(") prepareDevInformCmd 0x")); DBGHEXLN(cmd);
DBGPRINTLN(String(cmd, HEX));
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false); mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, false);
mPayload[iv->id].txCmd = cmd; mPayload[iv->id].txCmd = cmd;
} }
@ -170,9 +165,10 @@ class HmPayload {
DPRINTLN(DBG_DEBUG, F("Response from info request received")); DPRINTLN(DBG_DEBUG, F("Response from info request received"));
uint8_t *pid = &p->packet[9]; uint8_t *pid = &p->packet[9];
if (*pid == 0x00) { if (*pid == 0x00) {
DPRINT(DBG_DEBUG, F("fragment number zero received and ignored")); DPRINTLN(DBG_DEBUG, F("fragment number zero received and ignored"));
} else { } else {
DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX)); DPRINT(DBG_DEBUG, F("PID: 0x"));
DBGHEXLN(*pid);
if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) { if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) {
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11);
mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11;
@ -200,14 +196,14 @@ class HmPayload {
mApp->setMqttPowerLimitAck(iv); mApp->setMqttPowerLimitAck(iv);
else else
ok = false; ok = false;
DPRINT(DBG_INFO, F("(#"));
DBGPRINT(String(iv->id)); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F(" has ")); DBGPRINT(F("has "));
if(!ok) DBGPRINT(F("not ")); if(!ok) DBGPRINT(F("not "));
DBGPRINT(F("accepted power limit set point ")); DBGPRINT(F("accepted power limit set point "));
DBGPRINT(String(iv->powerLimit[0])); DBGPRINT(String(iv->powerLimit[0]));
DBGPRINT(F(" with PowerLimitControl ")); DBGPRINT(F(" with PowerLimitControl "));
DBGPRINT(String(iv->powerLimit[1])); DBGPRINTLN(String(iv->powerLimit[1]));
iv->clearCmdQueue(); iv->clearCmdQueue();
iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
@ -243,6 +239,7 @@ class HmPayload {
DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm..."));
mPayload[iv->id].retransmits = mMaxRetrans; mPayload[iv->id].retransmits = mMaxRetrans;
} else if(iv->devControlCmd == ActivePowerContr) { } else if(iv->devControlCmd == ActivePowerContr) {
DPRINT_IVID(DBG_INFO, iv->id);
DPRINTLN(DBG_INFO, F("retransmit power limit")); DPRINTLN(DBG_INFO, F("retransmit power limit"));
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true); mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true);
} else { } else {
@ -253,14 +250,14 @@ class HmPayload {
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX));
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
*/ */
DPRINT(DBG_INFO, F("(#")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(String(iv->id)); DBGPRINTLN(F("nothing received"));
DBGPRINTLN(F(") nothing received"));
mPayload[iv->id].retransmits = mMaxRetrans; mPayload[iv->id].retransmits = mMaxRetrans;
} else { } else {
for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) { for (uint8_t i = 0; i < (mPayload[iv->id].maxPackId - 1); i++) {
if (mPayload[iv->id].len[i] == 0) { if (mPayload[iv->id].len[i] == 0) {
DPRINT(DBG_WARN, F("Frame ")); DPRINT_IVID(DBG_WARN, iv->id);
DBGPRINT(F("Frame "));
DBGPRINT(String(i + 1)); DBGPRINT(String(i + 1));
DBGPRINTLN(F(" missing: Request Retransmit")); DBGPRINTLN(F(" missing: Request Retransmit"));
mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true); mSys->Radio.sendCmdPacket(iv->radioId.u64, TX_REQ_INFO, (SINGLE_FRAME + i), true);
@ -277,18 +274,18 @@ class HmPayload {
mPayload[iv->id].retransmits++; mPayload[iv->id].retransmits++;
DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit")); DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit"));
mPayload[iv->id].txCmd = iv->getQueuedCmd(); mPayload[iv->id].txCmd = iv->getQueuedCmd();
DPRINT(DBG_INFO, F("(#")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(String(iv->id)); DBGPRINT(F("prepareDevInformCmd 0x"));
DBGPRINT(F(") prepareDevInformCmd 0x")); DBGHEXLN(mPayload[iv->id].txCmd);
DBGPRINTLN(String(mPayload[iv->id].txCmd, HEX));
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
} }
} else { // payload complete } else { // payload complete
DPRINT(DBG_INFO, F("procPyld: cmd: 0x")); DPRINT(DBG_INFO, F("procPyld: cmd: 0x"));
DBGPRINTLN(String(mPayload[iv->id].txCmd, HEX)); DBGHEXLN(mPayload[iv->id].txCmd);
DPRINT(DBG_INFO, F("procPyld: txid: 0x")); DPRINT(DBG_INFO, F("procPyld: txid: 0x"));
DBGPRINTLN(String(mPayload[iv->id].txId, HEX)); DBGHEXLN(mPayload[iv->id].txId);
DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId)); DPRINT(DBG_DEBUG, F("procPyld: max: "));
DBGPRINTLN(String(mPayload[iv->id].maxPackId));
record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
mPayload[iv->id].complete = true; mPayload[iv->id].complete = true;

31
src/hm/hmRadio.h

@ -175,11 +175,12 @@ class HmRadio {
mSerialDebug = true; mSerialDebug = true;
} }
void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit) { void sendControlPacket(uint64_t invId, uint8_t cmd, uint16_t *data, bool isRetransmit, bool isNoMI = true) {
DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x")); DPRINT(DBG_INFO, F("sendControlPacket cmd: 0x"));
DBGPRINTLN(String(cmd, HEX)); DBGHEXLN(cmd);
initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME); initPacket(invId, TX_REQ_DEVCONTROL, SINGLE_FRAME);
uint8_t cnt = 10; uint8_t cnt = 10;
if (isNoMI) {
mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor mTxBuf[cnt++] = cmd; // cmd -> 0 on, 1 off, 2 restart, 11 active power, 12 reactive power, 13 power factor
mTxBuf[cnt++] = 0x00; mTxBuf[cnt++] = 0x00;
if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet if(cmd >= ActivePowerContr && cmd <= PFSet) { // ActivePowerContr, ReactivePowerContr, PFSet
@ -188,6 +189,27 @@ class HmRadio {
mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings mTxBuf[cnt++] = ((data[1] ) >> 8) & 0xff; // setting for persistens handlings
mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling mTxBuf[cnt++] = ((data[1] ) ) & 0xff; // setting for persistens handling
} }
} else { //MI 2nd gen. specific
switch (cmd) {
case TurnOn:
mTxBuf[9] = 0x55;
mTxBuf[10] = 0xaa;
break;
case TurnOff:
mTxBuf[9] = 0xaa;
mTxBuf[10] = 0x55;
break;
case ActivePowerContr:
cnt++;
mTxBuf[9] = 0x5a;
mTxBuf[10] = 0x5a;
mTxBuf[11] = data[0]; // power limit
break;
default:
return;
}
cnt++;
}
sendPacket(invId, cnt, isRetransmit, true); sendPacket(invId, cnt, isRetransmit, true);
} }
@ -253,7 +275,10 @@ class HmRadio {
mBufCtrl.push(p); mBufCtrl.push(p);
if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command if (p.packet[0] == (TX_REQ_INFO + ALL_FRAMES)) // response from get information command
isLastPackage = (p.packet[9] > 0x81); // > 0x81 indicates last packet received isLastPackage = (p.packet[9] > 0x81); // > 0x81 indicates last packet received
else if (p.packet[0] != 0x00) // ignore fragment number zero else if (p.packet[0] == ( 0x0f + ALL_FRAMES) ) // response from MI get information command
isLastPackage = (p.packet[9] > 0x11); // > 0x11 indicates last packet received
else if (p.packet[0] != 0x00 && p.packet[0] != 0x88 && p.packet[0] != 0x92)
// ignore fragment number zero and MI status messages
isLastPackage = true; // response from dev control command isLastPackage = true; // response from dev control command
yield(); yield();
} }

415
src/hm/miPayload.h

@ -15,16 +15,17 @@
typedef struct { typedef struct {
uint32_t ts; uint32_t ts;
bool requested; bool requested;
bool limitrequested;
uint8_t txCmd; uint8_t txCmd;
uint8_t len[MAX_PAYLOAD_ENTRIES]; uint8_t len[MAX_PAYLOAD_ENTRIES];
bool complete; bool complete;
bool dataAB[3]; bool dataAB[3];
bool stsAB[3]; bool stsAB[3];
uint8_t sts[5]; uint16_t sts[6];
uint8_t txId; uint8_t txId;
uint8_t invId; uint8_t invId;
uint8_t retransmits; uint8_t retransmits;
uint8_t skipfirstrepeat; //uint8_t skipfirstrepeat;
bool gotFragment; bool gotFragment;
/* /*
uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE]; uint8_t data[MAX_PAYLOAD_ENTRIES][MAX_RF_PAYLOAD_SIZE];
@ -48,7 +49,8 @@ class MiPayload {
mMaxRetrans = maxRetransmits; mMaxRetrans = maxRetransmits;
mTimestamp = timestamp; mTimestamp = timestamp;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) { for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
reset(i); reset(i, true);
mPayload[i].limitrequested = true;
} }
mSerialDebug = false; mSerialDebug = false;
mHighPrioIv = NULL; mHighPrioIv = NULL;
@ -68,33 +70,84 @@ class MiPayload {
} }
void loop() { void loop() {
/*if(NULL != mHighPrioIv) { if(NULL != mHighPrioIv) { // && mHighPrioIv->ivGen == IV_MI) {
iv->ivSend(mHighPrioIv, true); // should request firmware version etc.? ivSend(mHighPrioIv, true); // for devcontrol commands?
mHighPrioIv = NULL; mHighPrioIv = NULL;
}*/ }
} }
void ivSendHighPrio(Inverter<> *iv) { void ivSendHighPrio(Inverter<> *iv) {
mHighPrioIv = iv; mHighPrioIv = iv;
} }
void ivSend(Inverter<> *iv) { void ivSend(Inverter<> *iv, bool highPrio = false) {
if(!highPrio) {
if (mPayload[iv->id].requested) {
if (!mPayload[iv->id].complete)
process(false); // no retransmit
if (!mPayload[iv->id].complete) {
if (!mPayload[iv->id].gotFragment)
mStat->rxFailNoAnser++; // got nothing
else
mStat->rxFail++; // got fragments but not complete response
iv->setQueuedCmdFinished(); // command failed
if (mSerialDebug)
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("enqueued cmd failed/timeout"));
if (mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("no Payload received! (retransmits: "));
DBGPRINT(String(mPayload[iv->id].retransmits));
DBGPRINTLN(F(")"));
}
}
}
}
reset(iv->id); reset(iv->id);
mPayload[iv->id].requested = true; mPayload[iv->id].requested = true;
yield(); yield();
if (mSerialDebug) if (mSerialDebug){
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") Requesting Inv SN ") + String(iv->config->serial.u64, HEX)); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("Requesting Inv SN "));
DBGPRINTLN(String(iv->config->serial.u64, HEX));
}
if (iv->getDevControlRequest()) {
if (mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("Devcontrol request 0x"));
DHEX(iv->devControlCmd);
DBGPRINT(F(" power limit "));
DBGPRINTLN(String(iv->powerLimit[0]));
}
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, false, false);
mPayload[iv->id].txCmd = iv->devControlCmd;
mPayload[iv->id].limitrequested = true;
iv->clearCmdQueue();
iv->enqueCommand<InfoCommand>(SystemConfigPara); // try to read back power limit
} else {
uint8_t cmd = iv->getQueuedCmd(); uint8_t cmd = iv->getQueuedCmd();
DPRINT(DBG_INFO, F("(#")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(String(iv->id)); DBGPRINT(F("prepareDevInformCmd 0x"));
DBGPRINT(F(") prepareDevInformCmd 0x")); DBGHEXLN(cmd);
DBGPRINTLN(String(cmd, HEX));
uint8_t cmd2 = cmd; uint8_t cmd2 = cmd;
if (cmd == 0x1 ) { //0x1 if ( cmd == SystemConfigPara ) { //0x05 for HM-types
cmd = 0x0f; if (!mPayload[iv->id].limitrequested) { // only do once at startup
cmd2 = 0x00; iv->setQueuedCmdFinished();
cmd = iv->getQueuedCmd();
} else {
mPayload[iv->id].limitrequested = false;
}
}
if (cmd == 0x01 || cmd == SystemConfigPara ) { //0x1 and 0x05 for HM-types
cmd = 0x0f; // for MI, these seem to make part of the Polling the device software and hardware version number command
cmd2 = cmd == SystemConfigPara ? 0x01 : 0x00; //perhaps we can only try to get second frame?
mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd2, false); mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd2, false);
} else { } else {
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd2, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd); mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd2, mPayload[iv->id].ts, iv->alarmMesIndex, false, cmd);
@ -113,10 +166,10 @@ class MiPayload {
mPayload[iv->id].stsAB[CH2] = false; mPayload[iv->id].stsAB[CH2] = false;
} }
} }
}
void add(Inverter<> *iv, packet_t *p) { void add(Inverter<> *iv, packet_t *p) {
//DPRINTLN(DBG_INFO, F("MI got data [0]=") + String(p->packet[0], HEX)); //DPRINTLN(DBG_INFO, F("MI got data [0]=") + String(p->packet[0], HEX));
if (p->packet[0] == (0x08 + ALL_FRAMES)) { // 0x88; MI status response to 0x09 if (p->packet[0] == (0x08 + ALL_FRAMES)) { // 0x88; MI status response to 0x09
miStsDecode(iv, p); miStsDecode(iv, p);
} }
@ -127,7 +180,8 @@ class MiPayload {
else if ( p->packet[0] == 0x09 + ALL_FRAMES || else if ( p->packet[0] == 0x09 + ALL_FRAMES ||
p->packet[0] == 0x11 + ALL_FRAMES || p->packet[0] == 0x11 + ALL_FRAMES ||
( p->packet[0] >= (0x36 + ALL_FRAMES) && p->packet[0] < (0x39 + SINGLE_FRAME) ) ) { // small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39 ( p->packet[0] >= (0x36 + ALL_FRAMES) && p->packet[0] < (0x39 + SINGLE_FRAME)
&& mPayload[iv->id].txCmd != 0x0f) ) { // small MI or MI 1500 data responses to 0x09, 0x11, 0x36, 0x37, 0x38 and 0x39
mPayload[iv->id].txId = p->packet[0]; mPayload[iv->id].txId = p->packet[0];
miDataDecode(iv,p); miDataDecode(iv,p);
} }
@ -177,30 +231,65 @@ const byteAssign_t InfoAssignment[] = {
for (uint8_t i = 0; i < 5; i++) { for (uint8_t i = 0; i < 5; i++) {
iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1); iv->setValue(i, rec, (float) ((p->packet[(12+2*i)] << 8) + p->packet[(13+2*i)])/1);
} }
iv->isConnected = true;
if(mSerialDebug) {
DPRINT_IVID(DBG_INFO, iv->id);
DPRINT(DBG_INFO,F("HW_VER is "));
DBGPRINTLN(String((p->packet[24] << 8) + p->packet[25]));
}
/*iv->setQueuedCmdFinished();
mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x01, false);*/
} else if ( p->packet[9] == 0x01 || p->packet[9] == 0x10 ) {//second frame for MI, 3rd gen. answers in 0x10
DPRINT_IVID(DBG_INFO, iv->id);
if ( p->packet[9] == 0x01 ) {
DBGPRINTLN(F("got 2nd frame (hw info)"));
} else {
DBGPRINTLN(F("3rd gen. inverter!")); // see table in OpenDTU code, DevInfoParser.cpp devInfo[]
}
// xlsx: HW_ECapValue is total energy?!? (data coll. inst. #154)
DPRINT(DBG_INFO,F("HW_PartNo "));
DBGPRINTLN(String((uint32_t) (((p->packet[10] << 8) | p->packet[11]) << 8 | p->packet[12]) << 8 | p->packet[13]));
//DBGPRINTLN(String((p->packet[12] << 8) + p->packet[13]));
if ( p->packet[9] == 0x01 ) {
iv->setValue(iv->getPosByChFld(0, FLD_YT, rec), rec, (float) ((p->packet[20] << 8) + p->packet[21])/1);
if(mSerialDebug) {
DPRINT(DBG_INFO,F("HW_ECapValue "));
DBGPRINTLN(String((p->packet[20] << 8) + p->packet[21]));
DPRINT(DBG_INFO,F("HW_FB_TLmValue "));
DBGPRINTLN(String((p->packet[14] << 8) + p->packet[15]));
DPRINT(DBG_INFO,F("HW_FB_ReSPRT "));
DBGPRINTLN(String((p->packet[16] << 8) + p->packet[17]));
DPRINT(DBG_INFO,F("HW_GridSamp_ResValule "));
DBGPRINTLN(String((p->packet[18] << 8) + p->packet[19]));
}
}
} else if ( p->packet[9] == 0x12 ) {//3rd frame
DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("got 3rd frame (hw info)"));
iv->setQueuedCmdFinished(); iv->setQueuedCmdFinished();
mStat->rxSuccess++; mStat->rxSuccess++;
mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x01, false);
} else if ( p->packet[9] == 0x01 ) {//second frame
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") got 2nd frame (hw info)"));
mSys->Radio.sendCmdPacket(iv->radioId.u64, 0x0f, 0x12, false);
} else if ( p->packet[9] == 0x12 ) {//3rd frame
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") got 3rd frame (hw info)"));
} }
} else if (p->packet[0] == (TX_REQ_INFO + ALL_FRAMES)) { // response from get information command } else if ( p->packet[0] == (TX_REQ_INFO + ALL_FRAMES) // response from get information command
|| (p->packet[0] == 0xB6 && mPayload[iv->id].txCmd != 0x36)) { // strange short response from MI-1500 3rd gen; might be missleading!
// atm, we just do nothing else than print out what we got... // atm, we just do nothing else than print out what we got...
// for decoding see xls- Data collection instructions - #147ff // for decoding see xls- Data collection instructions - #147ff
mPayload[iv->id].txId = p->packet[0]; //mPayload[iv->id].txId = p->packet[0];
DPRINTLN(DBG_DEBUG, F("Response from info request received")); DPRINTLN(DBG_DEBUG, F("Response from info request received"));
uint8_t *pid = &p->packet[9]; uint8_t *pid = &p->packet[9];
if (*pid == 0x00) { if (*pid == 0x00) {
DPRINT(DBG_DEBUG, F("fragment number zero received")); DPRINT(DBG_DEBUG, F("fragment number zero received"));
iv->setQueuedCmdFinished(); iv->setQueuedCmdFinished();
} //else { } else if (p->packet[9] == 0x81) { // might need some additional check, as this is only ment for short answers!
DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX)); DPRINT_IVID(DBG_WARN, iv->id);
/* DBGPRINTLN(F("seems to use 3rd gen. protocol - switching ivGen!"));
if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) { iv->ivGen = IV_HM;
iv->setQueuedCmdFinished();
iv->clearCmdQueue();
//DPRINTLN(DBG_DEBUG, "PID: 0x" + String(*pid, HEX));
/* (old else-tree)
if ((*pid & 0x7F) < MAX_PAYLOAD_ENTRIES) {^
memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11); memcpy(mPayload[iv->id].data[(*pid & 0x7F) - 1], &p->packet[10], p->len - 11);
mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11; mPayload[iv->id].len[(*pid & 0x7F) - 1] = p->len - 11;
mPayload[iv->id].gotFragment = true; mPayload[iv->id].gotFragment = true;
@ -212,30 +301,36 @@ const byteAssign_t InfoAssignment[] = {
if (*pid > 0x81) if (*pid > 0x81)
mPayload[iv->id].lastFound = true; mPayload[iv->id].lastFound = true;
} }
}*/
} }
} //}
} */ } else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES ) // response from dev control command
} else if (p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES)) { // response from dev control command || p->packet[0] == (TX_REQ_DEVCONTROL + ALL_FRAMES -1)) { // response from DRED instruction
DPRINTLN(DBG_DEBUG, F("Response from devcontrol request received")); DPRINT_IVID(DBG_DEBUG, iv->id);
DBGPRINTLN(F("Response from devcontrol request received"));
mPayload[iv->id].txId = p->packet[0]; mPayload[iv->id].txId = p->packet[0];
iv->clearDevControlRequest(); iv->clearDevControlRequest();
if ((p->packet[12] == ActivePowerContr) && (p->packet[13] == 0x00)) { if ((p->packet[9] == 0x5a) && (p->packet[10] == 0x5a)) {
String msg = "";
if((p->packet[10] == 0x00) && (p->packet[11] == 0x00))
mApp->setMqttPowerLimitAck(iv); mApp->setMqttPowerLimitAck(iv);
else DPRINT_IVID(DBG_INFO, iv->id);
msg = "NOT "; DBGPRINT(F("has accepted power limit set point "));
DPRINTLN(DBG_INFO, F("Inverter ") + String(iv->id) + F(" has ") + msg + F("accepted power limit set point ") + String(iv->powerLimit[0]) + F(" with PowerLimitControl ") + String(iv->powerLimit[1])); DBGPRINT(String(iv->powerLimit[0]));
DBGPRINT(F(" with PowerLimitControl "));
DBGPRINTLN(String(iv->powerLimit[1]));
iv->clearCmdQueue(); iv->clearCmdQueue();
iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit iv->enqueCommand<InfoCommand>(SystemConfigPara); // read back power limit
} }
iv->devControlCmd = Init; iv->devControlCmd = Init;
} else { // some other response; copied from hmPayload:process; might not be correct to do that here!!! } else { // some other response; copied from hmPayload:process; might not be correct to do that here!!!
DPRINTLN(DBG_INFO, F("procPyld: cmd: 0x") + String(mPayload[iv->id].txCmd, HEX)); DPRINT(DBG_INFO, F("procPyld: cmd: 0x"));
DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + String(mPayload[iv->id].txId, HEX)); DBGHEXLN(mPayload[iv->id].txCmd);
//DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId)); DPRINT(DBG_INFO, F("procPyld: txid: 0x"));
DBGHEXLN(mPayload[iv->id].txId);
//DPRINT(DBG_DEBUG, F("procPyld: max: "));
//DBGPRINTLN(String(mPayload[iv->id].maxPackId));
record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
mPayload[iv->id].complete = true; mPayload[iv->id].complete = true;
@ -317,10 +412,10 @@ const byteAssign_t InfoAssignment[] = {
//delayed next message? //delayed next message?
//mPayload[iv->id].skipfirstrepeat++; //mPayload[iv->id].skipfirstrepeat++;
if (mPayload[iv->id].skipfirstrepeat) { /*if (mPayload[iv->id].skipfirstrepeat) {
mPayload[iv->id].skipfirstrepeat = 0; //reset counter*/ mPayload[iv->id].skipfirstrepeat = 0; //reset counter
continue; // skip to next inverter continue; // skip to next inverter
} }*/
if (!mPayload[iv->id].complete) { if (!mPayload[iv->id].complete) {
//DPRINTLN(DBG_INFO, F("Pyld incompl code")); //info for testing only //DPRINTLN(DBG_INFO, F("Pyld incompl code")); //info for testing only
@ -330,21 +425,20 @@ const byteAssign_t InfoAssignment[] = {
if ((mPayload[iv->id].requested) && (retransmit)) { if ((mPayload[iv->id].requested) && (retransmit)) {
if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) { if (iv->devControlCmd == Restart || iv->devControlCmd == CleanState_LockAndAlarm) {
// This is required to prevent retransmissions without answer. // This is required to prevent retransmissions without answer.
DPRINTLN(DBG_INFO, F("Prevent retransmit on Restart / CleanState_LockAndAlarm...")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("Prevent retransmit on Restart / CleanState_LockAndAlarm..."));
mPayload[iv->id].retransmits = mMaxRetrans; mPayload[iv->id].retransmits = mMaxRetrans;
} else if(iv->devControlCmd == ActivePowerContr) { } else if(iv->devControlCmd == ActivePowerContr) {
DPRINT(DBG_INFO, F("(#")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(String(iv->id)); DBGPRINTLN(F("retransmit power limit"));
DBGPRINTLN(F(") retransmit power limit")); mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true, false);
mSys->Radio.sendControlPacket(iv->radioId.u64, iv->devControlCmd, iv->powerLimit, true);
} else { } else {
uint8_t cmd = mPayload[iv->id].txCmd; uint8_t cmd = mPayload[iv->id].txCmd;
if (mPayload[iv->id].retransmits < mMaxRetrans) { if (mPayload[iv->id].retransmits < mMaxRetrans) {
mPayload[iv->id].retransmits++; mPayload[iv->id].retransmits++;
if( !mPayload[iv->id].gotFragment ) { if( !mPayload[iv->id].gotFragment ) {
DPRINT(DBG_INFO, F("(#")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(String(iv->id)); DBGPRINTLN(F("nothing received"));
DBGPRINTLN(F(") nothing received"));
mPayload[iv->id].retransmits = mMaxRetrans; mPayload[iv->id].retransmits = mMaxRetrans;
} else if ( cmd == 0x0f ) { } else if ( cmd == 0x0f ) {
//hard/firmware request //hard/firmware request
@ -358,28 +452,32 @@ const byteAssign_t InfoAssignment[] = {
change = true; change = true;
} else if ( cmd == 0x09 ) {//MI single or dual channel device } else if ( cmd == 0x09 ) {//MI single or dual channel device
if ( mPayload[iv->id].dataAB[CH1] && iv->type == INV_TYPE_2CH ) { if ( mPayload[iv->id].dataAB[CH1] && iv->type == INV_TYPE_2CH ) {
if (!mPayload[iv->id].stsAB[CH2] || !mPayload[iv->id].dataAB[CH2] ) { if (!mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].retransmits<2) {}
//first try to get missing sts for first channel a second time
else if (!mPayload[iv->id].stsAB[CH2] || !mPayload[iv->id].dataAB[CH2] ) {
cmd = 0x11; cmd = 0x11;
change = true; change = true;
mPayload[iv->id].retransmits = 0; //reset counter
} }
} }
} else if ( cmd == 0x11) { } else if ( cmd == 0x11) {
if ( mPayload[iv->id].dataAB[CH2] ) { // data is there, but no status if ( mPayload[iv->id].dataAB[CH2] ) { // data + status ch2 are there?
if (!mPayload[iv->id].stsAB[CH1] || !mPayload[iv->id].dataAB[CH1] ) { if (mPayload[iv->id].stsAB[CH2] && (!mPayload[iv->id].stsAB[CH1] || !mPayload[iv->id].dataAB[CH1])) {
cmd = 0x09; cmd = 0x09;
change = true; change = true;
} }
} }
} }
DPRINT(DBG_INFO, F("(#")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(String(iv->id));
DBGPRINT(F(") "));
if (change) { if (change) {
DBGPRINT(F("next request is 0x")); DBGPRINT(F("next request is"));
//mPayload[iv->id].skipfirstrepeat = 0;
} else { } else {
DBGPRINT(F("not complete: Request Retransmit 0x")); DBGPRINT(F("sth."));
DBGPRINT(F(" missing: Request Retransmit"));
} }
DBGPRINTLN(String(cmd, HEX)); DBGPRINT(F(" 0x"));
DBGHEXLN(cmd);
//mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd, true); //mSys->Radio.sendCmdPacket(iv->radioId.u64, cmd, cmd, true);
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, true, cmd); mSys->Radio.prepareDevInformCmd(iv->radioId.u64, cmd, mPayload[iv->id].ts, iv->alarmMesIndex, true, cmd);
mPayload[iv->id].txCmd = cmd; mPayload[iv->id].txCmd = cmd;
@ -391,9 +489,13 @@ const byteAssign_t InfoAssignment[] = {
} else if(!crcPass && pyldComplete) { // crc error on complete Payload } else if(!crcPass && pyldComplete) { // crc error on complete Payload
if (mPayload[iv->id].retransmits < mMaxRetrans) { if (mPayload[iv->id].retransmits < mMaxRetrans) {
mPayload[iv->id].retransmits++; mPayload[iv->id].retransmits++;
DPRINTLN(DBG_WARN, F("CRC Error: Request Complete Retransmit")); DPRINT_IVID(DBG_WARN, iv->id);
DBGPRINTLN(F("CRC Error: Request Complete Retransmit"));
mPayload[iv->id].txCmd = iv->getQueuedCmd(); mPayload[iv->id].txCmd = iv->getQueuedCmd();
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") prepareDevInformCmd 0x") + String(mPayload[iv->id].txCmd, HEX)); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINT(F("prepareDevInformCmd 0x"));
DBGHEXLN(mPayload[iv->id].txCmd);
mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true); mSys->Radio.prepareDevInformCmd(iv->radioId.u64, mPayload[iv->id].txCmd, mPayload[iv->id].ts, iv->alarmMesIndex, true);
} }
} }
@ -403,16 +505,6 @@ const byteAssign_t InfoAssignment[] = {
DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + String(mPayload[iv->id].txId, HEX)); DPRINTLN(DBG_INFO, F("procPyld: txid: 0x") + String(mPayload[iv->id].txId, HEX));
//DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId)); //DPRINTLN(DBG_DEBUG, F("procPyld: max: ") + String(mPayload[iv->id].maxPackId));
//record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser //record_t<> *rec = iv->getRecordStruct(mPayload[iv->id].txCmd); // choose the parser
mPayload[iv->id].complete = true;
uint8_t ac_pow = 0;
//if (mPayload[iv->id].sts[0] == 3) {
ac_pow = calcPowerDcCh0(iv, 0)*9.5;
//}
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the parser
iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) (ac_pow/10));
DPRINTLN(DBG_INFO, F("process: compl. set of msgs detected"));
iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0));
iv->doCalculations();
//uint8_t payload[128]; //uint8_t payload[128];
//uint8_t payloadLen = 0; //uint8_t payloadLen = 0;
//memset(payload, 0, 128); //memset(payload, 0, 128);
@ -468,48 +560,76 @@ const byteAssign_t InfoAssignment[] = {
(mCbMiPayload)(val); (mCbMiPayload)(val);
} }
void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t chan = CH1) { void miStsDecode(Inverter<> *iv, packet_t *p, uint8_t stschan = CH1) {
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") status msg 0x") + String(p->packet[0], HEX)); //DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") status msg 0x") + String(p->packet[0], HEX));
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); // choose the record structure
rec->ts = mPayload[iv->id].ts; rec->ts = mPayload[iv->id].ts;
mPayload[iv->id].gotFragment = true; mPayload[iv->id].gotFragment = true;
mPayload[iv->id].txId = p->packet[0]; mPayload[iv->id].txId = p->packet[0];
miStsConsolidate(iv, stschan, rec, p->packet[10], p->packet[12], p->packet[9], p->packet[11]);
uint8_t status = (p->packet[11] << 8) + p->packet[12];
uint8_t stschan = p->packet[0] == 0x88 ? CH1 : CH2;
mPayload[iv->id].sts[stschan] = status;
mPayload[iv->id].stsAB[stschan] = true; mPayload[iv->id].stsAB[stschan] = true;
if (mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].stsAB[CH2]) if (mPayload[iv->id].stsAB[CH1] && mPayload[iv->id].stsAB[CH2])
mPayload[iv->id].stsAB[CH0] = true; mPayload[iv->id].stsAB[CH0] = true;
if ( !mPayload[iv->id].sts[0] || status < mPayload[iv->id].sts[0]) { //mPayload[iv->id].skipfirstrepeat = 1;
mPayload[iv->id].sts[0] = status; if (mPayload[iv->id].stsAB[CH0] && mPayload[iv->id].dataAB[CH0] && !mPayload[iv->id].complete) {
iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, status); miComplete(iv);
}
}
void miStsConsolidate(Inverter<> *iv, uint8_t stschan, record_t<> *rec, uint8_t uState, uint8_t uEnum, uint8_t lState = 0, uint8_t lEnum = 0) {
//uint8_t status = (p->packet[11] << 8) + p->packet[12];
uint16_t status = 3; // regular status for MI, change to 1 later?
if ( uState == 2 ) {
status = 5050 + stschan; //first approach, needs review!
if (lState)
status += lState*10;
} else if ( uState > 3 ) {
status = uState*1000 + uEnum*10;
if (lState)
status += lState*100; //needs review, esp. for 4ch-8310 state!
//if (lEnum)
status += lEnum;
if (uEnum < 6) {
status += stschan;
}
if (status == 8000)
status = 8310; //trick?
}
uint16_t prntsts = status == 3 ? 1 : status;
if ( status != mPayload[iv->id].sts[stschan] ) { //sth.'s changed?
mPayload[iv->id].sts[stschan] = status;
DPRINT(DBG_WARN, F("Status change for CH"));
DBGPRINT(String(stschan)); DBGPRINT(F(" ("));
DBGPRINT(String(prntsts)); DBGPRINT(F("): "));
DBGPRINTLN(iv->getAlarmStr(prntsts));
}
if ( !mPayload[iv->id].sts[0] || prntsts < mPayload[iv->id].sts[0] ) {
mPayload[iv->id].sts[0] = prntsts;
iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, prntsts);
} }
if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){ if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){
iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; // seems there's no status per channel in 3rd gen. models?!? iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; // seems there's no status per channel in 3rd gen. models?!?
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") alarm ID incremented to ") + String(iv->alarmMesIndex)); DPRINT_IVID(DBG_INFO, iv->id);
iv->enqueCommand<InfoCommand>(AlarmData); DBGPRINT(F("alarm ID incremented to "));
DBGPRINTLN(String(iv->alarmMesIndex));
} }
//mPayload[iv->id].skipfirstrepeat = 1; /*if(AlarmData == mPayload[iv->id].txCmd) {
if (mPayload[iv->id].stsAB[CH0] && mPayload[iv->id].dataAB[CH0] && !mPayload[iv->id].complete) { uint8_t i = 0;
miComplete(iv); uint16_t code;
/*mPayload[iv->id].complete = true; uint32_t start, end;
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") got all msgs")); while(1) {
iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); code = iv->parseAlarmLog(i++, payload, payloadLen, &start, &end);
//preliminary AC calculation... if(0 == code)
uint8_t ac_pow = 0; break;
//if (mPayload[iv->id].sts[0] == 3) { if (NULL != mCbAlarm)
ac_pow = calcPowerDcCh0(iv, 0)*9.5; (mCbAlarm)(code, start, end);
//} yield();
iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) (ac_pow/10));
iv->setQueuedCmdFinished();
iv->doCalculations();
mPayload[iv->id].skipfirstrepeat = 0;
notify(mPayload[iv->id].txCmd);
yield();*/
} }
}*/
} }
void miDataDecode(Inverter<> *iv, packet_t *p) { void miDataDecode(Inverter<> *iv, packet_t *p) {
@ -521,7 +641,7 @@ const byteAssign_t InfoAssignment[] = {
( p->packet[0] == 0x91 || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 : ( p->packet[0] == 0x91 || p->packet[0] == (0x37 + ALL_FRAMES) ) ? CH2 :
p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 : p->packet[0] == (0x38 + ALL_FRAMES) ? CH3 :
CH4; CH4;
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") data msg 0x") + String(p->packet[0], HEX) + F(" channel ") + datachan); //DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") data msg 0x") + String(p->packet[0], HEX) + F(" channel ") + datachan);
// count in RF_communication_protocol.xlsx is with offset = -1 // count in RF_communication_protocol.xlsx is with offset = -1
iv->setValue(iv->getPosByChFld(datachan, FLD_UDC, rec), rec, (float)((p->packet[9] << 8) + p->packet[10])/10); iv->setValue(iv->getPosByChFld(datachan, FLD_UDC, rec), rec, (float)((p->packet[9] << 8) + p->packet[10])/10);
yield(); yield();
@ -554,12 +674,14 @@ const byteAssign_t InfoAssignment[] = {
FCODE = (uint8_t)(p->packet[27]); FCODE = (uint8_t)(p->packet[27]);
}*/ }*/
uint8_t status = (uint8_t)(p->packet[23]); /*uint16_t status = (uint8_t)(p->packet[23]);
mPayload[iv->id].sts[datachan] = status; mPayload[iv->id].sts[datachan] = status;
if ( !mPayload[iv->id].sts[0] || status < mPayload[iv->id].sts[0]) { if ( !mPayload[iv->id].sts[0] || status < mPayload[iv->id].sts[0]) {
mPayload[iv->id].sts[0] = status; mPayload[iv->id].sts[0] = status;
iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, status); iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, status);
} }*/
miStsConsolidate(iv, datachan, rec, p->packet[23], p->packet[24]);
if (p->packet[0] < (0x39 + ALL_FRAMES) ) { if (p->packet[0] < (0x39 + ALL_FRAMES) ) {
/*uint8_t cmd = p->packet[0] - ALL_FRAMES + 1; /*uint8_t cmd = p->packet[0] - ALL_FRAMES + 1;
@ -575,37 +697,21 @@ const byteAssign_t InfoAssignment[] = {
mPayload[iv->id].complete = true; mPayload[iv->id].complete = true;
} }
//iv->setValue(iv->getPosByChFld(0, FLD_EVT, rec), rec, calcMiSts(iv));yield(); /*if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){
if (iv->alarmMesIndex < rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]){
iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)]; iv->alarmMesIndex = rec->record[iv->getPosByChFld(0, FLD_EVT, rec)];
DPRINTLN(DBG_INFO, F("alarm ID incremented to ") + String(iv->alarmMesIndex)); DPRINT_IVID(DBG_INFO, iv->id);
//iv->enqueCommand<InfoCommand>(AlarmData); DBGPRINT_TXT(TXT_INCRALM);
} DBGPRINTLN(String(iv->alarmMesIndex));
}*/
} }
if ( mPayload[iv->id].complete || //4ch device if ( mPayload[iv->id].complete || //4ch device
(iv->type != INV_TYPE_4CH //other devices (iv->type != INV_TYPE_4CH //other devices
&& mPayload[iv->id].dataAB[CH0] && mPayload[iv->id].dataAB[CH0]
&& mPayload[iv->id].stsAB[CH0])) { && mPayload[iv->id].stsAB[CH0])) {
miComplete(iv); miComplete(iv);
/*mPayload[iv->id].complete = true; // For 2 CH devices, this might be too short...
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") got all msgs"));
iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0));
//preliminary AC calculation...
uint8_t ac_pow = 0;
//if (mPayload[iv->id].sts[0] == 3) {
ac_pow = calcPowerDcCh0(iv, 0)*9.5;
//}
iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) (ac_pow/10));
iv->doCalculations();
iv->setQueuedCmdFinished();
mStat->rxSuccess++;
yield();
notify(mPayload[iv->id].txCmd);*/
} }
@ -627,16 +733,25 @@ const byteAssign_t InfoAssignment[] = {
} }
void miComplete(Inverter<> *iv) { void miComplete(Inverter<> *iv) {
if (mPayload[iv->id].complete)
return; //if we got second message as well in repreated attempt
mPayload[iv->id].complete = true; // For 2 CH devices, this might be too short... mPayload[iv->id].complete = true; // For 2 CH devices, this might be too short...
DPRINTLN(DBG_INFO, F("(#") + String(iv->id) + F(") got all msgs")); DPRINT_IVID(DBG_INFO, iv->id);
DBGPRINTLN(F("got all msgs"));
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug); record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0)); iv->setValue(iv->getPosByChFld(0, FLD_YD, rec), rec, calcYieldDayCh0(iv,0));
//preliminary AC calculation... //preliminary AC calculation...
uint8_t ac_pow = 0; float ac_pow = 0;
//if (mPayload[iv->id].sts[0] == 3) { for(uint8_t i = 1; i <= iv->channels; i++) {
ac_pow = calcPowerDcCh0(iv, 0)*9.5; if (mPayload[iv->id].sts[i] == 3) {
//} uint8_t pos = iv->getPosByChFld(i, FLD_PDC, rec);
iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) (ac_pow/10)); ac_pow += iv->getValue(pos, rec);
}
}
ac_pow = (int) (ac_pow*9.5);
iv->setValue(iv->getPosByChFld(0, FLD_PAC, rec), rec, (float) ac_pow/10);
iv->doCalculations(); iv->doCalculations();
iv->setQueuedCmdFinished(); iv->setQueuedCmdFinished();
mStat->rxSuccess++; mStat->rxSuccess++;
@ -646,37 +761,24 @@ const byteAssign_t InfoAssignment[] = {
bool build(uint8_t id, bool *complete) { bool build(uint8_t id, bool *complete) {
DPRINTLN(DBG_VERBOSE, F("build")); DPRINTLN(DBG_VERBOSE, F("build"));
/*uint16_t crc = 0xffff, crcRcv = 0x0000;
if (mPayload[id].maxPackId > MAX_PAYLOAD_ENTRIES)
mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
*/
// check if all messages are there // check if all messages are there
*complete = mPayload[id].complete; *complete = mPayload[id].complete;
uint8_t txCmd = mPayload[id].txCmd; uint8_t txCmd = mPayload[id].txCmd;
//uint8_t cmd = getQueuedCmd();
if(!*complete) { if(!*complete) {
DPRINTLN(DBG_VERBOSE, F("incomlete, txCmd is 0x") + String(txCmd, HEX)); // + F("cmd is 0x") + String(cmd, HEX)); DPRINTLN(DBG_VERBOSE, F("incomlete, txCmd is 0x") + String(txCmd, HEX));
//DBGHEXLN(txCmd);
if (txCmd == 0x09 || txCmd == 0x11 || (txCmd >= 0x36 && txCmd <= 0x39)) if (txCmd == 0x09 || txCmd == 0x11 || (txCmd >= 0x36 && txCmd <= 0x39))
return false; return false;
} }
/*for (uint8_t i = 0; i < mPayload[id].maxPackId; i++) {
if (mPayload[id].len[i] > 0) {
if (i == (mPayload[id].maxPackId - 1)) {
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i] - 2, crc);
crcRcv = (mPayload[id].data[i][mPayload[id].len[i] - 2] << 8) | (mPayload[id].data[i][mPayload[id].len[i] - 1]);
} else
crc = ah::crc16(mPayload[id].data[i], mPayload[id].len[i], crc);
}
yield();
}
return (crc == crcRcv) ? true : false;*/
return true; return true;
} }
void reset(uint8_t id) { void reset(uint8_t id, bool clrSts = false) {
DPRINTLN(DBG_INFO, F("resetPayload: id: ") + String(id)); DPRINT_IVID(DBG_INFO, id);
DBGPRINTLN(F("resetPayload"));
memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES); memset(mPayload[id].len, 0, MAX_PAYLOAD_ENTRIES);
mPayload[id].gotFragment = false; mPayload[id].gotFragment = false;
/*mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES; /*mPayload[id].maxPackId = MAX_PAYLOAD_ENTRIES;
@ -690,14 +792,17 @@ const byteAssign_t InfoAssignment[] = {
mPayload[id].stsAB[CH1] = true; //required for 1CH and 2CH devices mPayload[id].stsAB[CH1] = true; //required for 1CH and 2CH devices
mPayload[id].stsAB[CH2] = true; //only required for 2CH devices mPayload[id].stsAB[CH2] = true; //only required for 2CH devices
mPayload[id].txCmd = 0; mPayload[id].txCmd = 0;
mPayload[id].skipfirstrepeat = 0; //mPayload[id].skipfirstrepeat = 0;
mPayload[id].requested = false; mPayload[id].requested = false;
mPayload[id].ts = *mTimestamp; mPayload[id].ts = *mTimestamp;
mPayload[id].sts[0] = 0; //disable this in case gotFragment is not working mPayload[id].sts[0] = 0;
if (clrSts) { // only clear channel states at startup
mPayload[id].sts[CH1] = 0; mPayload[id].sts[CH1] = 0;
mPayload[id].sts[CH2] = 0; mPayload[id].sts[CH2] = 0;
mPayload[id].sts[CH3] = 0; mPayload[id].sts[CH3] = 0;
mPayload[id].sts[CH4] = 0; mPayload[id].sts[CH4] = 0;
mPayload[id].sts[5] = 0; //remember last summarized state
}
} }

4
src/main.cpp

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 // 2023 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#include "utils/dbg.h" #include "utils/dbg.h"

19
src/platformio.ini

@ -35,25 +35,20 @@ extra_scripts =
lib_deps = lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer https://github.com/yubox-node-org/ESPAsyncWebServer
nrf24/RF24 nrf24/RF24 @ ^1.4.5
paulstoffregen/Time paulstoffregen/Time @ ^1.6.1
https://github.com/bertmelis/espMqttClient#v1.4.1 https://github.com/bertmelis/espMqttClient#v1.4.1
bblanchon/ArduinoJson bblanchon/ArduinoJson @ ^6.21.0
https://github.com/JChristensen/Timezone https://github.com/JChristensen/Timezone @ ^1.2.4
olikraus/U8g2 olikraus/U8g2 @ ^2.34.16
zinggjm/GxEPD2@^1.5.0 zinggjm/GxEPD2 @ ^1.5.0
;esp8266/DNSServer
;esp8266/EEPROM
;esp8266/ESP8266WiFi
;esp8266/SPI
;esp8266/Ticker
[env:esp8266-release] [env:esp8266-release]
platform = espressif8266 platform = espressif8266
board = esp12e board = esp12e
board_build.f_cpu = 80000000L board_build.f_cpu = 80000000L
build_flags = -D RELEASE build_flags = -D RELEASE -Wl,-Map,output.map
monitor_filters = monitor_filters =
;default ; Remove typical terminal control codes from input ;default ; Remove typical terminal control codes from input
;time ; Add timestamp with milliseconds for each new line ;time ; Add timestamp with milliseconds for each new line

10
src/plugins/Display/Display.h

@ -14,7 +14,7 @@ class Display {
public: public:
Display() {} Display() {}
void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, uint8_t disp_reset, const char *version) { void setup(display_t *cfg, HMSYSTEM *sys, uint32_t *utcTs, const char *version) {
mCfg = cfg; mCfg = cfg;
mSys = sys; mSys = sys;
mUtcTs = utcTs; mUtcTs = utcTs;
@ -25,9 +25,9 @@ class Display {
if (mCfg->type == 0) if (mCfg->type == 0)
return; return;
if ((1 < mCfg->type) && (mCfg->type < 10)) { if ((0 < mCfg->type) && (mCfg->type < 10)) {
mMono.config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast); mMono.config(mCfg->pwrSaveAtIvOffline, mCfg->pxShift, mCfg->contrast);
mMono.init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, mCfg->disp_reset, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion); mMono.init(mCfg->type, mCfg->rot, mCfg->disp_cs, mCfg->disp_dc, 0xff, mCfg->disp_clk, mCfg->disp_data, mUtcTs, mVersion);
} else if (mCfg->type >= 10) { } else if (mCfg->type >= 10) {
#if defined(ESP32) #if defined(ESP32)
mRefreshCycle = 0; mRefreshCycle = 0;
@ -42,7 +42,7 @@ class Display {
} }
void tickerSecond() { void tickerSecond() {
loop(); mMono.loop();
if (mNewPayload || ((++mLoopCnt % 10) == 0)) { if (mNewPayload || ((++mLoopCnt % 10) == 0)) {
mNewPayload = false; mNewPayload = false;
mLoopCnt = 0; mLoopCnt = 0;
@ -79,7 +79,7 @@ class Display {
totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec); totalYieldTotal += iv->getChannelFieldValue(CH0, FLD_YT, rec);
} }
if ((1 < mCfg->type) && (mCfg->type < 10)) { if ((0 < mCfg->type) && (mCfg->type < 10)) {
mMono.disp(totalPower, totalYieldDay, totalYieldTotal, isprod); mMono.disp(totalPower, totalYieldDay, totalYieldTotal, isprod);
} else if (mCfg->type >= 10) { } else if (mCfg->type >= 10) {
#if defined(ESP32) #if defined(ESP32)

6
src/plugins/Display/Display_Mono.cpp

@ -27,9 +27,9 @@ DisplayMono::DisplayMono() {
void DisplayMono::init(uint8_t type, uint8_t rot, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version) { void DisplayMono::init(uint8_t type, uint8_t rotation, uint8_t cs, uint8_t dc, uint8_t reset, uint8_t clock, uint8_t data, uint32_t *utcTs, const char* version) {
if ((0 < type) && (type < 4)) { if ((0 < type) && (type < 4)) {
u8g2_cb_t *rot = (u8g2_cb_t *)((rot != 0x00) ? U8G2_R2 : U8G2_R0); u8g2_cb_t *rot = (u8g2_cb_t *)((rotation != 0x00) ? U8G2_R2 : U8G2_R0);
mType = type; mType = type;
switch(type) { switch(type) {
case 1: case 1:
@ -108,7 +108,7 @@ void DisplayMono::disp(float totalPower, float totalYieldDay, float totalYieldTo
if (!(_mExtra % 10) && (ip)) { if (!(_mExtra % 10) && (ip)) {
printText(ip.toString().c_str(), 3); printText(ip.toString().c_str(), 3);
} else if (!(_mExtra % 5)) { } else if (!(_mExtra % 5)) {
snprintf(_fmtText, DISP_FMT_TEXT_LEN, "#%d Inverter online", isprod); snprintf(_fmtText, DISP_FMT_TEXT_LEN, "%d Inverter on", isprod);
printText(_fmtText, 3); printText(_fmtText, 3);
} else { } else {
if(mIsLarge && (NULL != mUtcTs)) if(mIsLarge && (NULL != mUtcTs))

2
src/plugins/Display/Display_ePaper.cpp

@ -168,7 +168,7 @@ void DisplayEPaper::actualPowerPaged(float _totalPower, float _totalYieldDay, fl
_display->println("kWh"); _display->println("kWh");
_display->setCursor(0, _display->height() - (mHeadFootPadding + 10)); _display->setCursor(0, _display->height() - (mHeadFootPadding + 10));
snprintf(_fmtText, sizeof(_fmtText), "#%d Inverter online", _isprod); snprintf(_fmtText, sizeof(_fmtText), "%d Inverter online", _isprod);
_display->println(_fmtText); _display->println(_fmtText);
} while (_display->nextPage()); } while (_display->nextPage());

245
src/publisher/pubMqtt.h

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de // 2023 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// https://bert.emelis.net/espMqttClient/ // https://bert.emelis.net/espMqttClient/
@ -34,6 +34,12 @@ struct alarm_t {
alarm_t(uint16_t c, uint32_t s, uint32_t e) : code(c), start(s), end(e) {} alarm_t(uint16_t c, uint32_t s, uint32_t e) : code(c), start(s), end(e) {}
}; };
typedef struct {
bool running;
uint8_t lastIvId;
uint8_t sub;
} discovery_t;
template<class HMSYSTEM> template<class HMSYSTEM>
class PubMqtt { class PubMqtt {
public: public:
@ -55,11 +61,13 @@ class PubMqtt {
mUtcTimestamp = utcTs; mUtcTimestamp = utcTs;
mIntervalTimeout = 1; mIntervalTimeout = 1;
mDiscovery.running = false;
snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic); snprintf(mLwtTopic, MQTT_TOPIC_LEN + 5, "%s/mqtt", mCfgMqtt->topic);
if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0)) if((strlen(mCfgMqtt->user) > 0) && (strlen(mCfgMqtt->pwd) > 0))
mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd); mClient.setCredentials(mCfgMqtt->user, mCfgMqtt->pwd);
snprintf(mClientId, 26, "%s-", mDevName); snprintf(mClientId, 24, "%s-", mDevName);
uint8_t pos = strlen(mClientId); uint8_t pos = strlen(mClientId);
mClientId[pos++] = WiFi.macAddress().substring( 9, 10).c_str()[0]; mClientId[pos++] = WiFi.macAddress().substring( 9, 10).c_str()[0];
mClientId[pos++] = WiFi.macAddress().substring(10, 11).c_str()[0]; mClientId[pos++] = WiFi.macAddress().substring(10, 11).c_str()[0];
@ -82,6 +90,9 @@ class PubMqtt {
mClient.loop(); mClient.loop();
yield(); yield();
#endif #endif
if(mDiscovery.running)
discoveryConfigLoop();
} }
@ -111,6 +122,9 @@ class PubMqtt {
publish(subtopics[MQTT_UPTIME], val); publish(subtopics[MQTT_UPTIME], val);
publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str()); publish(subtopics[MQTT_RSSI], String(WiFi.RSSI()).c_str());
publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str()); publish(subtopics[MQTT_FREE_HEAP], String(ESP.getFreeHeap()).c_str());
#ifndef ESP32
publish(subtopics[MQTT_HEAP_FRAG], String(ESP.getHeapFragmentation()).c_str());
#endif
} }
bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) { bool tickerSun(uint32_t sunrise, uint32_t sunset, uint32_t offs, bool disNightCom) {
@ -162,13 +176,15 @@ class PubMqtt {
if(!mClient.connected()) if(!mClient.connected())
return; return;
String topic = ""; memset(mTopic, 0, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1);
if(addTopic) if(addTopic){
topic = String(mCfgMqtt->topic) + "/"; snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s/%s", mCfgMqtt->topic, subTopic);
topic += String(subTopic); } else {
snprintf(mTopic, MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1, "%s", subTopic);
}
do { do {
if(0 != mClient.publish(topic.c_str(), QOS_0, retained, payload)) if(0 != mClient.publish(mTopic, QOS_0, retained, payload))
break; break;
if(!mClient.connected()) if(!mClient.connected())
break; break;
@ -205,92 +221,9 @@ class PubMqtt {
void sendDiscoveryConfig(void) { void sendDiscoveryConfig(void) {
DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig")); DPRINTLN(DBG_VERBOSE, F("sendMqttDiscoveryConfig"));
mDiscovery.running = true;
char topic[64], name[32], uniq_id[32]; mDiscovery.lastIvId = 0;
DynamicJsonDocument doc(256); mDiscovery.sub = 0;
uint8_t fldTotal[4] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC};
const char* unitTotal[4] = {"W", "kWh", "Wh", "W"};
String node_mac = WiFi.macAddress().substring(12,14)+ WiFi.macAddress().substring(15,17);
String node_id = "AHOY_DTU_" + node_mac;
bool total = false;
for (uint8_t id = 0; id < mSys->getNumInverters() ; id++) {
doc.clear();
if (total) // total become true at iv = NULL next cycle
continue;
Inverter<> *iv = mSys->getInverterByPos(id);
if (NULL == iv)
total = true;
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
if (!total) {
doc[F("name")] = iv->config->name;
doc[F("ids")] = String(iv->config->serial.u64, HEX);
doc[F("mdl")] = iv->config->name;
}
else {
doc[F("name")] = node_id;
doc[F("ids")] = node_id;
doc[F("mdl")] = node_id;
}
doc[F("cu")] = F("http://") + String(WiFi.localIP().toString());
doc[F("mf")] = F("Hoymiles");
JsonObject deviceObj = doc.as<JsonObject>(); // deviceObj is only pointer!?
for (uint8_t i = 0; i < ((!total) ? (rec->length) : (4) ) ; i++) {
const char *devCls, *stateCls;
if (!total) {
if (rec->assign[i].ch == CH0)
snprintf(name, 32, "%s %s", iv->config->name, iv->getFieldName(i, rec));
else
snprintf(name, 32, "%s CH%d %s", iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(topic, 64, "/ch%d/%s", rec->assign[i].ch, iv->getFieldName(i, rec));
snprintf(uniq_id, 32, "ch%d_%s", rec->assign[i].ch, iv->getFieldName(i, rec));
devCls = getFieldDeviceClass(rec->assign[i].fieldId);
stateCls = getFieldStateClass(rec->assign[i].fieldId);
}
else { // total values
snprintf(name, 32, "Total %s", fields[fldTotal[i]]);
snprintf(topic, 64, "/%s", fields[fldTotal[i]]);
snprintf(uniq_id, 32, "total_%s", fields[fldTotal[i]]);
devCls = getFieldDeviceClass(fldTotal[i]);
stateCls = getFieldStateClass(fldTotal[i]);
}
DynamicJsonDocument doc2(512);
doc2[F("name")] = name;
doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic);
doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(i,rec)) : (unitTotal[i]));
doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id;
doc2[F("dev")] = deviceObj;
if (!(String(stateCls) == String("total_increasing")))
doc2[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)
doc2[F("dev_cla")] = String(devCls);
if (stateCls != NULL)
doc2[F("stat_cla")] = String(stateCls);
if (!total)
snprintf(topic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[i].ch, iv->getFieldName(i, rec));
else // total values
snprintf(topic, 64, "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(),fields[fldTotal[i]]);
size_t size = measureJson(doc2) + 1;
char *buf = new char[size];
memset(buf, 0, size);
serializeJson(doc2, buf, size);
publish(topic, buf, true, false);
delete[] buf;
}
yield();
}
} }
void setPowerLimitAck(Inverter<> *iv) { void setPowerLimitAck(Inverter<> *iv) {
@ -312,13 +245,14 @@ class PubMqtt {
tickerMinute(); tickerMinute();
publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false); publish(mLwtTopic, mqttStr[MQTT_STR_LWT_CONN], true, false);
subscribe(subscr[MQTT_SUBS_LMT_PERI_REL]); char sub[20];
subscribe(subscr[MQTT_SUBS_LMT_PERI_ABS]); for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
subscribe(subscr[MQTT_SUBS_LMT_NONPERI_REL]); snprintf(sub, 20, "ctrl/limit/%d", i);
subscribe(subscr[MQTT_SUBS_LMT_NONPERI_ABS]); subscribe(sub);
snprintf(sub, 20, "ctrl/restart/%d", i);
subscribe(sub);
}
subscribe(subscr[MQTT_SUBS_SET_TIME]); subscribe(subscr[MQTT_SUBS_SET_TIME]);
subscribe(subscr[MQTT_SUBS_SYNC_NTP]);
//subscribe("status/#");
} }
void onDisconnect(espMqttClientTypes::DisconnectReason reason) { void onDisconnect(espMqttClientTypes::DisconnectReason reason) {
@ -358,11 +292,14 @@ class PubMqtt {
DynamicJsonDocument json(128); DynamicJsonDocument json(128);
JsonObject root = json.to<JsonObject>(); JsonObject root = json.to<JsonObject>();
bool limitAbs = false;
if(len > 0) { if(len > 0) {
char *pyld = new char[len + 1]; char *pyld = new char[len + 1];
strncpy(pyld, (const char*)payload, len); strncpy(pyld, (const char*)payload, len);
pyld[len] = '\0'; pyld[len] = '\0';
root["val"] = atoi(pyld); root[F("val")] = atoi(pyld);
if(pyld[len-1] == 'W')
limitAbs = true;
delete[] pyld; delete[] pyld;
} }
@ -377,7 +314,16 @@ class PubMqtt {
tmp[pos] = '\0'; tmp[pos] = '\0';
switch(elm++) { switch(elm++) {
case 1: root[F("path")] = String(tmp); break; case 1: root[F("path")] = String(tmp); break;
case 2: root[F("cmd")] = String(tmp); break; case 2:
if(strncmp("limit", tmp, 5) == 0) {
if(limitAbs)
root[F("cmd")] = F("limit_nonpersistent_absolute");
else
root[F("cmd")] = F("limit_nonpersistent_relative");
}
else
root[F("cmd")] = String(tmp);
break;
case 3: root[F("id")] = atoi(tmp); break; case 3: root[F("id")] = atoi(tmp); break;
default: break; default: break;
} }
@ -397,6 +343,95 @@ class PubMqtt {
mRxCnt++; mRxCnt++;
} }
void discoveryConfigLoop(void) {
char topic[64], name[32], uniq_id[32], buf[350];
DynamicJsonDocument doc(256);
uint8_t fldTotal[4] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC};
const char* unitTotal[4] = {"W", "kWh", "Wh", "W"};
String node_mac = WiFi.macAddress().substring(12,14)+ WiFi.macAddress().substring(15,17);
String node_id = "AHOY_DTU_" + node_mac;
bool total = (mDiscovery.lastIvId == MAX_NUM_INVERTERS);
Inverter<> *iv = mSys->getInverterByPos(mDiscovery.lastIvId);
record_t<> *rec;
if (NULL != iv)
rec = iv->getRecordStruct(RealTimeRunData_Debug);
if ((NULL != iv) || total) {
if (!total) {
doc[F("name")] = iv->config->name;
doc[F("ids")] = String(iv->config->serial.u64, HEX);
doc[F("mdl")] = iv->config->name;
}
else {
doc[F("name")] = node_id;
doc[F("ids")] = node_id;
doc[F("mdl")] = node_id;
}
doc[F("cu")] = F("http://") + String(WiFi.localIP().toString());
doc[F("mf")] = F("Hoymiles");
JsonObject deviceObj = doc.as<JsonObject>(); // deviceObj is only pointer!?
const char *devCls, *stateCls;
if (!total) {
if (rec->assign[mDiscovery.sub].ch == CH0)
snprintf(name, 32, "%s %s", iv->config->name, iv->getFieldName(mDiscovery.sub, rec));
else
snprintf(name, 32, "%s CH%d %s", iv->config->name, rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
snprintf(topic, 64, "/ch%d/%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
snprintf(uniq_id, 32, "ch%d_%s", rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
devCls = getFieldDeviceClass(rec->assign[mDiscovery.sub].fieldId);
stateCls = getFieldStateClass(rec->assign[mDiscovery.sub].fieldId);
}
else { // total values
snprintf(name, 32, "Total %s", fields[fldTotal[mDiscovery.sub]]);
snprintf(topic, 64, "/%s", fields[fldTotal[mDiscovery.sub]]);
snprintf(uniq_id, 32, "total_%s", fields[fldTotal[mDiscovery.sub]]);
devCls = getFieldDeviceClass(fldTotal[mDiscovery.sub]);
stateCls = getFieldStateClass(fldTotal[mDiscovery.sub]);
}
DynamicJsonDocument doc2(512);
doc2[F("name")] = name;
doc2[F("stat_t")] = String(mCfgMqtt->topic) + "/" + ((!total) ? String(iv->config->name) : "total" ) + String(topic);
doc2[F("unit_of_meas")] = ((!total) ? (iv->getUnit(mDiscovery.sub, rec)) : (unitTotal[mDiscovery.sub]));
doc2[F("uniq_id")] = ((!total) ? (String(iv->config->serial.u64, HEX)) : (node_id)) + "_" + uniq_id;
doc2[F("dev")] = deviceObj;
if (!(String(stateCls) == String("total_increasing")))
doc2[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)
doc2[F("dev_cla")] = String(devCls);
if (stateCls != NULL)
doc2[F("stat_cla")] = String(stateCls);
if (!total)
snprintf(topic, 64, "%s/sensor/%s/ch%d_%s/config", MQTT_DISCOVERY_PREFIX, iv->config->name, rec->assign[mDiscovery.sub].ch, iv->getFieldName(mDiscovery.sub, rec));
else // total values
snprintf(topic, 64, "%s/sensor/%s/total_%s/config", MQTT_DISCOVERY_PREFIX, node_id.c_str(),fields[fldTotal[mDiscovery.sub]]);
size_t size = measureJson(doc2) + 1;
memset(buf, 0, size);
serializeJson(doc2, buf, size);
publish(topic, buf, true, false);
if(++mDiscovery.sub == ((!total) ? (rec->length) : 4)) {
mDiscovery.sub = 0;
if(++mDiscovery.lastIvId == (MAX_NUM_INVERTERS + 1))
mDiscovery.running = false;
}
} else {
mDiscovery.sub = 0;
if(++mDiscovery.lastIvId == (MAX_NUM_INVERTERS + 1))
mDiscovery.running = false;
}
yield();
}
const char *getFieldDeviceClass(uint8_t fieldId) { const char *getFieldDeviceClass(uint8_t fieldId) {
uint8_t pos = 0; uint8_t pos = 0;
for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) { for (; pos < DEVICE_CLS_ASSIGN_LIST_LEN; pos++) {
@ -569,8 +604,8 @@ class PubMqtt {
if (sendTotals) { if (sendTotals) {
uint8_t fieldId; uint8_t fieldId;
bool retained = true;
for (uint8_t i = 0; i < 4; i++) { for (uint8_t i = 0; i < 4; i++) {
bool retained = true;
switch (i) { switch (i) {
default: default:
case 0: case 0:
@ -622,7 +657,11 @@ class PubMqtt {
// last will topic and payload must be available trough lifetime of 'espMqttClient' // last will topic and payload must be available trough lifetime of 'espMqttClient'
char mLwtTopic[MQTT_TOPIC_LEN+5]; char mLwtTopic[MQTT_TOPIC_LEN+5];
const char *mDevName, *mVersion; const char *mDevName, *mVersion;
char mClientId[26]; // number of chars is limited to 23 up to v3.1 of MQTT char mClientId[24]; // number of chars is limited to 23 up to v3.1 of MQTT
// global buffer for mqtt topic. Used when publishing mqtt messages.
char mTopic[MQTT_TOPIC_LEN + 32 + MAX_NAME_LENGTH + 1];
discovery_t mDiscovery;
}; };
#endif /*__PUB_MQTT_H__*/ #endif /*__PUB_MQTT_H__*/

18
src/publisher/pubMqttDefs.h

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2023 Ahoy, https://ahoydtu.de // 2023 Ahoy, https://ahoydtu.de
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef __PUB_MQTT_DEFS_H__ #ifndef __PUB_MQTT_DEFS_H__
@ -41,6 +41,7 @@ enum {
MQTT_UPTIME = 0, MQTT_UPTIME = 0,
MQTT_RSSI, MQTT_RSSI,
MQTT_FREE_HEAP, MQTT_FREE_HEAP,
MQTT_HEAP_FRAG,
MQTT_SUNRISE, MQTT_SUNRISE,
MQTT_SUNSET, MQTT_SUNSET,
MQTT_COMM_START, MQTT_COMM_START,
@ -64,6 +65,7 @@ const char* const subtopics[] PROGMEM = {
"uptime", "uptime",
"wifi_rssi", "wifi_rssi",
"free_heap", "free_heap",
"heap_frag",
"sunrise", "sunrise",
"sunset", "sunset",
"comm_start", "comm_start",
@ -84,21 +86,11 @@ const char* const subtopics[] PROGMEM = {
}; };
enum { enum {
MQTT_SUBS_LMT_PERI_REL, MQTT_SUBS_SET_TIME
MQTT_SUBS_LMT_PERI_ABS,
MQTT_SUBS_LMT_NONPERI_REL,
MQTT_SUBS_LMT_NONPERI_ABS,
MQTT_SUBS_SET_TIME,
MQTT_SUBS_SYNC_NTP
}; };
const char* const subscr[] PROGMEM = { const char* const subscr[] PROGMEM = {
"ctrl/limit_persistent_relative", "setup/set_time"
"ctrl/limit_persistent_absolute",
"ctrl/limit_nonpersistent_relative",
"ctrl/limit_nonpersistent_absolute",
"setup/set_time",
"setup/sync_ntp"
}; };
#endif /*__PUB_MQTT_DEFS_H__*/ #endif /*__PUB_MQTT_DEFS_H__*/

10
src/utils/dbg.h

@ -60,6 +60,11 @@
mCb(String(b, HEX)); mCb(String(b, HEX));
} }
} }
inline void DBGHEXLN(uint8_t b) {
DHEX(b);
DBGPRINT(F("\r\n"));
}
/*inline void DHEX(uint16_t b) { /*inline void DHEX(uint16_t b) {
if( b<0x10 ) DSERIAL.print(F("000")); if( b<0x10 ) DSERIAL.print(F("000"));
else if( b<0x100 ) DSERIAL.print(F("00")); else if( b<0x100 ) DSERIAL.print(F("00"));
@ -146,6 +151,10 @@
}\ }\
}) })
#define DPRINT_IVID(level, id) ({\
DPRINT(level, F("(#")); DBGPRINT(String(id)); DBGPRINT(F(") "));\
})
#define DPRINTLN(level, str) ({\ #define DPRINTLN(level, str) ({\
switch(level) {\ switch(level) {\
case DBG_ERROR: PERRLN(str); break; \ case DBG_ERROR: PERRLN(str); break; \
@ -156,6 +165,7 @@
}\ }\
}) })
/*class ahoyLog { /*class ahoyLog {
public: public:
ahoyLog() {} ahoyLog() {}

68
src/web/RestApi.h

@ -79,6 +79,7 @@ class RestApi {
String path = request->url().substring(5); String path = request->url().substring(5);
if(path == "html/system") getHtmlSystem(root); if(path == "html/system") getHtmlSystem(root);
else if(path == "html/logout") getHtmlLogout(root); else if(path == "html/logout") getHtmlLogout(root);
else if(path == "html/reboot") getHtmlReboot(root);
else if(path == "html/save") getHtmlSave(root); else if(path == "html/save") getHtmlSave(root);
else if(path == "system") getSysInfo(root); else if(path == "system") getSysInfo(root);
else if(path == "generic") getGeneric(root); else if(path == "generic") getGeneric(root);
@ -264,11 +265,17 @@ class RestApi {
obj[F("html")] = F("succesfully logged out"); obj[F("html")] = F("succesfully logged out");
} }
void getHtmlReboot(JsonObject obj) {
getGeneric(obj.createNestedObject(F("generic")));
obj[F("refresh")] = 20;
obj[F("refresh_url")] = "/";
obj[F("html")] = F("rebooting ...");
}
void getHtmlSave(JsonObject obj) { void getHtmlSave(JsonObject obj) {
getGeneric(obj.createNestedObject(F("generic"))); getGeneric(obj.createNestedObject(F("generic")));
obj[F("refresh")] = 2; obj["pending"] = (bool)mApp->getSavePending();
obj[F("refresh_url")] = "/setup"; obj["success"] = (bool)mApp->getLastSaveSucceed();
obj[F("html")] = F("settings succesfully save");
} }
void getReboot(JsonObject obj) { void getReboot(JsonObject obj) {
@ -413,10 +420,10 @@ class RestApi {
obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast; obj[F("disp_cont")] = (uint8_t)mConfig->plugin.display.contrast;
obj[F("disp_clk")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_clk; obj[F("disp_clk")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_clk;
obj[F("disp_data")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_data; obj[F("disp_data")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_data;
obj[F("disp_cs")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_cs; obj[F("disp_cs")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_cs;
obj[F("disp_dc")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc; obj[F("disp_dc")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_dc;
obj[F("disp_rst")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_reset; obj[F("disp_rst")] = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : mConfig->plugin.display.disp_reset;
obj[F("disp_bsy")] = (mConfig->plugin.display.type == 0) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy; obj[F("disp_bsy")] = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : mConfig->plugin.display.disp_busy;
} }
void getIndex(JsonObject obj) { void getIndex(JsonObject obj) {
@ -488,7 +495,6 @@ class RestApi {
void getLive(JsonObject obj) { void getLive(JsonObject obj) {
getGeneric(obj.createNestedObject(F("generic"))); getGeneric(obj.createNestedObject(F("generic")));
//JsonArray invArr = obj.createNestedArray(F("inverter"));
obj[F("refresh")] = mConfig->nrf.sendInterval; obj[F("refresh")] = mConfig->nrf.sendInterval;
for (uint8_t fld = 0; fld < sizeof(acList); fld++) { for (uint8_t fld = 0; fld < sizeof(acList); fld++) {
@ -508,52 +514,6 @@ class RestApi {
parse = iv->config->enabled; parse = iv->config->enabled;
obj[F("iv")][i] = parse; obj[F("iv")][i] = parse;
} }
/*Inverter<> *iv;
uint8_t pos;
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i ++) {
iv = mSys->getInverterByPos(i);
if(NULL != iv) {
record_t<> *rec = iv->getRecordStruct(RealTimeRunData_Debug);
JsonObject obj2 = invArr.createNestedObject();
obj2[F("enabled")] = (bool)iv->config->enabled;
obj2[F("name")] = String(iv->config->name);
obj2[F("channels")] = iv->channels;
obj2[F("power_limit_read")] = ah::round3(iv->actPowerLimit);
//obj2[F("last_alarm")] = String(iv->lastAlarmMsg);
obj2[F("ts_last_success")] = rec->ts;
JsonArray ch = obj2.createNestedArray("ch");
JsonArray ch0 = ch.createNestedArray();
obj2[F("ch_names")][0] = "AC";
for (uint8_t fld = 0; fld < sizeof(list); fld++) {
pos = (iv->getPosByChFld(CH0, list[fld], rec));
ch0[fld] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
obj[F("ch0_fld_units")][fld] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail;
obj[F("ch0_fld_names")][fld] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
}
for(uint8_t j = 1; j <= iv->channels; j ++) {
obj2[F("ch_names")][j] = String(iv->config->chName[j-1]);
JsonArray cur = ch.createNestedArray();
for (uint8_t k = 0; k < 6; k++) {
switch(k) {
default: pos = (iv->getPosByChFld(j, FLD_UDC, rec)); break;
case 1: pos = (iv->getPosByChFld(j, FLD_IDC, rec)); break;
case 2: pos = (iv->getPosByChFld(j, FLD_PDC, rec)); break;
case 3: pos = (iv->getPosByChFld(j, FLD_YD, rec)); break;
case 4: pos = (iv->getPosByChFld(j, FLD_YT, rec)); break;
case 5: pos = (iv->getPosByChFld(j, FLD_IRR, rec)); break;
}
cur[k] = (0xff != pos) ? ah::round3(iv->getValue(pos, rec)) : 0.0;
if(1 == j) {
obj[F("fld_units")][k] = (0xff != pos) ? String(iv->getUnit(pos, rec)) : notAvail;
obj[F("fld_names")][k] = (0xff != pos) ? String(iv->getFieldName(pos, rec)) : notAvail;
}
}
}
}
}*/
} }
void getRecord(JsonObject obj, uint8_t recType) { void getRecord(JsonObject obj, uint8_t recType) {

3
src/web/html/convert.py

@ -9,7 +9,10 @@ import subprocess
def get_git_sha(): def get_git_sha():
try:
return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip() return subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip()
except:
return "0000000"
def readVersion(path): def readVersion(path):
f = open(path, "r") f = open(path, "r")

2
src/web/html/includes/footer.html

@ -10,7 +10,7 @@
<ul> <ul>
<li>{#VERSION_GIT}</li> <li>{#VERSION_GIT}</li>
<li id="esp_type"></li> <li id="esp_type"></li>
<li><a href="https://creativecommons.org/licenses/by-nc-sa/3.0/de" target="_blank" >CC BY-NC-SA 3.0</a></li> <li><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed" target="_blank" >CC BY-NC-SA 4.0</a></li>
</ul> </ul>
</div> </div>
</div> </div>

4
src/web/html/index.html

@ -102,12 +102,12 @@
if(obj["disNightComm"]) { if(obj["disNightComm"]) {
if(((obj["ts_sunrise"] - obj["ts_offset"]) < obj["ts_now"]) if(((obj["ts_sunrise"] - obj["ts_offset"]) < obj["ts_now"])
&& ((obj["ts_sunset"] + obj["ts_offset"]) > obj["ts_now"])) { && ((obj["ts_sunset"] + obj["ts_offset"]) > obj["ts_now"])) {
commInfo = "Polling inverter(s), will stop at sunset " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); commInfo = "Polling inverter(s), will pause at sunset " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE'));
} }
else { else {
commInfo = "Night time, inverter polling disabled, "; commInfo = "Night time, inverter polling disabled, ";
if(obj["ts_now"] > (obj["ts_sunrise"] - obj["ts_offset"])) { if(obj["ts_now"] > (obj["ts_sunrise"] - obj["ts_offset"])) {
commInfo += "stopped at " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE')); commInfo += "paused at " + (new Date((obj["ts_sunset"] + obj["ts_offset"]) * 1000).toLocaleString('de-DE'));
} }
else { else {
commInfo += "will start polling at " + (new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 1000).toLocaleString('de-DE')); commInfo += "will start polling at " + (new Date((obj["ts_sunrise"] - obj["ts_offset"]) * 1000).toLocaleString('de-DE'));

51
src/web/html/save.html

@ -0,0 +1,51 @@
<!doctype html>
<html>
<head>
<title>Save</title>
{#HTML_HEADER}
</head>
<body>
{#HTML_NAV}
<div id="wrapper">
<div id="content">
<div id="html" class="mt-3 mb-3"></div>
</div>
</div>
{#HTML_FOOTER}
<script type="text/javascript">
function parseGeneric(obj) {
parseNav(obj);
parseESP(obj);
parseRssi(obj);
}
function parseHtml(obj) {
var html = "";
if(obj.pending)
html = "saving settings ...";
else {
if(obj.success)
html = "settings successfully saved";
else
html = "failed saving settings";
var meta = document.createElement('meta');
meta.httpEquiv = "refresh"
meta.content = 1 + "; URL=/setup";
document.getElementsByTagName('head')[0].appendChild(meta);
}
document.getElementById("html").innerHTML = html;
}
function parse(obj) {
if(null != obj) {
parseGeneric(obj["generic"]);
parseHtml(obj);
window.setInterval("getAjax('/api/html/save', parse)", 1100);
}
}
getAjax("/api/html/save", parse);
</script>
</body>
</html>

69
src/web/html/setup.html

@ -157,7 +157,7 @@
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstMid"/></div> <div class="col-4 col-sm-9"><input type="checkbox" name="invRstMid"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3 mb-2">Reset values when inverter polling stops at sunset</div> <div class="col-8 col-sm-3 mb-2">Reset values when inverter polling pauses at sunset</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="invRstComStop"/></div> <div class="col-4 col-sm-9"><input type="checkbox" name="invRstComStop"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
@ -209,7 +209,7 @@
<div class="col-12 col-sm-9"><select name="sunOffs"></select></div> <div class="col-12 col-sm-9"><select name="sunOffs"></select></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3">Stop polling inverters during night</div> <div class="col-8 col-sm-3">Pause polling inverters during night</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="sunDisNightCom"/></div> <div class="col-4 col-sm-9"><input type="checkbox" name="sunDisNightCom"/></div>
</div> </div>
</fieldset> </fieldset>
@ -265,12 +265,12 @@
<div class="col-4 col-sm-9"><input type="checkbox" name="disp_pwr"/></div> <div class="col-4 col-sm-9"><input type="checkbox" name="disp_pwr"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-8 col-sm-3">Enable Screensaver (pixel shifting)</div> <div class="col-8 col-sm-3">Enable Screensaver (pixel shifting, OLED only)</div>
<div class="col-4 col-sm-9"><input type="checkbox" name="disp_pxshift"/></div> <div class="col-4 col-sm-9"><input type="checkbox" name="disp_pxshift"/></div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-12 col-sm-3 my-2">Contrast</div> <div class="col-12 col-sm-3 my-2">Luminance</div>
<div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="1" max="100"></select></div> <div class="col-12 col-sm-9"><input type="number" name="disp_cont" min="0" max="100"></select></div>
</div> </div>
<p class="des">Pinout</p> <p class="des">Pinout</p>
<div id="dispPins"></div> <div id="dispPins"></div>
@ -279,25 +279,34 @@
<div class="row mb-4 mt-4"> <div class="row mb-4 mt-4">
<div class="col-8 col-sm-3">Reboot device after successful save</div> <div class="col-8 col-sm-3">Reboot device after successful save</div>
<div class="col-2 col-md-6"> <div class="col-4 col-sm-9">
<input type="checkbox" name="reboot" checked /> <input type="checkbox" name="reboot" checked />
<input type="submit" value="save" class="btn right"/> <input type="submit" value="save" class="btn right"/>
</div> </div>
</div> </div>
</form>
<div class="hr mb-3 mt-3"></div> <div class="hr mb-3 mt-3"></div>
<div class="mb-4 mt-4"> <div class="mb-4 mt-4">
<a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a> <a class="btn" href="/erase">ERASE SETTINGS (not WiFi)</a>
<fieldset class="mb-4"> <fieldset class="mb-4">
<legend class="des">Upload / Store JSON Settings</legend> <legend class="des">Import / Export JSON Settings</legend>
<div class="row mb-4 mt-4">
<div class="col-12 col-sm-3 my-2">Import</div>
<div class="col-12 col-sm-9">
<form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8"> <form id="form" method="POST" action="/upload" enctype="multipart/form-data" accept-charset="utf-8">
<input type="file" name="upload"> <input type="file" name="upload">
<input type="button" class="btn" value="Upload" onclick="hide()"> <input type="button" class="btn" value="Import" onclick="hide()">
</form> </form>
</div>
</div>
<div class="row mb-4 mt-4">
<div class="col-12 col-sm-3 my-2">Export</div>
<div class="col-12 col-sm-9">
<a class="btn" href="/get_setup" target="_blank">Export settings (JSON file)</a><span> (only values, passwords will be removed!)</span>
</div>
</div>
</fieldset> </fieldset>
<a class="btn" href="/get_setup" target="_blank">Download settings (JSON file)</a><span> (only saved values, passwords will be removed!)</span>
</div> </div>
</form>
</div> </div>
</div> </div>
{#HTML_FOOTER} {#HTML_FOOTER}
@ -359,7 +368,7 @@
document.getElementById("btnAdd").addEventListener("click", function() { document.getElementById("btnAdd").addEventListener("click", function() {
if(highestId <= (maxInv-1)) { if(highestId <= (maxInv-1)) {
ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_power":[0,0,0,0],"ch_name":["","","",""],"ch_yield_cor":[0,0,0,0]}'), highestId); ivHtml(JSON.parse('{"enabled":true,"name":"","serial":"","channels":4,"ch_max_pwr":[0,0,0,0],"ch_name":["","","",""],"ch_yield_cor":[0,0,0,0]}'), highestId);
} }
}); });
@ -638,12 +647,12 @@
document.getElementsByName(i)[0].checked = obj[i]; document.getElementsByName(i)[0].checked = obj[i];
var e = document.getElementById("dispPins"); var e = document.getElementById("dispPins");
pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst'], ['busy', 'disp_bsy']]; var pins = [['clock', 'disp_clk'], ['data', 'disp_data'], ['cs', 'disp_cs'], ['dc', 'disp_dc'], ['reset', 'disp_rst']];
if("ESP32" == type)
pins.push(['busy', 'disp_bsy']);
for(p of pins) { for(p of pins) {
if(("ESP8266" == type) && p[0] == "busy")
break;
e.append( e.append(
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3", id: "row_" + p[1]}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()), ml("div", {class: "col-12 col-sm-3 my-2"}, p[0].toUpperCase()),
ml("div", {class: "col-12 col-sm-9"}, ml("div", {class: "col-12 col-sm-9"},
sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]]) sel(p[1], ("ESP8266" == type) ? esp8266pins : esp32pins, obj[p[1]])
@ -655,12 +664,16 @@
var opts = [[0, "None"], [1, "SSD1306 0.96\""], [2, "SH1106 1.3\""], [3, "Nokia5110"]]; var opts = [[0, "None"], [1, "SSD1306 0.96\""], [2, "SH1106 1.3\""], [3, "Nokia5110"]];
if("ESP32" == type) if("ESP32" == type)
opts.push([10, "ePaper"]); opts.push([10, "ePaper"]);
var dispType = sel("disp_typ", opts, obj["disp_typ"]);
document.getElementById("dispType").append( document.getElementById("dispType").append(
ml("div", {class: "row mb-3"}, [ ml("div", {class: "row mb-3"}, [
ml("div", {class: "col-12 col-sm-3 my-2"}, "Type"), ml("div", {class: "col-12 col-sm-3 my-2"}, "Type"),
ml("div", {class: "col-12 col-sm-9"}, sel("disp_typ", opts, obj["disp_typ"])) ml("div", {class: "col-12 col-sm-9"}, dispType)
]) ])
); );
dispType.addEventListener('change', (e) => {
hideDispPins(pins, e.target.value)
});
opts = [[0, "0&deg;"], [2, "180&deg;"]]; opts = [[0, "0&deg;"], [2, "180&deg;"]];
if("ESP32" == type) { if("ESP32" == type) {
@ -675,6 +688,28 @@
); );
document.getElementsByName("disp_cont")[0].value = obj["disp_cont"]; document.getElementsByName("disp_cont")[0].value = obj["disp_cont"];
hideDispPins(pins, obj.disp_typ);
}
function hideDispPins(pins, dispType) {
for(var i = 0; i < pins.length; i++) {
var cl = document.getElementById("row_" + pins[i][1]).classList;
if(0 == dispType)
cl.add("hide");
else if(dispType <= 2) { // OLED
if(i < 2)
cl.remove("hide");
else
cl.add("hide");
} else if(dispType == 3) { // Nokia
if(i < 4)
cl.remove("hide");
else
cl.add("hide");
} else // ePaper
cl.remove("hide");
}
} }
function parse(root) { function parse(root) {

115
src/web/web.h

@ -1,6 +1,6 @@
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// 2022 Ahoy, https://www.mikrocontroller.net/topic/525778 // 2022 Ahoy, https://www.mikrocontroller.net/topic/525778
// Creative Commons - http://creativecommons.org/licenses/by-nc-sa/3.0/de/ // Creative Commons - http://creativecommons.org/licenses/by-nc-sa/4.0/deed
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
#ifndef __WEB_H__ #ifndef __WEB_H__
@ -27,6 +27,7 @@
#include "html/h/setup_html.h" #include "html/h/setup_html.h"
#include "html/h/style_css.h" #include "html/h/style_css.h"
#include "html/h/system_html.h" #include "html/h/system_html.h"
#include "html/h/save_html.h"
#include "html/h/update_html.h" #include "html/h/update_html.h"
#include "html/h/visualization_html.h" #include "html/h/visualization_html.h"
@ -67,7 +68,7 @@ class Web {
mWeb.on("/factory", HTTP_ANY, std::bind(&Web::showFactoryRst, this, std::placeholders::_1)); mWeb.on("/factory", HTTP_ANY, std::bind(&Web::showFactoryRst, this, std::placeholders::_1));
mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1)); mWeb.on("/setup", HTTP_GET, std::bind(&Web::onSetup, this, std::placeholders::_1));
mWeb.on("/save", HTTP_ANY, std::bind(&Web::showSave, this, std::placeholders::_1)); mWeb.on("/save", HTTP_POST, std::bind(&Web::showSave, this, std::placeholders::_1));
mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1)); mWeb.on("/live", HTTP_ANY, std::bind(&Web::onLive, this, std::placeholders::_1));
//mWeb.on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1)); //mWeb.on("/api1", HTTP_POST, std::bind(&Web::showWebApi, this, std::placeholders::_1));
@ -161,21 +162,23 @@ class Web {
DPRINTLN(DBG_ERROR, F("can't open file!")); DPRINTLN(DBG_ERROR, F("can't open file!"));
mUploadFail = true; mUploadFail = true;
mUploadFp.close(); mUploadFp.close();
return;
} }
} }
mUploadFp.write(data, len); mUploadFp.write(data, len);
if (final) { if (final) {
mUploadFp.close(); mUploadFp.close();
File fp = LittleFS.open("/tmp.json", "r"); char pwd[PWD_LEN];
if (!fp) strncpy(pwd, mConfig->sys.stationPwd, PWD_LEN); // backup WiFi PWD
mUploadFail = true; if (!mApp->readSettings("/tmp.json")) {
else {
if (!mApp->readSettings("tmp.json")) {
mUploadFail = true; mUploadFail = true;
DPRINTLN(DBG_ERROR, F("upload JSON error!")); DPRINTLN(DBG_ERROR, F("upload JSON error!"));
} else } else {
mApp->saveSettings(); LittleFS.remove("/tmp.json");
strncpy(mConfig->sys.stationPwd, pwd, PWD_LEN); // restore WiFi PWD
mApp->saveSettings(true);
} }
if (!mUploadFail)
DPRINTLN(DBG_INFO, F("upload finished!")); DPRINTLN(DBG_INFO, F("upload finished!"));
} }
} }
@ -414,10 +417,8 @@ class Web {
refresh = 120; refresh = 120;
} }
request->send(200, F("text/html; charset=UTF-8"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>")); request->send(200, F("text/html; charset=UTF-8"), F("<!doctype html><html><head><title>Factory Reset</title><meta http-equiv=\"refresh\" content=\"") + String(refresh) + F("; URL=/\"></head><body>") + content + F("</body></html>"));
if (refresh == 10) { if (refresh == 10)
delay(1000); onReboot(request);
ESP.restart();
}
} }
void onSetup(AsyncWebServerRequest *request) { void onSetup(AsyncWebServerRequest *request) {
@ -590,16 +591,12 @@ class Web {
mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_dc").toInt(); mConfig->plugin.display.disp_dc = (mConfig->plugin.display.type < 3) ? DEF_PIN_OFF : request->arg("disp_dc").toInt();
mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt(); mConfig->plugin.display.disp_busy = (mConfig->plugin.display.type < 10) ? DEF_PIN_OFF : request->arg("disp_bsy").toInt();
mApp->saveSettings(); mApp->saveSettings((request->arg("reboot") == "on"));
if (request->arg("reboot") == "on") AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), save_html, save_html_len);
onReboot(request);
else {
AsyncWebServerResponse *response = request->beginResponse_P(200, F("text/html; charset=UTF-8"), system_html, system_html_len);
response->addHeader(F("Content-Encoding"), "gzip"); response->addHeader(F("Content-Encoding"), "gzip");
request->send(response); request->send(response);
} }
}
void onLive(AsyncWebServerRequest *request) { void onLive(AsyncWebServerRequest *request) {
DPRINTLN(DBG_VERBOSE, F("onLive")); DPRINTLN(DBG_VERBOSE, F("onLive"));
@ -618,71 +615,6 @@ class Web {
request->send(response); request->send(response);
} }
/*void showWebApi(AsyncWebServerRequest *request) {
// TODO: remove
DPRINTLN(DBG_VERBOSE, F("web::showWebApi"));
DPRINTLN(DBG_DEBUG, request->arg("plain"));
const size_t capacity = 200; // Use arduinojson.org/assistant to compute the capacity.
DynamicJsonDocument response(capacity);
// Parse JSON object
deserializeJson(response, request->arg("plain"));
// ToDo: error handling for payload
uint8_t iv_id = response["inverter"];
uint8_t cmd = response["cmd"];
Inverter<> *iv = mSys->getInverterByPos(iv_id);
if (NULL != iv) {
if (response["tx_request"] == (uint8_t)TX_REQ_INFO) {
// if the AlarmData is requested set the Alarm Index to the requested one
if (cmd == AlarmData || cmd == AlarmUpdate) {
// set the AlarmMesIndex for the request from user input
iv->alarmMesIndex = response["payload"];
}
DPRINTLN(DBG_INFO, F("Will make tx-request 0x15 with subcmd ") + String(cmd) + F(" and payload ") + String((uint16_t) response["payload"]));
// process payload from web request corresponding to the cmd
iv->enqueCommand<InfoCommand>(cmd);
}
if (response["tx_request"] == (uint8_t)TX_REQ_DEVCONTROL) {
if (response["cmd"] == (uint8_t)ActivePowerContr) {
uint16_t webapiPayload = response["payload"];
uint16_t webapiPayload2 = response["payload2"];
if (webapiPayload > 0 && webapiPayload < 10000) {
iv->devControlCmd = ActivePowerContr;
iv->powerLimit[0] = webapiPayload;
if (webapiPayload2 > 0)
iv->powerLimit[1] = webapiPayload2; // dev option, no sanity check
else // if not set, set it to 0x0000 default
iv->powerLimit[1] = AbsolutNonPersistent; // payload will be seted temporary in Watt absolut
if (iv->powerLimit[1] & 0x0001)
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("% via REST API"));
else
DPRINTLN(DBG_INFO, F("Power limit for inverter ") + String(iv->id) + F(" set to ") + String(iv->powerLimit[0]) + F("W via REST API"));
iv->devControlRequest = true; // queue it in the request loop
}
}
if (response["cmd"] == (uint8_t)TurnOff) {
iv->devControlCmd = TurnOff;
iv->devControlRequest = true; // queue it in the request loop
}
if (response["cmd"] == (uint8_t)TurnOn) {
iv->devControlCmd = TurnOn;
iv->devControlRequest = true; // queue it in the request loop
}
if (response["cmd"] == (uint8_t)CleanState_LockAndAlarm) {
iv->devControlCmd = CleanState_LockAndAlarm;
iv->devControlRequest = true; // queue it in the request loop
}
if (response["cmd"] == (uint8_t)Restart) {
iv->devControlCmd = Restart;
iv->devControlRequest = true; // queue it in the request loop
}
}
}
request->send(200, "text/json", "{success:true}");
}*/
void onDebug(AsyncWebServerRequest *request) { void onDebug(AsyncWebServerRequest *request) {
mApp->getSchedulerNames(); mApp->getSchedulerNames();
AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok"); AsyncWebServerResponse *response = request->beginResponse(200, F("text/html; charset=UTF-8"), "ok");
@ -749,8 +681,17 @@ class Web {
mApp->getVersion(), mConfig->sys.deviceName); mApp->getVersion(), mConfig->sys.deviceName);
metrics = String(type) + String(topic); metrics = String(type) + String(topic);
snprintf(topic,sizeof(topic),"# TYPE ahoy_solar_freeheap gauge\nahoy_solar_freeheap{devicename=\"%s\"} %u\n",mConfig->sys.deviceName,ESP.getFreeHeap()); snprintf(type,sizeof(type),"# TYPE ahoy_solar_freeheap gauge\n");
metrics += String(topic); snprintf(topic,sizeof(topic),"ahoy_solar_freeheap{devicename=\"%s\"} %u\n",mConfig->sys.deviceName,ESP.getFreeHeap());
metrics += String(type) + String(topic);
snprintf(type,sizeof(type),"# TYPE ahoy_solar_uptime counter\n");
snprintf(topic,sizeof(topic),"ahoy_solar_uptime{devicename=\"%s\"} %u\n", mConfig->sys.deviceName, mApp->getUptime());
metrics += String(type) + String(topic);
snprintf(type,sizeof(type),"# TYPE ahoy_solar_wifi_rssi_db gauge\n");
snprintf(topic,sizeof(topic),"ahoy_solar_wifi_rssi_db{devicename=\"%s\"} %d\n", mConfig->sys.deviceName, WiFi.RSSI());
metrics += String(type) + String(topic);
// NRF Statistics // NRF Statistics
stat = mApp->getStatistics(); stat = mApp->getStatistics();
@ -857,7 +798,7 @@ class Web {
String radioStatistic(String statistic, uint32_t value) { String radioStatistic(String statistic, uint32_t value) {
char type[60], topic[80], val[25]; char type[60], topic[80], val[25];
snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s gauge",statistic.c_str()); snprintf(type, sizeof(type), "# TYPE ahoy_solar_radio_%s counter",statistic.c_str());
snprintf(topic, sizeof(topic), "ahoy_solar_radio_%s",statistic.c_str()); snprintf(topic, sizeof(topic), "ahoy_solar_radio_%s",statistic.c_str());
snprintf(val, sizeof(val), "%d", value); snprintf(val, sizeof(val), "%d", value);
return ( String(type) + "\n" + String(topic) + " " + String(val) + "\n"); return ( String(type) + "\n" + String(topic) + " " + String(val) + "\n");

12
src/wifi/ahoywifi.cpp

@ -88,12 +88,11 @@ void ahoywifi::tickWifiLoop() {
} }
mCnt++; mCnt++;
uint8_t timeout = 10; // seconds uint8_t timeout = (mStaConn == DISCONNECTED) ? 10 : 20; // seconds
if (mStaConn == CONNECTED) // connected but no ip if (mStaConn == CONNECTED) // connected but no ip
timeout = 20; timeout = 20;
if(!mScanActive && mBSSIDList.empty() && (mStaConn == DISCONNECTED)) { // start scanning APs with the given SSID
if(!mScanActive && mBSSIDList.empty() && ((mCnt % timeout) == 0)) { // start scanning APs with the given SSID
DBGPRINT(F("scanning APs with SSID ")); DBGPRINT(F("scanning APs with SSID "));
DBGPRINTLN(String(mConfig->sys.stationSsid)); DBGPRINTLN(String(mConfig->sys.stationSsid));
mScanCnt = 0; mScanCnt = 0;
@ -115,8 +114,9 @@ void ahoywifi::tickWifiLoop() {
mCnt = timeout - 2; mCnt = timeout - 2;
} }
if((mCnt % timeout) == 0) { // try to reconnect after x sec without connection if((mCnt % timeout) == 0) { // try to reconnect after x sec without connection
if(mStaConn != CONNECTED)
mStaConn = CONNECTING; mStaConn = CONNECTING;
WiFi.disconnect();
if(mBSSIDList.size() > 0) { // get first BSSID in list if(mBSSIDList.size() > 0) { // get first BSSID in list
DBGPRINT(F("try to connect to AP with BSSID:")); DBGPRINT(F("try to connect to AP with BSSID:"));
uint8_t bssid[6]; uint8_t bssid[6];
@ -126,9 +126,11 @@ void ahoywifi::tickWifiLoop() {
DBGPRINT(" " + String(bssid[j], HEX)); DBGPRINT(" " + String(bssid[j], HEX));
} }
DBGPRINTLN(""); DBGPRINTLN("");
WiFi.disconnect();
WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]); WiFi.begin(mConfig->sys.stationSsid, mConfig->sys.stationPwd, 0, &bssid[0]);
} }
else
mStaConn = DISCONNECTED;
mCnt = 0; mCnt = 0;
} }
} }

15
tools/rpi/Dockerfile

@ -0,0 +1,15 @@
############################
# build executable binary
############################
FROM python:slim-bullseye
COPY . /hoymiles
WORKDIR /hoymiles
RUN python3 -m pip install pyrf24 influxdb_client && \
python3 -m pip list #watch for RF24 module - if its there its installed
RUN pip install crcmod pyyaml paho-mqtt SunTimes
CMD python3 -um hoymiles --log-transactions --verbose --config /etc/ahoy.yml

18
tools/rpi/README.md

@ -21,14 +21,22 @@ Required Hardware Setup
`ahoy.py` has been successfully tested with the following setup `ahoy.py` has been successfully tested with the following setup
- RaspberryPi Model 2B (any model should work) - RaspberryPi Model 2B, 4B (any model should work)
- NRF24L01+ Radio Module connected as described, e.g., in [2] - NRF24L01+ Radio Module connected as described, e.g., in [2]
(Instructions at [3] should work identically, but [2] has more (Instructions at [3] should work identically, but [2] has more
pretty pictures.) pretty pictures.)
- or the [PaHoy board](https://github.com/DM6JM/PaHoy/)
- TMRh20's 'Optimized High Speed nRF24L01+ Driver' [3], installed - TMRh20's 'Optimized High Speed nRF24L01+ Driver' [3], installed
as per the instructions given in [4] as per the instructions given in [4]
- Python Library Wrapper, as per [5] - Python Library Wrapper, as per [5]
- or the easy way, using [pyRF24](https://github.com/nRF24/pyRF24)[6]
How to talk to the nRF24L01+ in Python?
---------------------------------------
Either you make use of the way proposed in the following, using the NRF24 Python Wrapper and the 'Optimized High Speed nRF24L01+ Driver' OR you just use pip and let it install pyRF24.
- If you go with pyRF24, all that needs to be done is installing pyRF24 as described in [6]. Please be aware that not all examples provided in this repo are prepared to use pyRF24. It might be nescessary to adjust the imports from RF24 to pyRF24 to get them running. Once you installed pyRF24, go on at 'Required python modules'
- If you go with the RF24 wrapper, do the following steps
Building the NRF24 Python Wrapper Building the NRF24 Python Wrapper
--------------------------------- ---------------------------------
@ -220,7 +228,12 @@ Example injects exactly the same as we normally use to poll data
This allows for even faster hacking during runtime This allows for even faster hacking during runtime
Running it as a service
-----------------------
If you want to run directly from the start, you might want to install it as a service.
Depending on if you want to run it once a user is logged in or as soon as the system is booted, two service examples are included.
ahoy.service allows you to start it as a user service upon login.
ahoy_system.service allows you to start it as a system service already before login without user interaction.
Analysing the Logs Analysing the Logs
------------------ ------------------
@ -263,3 +276,4 @@ References
- [3] https://nrf24.github.io/RF24/index.html - [3] https://nrf24.github.io/RF24/index.html
- [4] https://nrf24.github.io/RF24/md_docs_linux_install.html - [4] https://nrf24.github.io/RF24/md_docs_linux_install.html
- [5] https://nrf24.github.io/RF24/md_docs_python_wrapper.html - [5] https://nrf24.github.io/RF24/md_docs_python_wrapper.html
- [6] https://github.com/nRF24/pyRF24

42
tools/rpi/ahoy_system.service

@ -0,0 +1,42 @@
######################################################################
# systemd.service configuration for ahoy (lumapu)
# users can modify the lines:
# Description
# ExecStart (example: name of config file)
# WorkingDirectory (absolute path to your private ahoy dir)
# To change other config parameter, please consult systemd documentation
#
# To activate this service, enable and start ahoy.service:
# - Create folder ahoy in /home/ and set owner to the user that the
# service should be executed for (e.g. pi)
# - Copy folder contents to new folder
# - Adjust the user that this service should be executed as, avoid root
# - Execute commands to setup, check and start/stop as wanted
# $ sudo systemctl enable /home/ahoy/tools/rpi/ahoy.service
# $ sudo systemctl status ahoy
# $ sudo systemctl start ahoy
# $ sudo systemctl stop ahoy
#
# 2023.01 <PaeserBastelstube>
# 2023.03 <DM6JM>
######################################################################
[Unit]
Description=ahoy (lumapu) as Service
After=network.target local-fs.target time-sync.target
[Service]
ExecStart=/usr/bin/env python3 -um hoymiles --log-transactions --verbose --config ahoy.yml
RestartSec=10
Restart=on-failure
Type=simple
User=pi
# WorkingDirectory must be an absolute path - not relative path
WorkingDirectory=/home/ahoy/tools/rpi
EnvironmentFile=/etc/environment
[Install]
WantedBy=default.target
Loading…
Cancel
Save