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 {
|
class MqttBatteryStats : public BatteryStats {
|
||||||
|
friend class MqttBattery;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// since the source of information was MQTT in the first place,
|
// since the source of information was MQTT in the first place,
|
||||||
// we do NOT publish the same data under a different topic.
|
// we do NOT publish the same data under a different topic.
|
||||||
|
|||||||
@ -231,7 +231,8 @@ struct CONFIG_T {
|
|||||||
uint8_t Provider;
|
uint8_t Provider;
|
||||||
uint8_t JkBmsInterface;
|
uint8_t JkBmsInterface;
|
||||||
uint8_t JkBmsPollingInterval;
|
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;
|
} Battery;
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include "Battery.h"
|
#include "Battery.h"
|
||||||
#include <espMqttClient.h>
|
#include <espMqttClient.h>
|
||||||
|
|
||||||
@ -15,8 +16,12 @@ class MqttBattery : public BatteryProvider {
|
|||||||
private:
|
private:
|
||||||
bool _verboseLogging = false;
|
bool _verboseLogging = false;
|
||||||
String _socTopic;
|
String _socTopic;
|
||||||
|
String _voltageTopic;
|
||||||
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();
|
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);
|
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["provider"] = config.Battery.Provider;
|
||||||
battery["jkbms_interface"] = config.Battery.JkBmsInterface;
|
battery["jkbms_interface"] = config.Battery.JkBmsInterface;
|
||||||
battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
|
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");
|
JsonObject huawei = doc.createNestedObject("huawei");
|
||||||
huawei["enabled"] = config.Huawei.Enabled;
|
huawei["enabled"] = config.Huawei.Enabled;
|
||||||
@ -453,7 +454,8 @@ bool ConfigurationClass::read()
|
|||||||
config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER;
|
config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER;
|
||||||
config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE;
|
config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE;
|
||||||
config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL;
|
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"];
|
JsonObject huawei = doc["huawei"];
|
||||||
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;
|
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;
|
||||||
|
|||||||
@ -10,20 +10,35 @@ bool MqttBattery::init(bool verboseLogging)
|
|||||||
_verboseLogging = verboseLogging;
|
_verboseLogging = verboseLogging;
|
||||||
|
|
||||||
auto const& config = Configuration.get();
|
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::onMqttMessageSoC,
|
||||||
|
this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4,
|
||||||
|
std::placeholders::_5, std::placeholders::_6)
|
||||||
|
);
|
||||||
|
|
||||||
MqttSettings.subscribe(_socTopic, 0/*QoS*/,
|
if (_verboseLogging) {
|
||||||
std::bind(&MqttBattery::onMqttMessage,
|
MessageOutput.printf("MqttBattery: Subscribed to '%s' for SoC readings\r\n",
|
||||||
this, std::placeholders::_1, std::placeholders::_2,
|
_socTopic.c_str());
|
||||||
std::placeholders::_3, std::placeholders::_4,
|
}
|
||||||
std::placeholders::_5, std::placeholders::_6)
|
}
|
||||||
);
|
|
||||||
|
|
||||||
if (_verboseLogging) {
|
_voltageTopic = config.Battery.MqttVoltageTopic;
|
||||||
MessageOutput.printf("MqttBattery: Subscribed to '%s'\r\n",
|
if (!_voltageTopic.isEmpty()) {
|
||||||
_socTopic.c_str());
|
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;
|
return true;
|
||||||
@ -31,35 +46,69 @@ bool MqttBattery::init(bool verboseLogging)
|
|||||||
|
|
||||||
void MqttBattery::deinit()
|
void MqttBattery::deinit()
|
||||||
{
|
{
|
||||||
if (_socTopic.isEmpty()) { return; }
|
if (!_voltageTopic.isEmpty()) {
|
||||||
MqttSettings.unsubscribe(_socTopic);
|
MqttSettings.unsubscribe(_voltageTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_socTopic.isEmpty()) {
|
||||||
|
MqttSettings.unsubscribe(_socTopic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MqttBattery::onMqttMessage(espMqttClientTypes::MessageProperties const& properties,
|
std::optional<float> MqttBattery::getFloat(std::string const& src, char const* topic) {
|
||||||
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total)
|
float res = 0;
|
||||||
{
|
|
||||||
float soc = 0;
|
|
||||||
std::string value(reinterpret_cast<const char*>(payload), len);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
soc = std::stof(value);
|
res = std::stof(src);
|
||||||
}
|
}
|
||||||
catch(std::invalid_argument const& e) {
|
catch(std::invalid_argument const& e) {
|
||||||
MessageOutput.printf("MqttBattery: Cannot parse payload '%s' in topic '%s' as float\r\n",
|
MessageOutput.printf("MqttBattery: Cannot parse payload '%s' in topic '%s' as float\r\n",
|
||||||
value.c_str(), topic);
|
src.c_str(), topic);
|
||||||
return;
|
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",
|
MessageOutput.printf("MqttBattery: Implausible SoC '%.2f' in topic '%s'\r\n",
|
||||||
soc, topic);
|
*soc, topic);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_stats->setSoC(static_cast<uint8_t>(soc));
|
_stats->setSoC(static_cast<uint8_t>(*soc));
|
||||||
|
|
||||||
if (_verboseLogging) {
|
if (_verboseLogging) {
|
||||||
MessageOutput.printf("MqttBattery: Updated SoC to %d from '%s'\r\n",
|
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["provider"] = config.Battery.Provider;
|
||||||
root["jkbms_interface"] = config.Battery.JkBmsInterface;
|
root["jkbms_interface"] = config.Battery.JkBmsInterface;
|
||||||
root["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval;
|
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();
|
response->setLength();
|
||||||
request->send(response);
|
request->send(response);
|
||||||
@ -103,8 +104,9 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
|
|||||||
config.Battery.Provider = root["provider"].as<uint8_t>();
|
config.Battery.Provider = root["provider"].as<uint8_t>();
|
||||||
config.Battery.JkBmsInterface = root["jkbms_interface"].as<uint8_t>();
|
config.Battery.JkBmsInterface = root["jkbms_interface"].as<uint8_t>();
|
||||||
config.Battery.JkBmsPollingInterval = root["jkbms_polling_interval"].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);
|
WebApi.writeConfig(retMsg);
|
||||||
|
|
||||||
response->setLength();
|
response->setLength();
|
||||||
|
|||||||
@ -622,10 +622,11 @@
|
|||||||
"Provider": "Datenanbieter",
|
"Provider": "Datenanbieter",
|
||||||
"ProviderPylontechCan": "Pylontech per CAN-Bus",
|
"ProviderPylontechCan": "Pylontech per CAN-Bus",
|
||||||
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
|
"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",
|
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
|
||||||
"MqttConfiguration": "MQTT Einstellungen",
|
"MqttConfiguration": "MQTT Einstellungen",
|
||||||
"MqttTopic": "SoC-Wert Topic",
|
"MqttSocTopic": "Topic für Batterie-SoC",
|
||||||
|
"MqttVoltageTopic": "Topic für Batteriespannung",
|
||||||
"JkBmsConfiguration": "JK BMS Einstellungen",
|
"JkBmsConfiguration": "JK BMS Einstellungen",
|
||||||
"JkBmsInterface": "Schnittstellentyp",
|
"JkBmsInterface": "Schnittstellentyp",
|
||||||
"JkBmsInterfaceUart": "TTL-UART an der MCU",
|
"JkBmsInterfaceUart": "TTL-UART an der MCU",
|
||||||
|
|||||||
@ -628,10 +628,11 @@
|
|||||||
"Provider": "Data Provider",
|
"Provider": "Data Provider",
|
||||||
"ProviderPylontechCan": "Pylontech using CAN bus",
|
"ProviderPylontechCan": "Pylontech using CAN bus",
|
||||||
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
|
"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",
|
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
|
||||||
"MqttConfiguration": "MQTT Settings",
|
"MqttConfiguration": "MQTT Settings",
|
||||||
"MqttTopic": "SoC value topic",
|
"MqttSocTopic": "SoC value topic",
|
||||||
|
"MqttVoltageTopic": "Voltage value topic",
|
||||||
"JkBmsConfiguration": "JK BMS Settings",
|
"JkBmsConfiguration": "JK BMS Settings",
|
||||||
"JkBmsInterface": "Interface Type",
|
"JkBmsInterface": "Interface Type",
|
||||||
"JkBmsInterfaceUart": "TTL-UART on MCU",
|
"JkBmsInterfaceUart": "TTL-UART on MCU",
|
||||||
|
|||||||
@ -546,10 +546,11 @@
|
|||||||
"Provider": "Data Provider",
|
"Provider": "Data Provider",
|
||||||
"ProviderPylontechCan": "Pylontech using CAN bus",
|
"ProviderPylontechCan": "Pylontech using CAN bus",
|
||||||
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
|
"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",
|
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
|
||||||
"MqttConfiguration": "MQTT Settings",
|
"MqttConfiguration": "MQTT Settings",
|
||||||
"MqttTopic": "SoC value topic",
|
"MqttSocTopic": "SoC value topic",
|
||||||
|
"MqttVoltageTopic": "Voltage value topic",
|
||||||
"JkBmsConfiguration": "JK BMS Settings",
|
"JkBmsConfiguration": "JK BMS Settings",
|
||||||
"JkBmsInterface": "Interface Type",
|
"JkBmsInterface": "Interface Type",
|
||||||
"JkBmsInterfaceUart": "TTL-UART on MCU",
|
"JkBmsInterfaceUart": "TTL-UART on MCU",
|
||||||
|
|||||||
@ -4,5 +4,6 @@ export interface BatteryConfig {
|
|||||||
provider: number;
|
provider: number;
|
||||||
jkbms_interface: number;
|
jkbms_interface: number;
|
||||||
jkbms_polling_interval: 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>
|
:text="$t('batteryadmin.MqttConfiguration')" textVariant="text-bg-primary" addSpace>
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<label class="col-sm-2 col-form-label">
|
<label class="col-sm-2 col-form-label">
|
||||||
{{ $t('batteryadmin.MqttTopic') }}
|
{{ $t('batteryadmin.MqttSocTopic') }}
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<div class="input-group">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user