diff --git a/src/config/settings.h b/src/config/settings.h index 8f20ba8f..016af2e3 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -151,6 +151,7 @@ typedef struct { char user[MQTT_USER_LEN]; char pwd[MQTT_PWD_LEN]; char topic[MQTT_TOPIC_LEN]; + bool json; uint16_t interval; } cfgMqtt_t; @@ -477,6 +478,7 @@ class settings { snprintf(mCfg.mqtt.pwd, MQTT_PWD_LEN, "%s", DEF_MQTT_PWD); snprintf(mCfg.mqtt.topic, MQTT_TOPIC_LEN, "%s", DEF_MQTT_TOPIC); mCfg.mqtt.interval = 0; // off + mCfg.mqtt.json = 0; // off mCfg.inst.sendInterval = SEND_INTERVAL; mCfg.inst.rstYieldMidNight = false; @@ -732,11 +734,13 @@ class settings { obj[F("user")] = mCfg.mqtt.user; obj[F("pwd")] = mCfg.mqtt.pwd; obj[F("topic")] = mCfg.mqtt.topic; + obj[F("json")] = mCfg.mqtt.json; obj[F("intvl")] = mCfg.mqtt.interval; } else { getVal(obj, F("port"), &mCfg.mqtt.port); getVal(obj, F("intvl"), &mCfg.mqtt.interval); + getVal(obj, F("json"), &mCfg.mqtt.json); getChar(obj, F("broker"), mCfg.mqtt.broker, MQTT_ADDR_LEN); getChar(obj, F("user"), mCfg.mqtt.user, MQTT_USER_LEN); getChar(obj, F("clientId"), mCfg.mqtt.clientId, MQTT_CLIENTID_LEN); diff --git a/src/publisher/pubMqtt.h b/src/publisher/pubMqtt.h index 2d393b8b..14445a2b 100644 --- a/src/publisher/pubMqtt.h +++ b/src/publisher/pubMqtt.h @@ -62,7 +62,7 @@ class PubMqtt { mUptime = uptime; mIntervalTimeout = 1; - SendIvData.setup(sys, utcTs, &mSendList); + SendIvData.setup(sys, cfg_mqtt->json, utcTs, &mSendList); SendIvData.setPublishFunc([this](const char *subTopic, const char *payload, bool retained, uint8_t qos) { publish(subTopic, payload, retained, true, qos); }); diff --git a/src/publisher/pubMqttIvData.h b/src/publisher/pubMqttIvData.h index 6ddd63a9..6ac59cca 100644 --- a/src/publisher/pubMqttIvData.h +++ b/src/publisher/pubMqttIvData.h @@ -24,8 +24,9 @@ class PubMqttIvData { public: PubMqttIvData() : mTotal{}, mSubTopic{}, mVal{} {} - void setup(HMSYSTEM *sys, uint32_t *utcTs, std::queue *sendList) { + void setup(HMSYSTEM *sys, bool json, uint32_t *utcTs, std::queue *sendList) { mSys = sys; + mJson = json; mUtcTimestamp = utcTs; mSendList = sendList; mState = IDLE; @@ -197,18 +198,43 @@ class PubMqttIvData { static_cast(mIv->getChannelFieldValue(CH0, FLD_GRID_PROFILE_VERSION, rec))); retained = true; } else { - 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))); + if (!mJson) { + 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))); + } } - 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)) - mPublish(mSubTopic.data(), mVal.data(), retained, qos); + if (InverterDevInform_All == mCmd || InverterDevInform_Simple == mCmd || !mJson) { + 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)) + mPublish(mSubTopic.data(), mVal.data(), retained, qos); + } } mPos++; } else { if (MqttSentStatus::LAST_SUCCESS_SENT == rec->mqttSentStatus) { + if (mJson) { + DynamicJsonDocument doc(300); + std::array buf; + int ch = rec->assign[0].ch; + + for (mPos = 0; mPos <= rec->length; mPos++) { + if (rec->assign[mPos].ch != ch) { + // if next channel.. publish + serializeJson(doc, buf.data(), buf.size()); + doc.clear(); + snprintf(mSubTopic.data(), mSubTopic.size(), "%s/ch%d", mIv->config->name, ch); + mPublish(mSubTopic.data(), buf.data(), false, QOS_0); + ch = rec->assign[mPos].ch; + } + if (mPos == rec->length) + break; + + doc[fields[rec->assign[mPos].fieldId]] = ah::round3(mIv->getValue(mPos, rec)); + } + } + sendRadioStat(rec->length); rec->mqttSentStatus = MqttSentStatus::DATA_SENT; } @@ -265,11 +291,27 @@ class PubMqttIvData { retained = false; break; } - snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[fieldId]); - snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mTotal[mPos])); - mPublish(mSubTopic.data(), mVal.data(), retained, QOS_0); + if (!mJson) { + snprintf(mSubTopic.data(), mSubTopic.size(), "total/%s", fields[fieldId]); + snprintf(mVal.data(), mVal.size(), "%g", ah::round3(mTotal[mPos])); + mPublish(mSubTopic.data(), mVal.data(), retained, QOS_0); + } mPos++; } else { + if (mJson) { + int type[5] = {FLD_PAC, FLD_YT, FLD_YD, FLD_PDC, FLD_MP}; + snprintf(mVal.data(), mVal.size(), "{"); + + for (mPos = 0; mPos < 5; mPos++) { + snprintf(mSubTopic.data(), mSubTopic.size(), "\"%s\":%g", fields[type[mPos]], ah::round3(mTotal[mPos])); + strcat(mVal.data(), mSubTopic.data()); + if (mPos < 4) + strcat(mVal.data(), ","); + else + strcat(mVal.data(), "}"); + } + mPublish(F("total"), mVal.data(), true, QOS_0); + } mSendList->pop(); mSendTotals = false; mState = IDLE; @@ -294,6 +336,7 @@ class PubMqttIvData { std::array mSubTopic; std::array mVal; + bool mJson; std::queue *mSendList = nullptr; }; diff --git a/src/web/RestApi.h b/src/web/RestApi.h index 8087883e..31104af0 100644 --- a/src/web/RestApi.h +++ b/src/web/RestApi.h @@ -80,7 +80,7 @@ class RestApi { mHeapFrag = ESP.getHeapFragmentation(); #endif - AsyncJsonResponse* response = new AsyncJsonResponse(false, 6000); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 8000); JsonObject root = response->getRoot(); String path = request->url().substring(5); @@ -709,6 +709,7 @@ class RestApi { obj[F("user")] = String(mConfig->mqtt.user); obj[F("pwd")] = (strlen(mConfig->mqtt.pwd) > 0) ? F("{PWD}") : String(""); obj[F("topic")] = String(mConfig->mqtt.topic); + obj[F("json")] = (bool) mConfig->mqtt.json; obj[F("interval")] = String(mConfig->mqtt.interval); } diff --git a/src/web/html/setup.html b/src/web/html/setup.html index fb1eff97..0e248c08 100644 --- a/src/web/html/setup.html +++ b/src/web/html/setup.html @@ -242,6 +242,10 @@
Topic
+
+
{#MQTT_JSON}
+
+

{#MQTT_NOTE}

{#INTERVAL}
@@ -931,6 +935,7 @@ function parseMqtt(obj) { for(var i of [["Addr", "broker"], ["Port", "port"], ["ClientId", "clientId"], ["User", "user"], ["Pwd", "pwd"], ["Topic", "topic"], ["Interval", "interval"]]) document.getElementsByName("mqtt"+i[0])[0].value = obj[i[1]]; + document.getElementsByName("mqttJson")[0].checked = obj["json"]; } function parseNtp(obj) { diff --git a/src/web/lang.json b/src/web/lang.json index ee942608..70ca2015 100644 --- a/src/web/lang.json +++ b/src/web/lang.json @@ -418,6 +418,11 @@ "en": "Password (optional)", "de": "Passwort (optional)" }, + { + "token": "MQTT_JSON", + "en": "Payload as JSON", + "de": "Ausgabe als JSON" + }, { "token": "MQTT_NOTE", "en": "Send Inverter data in a fixed interval, even if there is no change. A value of '0' disables the fixed interval. The data is published once it was successfully received from inverter. (default: 0)", diff --git a/src/web/web.h b/src/web/web.h index 419d6826..bd806b84 100644 --- a/src/web/web.h +++ b/src/web/web.h @@ -574,6 +574,7 @@ class Web { if (request->arg("mqttPwd") != "{PWD}") request->arg("mqttPwd").toCharArray(mConfig->mqtt.pwd, MQTT_PWD_LEN); request->arg("mqttTopic").toCharArray(mConfig->mqtt.topic, MQTT_TOPIC_LEN); + mConfig->mqtt.json = (request->arg("mqttJson") == "on"); mConfig->mqtt.port = request->arg("mqttPort").toInt(); mConfig->mqtt.interval = request->arg("mqttInterval").toInt(); diff --git a/tools/NodeRED/flows-mqtt-json-example.json b/tools/NodeRED/flows-mqtt-json-example.json new file mode 100644 index 00000000..5e2e09a1 --- /dev/null +++ b/tools/NodeRED/flows-mqtt-json-example.json @@ -0,0 +1,466 @@ +[ + { + "id": "67bced2c4e728783", + "type": "mqtt in", + "z": "5de5756d190f9086", + "name": "", + "topic": "hoymiles/+", + "qos": "0", + "datatype": "auto-detect", + "broker": "319864a4e0fd913f", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 80, + "y": 2100, + "wires": [ + [ + "a55632ad0dff0b69" + ] + ] + }, + { + "id": "a7f0d307d7cf77e2", + "type": "mqtt in", + "z": "5de5756d190f9086", + "name": "", + "topic": "hoymiles/X/#", + "qos": "0", + "datatype": "auto-detect", + "broker": "319864a4e0fd913f", + "nl": false, + "rap": true, + "rh": 0, + "inputs": 0, + "x": 90, + "y": 2260, + "wires": [ + [ + "7e17e5a3f4df3011", + "1a8cca488d53394a" + ] + ] + }, + { + "id": "7e17e5a3f4df3011", + "type": "debug", + "z": "5de5756d190f9086", + "name": "Inverter X", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 340, + "y": 2260, + "wires": [] + }, + { + "id": "fb7357db50501627", + "type": "change", + "z": "5de5756d190f9086", + "name": "Tags setzen", + "rules": [ + { + "t": "set", + "p": "payload", + "pt": "msg", + "to": "(\t $a := $split(topic, '/');\t [\t payload,\t {\t \"device\":$a[0],\t \"name\":$a[1],\t \"channel\":$a[2]\t }\t ]\t)\t", + "tot": "jsonata" + }, + { + "t": "delete", + "p": "topic", + "pt": "msg" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 610, + "y": 2360, + "wires": [ + [ + "91a4607dfda84b67" + ] + ] + }, + { + "id": "670eb9fbb5c31b2c", + "type": "debug", + "z": "5de5756d190f9086", + "name": "InfluxDB", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 940, + "y": 2360, + "wires": [] + }, + { + "id": "1a8cca488d53394a", + "type": "switch", + "z": "5de5756d190f9086", + "name": "", + "property": "$split(topic, '/')[2]", + "propertyType": "jsonata", + "rules": [ + { + "t": "eq", + "v": "available", + "vt": "str" + }, + { + "t": "eq", + "v": "last_success", + "vt": "str" + }, + { + "t": "regex", + "v": "(ch[0-6])\\b", + "vt": "str", + "case": false + }, + { + "t": "eq", + "v": "radio_stat", + "vt": "str" + }, + { + "t": "eq", + "v": "firmware", + "vt": "str" + }, + { + "t": "eq", + "v": "hardware", + "vt": "str" + }, + { + "t": "eq", + "v": "alarm", + "vt": "str" + } + ], + "checkall": "true", + "repair": false, + "outputs": 7, + "x": 330, + "y": 2380, + "wires": [ + [ + "845aeb93e39092c5" + ], + [ + "241a8e70e9fde93c" + ], + [ + "fb7357db50501627" + ], + [ + "9d38f021308664c1" + ], + [ + "a508355f0cc87966" + ], + [ + "d2c9aa1a8978aca6" + ], + [ + "b27032beb597d5a7" + ] + ] + }, + { + "id": "845aeb93e39092c5", + "type": "debug", + "z": "5de5756d190f9086", + "name": "available", + "active": true, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 600, + "y": 2240, + "wires": [] + }, + { + "id": "241a8e70e9fde93c", + "type": "debug", + "z": "5de5756d190f9086", + "name": "last_success", + "active": true, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 610, + "y": 2300, + "wires": [] + }, + { + "id": "9d38f021308664c1", + "type": "debug", + "z": "5de5756d190f9086", + "name": "radio_stat", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 600, + "y": 2400, + "wires": [] + }, + { + "id": "a508355f0cc87966", + "type": "debug", + "z": "5de5756d190f9086", + "name": "firmware", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 600, + "y": 2440, + "wires": [] + }, + { + "id": "d2c9aa1a8978aca6", + "type": "debug", + "z": "5de5756d190f9086", + "name": "hardware", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 600, + "y": 2480, + "wires": [] + }, + { + "id": "b27032beb597d5a7", + "type": "debug", + "z": "5de5756d190f9086", + "name": "alarm", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 590, + "y": 2520, + "wires": [] + }, + { + "id": "d814738cf55ad663", + "type": "debug", + "z": "5de5756d190f9086", + "name": "total", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "targetType": "msg", + "statusVal": "", + "statusType": "auto", + "x": 590, + "y": 2160, + "wires": [] + }, + { + "id": "a55632ad0dff0b69", + "type": "switch", + "z": "5de5756d190f9086", + "name": "", + "property": "$split(topic, '/')[1]", + "propertyType": "jsonata", + "rules": [ + { + "t": "eq", + "v": "uptime", + "vt": "str" + }, + { + "t": "eq", + "v": "wifi_rssi", + "vt": "str" + }, + { + "t": "eq", + "v": "status", + "vt": "str" + }, + { + "t": "eq", + "v": "total", + "vt": "str" + } + ], + "checkall": "true", + "repair": false, + "outputs": 4, + "x": 330, + "y": 2100, + "wires": [ + [ + "1fbb0674d2576ee7" + ], + [ + "e6be1c98ac55f511" + ], + [ + "f9c2d3b30e34fdda" + ], + [ + "d814738cf55ad663" + ] + ] + }, + { + "id": "f9c2d3b30e34fdda", + "type": "debug", + "z": "5de5756d190f9086", + "name": "status", + "active": false, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 590, + "y": 2100, + "wires": [] + }, + { + "id": "e6be1c98ac55f511", + "type": "debug", + "z": "5de5756d190f9086", + "name": "wifi_rssi", + "active": false, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 600, + "y": 2040, + "wires": [] + }, + { + "id": "1fbb0674d2576ee7", + "type": "debug", + "z": "5de5756d190f9086", + "name": "uptime", + "active": false, + "tosidebar": false, + "console": false, + "tostatus": true, + "complete": "payload", + "targetType": "msg", + "statusVal": "payload", + "statusType": "auto", + "x": 590, + "y": 1980, + "wires": [] + }, + { + "id": "91a4607dfda84b67", + "type": "change", + "z": "5de5756d190f9086", + "name": "Lösche", + "rules": [ + { + "t": "delete", + "p": "payload[0].YieldDay", + "pt": "msg" + }, + { + "t": "delete", + "p": "payload[0].MaxPower", + "pt": "msg" + }, + { + "t": "delete", + "p": "payload[0].ALARM_MES_ID", + "pt": "msg" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 780, + "y": 2360, + "wires": [ + [ + "670eb9fbb5c31b2c" + ] + ] + }, + { + "id": "319864a4e0fd913f", + "type": "mqtt-broker", + "name": "broker", + "broker": "localhost", + "port": "1883", + "clientid": "", + "autoConnect": true, + "usetls": false, + "protocolVersion": "4", + "keepalive": "60", + "cleansession": true, + "birthTopic": "", + "birthQos": "0", + "birthPayload": "", + "birthMsg": {}, + "closeTopic": "", + "closeQos": "0", + "closePayload": "", + "closeMsg": {}, + "willTopic": "", + "willQos": "0", + "willPayload": "", + "willMsg": {}, + "userProps": "", + "sessionExpiry": "" + } +] \ No newline at end of file