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:
parent
347dd67684
commit
15b6a32b92
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
try {
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<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]);
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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> — <code>{ "power": { "phase1": { "factor": 0.98, "watts": 42 }, "total": { "watts": 123.4 } } }</code></li>
|
||||
<li><code>data/[1]/power</code> — <code>{ "data": [ { "factor": 0.98, "power": 42 }, { "factor": 1.0, "power": 123.4 } ] } }</code></li>
|
||||
<li><code>total</code> — <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> — <code>{ "power": { "phase1": { "factor": 0.98, "watts": 42 }, "total": { "watts": 123.4 } } }</code></li>
|
||||
<li><code>data/[1]/power</code> — <code>{ "data": [ { "factor": 0.98, "power": 42 }, { "factor": 1.0, "power": 123.4 } ] } }</code></li>
|
||||
<li><code>total</code> — <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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user