Feature: implement subscription to battery voltage MQTT topic

this extends the MqttBattery implementation by an additional topic which
allows to subscribe to receive battery voltage readings through the MQTT
broker. similar to the battery SoC topic, this allows to import a
critical battery data point for the DPL, in case the user chooses to use
voltage thresholds rather than SoC thresholds to control the DPL. if an
otherwise incompatible BMS is available which publishes the battery pack
voltage through MQTT, this can now be used to feed accurate voltage
readings to the DPL.
This commit is contained in:
Bernhard Kirchen 2024-02-17 12:25:07 +01:00
parent 30bfffb848
commit 3595725f8a
11 changed files with 116 additions and 41 deletions

View File

@ -148,6 +148,8 @@ class VictronSmartShuntStats : public BatteryStats {
};
class MqttBatteryStats : public BatteryStats {
friend class MqttBattery;
public:
// since the source of information was MQTT in the first place,
// we do NOT publish the same data under a different topic.

View File

@ -231,7 +231,8 @@ struct CONFIG_T {
uint8_t Provider;
uint8_t JkBmsInterface;
uint8_t JkBmsPollingInterval;
char MqttTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1];
} Battery;
struct {

View File

@ -1,5 +1,6 @@
#pragma once
#include <optional>
#include "Battery.h"
#include <espMqttClient.h>
@ -15,8 +16,12 @@ class MqttBattery : public BatteryProvider {
private:
bool _verboseLogging = false;
String _socTopic;
String _voltageTopic;
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();
void onMqttMessage(espMqttClientTypes::MessageProperties const& properties,
std::optional<float> getFloat(std::string const& src, char const* topic);
void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
};

View File

@ -208,7 +208,8 @@ bool ConfigurationClass::write()
battery["provider"] = config.Battery.Provider;
battery["jkbms_interface"] = config.Battery.JkBmsInterface;
battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
battery["mqtt_topic"] = config.Battery.MqttTopic;
battery["mqtt_topic"] = config.Battery.MqttSocTopic;
battery["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic;
JsonObject huawei = doc.createNestedObject("huawei");
huawei["enabled"] = config.Huawei.Enabled;
@ -453,7 +454,8 @@ bool ConfigurationClass::read()
config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER;
config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE;
config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL;
strlcpy(config.Battery.MqttTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttTopic));
strlcpy(config.Battery.MqttSocTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttSocTopic));
strlcpy(config.Battery.MqttVoltageTopic, battery["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic));
JsonObject huawei = doc["huawei"];
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;

View File

@ -10,56 +10,105 @@ bool MqttBattery::init(bool verboseLogging)
_verboseLogging = verboseLogging;
auto const& config = Configuration.get();
_socTopic = config.Battery.MqttTopic;
if (_socTopic.isEmpty()) { return false; }
_socTopic = config.Battery.MqttSocTopic;
if (!_socTopic.isEmpty()) {
MqttSettings.subscribe(_socTopic, 0/*QoS*/,
std::bind(&MqttBattery::onMqttMessage,
std::bind(&MqttBattery::onMqttMessageSoC,
this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4,
std::placeholders::_5, std::placeholders::_6)
);
if (_verboseLogging) {
MessageOutput.printf("MqttBattery: Subscribed to '%s'\r\n",
MessageOutput.printf("MqttBattery: Subscribed to '%s' for SoC readings\r\n",
_socTopic.c_str());
}
}
_voltageTopic = config.Battery.MqttVoltageTopic;
if (!_voltageTopic.isEmpty()) {
MqttSettings.subscribe(_voltageTopic, 0/*QoS*/,
std::bind(&MqttBattery::onMqttMessageVoltage,
this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4,
std::placeholders::_5, std::placeholders::_6)
);
if (_verboseLogging) {
MessageOutput.printf("MqttBattery: Subscribed to '%s' for voltage readings\r\n",
_voltageTopic.c_str());
}
}
return true;
}
void MqttBattery::deinit()
{
if (_socTopic.isEmpty()) { return; }
MqttSettings.unsubscribe(_socTopic);
if (!_voltageTopic.isEmpty()) {
MqttSettings.unsubscribe(_voltageTopic);
}
void MqttBattery::onMqttMessage(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
{
float soc = 0;
std::string value(reinterpret_cast<const char*>(payload), len);
if (!_socTopic.isEmpty()) {
MqttSettings.unsubscribe(_socTopic);
}
}
std::optional<float> MqttBattery::getFloat(std::string const& src, char const* topic) {
float res = 0;
try {
soc = std::stof(value);
res = std::stof(src);
}
catch(std::invalid_argument const& e) {
MessageOutput.printf("MqttBattery: Cannot parse payload '%s' in topic '%s' as float\r\n",
value.c_str(), topic);
return;
src.c_str(), topic);
return std::nullopt;
}
if (soc < 0 || soc > 100) {
return res;
}
void MqttBattery::onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
{
auto soc = getFloat(std::string(reinterpret_cast<const char*>(payload), len), topic);
if (!soc.has_value()) { return; }
if (*soc < 0 || *soc > 100) {
MessageOutput.printf("MqttBattery: Implausible SoC '%.2f' in topic '%s'\r\n",
soc, topic);
*soc, topic);
return;
}
_stats->setSoC(static_cast<uint8_t>(soc));
_stats->setSoC(static_cast<uint8_t>(*soc));
if (_verboseLogging) {
MessageOutput.printf("MqttBattery: Updated SoC to %d from '%s'\r\n",
static_cast<uint8_t>(soc), topic);
static_cast<uint8_t>(*soc), topic);
}
}
void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
{
auto voltage = getFloat(std::string(reinterpret_cast<const char*>(payload), len), topic);
if (!voltage.has_value()) { return; }
// since this project is revolving around Hoymiles microinverters, which can
// only handle up to 65V of input voltage at best, it is safe to assume that
// an even higher voltage is implausible.
if (*voltage < 0 || *voltage > 65) {
MessageOutput.printf("MqttBattery: Implausible voltage '%.2f' in topic '%s'\r\n",
*voltage, topic);
return;
}
_stats->setVoltage(*voltage, millis());
if (_verboseLogging) {
MessageOutput.printf("MqttBattery: Updated voltage to %.2f from '%s'\r\n",
*voltage, topic);
}
}

View File

@ -39,7 +39,8 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request)
root["provider"] = config.Battery.Provider;
root["jkbms_interface"] = config.Battery.JkBmsInterface;
root["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
root["mqtt_topic"] = config.Battery.MqttTopic;
root["mqtt_soc_topic"] = config.Battery.MqttSocTopic;
root["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic;
response->setLength();
request->send(response);
@ -103,7 +104,8 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
config.Battery.Provider = root["provider"].as<uint8_t>();
config.Battery.JkBmsInterface = root["jkbms_interface"].as<uint8_t>();
config.Battery.JkBmsPollingInterval = root["jkbms_polling_interval"].as<uint8_t>();
strlcpy(config.Battery.MqttTopic, root["mqtt_topic"].as<String>().c_str(), sizeof(config.Battery.MqttTopic));
strlcpy(config.Battery.MqttSocTopic, root["mqtt_soc_topic"].as<String>().c_str(), sizeof(config.Battery.MqttSocTopic));
strlcpy(config.Battery.MqttVoltageTopic, root["mqtt_voltage_topic"].as<String>().c_str(), sizeof(config.Battery.MqttVoltageTopic));
WebApi.writeConfig(retMsg);

View File

@ -622,10 +622,11 @@
"Provider": "Datenanbieter",
"ProviderPylontechCan": "Pylontech per CAN-Bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
"ProviderMqtt": "State of Charge (SoC) Wert aus MQTT Broker",
"ProviderMqtt": "Batteriewerte aus MQTT Broker",
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
"MqttConfiguration": "MQTT Einstellungen",
"MqttTopic": "SoC-Wert Topic",
"MqttSocTopic": "Topic für Batterie-SoC",
"MqttVoltageTopic": "Topic für Batteriespannung",
"JkBmsConfiguration": "JK BMS Einstellungen",
"JkBmsInterface": "Schnittstellentyp",
"JkBmsInterfaceUart": "TTL-UART an der MCU",

View File

@ -628,10 +628,11 @@
"Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "State of Charge (SoC) value from MQTT broker",
"ProviderMqtt": "Battery data from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"MqttConfiguration": "MQTT Settings",
"MqttTopic": "SoC value topic",
"MqttSocTopic": "SoC value topic",
"MqttVoltageTopic": "Voltage value topic",
"JkBmsConfiguration": "JK BMS Settings",
"JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU",

View File

@ -546,10 +546,11 @@
"Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "State of Charge (SoC) value from MQTT broker",
"ProviderMqtt": "Battery data from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"MqttConfiguration": "MQTT Settings",
"MqttTopic": "SoC value topic",
"MqttSocTopic": "SoC value topic",
"MqttVoltageTopic": "Voltage value topic",
"JkBmsConfiguration": "JK BMS Settings",
"JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU",

View File

@ -4,5 +4,6 @@ export interface BatteryConfig {
provider: number;
jkbms_interface: number;
jkbms_polling_interval: number;
mqtt_topic: string;
mqtt_soc_topic: string;
mqtt_voltage_topic: string;
}

View File

@ -53,11 +53,21 @@
:text="$t('batteryadmin.MqttConfiguration')" textVariant="text-bg-primary" addSpace>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">
{{ $t('batteryadmin.MqttTopic') }}
{{ $t('batteryadmin.MqttSocTopic') }}
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" v-model="batteryConfigList.mqtt_topic" />
<input type="text" class="form-control" v-model="batteryConfigList.mqtt_soc_topic" />
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">
{{ $t('batteryadmin.MqttVoltageTopic') }}
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" v-model="batteryConfigList.mqtt_voltage_topic" />
</div>
</div>
</div>