Skip to content

Commit

Permalink
Feature: implement subscription to battery voltage MQTT topic
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
schlimmchen committed Feb 18, 2024
1 parent 30bfffb commit 3595725
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 41 deletions.
2 changes: 2 additions & 0 deletions include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion include/MqttBattery.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <optional>
#include "Battery.h"
#include <espMqttClient.h>

Expand All @@ -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);
};
6 changes: 4 additions & 2 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
99 changes: 74 additions & 25 deletions src/MqttBattery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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::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*/,
std::bind(&MqttBattery::onMqttMessage,
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 SoC readings\r\n",
_socTopic.c_str());
}
}

if (_verboseLogging) {
MessageOutput.printf("MqttBattery: Subscribed to '%s'\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);
}

if (!_socTopic.isEmpty()) {
MqttSettings.unsubscribe(_socTopic);
}
}

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);
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);
}
}
8 changes: 5 additions & 3 deletions src/WebApi_battery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -103,8 +104,9 @@ 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);

response->setLength();
Expand Down
5 changes: 3 additions & 2 deletions webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 3 additions & 2 deletions webapp/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion webapp/src/types/BatteryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
14 changes: 12 additions & 2 deletions webapp/src/views/BatteryAdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit 3595725

Please sign in to comment.