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:
parent
30bfffb848
commit
3595725f8a
@ -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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user