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.
This commit is contained in:
Bernhard Kirchen 2024-05-24 21:20:28 +02:00
parent 347dd67684
commit 15b6a32b92
8 changed files with 154 additions and 59 deletions

View File

@ -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;

View File

@ -6,6 +6,7 @@
#include <espMqttClient.h>
#include <vector>
#include <mutex>
#include <array>
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<float, POWERMETER_MQTT_MAX_VALUES>;
power_values_t _powerValues;
std::vector<String> _mqttSubscriptions;

View File

@ -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<JsonArray>();
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;
}
}

View File

@ -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<char const*>(payload), len);
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<std::mutex> l(_mutex);
*targetVariable = std::stof(value);
}
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;
return log("cannot parse payload '%s' as float", logValue.c_str());
}
}
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<float>(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<std::mutex> l(_mutex);
return _powerValueOne + _powerValueTwo + _powerValueThree;
float sum = 0.0;
std::unique_lock<std::mutex> lock(_mutex);
for (auto v: _powerValues) { sum += v; }
return sum;
}
void PowerMeterMqtt::doMqttPublish() const
{
std::lock_guard<std::mutex> l(_mutex);
mqttPublish("power1", _powerValueOne);
mqttPublish("power2", _powerValueTwo);
mqttPublish("power3", _powerValueThree);
mqttPublish("power1", _powerValues[0]);
mqttPublish("power2", _powerValues[1]);
mqttPublish("power3", _powerValues[2]);
}

View File

@ -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",

View File

@ -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",

View File

@ -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 {

View File

@ -30,6 +30,18 @@
</CardElement>
<div v-if="powerMeterConfigList.enabled">
<div v-if="powerMeterConfigList.source === 0 || powerMeterConfigList.source === 3">
<div class="alert alert-secondary mt-5" role="alert">
<h2>{{ $t('powermeteradmin.jsonPathExamplesHeading') }}:</h2>
{{ $t('powermeteradmin.jsonPathExamplesExplanation') }}
<ul>
<li><code>power/total/watts</code> &mdash; <code>{ "power": { "phase1": { "factor": 0.98, "watts": 42 }, "total": { "watts": 123.4 } } }</code></li>
<li><code>data/[1]/power</code> &mdash; <code>{ "data": [ { "factor": 0.98, "power": 42 }, { "factor": 1.0, "power": 123.4 } ] } }</code></li>
<li><code>total</code> &mdash; <code>{ "othervalue": 66, "total": 123.4 }</code></li>
</ul>
</div>
</div>
<CardElement v-if="powerMeterConfigList.source === 0"
v-for="(mqtt, index) in powerMeterConfigList.mqtt.values"
:text="$t('powermeteradmin.MqttValue', { valueNumber: index + 1})"
@ -41,6 +53,33 @@
type="text"
maxlength="256"
wide />
<InputElement :label="$t('powermeteradmin.mqttJsonPath')"
v-model="mqtt.json_path"
type="text"
maxlength="256"
:tooltip="$t('powermeteradmin.valueJsonPathDescription')"
wide />
<div class="row mb-3">
<label for="mqtt_power_unit" class="col-sm-4 col-form-label">
{{ $t('powermeteradmin.valueUnit') }}
</label>
<div class="col-sm-8">
<select id="mqtt_power_unit" class="form-select" v-model="mqtt.unit">
<option v-for="u in unitTypeList" :key="u.key" :value="u.key">
{{ u.value }}
</option>
</select>
</div>
</div>
<InputElement
:label="$t('powermeteradmin.valueSignInverted')"
v-model="mqtt.sign_inverted"
:tooltip="$t('powermeteradmin.valueSignInvertedHint')"
type="checkbox"
wide />
</CardElement>
<CardElement v-if="(powerMeterConfigList.source === 1 || powerMeterConfigList.source === 2)"
@ -63,6 +102,16 @@
</CardElement>
<div v-if="powerMeterConfigList.source === 3">
<div class="alert alert-secondary mt-5" role="alert">
<h2>{{ $t('powermeteradmin.urlExamplesHeading') }}:</h2>
<ul>
<li>http://shelly3em.home/status</li>
<li>https://shelly3em.home/status</li>
<li>http://tasmota-123.home/cm?cmnd=status%208</li>
<li>http://12.34.56.78:8080/emeter/0</li>
</ul>
</div>
<CardElement :text="$t('powermeteradmin.HTTP')"
textVariant="text-bg-primary"
add-space>
@ -80,24 +129,6 @@
wide />
</CardElement>
<div class="alert alert-secondary mt-5" role="alert">
<h2>{{ $t('powermeteradmin.urlExamplesHeading') }}:</h2>
<ul>
<li>http://shelly3em.home/status</li>
<li>https://shelly3em.home/status</li>
<li>http://tasmota-123.home/cm?cmnd=status%208</li>
<li>http://12.34.56.78:8080/emeter/0</li>
</ul>
<h2>{{ $t('powermeteradmin.jsonPathExamplesHeading') }}:</h2>
{{ $t('powermeteradmin.jsonPathExamplesExplanation') }}
<ul>
<li><code>power/total/watts</code> &mdash; <code>{ "power": { "phase1": { "factor": 0.98, "watts": 42 }, "total": { "watts": 123.4 } } }</code></li>
<li><code>data/[1]/power</code> &mdash; <code>{ "data": [ { "factor": 0.98, "power": 42 }, { "factor": 1.0, "power": 123.4 } ] } }</code></li>
<li><code>total</code> &mdash; <code>{ "othervalue": 66, "total": 123.4 }</code></li>
</ul>
</div>
<CardElement
v-for="(httpJson, index) in powerMeterConfigList.http_json.values"
:key="index"
@ -114,17 +145,17 @@
<HttpRequestSettings :cfg="httpJson.http_request" v-if="index == 0 || powerMeterConfigList.http_json.individual_requests"/>
<InputElement :label="$t('powermeteradmin.httpJsonPath')"
<InputElement :label="$t('powermeteradmin.valueJsonPath')"
v-model="httpJson.json_path"
type="text"
maxlength="256"
placeholder="total_power"
:tooltip="$t('powermeteradmin.httpJsonPathDescription')"
:tooltip="$t('powermeteradmin.valueJsonPathDescription')"
wide />
<div class="row mb-3">
<label for="power_unit" class="col-sm-4 col-form-label">
{{ $t('powermeteradmin.httpUnit') }}
{{ $t('powermeteradmin.valueUnit') }}
</label>
<div class="col-sm-8">
<select id="power_unit" class="form-select" v-model="httpJson.unit">
@ -136,9 +167,9 @@
</div>
<InputElement
:label="$t('powermeteradmin.httpSignInverted')"
:label="$t('powermeteradmin.valueSignInverted')"
v-model="httpJson.sign_inverted"
:tooltip="$t('powermeteradmin.httpSignInvertedHint')"
:tooltip="$t('powermeteradmin.valueSignInvertedHint')"
type="checkbox"
wide />
</div>