Browse Source

0.8.1280001

0.8.1280001
Merge pull request #1701 from tictrick/zero-export
zero-export
tictrick 6 months ago
committed by GitHub
parent
commit
6fd1a85764
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      .github/workflows/compile_zero-export.yml
  2. 24
      src/CHANGES.md
  3. 187
      src/app.cpp
  4. 5
      src/app.h
  5. 3
      src/appInterface.h
  6. 8
      src/config/config.h
  7. 3
      src/config/config_override_example.h
  8. 1
      src/config/settings.h
  9. 2
      src/defines.h
  10. 21
      src/hm/hmInverter.h
  11. 2
      src/network/AhoyWifiEsp32.h
  12. 70
      src/platformio.ini
  13. 2
      src/plugins/MaxPower.h
  14. 126
      src/plugins/zeroExport/powermeter.h
  15. 5
      src/plugins/zeroExport/powermeter.txt
  16. 365
      src/plugins/zeroExport/zeroExport.h
  17. 199
      src/publisher/pubMqtt.h
  18. 13
      src/publisher/pubMqttIvData.h
  19. 101
      src/utils/mqttHelper.cpp
  20. 46
      src/utils/mqttHelper.h
  21. 64
      src/web/RestApi.h
  22. 1
      src/web/html/grid_info.json
  23. 18
      src/web/html/index.html
  24. 37
      src/web/html/setup.html
  25. 2
      src/web/html/visualization.html
  26. 15
      src/web/lang.json

27
.github/workflows/compile_zero-export.yml

@ -25,6 +25,8 @@ jobs:
variant:
#- opendtufusion
#- opendtufusion-ethernet
#- opendtufusion-16MB
#- opendtufusion-ethernet-16MB
#- esp8266
#- esp8266-all
#- esp8266-minimal
@ -38,6 +40,9 @@ jobs:
#- esp32-c3-mini
- opendtufusion-zero_export
- opendtufusion-ethernet-zero_export
- opendtufusion-16MB-zero_export
- opendtufusion-ethernet-16MB-zero_export
steps:
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
@ -96,6 +101,8 @@ jobs:
variant:
#- opendtufusion-de
#- opendtufusion-ethernet-de
#- opendtufusion-16MB-de
#- opendtufusion-ethernet-16MB-de
#- esp8266-de
#- esp8266-all-de
#- esp8266-prometheus-de
@ -107,6 +114,9 @@ jobs:
#- esp32-c3-mini-de
- opendtufusion-zero_export-de
- opendtufusion-ethernet-zero_export-de
- opendtufusion-16MB-zero_export-de
- opendtufusion-ethernet-16MB-zero_export-de
steps:
- uses: actions/checkout@v4
- uses: benjlevesque/short-sha@v3.0
@ -217,10 +227,25 @@ jobs:
rm -f \
${{ steps.version_name.outputs.name }}/*/*.elf.7z
# - name: Deploy
# uses: nogsantos/scp-deploy@master
# with:
# src: ${{ steps.version_name.outputs.name }}/
# host: ${{ secrets.FW_SSH_HOST }}
# remote: ${{ secrets.FW_SSH_DIR }}/dev
# port: ${{ secrets.FW_SSH_PORT }}
# user: ${{ secrets.FW_SSH_USER }}
# key: ${{ secrets.FW_SSH_KEY }}
- name: Clean elf files (7z compressed) for Artifact
run: |
rm -f \
${{ steps.version_name.outputs.name }}/*/*.elf.7z
- name: Create Artifact
uses: actions/upload-artifact@v4
with:
name: zero-${{ steps.version_name.outputs.name }}
name: dev-${{ steps.version_name.outputs.name }}
path: |
${{ steps.version_name.outputs.name }}/*
manual/User_Manual.md

24
src/CHANGES.md

@ -1,10 +1,30 @@
# Development Changes
## 0.8.128 - 2024-07-10
* add environments for 16MB flash size ESP32-S3 aka opendtufusion
* prevent duplicate alarms, update end time once it is received
## 0.8.127 - 2024-06-21
* add grid file #1677
* merge PR: Bugfix Inv delete not working with password protection #1678
## 0.8.126 - 2024-06-12
* merge PR: Update pubMqtt.h - Bugfix #1673 #1674
## 0.8.125 - 2024-06-09
* fix ESP8266 compilation
* merge PR: active_PowerLimit #1663
## 0.8.124 - 2024-06-06
* improved MqTT `OnMessage` (threadsafe)
* support of HERF inverters, serial number is converted in Javascript #1425
* revert buffer size in `RestAPI` for ESP8266 #1650
## 0.8.123 - 2024-05-30
* fix ESP8266, ESP32 static IP #1643 #1608
* update MqTT library which enhances stability #1646
* merge PR: MQTT JSON Payload pro Kanal und total, auswählbar #1541
* add option to publish mqtt as json
* merge PR: MqTT JSON Payload pro Kanal und total, auswählbar #1541
* add option to publish MqTT as json
* publish rssi not on ch0 any more, published on `topic/rssi`
* add total power to index page (if multiple inverters are configured)
* show device name in html title #1639

187
src/app.cpp

@ -102,7 +102,7 @@ void app::setup() {
if (mMqttEnabled) {
mMqtt.setup(this, &mConfig->mqtt, mConfig->sys.deviceName, mVersion, &mSys, &mTimestamp, &mUptime);
mMqtt.setConnectionCb(std::bind(&app::mqttConnectCb, this));
mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1));
mMqtt.setSubscriptionCb(std::bind(&app::mqttSubRxCb, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5));
mCommunication.addAlarmListener([this](Inverter<> *iv) { mMqtt.alarmEvent(iv); });
}
#endif
@ -610,13 +610,177 @@ void app::mqttConnectCb(void) {
}
//-----------------------------------------------------------------------------
void app::mqttSubRxCb(JsonObject obj) {
mApi.ctrlRequest(obj);
#if defined(PLUGIN_ZEROEXPORT)
mZeroExport.onMqttMessage(obj);
#endif
void app::mqttSubRxCb(const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
if (mConfig->serial.debug)
{
DPRINT(DBG_INFO, mqttStr[MQTT_STR_GOT_TOPIC]);
DBGPRINTLN(String(topic));
}
#if defined(PLUGIN_ZEROEXPORT)
// FremdTopic ist für ZeroExport->Powermeter
// AhoyTopic ist für ZeroExport
if (mZeroExport.onMqttMessage(topic, payload, len)) return;
#endif
// AhoyTopic ist für Ahoy
int baseTopicLen = strlen(mConfig->mqtt.topic) + strlen("/ctrl") + 1;
char baseTopic[baseTopicLen];
strcpy(baseTopic, mConfig->mqtt.topic); // copy mqtt.topic
strcat(baseTopic, "/ctrl"); // '/ctrl' concat
if (strncmp(topic, baseTopic, strlen(baseTopic)) == 0)
{
DPRINT(DBG_INFO, mqttStr[MQTT_STR_GOT_TOPIC]);
DBGPRINT("alles super");
DBGPRINTLN(String(topic));
const char* p = topic + strlen(baseTopic);
// extract number from topic
int IvID = -1;
while (*p) {
if (isdigit(*p)) {
IvID = atoi(p);
break;
}
p++;
}
// reset to pointer with offset
p = topic + strlen(baseTopic);
Inverter<> *iv = mSys.getInverterByPos(IvID);
String sPayload = String((const char*)payload).substring(0, len);
// ???/ctrl/limit/+ 100 % oder 400 W
if (strncmp(p, "/limit", strlen("/limit")) == 0) {
// immer
DBGPRINT(String("limit "));
DBGPRINTLN(String(IvID));
iv->powerLimit[0] = static_cast<uint16_t>(sPayload.toInt() * 10.0);
if (sPayload.endsWith("W"))
iv->powerLimit[1] = AbsolutNonPersistent;
else if (sPayload.endsWith("%"))
iv->powerLimit[1] = RelativNonPersistent;
if (iv->setDevControlRequest(ActivePowerContr))
triggerTickSend(iv->id);
return;
}
// ???/ctrl/power/+ 0/1
if (strncmp(p, "/power", strlen("/power")) == 0) {
// immer
DBGPRINT(String("power "));
DBGPRINTLN(String(IvID));
if (sPayload.equals("1") || sPayload.equals("true"))
{
if (iv->setDevControlRequest(TurnOn))
triggerTickSend(iv->id);
}
else if (sPayload.equals("0") || sPayload.equals("false"))
{
if (iv->setDevControlRequest(TurnOff))
triggerTickSend(iv->id);
}
return;
}
// ???/ctrl/restart/+ 0/1
if (strncmp(p, "/restart", strlen("/restart")) == 0) {
// mit NR = WR
if (IvID != -1)
{
DBGPRINT(String("restart Iv "));
DBGPRINTLN(String(IvID));
if (sPayload.equals("1") || sPayload.equals("true"))
{
if (iv->setDevControlRequest(Restart)) {
triggerTickSend(iv->id);
}
mMqtt.publish(topic, "successful", false, QOS_2);
}
}
// ohne NR = Ahoy
else
{
//TODO: set mqtt-topic back to false (=>successful)? wait a moment
DBGPRINTLN(String("restart Ahoy"));
if (sPayload.equals("1") || sPayload.equals("true"))
{
mMqtt.publish(topic, "successful", false, QOS_2);
yield();
delay(1000);
yield();
ESP.restart();
}
}
return;
}
return;
}
/// TODO: discuss setup???
// ???/setup/set_time unix timestamp
/// TODO: Wunschdenken?
}
/*
bool limitAbs = false;
if(len > 0) {
char *pyld = new char[len + 1];
memcpy(pyld, payload, len);
pyld[len] = '\0';
if(NULL == strstr(topic, "limit"))
root[F("val")] = atoi(pyld);
else
root[F("val")] = atof(pyld);
if(pyld[len-1] == 'W')
limitAbs = true;
delete[] pyld;
}
const char *p = topic + strlen(mCfgMqtt->topic);
uint8_t pos = 0, elm = 0;
char tmp[30];
while(1) {
if(('/' == p[pos]) || ('\0' == p[pos])) {
memcpy(tmp, p, pos);
tmp[pos] = '\0';
switch(elm++) {
case 1: root[F("path")] = 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;
default: break;
}
if('\0' == p[pos])
break;
p = p + pos + 1;
pos = 0;
}
pos++;
}
*/
//-----------------------------------------------------------------------------
void app::setupLed(void) {
uint8_t led_off = (mConfig->led.high_active) ? 0 : 255;
@ -663,3 +827,14 @@ void app::updateLed(void) {
analogWrite(mConfig->led.led[2], led_off);
}
}
void app::subscribe(const char *subTopic, uint8_t qos) {
mMqtt.subscribe(subTopic, qos);
}
void app::unsubscribe(const char *subTopic) {
mMqtt.unsubscribe(subTopic);
}

5
src/app.h

@ -360,6 +360,9 @@ class app : public IApp, public ah::Scheduler {
}
#endif
void subscribe(const char *subTopic, uint8_t qos = QOS_0);
void unsubscribe(const char *subTopic);
private:
#define CHECK_AVAIL true
#define SKIP_YIELD_DAY true
@ -381,7 +384,7 @@ class app : public IApp, public ah::Scheduler {
}
void mqttConnectCb(void);
void mqttSubRxCb(JsonObject obj);
void mqttSubRxCb(const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
void setupLed();
void updateLed();

3
src/appInterface.h

@ -73,6 +73,9 @@ class IApp {
virtual void addValueToHistory(uint8_t historyType, uint8_t valueType, uint32_t value) = 0;
#endif
virtual void* getRadioObj(bool nrf) = 0;
virtual void subscribe(const char *subTopic, uint8_t qos) = 0;
virtual void unsubscribe(const char *subTopic) = 0;
};
#endif /*__IAPP_H__*/

8
src/config/config.h

@ -6,6 +6,9 @@
#ifndef __CONFIG_H__
#define __CONFIG_H__
// set EMC_ALLOW_NOT_CONNECTED_PUBLISH flag to 0
// (https://www.emelis.net/espMqttClient/#compile-time-configuration) to avoid the memory exhaustion when disconnected.
#define EMC_ALLOW_NOT_CONNECTED_PUBLISH 0
// globally used
#define DEF_PIN_OFF 255
@ -16,9 +19,8 @@
//-------------------------------------
// Fallback WiFi Info
#define FB_WIFI_SSID "YOUR_WIFI_SSID"
#define FB_WIFI_PWD "YOUR_WIFI_PWD"
#define FB_WIFI_SSID ""
#define FB_WIFI_PWD ""
// Access Point Info
// In case there is no WiFi Network or Ahoy can not connect to it, it will act as an Access Point

3
src/config/config_override_example.h

@ -6,9 +6,6 @@
#ifndef __CONFIG_OVERRIDE_H__
#define __CONFIG_OVERRIDE_H__
// override fallback WiFi info
#define FB_WIFI_OVERRIDDEN
// each override must be preceded with an #undef statement
#undef FB_WIFI_SSID
#define FB_WIFI_SSID "MY_SSID"

1
src/config/settings.h

@ -221,6 +221,7 @@ typedef struct {
// Plugin ZeroExport
#if defined(PLUGIN_ZEROEXPORT)
//#define ZEROEXPORT_DEBUG
#define ZEROEXPORT_MAX_QUEUE_ENTRIES 64
#define ZEROEXPORT_MAX_GROUPS 8
#define ZEROEXPORT_GROUP_MAX_LEN_NAME 25

2
src/defines.h

@ -13,7 +13,7 @@
//-------------------------------------
#define VERSION_MAJOR 0
#define VERSION_MINOR 8
#define VERSION_PATCH 1230003
#define VERSION_PATCH 1280001
//-------------------------------------
typedef struct {
uint8_t ch;

21
src/hm/hmInverter.h

@ -670,7 +670,6 @@ class Inverter {
DPRINTLN(DBG_DEBUG, "Alarm #" + String(pyld[startOff+1]) + " '" + String(getAlarmStr(pyld[startOff+1])) + "' start: " + ah::getTimeStr(start) + ", end: " + ah::getTimeStr(endTime));
addAlarm(pyld[startOff+1], start, endTime);
alarmCnt++;
alarmLastId = alarmMesIndex;
return pyld[startOff+1];
@ -818,6 +817,26 @@ class Inverter {
private:
inline void addAlarm(uint16_t code, uint32_t start, uint32_t end) {
bool found = false;
uint8_t i = 0;
if(start > end)
end = 0;
for(; i < 10; i++) {
mAlarmNxtWrPos = (++mAlarmNxtWrPos) % 10;
if(lastAlarm[mAlarmNxtWrPos].code == code && lastAlarm[mAlarmNxtWrPos].start == start) {
// replace with same or update end time
if(lastAlarm[mAlarmNxtWrPos].end == 0 || lastAlarm[mAlarmNxtWrPos].end == end) {
break;
}
}
}
if(alarmCnt < 10 && alarmCnt < mAlarmNxtWrPos)
alarmCnt = mAlarmNxtWrPos + 1;
lastAlarm[mAlarmNxtWrPos] = alarm_t(code, start, end);
if(++mAlarmNxtWrPos >= 10) // rolling buffer
mAlarmNxtWrPos = 0;

2
src/network/AhoyWifiEsp32.h

@ -17,7 +17,7 @@ class AhoyWifi : public AhoyNetwork {
void begin() override {
mAp.enable();
if(String(FB_WIFI_SSID) == mConfig->sys.stationSsid)
if(strlen(mConfig->sys.stationSsid) == 0)
return; // no station wifi defined
WiFi.disconnect(); // clean up

70
src/platformio.ini

@ -394,3 +394,73 @@ board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env:opendtufusion-ethernet-zero_export.build_flags}
-DLANG_DE
[env:opendtufusion-16MB]
platform = espressif32@6.6.0
board = esp32-s3-devkitc-1
board_upload.flash_size = 16MB
upload_protocol = esp-builtin
build_flags = ${env:opendtufusion.build_flags}
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-16MB-de]
platform = espressif32@6.6.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env:opendtufusion-16MB.build_flags}
-DLANG_DE
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-16MB-zero_export]
platform = espressif32@6.6.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env:opendtufusion-16MB.build_flags}
-DPLUGIN_ZEROEXPORT
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-16MB-zero_export-de]
platform = espressif32@6.6.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env:opendtufusion-16MB-zero_export.build_flags}
-DLANG_DE
[env:opendtufusion-ethernet-16MB]
platform = espressif32@6.6.0
board = esp32-s3-devkitc-1
board_upload.flash_size = 16MB
upload_protocol = esp-builtin
build_flags = ${env:opendtufusion-ethernet.build_flags}
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-ethernet-16MB-de]
platform = espressif32@6.6.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env:opendtufusion-ethernet-16MB.build_flags}
-DLANG_DE
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-ethernet-16MB-zero_export]
platform = espressif32@6.6.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env:opendtufusion-ethernet-16MB.build_flags}
-DPLUGIN_ZEROEXPORT
monitor_filters =
esp32_exception_decoder, colorize
[env:opendtufusion-ethernet-16MB-zero_export-de]
platform = espressif32@6.6.0
board = esp32-s3-devkitc-1
upload_protocol = esp-builtin
build_flags = ${env:opendtufusion-ethernet-16MB-zero_export.build_flags}
-DLANG_DE

2
src/plugins/MaxPower.h

@ -50,7 +50,7 @@ class MaxPower {
if((mValues[i].first + mMaxDiff) >= *mTs)
val += mValues[i].second;
else if(mValues[i].first > 0)
return mLast; // old data
break; // old data
}
if(val > mLast)
mLast = val;

126
src/plugins/zeroExport/powermeter.h

@ -20,6 +20,7 @@
#include "plugins/zeroExport/lib/sml.h"
#include "utils/DynamicJsonHandler.h"
typedef struct {
const unsigned char OBIS[6];
void (*Fn)(double &);
@ -46,9 +47,10 @@ class powermeter {
* @param *log
* @returns void
*/
bool setup(IApp *app, zeroExport_t *cfg, PubMqttType *mqtt, DynamicJsonHandler *log) {
bool setup(IApp *app, zeroExport_t *cfg, settings_t *config, PubMqttType *mqtt, DynamicJsonHandler *log) {
mApp = app;
mCfg = cfg;
mConfig = config;
mMqtt = mqtt;
mLog = log;
@ -65,7 +67,9 @@ class powermeter {
if (millis() - mPreviousTsp <= 1000) return; // skip when it is to fast
mPreviousTsp = millis();
if (mCfg->debug) DBGPRINTLN(F("pm Takt:"));
#ifdef ZEROEXPORT_DEBUG
if (mCfg->debug) DBGPRINTLN(F("pm Takt:"));
#endif /*ZEROEXPORT_DEBUG*/
bool result = false;
float power = 0.0;
@ -76,7 +80,9 @@ class powermeter {
if ((millis() - mCfg->groups[group].pm_peviousTsp) < ((uint16_t)mCfg->groups[group].pm_refresh * 1000)) continue;
mCfg->groups[group].pm_peviousTsp = millis();
if (mCfg->debug) DBGPRINTLN(F("pm Do:"));
#ifdef ZEROEXPORT_DEBUG
if (mCfg->debug) DBGPRINTLN(F("pm Do:"));
#endif /*ZEROEXPORT_DEBUG*/
result = false;
power = 0.0;
@ -120,11 +126,9 @@ class powermeter {
mCfg->groups[group].power = power;
// MQTT - Powermeter
/// BUG: 002 Anfang - Muss dieser Teil raus? Führt er zu abstürzen wie BUG 001?
if (mMqtt->isConnected()) {
mMqtt->publish(String("zero/state/groups/" + String(group) + "/powermeter/P").c_str(), String(ah::round1(power)).c_str(), false);
}
/// BUG: 002 Ende
}
}
}
@ -203,49 +207,63 @@ class powermeter {
/** onMqttMessage
* This function is needed for all mqtt connections between ahoy and other devices.
*/
void onMqttMessage(JsonObject obj) {
String topic = String(obj["topic"]);
#if defined(ZEROEXPORT_POWERMETER_MQTT)
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
if (!mCfg->groups[group].enabled) continue;
if (!mCfg->groups[group].pm_type == zeroExportPowermeterType_t::Mqtt) continue;
bool onMqttMessage(const char* topic, const uint8_t* payload, size_t len)
{
bool result = false;
if (!strcmp(mCfg->groups[group].pm_src, "")) continue;
#if defined(ZEROEXPORT_POWERMETER_MQTT)
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++)
{
if (!mCfg->groups[group].enabled) continue;
if (!mCfg->groups[group].pm_type == zeroExportPowermeterType_t::Mqtt) continue;
if (!strcmp(mCfg->groups[group].pm_src, "")) continue;
if (strcmp(mCfg->groups[group].pm_src, topic) != 0) continue; // strcmp liefert 0 wenn gleich
float power = 0.0;
String sPayload = String((const char*)payload).substring(0, len);
if (sPayload.startsWith("{") && sPayload.endsWith("}") || sPayload.startsWith("[") && sPayload.endsWith("]"))
{
#ifdef ZEROEXPORT_DEBUG
DPRINTLN(DBG_INFO, String("ze: mqtt powermeter val: ") + sPayload);
#endif /*ZEROEXPORT_DEBUG*/
DynamicJsonDocument datajson(2048); // TODO: JSON größe dynamisch machen?
if(!deserializeJson(datajson, sPayload.c_str()))
{
#ifdef ZEROEXPORT_DEBUG
DPRINTLN(DBG_INFO, String("ze: mqtt powermeter deserialize ok"));
DPRINTLN(DBG_INFO, String(datajson.as<String>()));
#endif /*ZEROEXPORT_DEBUG*/
power = extractJsonKey(datajson, mCfg->groups[group].pm_jsonPath);
}
if (strcmp(mCfg->groups[group].pm_src, String(topic).c_str())) continue;
}
else
{
#ifdef ZEROEXPORT_DEBUG
DPRINTLN(DBG_INFO, String("ze: mqtt powermeter kein json"));
#endif /*ZEROEXPORT_DEBUG*/
power = sPayload.toFloat();
}
float power = 0.0;
bufferWrite(power, group);
mCfg->groups[group].power = power;
DynamicJsonDocument datajson(512);
if (!deserializeJson(datajson, obj["val"]))
{
switch (mCfg->groups[group].pm_target) {
case 0: power = datajson["a_act_power"]; break;
case 1: power = datajson["b_act_power"]; break;
case 2: power = datajson["c_act_power"]; break;
case 3: power = datajson["total_act_power"]; break;
// MQTT - Powermeter
DPRINTLN(DBG_INFO, String("ze: mqtt powermeter ") + String(power));
if (mCfg->debug) {
if (mMqtt->isConnected()) {
mMqtt->publish(String("zero/state/groups/" + String(group) + "/powermeter/P").c_str(), String(ah::round1(power)).c_str(), false);
}
}
} else {
power = (float)obj["val"];
result = true;
}
bufferWrite(power, group);
mCfg->groups[group].power = power; // TODO: join two sites together (PM & MQTT)
// MQTT - Powermeter
/// BUG: 001 Anfang - Dieser Teil ist deaktiviert weil er zu abstürzen der DTU führt
// if (mCfg->debug) {
// if (mMqtt->isConnected()) {
// mMqtt->publish(String("zero/state/groups/" + String(group) + "/powermeter/P").c_str(), String(ah::round1(power)).c_str(), false);
// }
// }
/// BUG: 001 Ende
}
#endif /*defined(ZEROEXPORT_POWERMETER_MQTT)*/
#endif /*defined(ZEROEXPORT_POWERMETER_MQTT)*/
return result;
}
private:
@ -260,6 +278,11 @@ class powermeter {
mMqtt->subscribe(gr.c_str(), QOS_2);
}
/*uint16_t mqttUnsubscribe(const char *subTopic,)
{ TODO: hier weiter?
return mMqtt->unsubscribe(topic); // add as many topics as you like
}*/
/** mqttPublish
* when a MQTT Msg is needed to Publish, but not to subscribe.
* @param gr
@ -274,8 +297,9 @@ class powermeter {
HTTPClient http;
zeroExport_t *mCfg;
settings_t *mConfig = nullptr;
PubMqttType *mMqtt = nullptr;
DynamicJsonHandler* mLog;
DynamicJsonHandler *mLog;
IApp *mApp = nullptr;
unsigned long mPreviousTsp = millis();
@ -291,8 +315,8 @@ class powermeter {
*/
void setHeader(HTTPClient *h, String auth = "", u8_t realm = 0) {
h->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
/// h->setUserAgent("Ahoy-Agent");
/// // TODO: Ahoy-0.8.850024-zero
/// h->setUserAgent("Ahoy-Agent");
/// // TODO: Ahoy-0.8.850024-zero
h->setUserAgent(mApp->getVersion());
h->setConnectTimeout(500);
h->setTimeout(1000);
@ -321,7 +345,6 @@ class powermeter {
*/
/*if (auth != NULL && realm) http.addHeader("WWW-Authenticate", "Digest qop=\"auth\", realm=\"" + shellypro4pm-f008d1d8b8b8 + "\", nonce=\"60dc59c6\", algorithm=SHA-256");
else if (auth != NULL) http.addHeader("Authorization", "Basic " + auth);*/
/*
@ -337,6 +360,20 @@ class powermeter {
*/
}
/**
*
*
*/
float extractJsonKey(DynamicJsonDocument data, const char* key)
{
if (data.containsKey(key))
return (float)data[key];
else {
DPRINTLN(DBG_INFO, String("ze: mqtt powermeter deserialize no key ") + String(key));
return 0.0F;
}
}
#if defined(ZEROEXPORT_POWERMETER_SHELLY)
/** getPowermeterWattsShelly
* ...
@ -577,6 +614,7 @@ class powermeter {
switch (smlCurrentState) {
case SML_FINAL:
*power = _powerMeterTotal;
// TODO: pm_taget auswerten und damit eine Regelung auf Sum, L1, L2, L3 ermöglichen (setup.html nicht vergessen)
result = true;
break;
case SML_LISTEND:

5
src/plugins/zeroExport/powermeter.txt

@ -1,3 +1,8 @@
//TODO:
//- setup.html (Power + Sleep-Spalte Value) refresh einbauen (ticker)
//-----------------------------------------------------------------------------
// 2024 Ahoy, https://ahoydtu.de
// Creative Commons - https://creativecommons.org/licenses/by-nc-sa/4.0/deed

365
src/plugins/zeroExport/zeroExport.h

@ -15,6 +15,9 @@
#include "AsyncJson.h"
#include "powermeter.h"
#include "utils/DynamicJsonHandler.h"
#include "utils/mqttHelper.h"
using namespace mqttHelper;
template <class HMSYSTEM>
@ -48,15 +51,9 @@ class ZeroExport {
mApi = api;
mMqtt = mqtt;
mIsInitialized = mPowermeter.setup(mApp, mCfg, mqtt, &_log);
mIsInitialized = mPowermeter.setup(mApp, mCfg, mConfig, mMqtt, &_log);
}
/*void printJson() {
serializeJson(doc, Serial);
Serial.println();
serializeJsonPretty(doc, Serial);
}*/
/** loop
* Arbeitsschleife
* @param void
@ -75,13 +72,17 @@ class ZeroExport {
if (mLastRun > (Tsp - 1000)) return;
mLastRun = Tsp;
if (mCfg->debug) DBGPRINTLN(F("Takt:"));
#ifdef ZEROEXPORT_DEBUG
if (mCfg->debug) DBGPRINTLN(F("Takt:"));
#endif /*ZEROEXPORT_DEBUG*/
// Exit if Queue is empty
zeroExportQueue_t Queue;
if (!getQueue(&Queue)) return;
if (mCfg->debug) DBGPRINTLN(F("Queue:"));
#ifdef ZEROEXPORT_DEBUG
if (mCfg->debug) DBGPRINTLN(F("Queue:"));
#endif /*ZEROEXPORT_DEBUG*/
// Load Data from Queue
uint8_t group = Queue.group;
@ -641,6 +642,8 @@ class ZeroExport {
* @returns void
*/
void onMqttConnect(void) {
mMqtt->subscribe("zero/ctrl/#", QOS_2);
if (!mCfg->enabled) return;
mPowermeter.onMqttConnect();
@ -662,150 +665,31 @@ class ZeroExport {
* @param
* @returns void
*/
void onMqttMessage(JsonObject obj) {
if (!mIsInitialized) return;
mPowermeter.onMqttMessage(obj);
String topic = String(obj["topic"]);
// "topic":"userdefined battSoCTopic" oder "userdefinedUTopic"
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
if (!mCfg->groups[group].enabled) continue;
if ((!mCfg->groups[group].battCfg == zeroExportBatteryCfg::mqttU) && (!mCfg->groups[group].battCfg == zeroExportBatteryCfg::mqttSoC)) continue;
if (!strcmp(mCfg->groups[group].battTopic, "")) continue;
if (strcmp(mCfg->groups[group].battTopic, String(topic).c_str())) {
mCfg->groups[group].battValue = (bool)obj["val"];
_log.addProperty("k", mCfg->groups[group].battTopic);
_log.addProperty("v", mCfg->groups[group].battValue);
}
}
// "topic":"ctrl/zero"
if (topic.indexOf("ctrl/zero") == -1) return;
_log.addProperty("d", obj);
if (obj["path"] == "ctrl" && obj["cmd"] == "zero") {
int8_t topicGroup = getGroupFromTopic(topic.c_str());
int8_t topicInverter = getInverterFromTopic(topic.c_str());
if (topicGroup != -1) {
_log.addProperty("g", topicGroup);
}
if (topicInverter == -1) {
_log.addProperty("i", topicInverter);
}
_log.addProperty("k", topic);
// "topic":"ctrl/zero/enabled"
if (topic.indexOf("ctrl/zero/enabled") != -1) {
_log.addProperty("v", (bool)obj["val"]);
mCfg->enabled = (bool)obj["val"];
}
// "topic":"ctrl/zero/sleep"
else if (topic.indexOf("ctrl/zero/sleep") != -1) {
_log.addProperty("v", (bool)obj["val"]);
mCfg->sleep = (bool)obj["val"];
}
else if ((topicGroup >= 0) && (topicGroup < ZEROEXPORT_MAX_GROUPS)) {
String stopicGroup = String(topicGroup);
// "topic":"ctrl/zero/groups/+/enabled"
if (topic.endsWith("/enabled")) {
_log.addProperty("v", (bool)obj["val"]);
mCfg->groups[topicGroup].enabled = (bool)obj["val"];
}
// "topic":"ctrl/zero/groups/+/sleep"
else if (topic.endsWith("/sleep")) {
_log.addProperty("v", (bool)obj["val"]);
mCfg->groups[topicGroup].sleep = (bool)obj["val"];
}
// Auf Eis gelegt, dafür 2 Gruppen mehr
// 0.8.103008.2
// // "topic":"ctrl/zero/groups/+/pm_ip"
// if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/pm_ip") != -1) {
// snprintf(mCfg->groups[topicGroup].pm_src, ZEROEXPORT_GROUP_MAX_LEN_PM_SRC, "%s", obj[F("val")].as<const char *>());
/// TODO:
// snprintf(mCfg->groups[topicGroup].pm_src, ZEROEXPORT_GROUP_MAX_LEN_PM_SRC, "%s", obj[F("val")].as<const char *>());
// strncpy(mCfg->groups[topicGroup].pm_src, obj[F("val")], ZEROEXPORT_GROUP_MAX_LEN_PM_SRC);
// strncpy(mCfg->groups[topicGroup].pm_src, String(obj[F("val")]).c_str(), ZEROEXPORT_GROUP_MAX_LEN_PM_SRC);
// snprintf(mCfg->groups[topicGroup].pm_src, ZEROEXPORT_GROUP_MAX_LEN_PM_SRC, "%s", String(obj[F("val")]).c_str());
// mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/pm_ip";
// mLog["v"] = mCfg->groups[topicGroup].pm_src;
// }
//
// // "topic":"ctrl/zero/groups/+/pm_jsonPath"
// if (topic.indexOf("ctrl/zero/groups/" + String(topicGroup) + "/pm_jsonPath") != -1) {
/// TODO:
// snprintf(mCfg->groups[topicGroup].pm_jsonPath, ZEROEXPORT_GROUP_MAX_LEN_PM_JSONPATH, "%s", obj[F("val")].as<const char *>());
// mLog["k"] = "ctrl/zero/groups/" + String(topicGroup) + "/pm_jsonPath";
// mLog["v"] = mCfg->groups[topicGroup].pm_jsonPath;
// }
// "topic":"ctrl/zero/groups/+/battery/switch"
else if (topic.endsWith("/battery/switch")) {
_log.addProperty("v", (bool)obj["val"]);
mCfg->groups[topicGroup].battSwitch = (bool)obj["val"];
}
else if (topic.indexOf("/advanced/") != -1) {
// "topic":"ctrl/zero/groups/+/advanced/setPoint"
if (topic.endsWith("/setPoint")) {
_log.addProperty("v", (int16_t)obj["val"]);
mCfg->groups[topicGroup].setPoint = (int16_t)obj["val"];
}
// "topic":"ctrl/zero/groups/+/advanced/powerTolerance"
else if (topic.endsWith("/powerTolerance")) {
_log.addProperty("v", (uint8_t)obj["val"]);
mCfg->groups[topicGroup].powerTolerance = (uint8_t)obj["val"];
}
// "topic":"ctrl/zero/groups/+/advanced/powerMax"
else if (topic.endsWith("/powerMax")) {
_log.addProperty("v", (uint16_t)obj["val"]);
mCfg->groups[topicGroup].powerMax = (uint16_t)obj["val"];
}
} else if (topic.indexOf("/inverter/") != -1) {
if ((topicInverter >= 0) && (topicInverter < ZEROEXPORT_GROUP_MAX_INVERTERS)) {
// "topic":"ctrl/zero/groups/+/inverter/+/enabled"
if (topic.endsWith("/enabled")) {
_log.addProperty("v", (bool)obj["val"]);
mCfg->groups[topicGroup].inverters[topicInverter].enabled = (bool)obj["val"];
}
// "topic":"ctrl/zero/groups/+/inverter/+/powerMin"
else if (topic.endsWith("/powerMin")) {
_log.addProperty("v", (uint16_t)obj["val"]);
mCfg->groups[topicGroup].inverters[topicInverter].powerMin = (uint16_t)obj["val"];
}
// "topic":"ctrl/zero/groups/+/inverter/+/powerMax"
else if (topic.endsWith("/powerMax")) {
_log.addProperty("v", (uint16_t)obj["val"]);
mCfg->groups[topicGroup].inverters[topicInverter].powerMax = (uint16_t)obj["val"];
} else {
_log.addProperty("k", "error");
}
}
} else {
_log.addProperty("k", "error");
bool onMqttMessage(const char* topic, const uint8_t* payload, size_t len)
{
// Check if ZE is init, when not, directly out of here!
if (!mIsInitialized) return false;
bool result = true;
// FremdTopic "topic":"userdefined power" ist für ZeroExport->Powermeter
if (!mPowermeter.onMqttMessage(topic, payload, len))
{
// FremdTopic "topic":"userdefined battSoCTopic" oder "userdefinedUTopic" ist für ZeroExport(Batterie)
if (!onMqttMessageBattery(topic, payload, len))
{
// LokalerTopic "topic": ???/zero ist für ZeroExport
if (!onMqttMessageZeroExport(topic, payload, len))
{
result = false;
}
}
}
sendLog();
clearLog();
return;
return result;
}
private:
@ -833,42 +717,6 @@ class ZeroExport {
return true;
}
/** getGroupFromTopic
* Extahiert die Gruppe aus dem mqttTopic.
* @param *topic
* @returns group
*/
int8_t getGroupFromTopic(const char *topic) {
const char *pGroupSection = strstr(topic, "groups/");
if (pGroupSection == NULL) return -1;
pGroupSection += 7;
char strGroup[3];
uint8_t digitsCopied = 0;
while (*pGroupSection != '/' && digitsCopied < 2) strGroup[digitsCopied++] = *pGroupSection++;
strGroup[digitsCopied] = '\0';
int8_t group = atoi(strGroup);
_log.addProperty("getGroupFromTopic", "group");
return group;
}
/** getInverterFromTopic
* Extrahiert dden Inverter aus dem mqttTopic
* @param *topic
* @returns inv
*/
int8_t getInverterFromTopic(const char *topic) {
const char *pInverterSection = strstr(topic, "inverters/");
if (pInverterSection == NULL) return -1;
pInverterSection += 10;
char strInverter[3];
uint8_t digitsCopied = 0;
while (*pInverterSection != '/' && digitsCopied < 2) strInverter[digitsCopied++] = *pInverterSection++;
strInverter[digitsCopied] = '\0';
int8_t inverter = atoi(strInverter);
return inverter;
}
/** mqttSubscribe
* when a MQTT Msg is needed to subscribe, then a publish is leading
* @param gr
@ -891,6 +739,154 @@ class ZeroExport {
mMqtt->publish(gr.c_str(), payload.c_str(), retain);
}
/** onMqttMessageBattery
* Subscribe section
* @param
* @returns void
*/
bool onMqttMessageBattery(const char* topic, const uint8_t* payload, size_t len) {
// check if topic is Fremdtopic
String baseTopic = String(mConfig->mqtt.topic);
if (strncmp(topic, baseTopic.c_str(), baseTopic.length()) != 0)
{
for (uint8_t group = 0; group < ZEROEXPORT_MAX_GROUPS; group++) {
if (!mCfg->groups[group].enabled) continue;
if ((!mCfg->groups[group].battCfg == zeroExportBatteryCfg::mqttU) && (!mCfg->groups[group].battCfg == zeroExportBatteryCfg::mqttSoC)) continue;
if (!strcmp(mCfg->groups[group].battTopic, "")) continue;
if (checkIntegerProperty(topic, mCfg->groups[group].battTopic, payload, len, &mCfg->groups[group].battValue, &_log)) return true;
}
}
return false;
}
/** onMqttMessageZeroExport
* Subscribe section
* @param
* @returns true when topic is for this class specified or false when its not fit in here.
*/
bool onMqttMessageZeroExport(const char* topic, const uint8_t* payload, size_t len)
{
// check if topic is for zeroExport
String baseTopic = String(mConfig->mqtt.topic) + String("/zero/ctrl");
if (strncmp(topic, baseTopic.c_str(), baseTopic.length()) == 0)
{
_log.addProperty("k", topic);
const char* p = topic + strlen(baseTopic.c_str());
// "topic":"???/zero/ctrl/enabled"
if (checkBoolProperty(p, "/enabled", payload, len, &mCfg->enabled, &_log)) return true;
// reconnect
// "topic":"???/zero/ctrl/sleep"
if (checkBoolProperty(p, "/sleep", payload, len, &mCfg->sleep, &_log)) return true;
// "topic":"???/zero/ctrl/groups"
if (strncmp(p, "/groups", strlen("/groups")) == 0) {
baseTopic += String("/groups"); // add '/groups'
p = topic + strlen(baseTopic.c_str());
// extract number from topic
int topicGroup = -1;
while (*p) {
if (isdigit(*p)) {
topicGroup = atoi(p);
break;
}
p++;
}
// reset to pointer with offset
p = topic + strlen(baseTopic.c_str());
#ifdef ZEROEXPORT_DEBUG
DBGPRINT(String("groups "));
DBGPRINTLN(String(topicGroup));
#endif /*ZEROEXPORT_DEBUG*/
baseTopic += String("/") + String(topicGroup); // add '/+'
p = topic + strlen(baseTopic.c_str());
// "topic":"???/zero/ctrl/groups/+/enabled"
if (checkBoolProperty(p, "/enabled", payload, len, &mCfg->groups[topicGroup].enabled, &_log)) return true;
// Reconnect
// "topic":"???/zero/ctrl/groups/+/sleep"
if (checkBoolProperty(p, "/sleep", payload, len, &mCfg->groups[topicGroup].sleep, &_log)) return true;
// "topic":"???/zero/ctrl/groups/+/pm_ip"
if (checkCharProperty(p, "/pm_ip", payload, len, mCfg->groups[topicGroup].pm_src, ZEROEXPORT_GROUP_MAX_LEN_PM_SRC, &_log)) return true;
// Reconnect
// "topic":"???/zero/ctrl/groups/+/pm_jsonPath"
if (checkCharProperty(p, "/pm_jsonPath", payload, len, mCfg->groups[topicGroup].pm_jsonPath, ZEROEXPORT_GROUP_MAX_LEN_PM_JSONPATH, &_log)) return true;
// Reconnect
// "topic":"???/zero/ctrl/groups/+/battery/switch"
if (checkBoolProperty(p, "/battery/switch", payload, len, &mCfg->groups[topicGroup].battSwitch, &_log)) return true;
// "topic":"???/zero/ctrl/groups/+/advanced/setPoint"
if (checkIntegerProperty(p, "/advanced/setPoint", payload, len, &mCfg->groups[topicGroup].setPoint, &_log)) return true;
// "topic":"???/zero/ctrl/groups/+/advanced/powerTolerance"
if (checkIntegerProperty(p, "/advanced/powerTolerance", payload, len, &mCfg->groups[topicGroup].powerTolerance, &_log)) return true;
// "topic":"???/zero/ctrl/groups/+/advanced/powerMax"
if (checkIntegerProperty(p, "/advanced/powerMax", payload, len, &mCfg->groups[topicGroup].powerMax, &_log)) return true;
// "topic":"???/zero/ctrl/groups/+/inverter"
if (strncmp(p, "/inverter", strlen("/inverter")) == 0) {
baseTopic += String("/inverter"); // add '/inverter'
p = topic + strlen(baseTopic.c_str());
// extract number from topic
int topicInverter = -1;
while (*p) {
if (isdigit(*p)) {
topicInverter = atoi(p);
break;
}
p++;
}
// reset to pointer with offset
p = topic + strlen(baseTopic.c_str());
#ifdef ZEROEXPORT_DEBUG
DBGPRINT(String("inverter "));
DBGPRINTLN(String(topicInverter));
#endif /*ZEROEXPORT_DEBUG*/
baseTopic += String("/") + String(topicInverter); // add '/+'
p = topic + strlen(baseTopic.c_str());
// "topic":"???/zero/ctrl/groups/+/inverter/+/enabled"
if (checkBoolProperty(p, "/enabled", payload, len, &mCfg->groups[topicGroup].inverters[topicInverter].enabled, &_log)) return true;
// "topic":"???/zero/ctrl/groups/+/inverter/+/powerMin"
if (checkIntegerProperty(p, "/powerMin", payload, len, &mCfg->groups[topicGroup].inverters[topicInverter].powerMin, &_log)) return true;
// "topic":"???/zero/ctrl/groups/+/inverter/+/powerMax"
if (checkIntegerProperty(p, "/powerMax", payload, len, &mCfg->groups[topicGroup].inverters[topicInverter].powerMax, &_log)) return true;
}
return true;
}
}
return false;
}
/** sendLog
* Sendet den LogSpeicher über Webserial und/oder MQTT
*/
@ -915,6 +911,9 @@ class ZeroExport {
_log.clear();
}
// private member variables
bool mIsInitialized = false;

199
src/publisher/pubMqtt.h

@ -11,6 +11,8 @@
#if defined(ENABLE_MQTT)
#ifdef ESP8266
#include <ESP8266WiFi.h>
#define xSemaphoreTake(a, b) { while(a) { yield(); } a = true; }
#define xSemaphoreGive(a) { a = false; }
#elif defined(ESP32)
#include <WiFi.h>
#endif
@ -26,7 +28,7 @@
#include "pubMqttDefs.h"
#include "pubMqttIvData.h"
typedef std::function<void(JsonObject)> subscriptionCb;
typedef std::function<void(const char*, const uint8_t*, size_t, size_t, size_t)> subscriptionCb;
typedef std::function<void(void)> connectionCb;
typedef struct {
@ -40,6 +42,13 @@ template<class HMSYSTEM>
class PubMqtt {
public:
PubMqtt() : SendIvData() {
#if defined(ESP32)
mutex = xSemaphoreCreateBinaryStatic(&mutexBuffer);
xSemaphoreGive(mutex);
#else
mutex = false;
#endif
mLastIvState.fill(InverterStatus::OFF);
mIvLastRTRpub.fill(0);
@ -51,7 +60,11 @@ class PubMqtt {
mSendAlarm.fill(false);
}
~PubMqtt() { }
~PubMqtt() {
#if defined(ESP32)
vSemaphoreDelete(mutex);
#endif
}
void setup(IApp *app, cfgMqtt_t *cfg_mqtt, const char *devName, const char *version, HMSYSTEM *sys, uint32_t *utcTs, uint32_t *uptime) {
mApp = app;
@ -97,6 +110,24 @@ class PubMqtt {
}
void loop() {
std::queue<message_s> queue;
xSemaphoreTake(mutex, portMAX_DELAY);
queue.swap(mReceiveQueue);
xSemaphoreGive(mutex);
while (!queue.empty())
{
#warning "TODO: sTopic und sPayload hier fällen und dann übergeben?";
#warning "Ist es wirklich so, dass die Payload eines Topics aus mehreren Nachrichten bestehen kann und erst zusammengesetzt werden muss? Was ist dann mit der Reihenfolge?";
message_s *entry = &queue.front();
if(NULL != mSubscriptionCb)
{
(mSubscriptionCb)(entry->topic, entry->payload, entry->len, entry->index, entry->total);
mRxCnt++;
}
queue.pop();
}
SendIvData.loop();
#if defined(ESP8266)
@ -220,6 +251,12 @@ class PubMqtt {
mClient.subscribe(topic, qos);
}
// new - need to unsubscribe the topics.
void unsubscribe(const char *subTopic)
{
mClient.unsubscribe(subTopic); // add as many topics as you like
}
void subscribeExtern(const char *subTopic, uint8_t qos = QOS_0) {
char topic[MQTT_TOPIC_LEN + 20];
snprintf(topic, (MQTT_TOPIC_LEN + 20), "%s", subTopic);
@ -272,14 +309,14 @@ class PubMqtt {
tickerMinute();
publish(mLwtTopic.data(), mqttStr[MQTT_STR_LWT_CONN], true, false);
// for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
// snprintf(mVal.data(), mVal.size(), "ctrl/limit/%d", i);
// subscribe(mVal.data(), QOS_2);
// snprintf(mVal.data(), mVal.size(), "ctrl/restart/%d", i);
// subscribe(mVal.data());
// snprintf(mVal.data(), mVal.size(), "ctrl/power/%d", i);
// subscribe(mVal.data());
// }
for(uint8_t i = 0; i < MAX_NUM_INVERTERS; i++) {
snprintf(mVal.data(), mVal.size(), "ctrl/limit/%d", i);
subscribe(mVal.data(), QOS_2);
snprintf(mVal.data(), mVal.size(), "ctrl/restart/%d", i);
subscribe(mVal.data());
snprintf(mVal.data(), mVal.size(), "ctrl/power/%d", i);
subscribe(mVal.data());
}
snprintf(mVal.data(), mVal.size(), "ctrl/#");
subscribe(mVal.data(), QOS_2);
subscribe(subscr[MQTT_SUBS_SET_TIME]);
@ -315,69 +352,24 @@ class PubMqtt {
}
}
void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) {
if(len == 0)
return;
DPRINT(DBG_INFO, mqttStr[MQTT_STR_GOT_TOPIC]);
DBGPRINTLN(String(topic));
if(NULL == mSubscriptionCb)
return;
DynamicJsonDocument json(128);
JsonObject root = json.to<JsonObject>();
root["topic"] = String(topic);
bool limitAbs = false;
if(len > 0) {
char *pyld = new char[len + 1];
memcpy(pyld, payload, len);
pyld[len] = '\0';
if(NULL == strstr(topic, "limit"))
root[F("val")] = atoi(pyld);
else
root[F("val")] = atof(pyld);
if(pyld[len-1] == 'W')
limitAbs = true;
delete[] pyld;
}
void onMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
#warning "TODO: if aktivieren nach Logprüfung. Was bedeutet index und total?";
// if (total != 1) {
// DPRINTLN(DBG_ERROR, String("pubMqtt.h: onMessage ERROR: index=") + String(index) + String(" total=") + String(total));
// return;
// }
const char *p = topic + strlen(mCfgMqtt->topic);
uint8_t pos = 0, elm = 0;
char tmp[30];
while(1) {
if(('/' == p[pos]) || ('\0' == p[pos])) {
memcpy(tmp, p, pos);
tmp[pos] = '\0';
switch(elm++) {
case 1: root[F("path")] = 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;
default: break;
}
if('\0' == p[pos])
break;
p = p + pos + 1;
pos = 0;
}
pos++;
if (len == 0) {
DPRINT(DBG_INFO, String("MQTT-topic: "));
DPRINT(DBG_INFO, String(topic));
DPRINTLN(DBG_INFO, String(" is empty."));
return;
}
/*char out[128];
serializeJson(root, out, 128);
DPRINTLN(DBG_INFO, "json: " + String(out));*/
(mSubscriptionCb)(root);
mRxCnt++;
xSemaphoreTake(mutex, portMAX_DELAY);
mReceiveQueue.push(message_s(topic, payload, len, index, total));
xSemaphoreGive(mutex);
}
void discoveryConfigLoop(void) {
@ -631,12 +623,77 @@ class PubMqtt {
private:
enum {MQTT_STATUS_OFFLINE = 0, MQTT_STATUS_PARTIAL, MQTT_STATUS_ONLINE};
struct message_s
{
char *topic;
uint8_t *payload;
size_t len;
size_t index;
size_t total;
message_s()
: topic { nullptr }
, payload { nullptr }
, len { 0 }
, index { 0 }
, total { 0 }
{}
message_s(const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
{
uint8_t topic_len = strlen(topic) + 1;
this->topic = new char[topic_len];
this->payload = new uint8_t[len];
memcpy(this->topic, topic, topic_len);
memcpy(this->payload, payload, len);
this->len = len;
this->index = index;
this->total = total;
}
message_s(const message_s &) = delete;
message_s(message_s && other) : message_s {}
{
this->swap( other );
}
~message_s()
{
delete[] this->topic;
delete[] this->payload;
}
message_s &operator = (const message_s &) = delete;
message_s &operator = (message_s &&other)
{
this->swap(other);
return *this;
}
void swap(message_s &other)
{
std::swap(this->topic, other.topic);
std::swap(this->payload, other.payload);
std::swap(this->len, other.len);
std::swap(this->index, other.index);
std::swap(this->total, other.total);
}
};
private:
espMqttClient mClient;
cfgMqtt_t *mCfgMqtt = nullptr;
IApp *mApp;
#if defined(ESP8266)
WiFiEventHandler mHWifiCon, mHWifiDiscon;
volatile bool mutex;
#else
SemaphoreHandle_t mutex;
StaticSemaphore_t mutexBuffer;
#endif
HMSYSTEM *mSys = nullptr;
@ -653,6 +710,8 @@ class PubMqtt {
std::array<uint32_t, MAX_NUM_INVERTERS> mIvLastRTRpub;
uint16_t mIntervalTimeout = 0;
std::queue<message_s> mReceiveQueue;
// last will topic and payload must be available through lifetime of 'espMqttClient'
std::array<char, (MQTT_TOPIC_LEN + 5)> mLwtTopic;
const char *mDevName = nullptr, *mVersion = nullptr;

13
src/publisher/pubMqttIvData.h

@ -197,13 +197,20 @@ class PubMqttIvData {
if (!mCfg->json) {
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d/%s", mIv->config->name, rec->assign[mPos].ch, fields[rec->assign[mPos].fieldId]);
snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec)));
} else {
if (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) {
uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0;
snprintf(mSubTopic.data(), mSubTopic.size(), "%s/%s", mIv->config->name, fields[rec->assign[mPos].fieldId]);
snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mIv->getValue(mPos, rec)));
mPublish(mSubTopic.data(), mVal.data(), retained, qos);
}
}
}
if ((InverterDevInform_All == mCmd) || (InverterDevInform_Simple == mCmd) || !mCfg->json) {
if ((InverterDevInform_All == mCmd) || (InverterDevInform_Simple == mCmd) || !mCfg->json)
{
uint8_t qos = (FLD_ACT_ACTIVE_PWR_LIMIT == rec->assign[mPos].fieldId) ? QOS_2 : QOS_0;
if((FLD_EVT != rec->assign[mPos].fieldId)
&& (FLD_LAST_ALARM_CODE != rec->assign[mPos].fieldId))
if((FLD_EVT != rec->assign[mPos].fieldId) && (FLD_LAST_ALARM_CODE != rec->assign[mPos].fieldId))
mPublish(mSubTopic.data(), mVal.data(), retained, qos);
}
}

101
src/utils/mqttHelper.cpp

@ -0,0 +1,101 @@
#include "mqttHelper.h"
namespace mqttHelper {
/** checkBoolProperty
* TODO: modify header
* @param tmpTopic
* @param subTopic
* @param payload
* @param len
* @param cfg
* @param log
* @returns bool
*/
bool checkCharProperty(const char *tmpTopic, const char *subTopic, const uint8_t *payload, size_t len, char *cfg, int cfgSize, DynamicJsonHandler *log) {
// Check if tmpTopic starts with subTopic
if (strncmp(tmpTopic, subTopic, strlen(subTopic)) == 0) {
// Convert payload to a String instance
String sPayload = String((const char*)payload).substring(0, len);
// Copy the String into cfg and ensure cfg is null-terminated
strncpy(cfg, sPayload.c_str(), cfgSize - 1);
cfg[cfgSize - 1] = '\0'; // Ensure cfg is null-terminated
// Add the property to the log
log->addProperty("v", cfg);
return true; // Successfully processed the property
}
return false; // Did not process the property
}
bool checkBoolProperty(const char *tmpTopic, const char *subTopic, const uint8_t *payload, size_t len, bool *cfg, DynamicJsonHandler *log) {
if (strncmp(tmpTopic, subTopic, strlen(subTopic)) == 0) {
String sPayload = String((const char*)payload).substring(0, len);
if (sPayload == "1" || sPayload == "true") {
*cfg = true;
log->addProperty("v", *cfg);
} else if (sPayload == "0" || sPayload == "false") {
*cfg = false;
log->addProperty("v", *cfg);
} else {
DBGPRINTLN(F("Payload is not a valid boolean value"));
log->addProperty("v", "payload error");
}
return true;
}
return false;
}
}
// --- END Namespace ---
/** checkProperty
*
*/
/*
// final function what "all" combines other function into one
template <typename T>
bool checkProperty(const char *tmpTopic, const char *subTopic, const uint8_t *payload, size_t len, T *cfg, DynamicJsonHandler *log) {
if (strncmp(tmpTopic, subTopic, strlen(subTopic)) == 0) {
String sPayload = String((const char*)payload).substring(0, len);
// Überprüfung und Zuweisung je nach Typ T
if constexpr (std::is_integral_v<T>) {
T value;
sscanf(sPayload.c_str(), "%lld", &value); // Beachte "%lld" für int64_t und uint64_t
if (value <= std::numeric_limits<T>::max() && value >= std::numeric_limits<T>::min()) {
*cfg = value;
} else {
log->addProperty("v", F("Fehler: Der Wert passt nicht in den Ziel-Typ T"));
return false;
}
} else if constexpr (std::is_same_v<T, bool>) {
*cfg = (sPayload == "1" || sPayload == "true");
if (*cfg) {
log->addProperty("v", "true");
} else {
log->addProperty("v", "false");
}
} else if constexpr (std::is_same_v<T, char*> || std::is_same_v<T, const char*>) {
strncpy(*cfg, sPayload.c_str(), cfgSize - 1);
(*cfg)[cfgSize - 1] = '\0';
} else {
// Handle andere Datentypen
return false;
}
// Füge die Eigenschaft zum Log hinzu
log->addProperty("v", sPayload.c_str());
return true;
}
return false;
}
*/

46
src/utils/mqttHelper.h

@ -0,0 +1,46 @@
#ifndef __MQTT_HELPER_H__
#define __MQTT_HELPER_H__
#include <Arduino.h>
#include <cstdint>
#include <cstring>
#include <stdio.h>
#include <stdlib.h>
#include "DynamicJsonHandler.h"
#include "helper.h"
#include <limits>
namespace mqttHelper {
template <typename T>
bool checkIntegerProperty(const char *tmpTopic, const char *subTopic, const uint8_t *payload, size_t len, T *cfg, DynamicJsonHandler *log) {
if (strncmp(tmpTopic, subTopic, strlen(subTopic)) == 0) {
// Konvertiere payload in einen String
String sPayload = String((const char*)payload).substring(0, len);
// Konvertiere den String in den gewünschten Integer-Typ T
T value;
sscanf(sPayload.c_str(), "%d", &value); // Beispielhaft für int, anpassen je nach T
// Überprüfung, ob der Wert in den Ziel-Typ T passt
if (sPayload.toInt() <= std::numeric_limits<T>::max() && sPayload.toInt() >= std::numeric_limits<T>::min()) {
// Weise den Wert cfg zu
*cfg = value;
// Füge die Eigenschaft zum Log hinzu
log->addProperty("v", ah::round1(*cfg));
return true;
} else {
// Handle den Fall, wenn der Wert außerhalb des gültigen Bereichs liegt
log->addProperty("v", F("Fehler: Der Wert passt nicht in den Ziel-Typ T"));
return false;
}
}
return false;
}
bool checkCharProperty(const char *tmpTopic, const char *subTopic, const uint8_t *payload, size_t len, char *cfg, int cfgSize, DynamicJsonHandler *log);
bool checkBoolProperty(const char *tmpTopic, const char *subTopic, const uint8_t *payload, size_t len, bool *cfg, DynamicJsonHandler *log);
}
#endif /*__MQTT_HELPER_H__*/

64
src/web/RestApi.h

@ -46,6 +46,7 @@ class RestApi {
mRadioCmt = (CmtRadio<>*)mApp->getRadioObj(false);
#endif
mConfig = config;
#if defined(ENABLE_HISTORY_LOAD_DATA)
mSrv->on("/api/addYDHist",
HTTP_POST, std::bind(&RestApi::onApiPost, this, std::placeholders::_1),
@ -65,14 +66,14 @@ class RestApi {
return mTimezoneOffset;
}
void ctrlRequest(JsonObject obj) {
DynamicJsonDocument json(128);
JsonObject dummy = json.as<JsonObject>();
if(obj[F("path")] == "ctrl")
setCtrl(obj, dummy, "*");
else if(obj[F("path")] == "setup")
setSetup(obj, dummy, "*");
}
// void ctrlRequest(const char* topic, const uint8_t* payload, size_t len) {
// DynamicJsonDocument json(128);
// JsonObject dummy = json.as<JsonObject>();
// if(obj[F("path")] == "ctrl")
// setCtrl(obj, dummy, "*");
// else if(obj[F("path")] == "setup")
// setSetup(obj, dummy, "*");
// }
private:
void onApi(AsyncWebServerRequest *request) {
@ -84,7 +85,11 @@ class RestApi {
mHeapFrag = ESP.getHeapFragmentation();
#endif
#if defined(ESP32)
AsyncJsonResponse* response = new AsyncJsonResponse(false, 10000);
#else
AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000);
#endif
JsonObject root = response->getRoot();
String path = request->url().substring(5);
@ -880,6 +885,7 @@ class RestApi {
// General
objGroup[F("id")] = (uint8_t)group;
objGroup[F("enabled")] = (bool)mConfig->plugin.zeroExport.groups[group].enabled;
objGroup[F("sleep")] = (bool)mConfig->plugin.zeroExport.groups[group].sleep;
objGroup[F("name")] = String(mConfig->plugin.zeroExport.groups[group].name);
// Powermeter
objGroup[F("pm_refresh")] = (uint8_t)mConfig->plugin.zeroExport.groups[group].pm_refresh;
@ -1198,7 +1204,25 @@ class RestApi {
// Powermeter
mConfig->plugin.zeroExport.groups[group].pm_refresh = jsonIn[F("pm_refresh")];
mConfig->plugin.zeroExport.groups[group].pm_type = jsonIn[F("pm_type")];
snprintf(mConfig->plugin.zeroExport.groups[group].pm_src, ZEROEXPORT_GROUP_MAX_LEN_PM_SRC, "%s", jsonIn[F("pm_src")].as<const char*>());
// pm_src
const char *neu = jsonIn[F("pm_src")].as<const char*>();
if (strncmp(mConfig->plugin.zeroExport.groups[group].pm_src, neu, strlen(neu)) != 0) {
// unsubscribe
if(mConfig->plugin.zeroExport.groups[group].pm_type == zeroExportPowermeterType_t::Mqtt)
{
mApp->unsubscribe(mConfig->plugin.zeroExport.groups[group].pm_src);
}
// save
snprintf(mConfig->plugin.zeroExport.groups[group].pm_src, ZEROEXPORT_GROUP_MAX_LEN_PM_SRC, "%s", jsonIn[F("pm_src")].as<const char*>());
// subsrcribe
if(mConfig->plugin.zeroExport.groups[group].pm_type == zeroExportPowermeterType_t::Mqtt)
{
mApp->subscribe(mConfig->plugin.zeroExport.groups[group].pm_src, QOS_2);
}
}
snprintf(mConfig->plugin.zeroExport.groups[group].pm_jsonPath, ZEROEXPORT_GROUP_MAX_LEN_PM_JSONPATH, "%s", jsonIn[F("pm_jsonPath")].as<const char*>());
@ -1220,7 +1244,27 @@ class RestApi {
}
// Battery
mConfig->plugin.zeroExport.groups[group].battCfg = jsonIn[F("battCfg")];
snprintf(mConfig->plugin.zeroExport.groups[group].battTopic, ZEROEXPORT_GROUP_MAX_LEN_BATT_TOPIC, "%s", jsonIn[F("battTopic")].as<const char*>());
// battTopic
const char *battneu = jsonIn[F("battTopic")].as<const char*>();
if (strncmp(mConfig->plugin.zeroExport.groups[group].battTopic, battneu, strlen(battneu)) != 0) {
// unsubscribe
if(mConfig->plugin.zeroExport.groups[group].pm_type == zeroExportBatteryCfg::mqttSoC ||
mConfig->plugin.zeroExport.groups[group].pm_type == zeroExportBatteryCfg::mqttU )
{
mApp->unsubscribe(mConfig->plugin.zeroExport.groups[group].battTopic);
}
// save
snprintf(mConfig->plugin.zeroExport.groups[group].battTopic, ZEROEXPORT_GROUP_MAX_LEN_BATT_TOPIC, "%s", jsonIn[F("battTopic")].as<const char*>());
// subsrcribe
if(mConfig->plugin.zeroExport.groups[group].pm_type == zeroExportBatteryCfg::mqttSoC ||
mConfig->plugin.zeroExport.groups[group].pm_type == zeroExportBatteryCfg::mqttU)
{
mApp->subscribe(mConfig->plugin.zeroExport.groups[group].battTopic, QOS_2);
}
}
mConfig->plugin.zeroExport.groups[group].battLimitOn = jsonIn[F("battLimitOn")];
mConfig->plugin.zeroExport.groups[group].battLimitOff = jsonIn[F("battLimitOff")];
// Advanced

1
src/web/html/grid_info.json

@ -10,6 +10,7 @@
{"0x0908": "France_VFR2014"},
{"0x0a00": "DE NF_EN_50549-1:2019"},
{"0x0c00": "AT_TOR_Erzeuger_default"},
{"0x0c03": "AT_TOR_Erzeuger_cosphi=1"},
{"0x0c04": "AT_TOR_Erzeuger_default"},
{"0x0d00": "FR_VFR2019"},
{"0x0d04": "NF_EN_50549-1:2019"},

18
src/web/html/index.html

@ -116,6 +116,8 @@
var p = div(["none"]);
var total = 0;
var count = 0;
var mobile = window.screen.width < 470;
for(var i of obj) {
var icon = iconSuccess;
var cl = "icon-success";
@ -131,7 +133,8 @@
} else if(0 == i["ts_last_success"]) {
avail = "{#AVAIL_NO_DATA}";
} else {
avail = "{#AVAIL} ";
if (!mobile)
avail = "{#AVAIL} ";
if(false == i["is_producing"])
avail += "{#NOT_PRODUCING}";
else {
@ -142,11 +145,16 @@
}
}
var text;
if (mobile)
text = "#";
else
text = "{#INVERTER} #";
p.append(
svg(icon, 30, 30, "icon " + cl),
span("{#INVERTER} #" + i["id"] + ": " + i["name"] + " {#IS} " + avail),
br()
);
svg(icon, 30, 30, "icon " + cl),
span(text + i["id"] + ": " + i["name"] + " {#IS} " + avail),
br()
);
if(false == i["is_avail"]) {
if(i["ts_last_success"] > 0) {

37
src/web/html/setup.html

@ -903,11 +903,16 @@
ser.dispatchEvent(new Event('change'));
function ivSave() {
var o = new Object();
var o = {}
o.cmd = "save_iv"
o.token = "*"
o.id = obj.id
o.ser = parseInt(document.getElementsByName("ser")[0].value, 16);
let sn = document.getElementsByName("ser")[0].value
if(sn[0] == 'A')
sn = convHerf(sn)
o.ser = parseInt(sn, 16)
o.name = document.getElementsByName("name")[0].value;
o.en = document.getElementsByName("enable")[0].checked;
o.ch = [];
@ -927,6 +932,30 @@
getAjax("/api/setup", cb, "POST", JSON.stringify(o));
}
function convHerf(sn) {
let sn_int = 0n;
const CHARS = "0123456789ABCDEFGHJKLMNPRSTUVWXY";
for (let i = 0; i < 9; ++i) {
const pos = CHARS.indexOf(sn[i])
const shift = 42 - 5 * i - (i <= 2 ? 0 : 2)
sn_int |= BigInt(pos) << BigInt(shift)
}
let first4Hex = (sn_int >> 32n) & 0xFFFFn
if (first4Hex === 0x2841n)
first4Hex = 0x1121n
else if (first4Hex === 0x2821n)
first4Hex = 0x1141n
else if (first4Hex === 0x2801n)
first4Hex = 0x1161n
sn_int = (sn_int & ~(0xFFFFn << 32n)) | (first4Hex << 32n);
return sn_int.toString(16)
}
function cb(obj2) {
var e = document.getElementById("res");
if(!obj2.success)
@ -948,6 +977,7 @@
function del() {
var o = new Object();
o.cmd = "save_iv";
o.token = "*"
o.id = obj.id;
o.ser = 0;
o.name = "";
@ -1643,7 +1673,6 @@
else if(value == "Mqtt") {
divsToHide.childNodes[1].style.display = 'none';
divsToHide.childNodes[2].style.display = 'none';
divsToHide.childNodes[4].style.display = 'none';
divsToHide.childNodes[5].style.display = 'none';
divsToHide.childNodes[6].style.display = 'none';
}
@ -1807,6 +1836,7 @@
lines.push(ml("tr", {}, [
ml("th", {style: "width: 10%; text-align: center;"}, "{#ZE_GROUP_ENABLED}"),
ml("th", {style: "width: 10%; text-align: center;"}, "{#ZE_GROUP_MODE}"),
ml("th", {style: "width: 10%; text-align: center;"}, "{#ZE_GROUP_ID}"),
ml("th", {style: "text-align: center;"}, "{#ZE_GROUP_NAME}"),
ml("th", {style: "width: 10%; text-align: center;"}, "{#ZE_GROUP_POWERTOTAL}"),
@ -1817,6 +1847,7 @@
for(let group = 0; group < obj.groups.length; group++) {
lines.push(ml("tr", {}, [
ml("td", {style: "text-align: left;", }, badge(obj.groups[group].enabled, (obj.groups[group].enabled) ? "{#ENABLED}" : "{#DISABLED}")),
ml("td", {style: "text-align: left;", }, badge(!obj.groups[group].sleep, (obj.groups[group].sleep) ? "{#ZE_MODE_SLEEP}" : "{#ZE_MODE_NORMAL}" , "warning")),
ml("td", {style: "text-align: center;", }, String(obj.groups[group].id)),
ml("td", {style: "text-align: left;", }, String(obj.groups[group].name)),
// ml("td", {style: "text-align: right;", id: "groupPowerTotal"+group}, "n/a"),

2
src/web/html/visualization.html

@ -278,7 +278,7 @@
ml("div", {class: "col mt-3"}, String(a.str)),
ml("div", {class: "col mt-3"}, String(a.code)),
ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.start + offs) * 1000)))),
ml("div", {class: "col mt-3"}, String(toIsoTimeStr(new Date((a.end + offs) * 1000))))
ml("div", {class: "col mt-3"}, (a.end == 0) ? "-" : String(toIsoTimeStr(new Date((a.end + offs) * 1000))))
])
);
}

15
src/web/lang.json

@ -843,6 +843,21 @@
"en": "State:",
"de": "Status:"
},
{
"token": "ZE_GROUP_MODE",
"en": "Mode:",
"de": "Modus:"
},
{
"token": "ZE_MODE_SLEEP",
"en": "sleep",
"de": "Standby"
},
{
"token": "ZE_MODE_NORMAL",
"en": "normal",
"de": "Normal"
},
{
"token": "ZE_GROUP_ID",
"en": "Group:",

Loading…
Cancel
Save