From 15b6a32b92f08afa16699ce25fa100581658f18a Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 24 May 2024 21:20:28 +0200 Subject: [PATCH] Feature: support JSON payload in MQTT power meter the MQTT power meter can now process the messages published at the respective topics as JSON and extract a power value using a JSON path (same as in HTTP+JSON power meter). additionally, selecting a unit for the power value as well as an option to invert the value's sign was added as well, similar to the HTTPS+JSON power meter. --- include/Configuration.h | 6 ++ include/PowerMeterMqtt.h | 8 +-- src/Configuration.cpp | 11 ++- src/PowerMeterMqtt.cpp | 86 ++++++++++++++++++------ webapp/src/locales/de.json | 11 +-- webapp/src/locales/en.json | 11 +-- webapp/src/types/PowerMeterConfig.ts | 3 + webapp/src/views/PowerMeterAdminView.vue | 77 ++++++++++++++------- 8 files changed, 154 insertions(+), 59 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 1f185bef..f7a8e082 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -79,6 +79,12 @@ using HttpRequestConfig = struct HTTP_REQUEST_CONFIG_T; struct POWERMETER_MQTT_VALUE_T { char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; + char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1]; + + enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; + Unit PowerUnit; + + bool SignInverted; }; using PowerMeterMqttValue = struct POWERMETER_MQTT_VALUE_T; diff --git a/include/PowerMeterMqtt.h b/include/PowerMeterMqtt.h index 39257187..e63309f6 100644 --- a/include/PowerMeterMqtt.h +++ b/include/PowerMeterMqtt.h @@ -6,6 +6,7 @@ #include #include #include +#include class PowerMeterMqtt : public PowerMeterProvider { public: @@ -23,13 +24,12 @@ private: using MsgProperties = espMqttClientTypes::MessageProperties; void onMessage(MsgProperties const& properties, char const* topic, uint8_t const* payload, size_t len, size_t index, - size_t total, float* targetVariable); + size_t total, float* targetVariable, PowerMeterMqttValue const* cfg); PowerMeterMqttConfig const _cfg; - float _powerValueOne = 0; - float _powerValueTwo = 0; - float _powerValueThree = 0; + using power_values_t = std::array; + power_values_t _powerValues; std::vector _mqttSubscriptions; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index e2067fb1..0aa9bb29 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -36,6 +36,9 @@ void ConfigurationClass::serializePowerMeterMqttConfig(PowerMeterMqttConfig cons PowerMeterMqttValue const& s = source.Values[i]; t["topic"] = s.Topic; + t["json_path"] = s.JsonPath; + t["unit"] = s.PowerUnit; + t["sign_inverted"] = s.SignInverted; } } @@ -301,10 +304,14 @@ void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source, void ConfigurationClass::deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target) { - JsonArray s = source["values"].as(); for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) { PowerMeterMqttValue& t = target.Values[i]; - strlcpy(t.Topic, s[i]["topic"] | "", sizeof(t.Topic)); + JsonObject s = source["values"][i]; + + strlcpy(t.Topic, s["topic"] | "", sizeof(t.Topic)); + strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath)); + t.PowerUnit = s["unit"] | PowerMeterMqttValue::Unit::Watts; + t.SignInverted = s["sign_inverted"] | false; } } diff --git a/src/PowerMeterMqtt.cpp b/src/PowerMeterMqtt.cpp index 1ce9fbb0..fea0fea1 100644 --- a/src/PowerMeterMqtt.cpp +++ b/src/PowerMeterMqtt.cpp @@ -2,24 +2,27 @@ #include "PowerMeterMqtt.h" #include "MqttSettings.h" #include "MessageOutput.h" +#include "ArduinoJson.h" +#include "Utils.h" bool PowerMeterMqtt::init() { - auto subscribe = [this](char const* topic, float* targetVariable) { + auto subscribe = [this](PowerMeterMqttValue const& val, float* targetVariable) { + char const* topic = val.Topic; if (strlen(topic) == 0) { return; } MqttSettings.subscribe(topic, 0, std::bind(&PowerMeterMqtt::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6, - targetVariable) + targetVariable, &val) ); _mqttSubscriptions.push_back(topic); }; - subscribe(_cfg.Values[0].Topic, &_powerValueOne); - subscribe(_cfg.Values[1].Topic, &_powerValueTwo); - subscribe(_cfg.Values[2].Topic, &_powerValueThree); + for (size_t i = 0; i < _powerValues.size(); ++i) { + subscribe(_cfg.Values[i], &_powerValues[i]); + } return _mqttSubscriptions.size() > 0; } @@ -32,22 +35,63 @@ PowerMeterMqtt::~PowerMeterMqtt() void PowerMeterMqtt::onMessage(PowerMeterMqtt::MsgProperties const& properties, char const* topic, uint8_t const* payload, size_t len, size_t index, - size_t total, float* targetVariable) + size_t total, float* targetVariable, PowerMeterMqttValue const* cfg) { std::string value(reinterpret_cast(payload), len); - try { - std::lock_guard l(_mutex); - *targetVariable = std::stof(value); + std::string logValue = value.substr(0, 32); + if (value.length() > logValue.length()) { logValue += "..."; } + + auto log= [topic](char const* format, auto&&... args) -> void { + MessageOutput.printf("[PowerMeterMqtt] Topic '%s': ", topic); + MessageOutput.printf(format, args...); + MessageOutput.println(); + }; + + if (strlen(cfg->JsonPath) == 0) { + try { + std::lock_guard l(_mutex); + *targetVariable = std::stof(value); + } + catch (std::invalid_argument const& e) { + return log("cannot parse payload '%s' as float", logValue.c_str()); + } } - catch (std::invalid_argument const& e) { - MessageOutput.printf("[PowerMeterMqtt] cannot parse payload of topic " - "'%s' as float: %s\r\n", topic, value.c_str()); - return; + else { + JsonDocument json; + + const DeserializationError error = deserializeJson(json, value); + if (error) { + return log("cannot parse payload '%s' as JSON", logValue.c_str()); + } + + if (json.overflowed()) { + return log("payload too large to process as JSON"); + } + + auto pathResolutionResult = Utils::getJsonValueByPath(json, cfg->JsonPath); + if (!pathResolutionResult.second.isEmpty()) { + return log("%s", pathResolutionResult.second.c_str()); + } + + *targetVariable = pathResolutionResult.first; } + using Unit_t = PowerMeterMqttValue::Unit; + switch (cfg->PowerUnit) { + case Unit_t::MilliWatts: + *targetVariable /= 1000; + break; + case Unit_t::KiloWatts: + *targetVariable *= 1000; + break; + default: + break; + } + + if (cfg->SignInverted) { *targetVariable *= -1; } + if (_verboseLogging) { - MessageOutput.printf("[PowerMeterMqtt] Updated from '%s', TotalPower: %5.2f\r\n", - topic, getPowerTotal()); + log("new value: %5.2f, total: %5.2f", *targetVariable, getPowerTotal()); } gotUpdate(); @@ -55,14 +99,16 @@ void PowerMeterMqtt::onMessage(PowerMeterMqtt::MsgProperties const& properties, float PowerMeterMqtt::getPowerTotal() const { - std::lock_guard l(_mutex); - return _powerValueOne + _powerValueTwo + _powerValueThree; + float sum = 0.0; + std::unique_lock lock(_mutex); + for (auto v: _powerValues) { sum += v; } + return sum; } void PowerMeterMqtt::doMqttPublish() const { std::lock_guard l(_mutex); - mqttPublish("power1", _powerValueOne); - mqttPublish("power2", _powerValueTwo); - mqttPublish("power3", _powerValueThree); + mqttPublish("power1", _powerValues[0]); + mqttPublish("power2", _powerValues[1]); + mqttPublish("power3", _powerValues[2]); } diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 72a53e75..34fc4c49 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -566,6 +566,7 @@ "typeHTTP_SML": "HTTP(S) + SML (z.B. Tibber Pulse via Tibber Bridge)", "MqttValue": "Konfiguration Wert {valueNumber}", "MqttTopic": "MQTT Topic", + "mqttJsonPath": "Optional: JSON-Pfad", "SDM": "SDM-Stromzähler Konfiguration", "sdmaddress": "Modbus Adresse", "HTTP_JSON": "HTTP(S) + JSON - Allgemeine Konfiguration", @@ -575,11 +576,11 @@ "jsonPathExamplesExplanation": "Die folgenden Pfade finden jeweils den Wert '123.4' im jeweiligen Beispiel-JSON.", "httpValue": "Konfiguration Wert {valueNumber}", "httpEnabled": "Wert aktiviert", - "httpJsonPath": "JSON-Pfad", - "httpJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Leistungswert in the HTTP(S) Antwort zu finden, z.B. 'power/total/watts' oder nur 'total'.", - "httpUnit": "Einheit", - "httpSignInverted": "Vorzeichen umkehren", - "httpSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.", + "valueJsonPath": "JSON-Pfad", + "valueJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Leistungswert in den JSON Nutzdatzen zu finden, z.B. 'power/total/watts' oder nur 'total'.", + "valueUnit": "Einheit", + "valueSignInverted": "Vorzeichen umkehren", + "valueSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.", "testHttpJsonHeader": "Konfiguration testen", "testHttpJsonRequest": "HTTP(S)-Anfrage(n) senden und Antwort(en) verarbeiten", "testHttpSmlHeader": "Konfiguration testen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 20f0655f..95770604 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -567,6 +567,7 @@ "typeSMAHM2": "SMA Homemanager 2.0", "typeHTTP_SML": "HTTP(S) + SML (e.g. Tibber Pulse via Tibber Bridge)", "MqttValue": "Value {valueNumber} Configuration", + "mqttJsonPath": "Optional: JSON Path", "MqttTopic": "MQTT Topic", "SDM": "SDM-Power Meter Parameter", "sdmaddress": "Modbus Address", @@ -577,11 +578,11 @@ "jsonPathExamplesExplanation": "The following paths each find the value '123.4' in the respective example JSON.", "httpValue": "Value {valueNumber} Configuration", "httpEnabled": "Value Enabled", - "httpJsonPath": "JSON Path", - "httpJsonPathDescription": "Application specific JSON path to find the power value in the HTTP(S) response, e.g., 'power/total/watts' or simply 'total'.", - "httpUnit": "Unit", - "httpSignInverted": "Change Sign", - "httpSignInvertedHint": "Is is expected that positive values denote power usage from the grid. Check this option if the sign of this value has the opposite meaning.", + "valueJsonPath": "JSON Path", + "valueJsonPathDescription": "Application specific JSON path to find the power value in the JSON payload, e.g., 'power/total/watts' or simply 'total'.", + "valueUnit": "Unit", + "valueSignInverted": "Change Sign", + "valueSignInvertedHint": "Is is expected that positive values denote power usage from the grid. Check this option if the sign of this value has the opposite meaning.", "testHttpJsonHeader": "Test Configuration", "testHttpJsonRequest": "Send HTTP(S) request(s) and process response(s)", "testHttpSmlHeader": "Test Configuration", diff --git a/webapp/src/types/PowerMeterConfig.ts b/webapp/src/types/PowerMeterConfig.ts index 07c4c7de..e1f6892e 100644 --- a/webapp/src/types/PowerMeterConfig.ts +++ b/webapp/src/types/PowerMeterConfig.ts @@ -2,6 +2,9 @@ import type { HttpRequestConfig } from '@/types/HttpRequestConfig'; export interface PowerMeterMqttValue { topic: string; + json_path: string; + unit: number; + sign_inverted: boolean; } export interface PowerMeterMqttConfig { diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index a680b64e..4b9f4593 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -30,6 +30,18 @@
+
+ +
+ + + + +
+ +
+ +
+
+ +
+ + @@ -80,24 +129,6 @@ wide /> - - -