Skip to content

Commit

Permalink
Feature: implement MQTT-driven battery provider (#589)
Browse files Browse the repository at this point in the history
this battery provider implementation subscribes to a user-configurable
MQTT topic to retrieve the battery SoC value. the value is not
re-published under a different topic. there is no card created in the
web app's live view, since the SoC is already part of the totals at the
top of the live view. that is the only info this battery provider
implements.

closes #293.
relates to #581.
  • Loading branch information
schlimmchen authored Jan 4, 2024
1 parent 65319ed commit e7a0058
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 2 deletions.
13 changes: 13 additions & 0 deletions include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,16 @@ class VictronSmartShuntStats : public BatteryStats {
bool _alarmLowTemperature;
bool _alarmHighTemperature;
};

class MqttBatteryStats : public BatteryStats {
public:
// since the source of information was MQTT in the first place,
// we do NOT publish the same data under a different topic.
void mqttPublish() const final { }

// the SoC is the only interesting value in this case, which is already
// displayed at the top of the live view. do not generate a card.
void getLiveViewData(JsonVariant& root) const final { }

void setSoC(uint8_t SoC) { _SoC = SoC; _lastUpdateSoC = _lastUpdate = millis(); }
};
1 change: 1 addition & 0 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ struct CONFIG_T {
uint8_t Provider;
uint8_t JkBmsInterface;
uint8_t JkBmsPollingInterval;
char MqttTopic[MQTT_MAX_TOPIC_STRLEN + 1];
} Battery;

struct {
Expand Down
22 changes: 22 additions & 0 deletions include/MqttBattery.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

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

class MqttBattery : public BatteryProvider {
public:
MqttBattery() = default;

bool init(bool verboseLogging) final;
void deinit() final;
void loop() final { return; } // this class is event-driven
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }

private:
bool _verboseLogging = false;
String _socTopic;
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();

void onMqttMessage(espMqttClientTypes::MessageProperties const& properties,
char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total);
};
6 changes: 5 additions & 1 deletion src/Battery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "PylontechCanReceiver.h"
#include "JkBmsController.h"
#include "VictronSmartShunt.h"
#include "MqttBattery.h"

BatteryClass Battery;

Expand Down Expand Up @@ -53,6 +54,10 @@ void BatteryClass::updateSettings()
_upProvider = std::make_unique<JkBms::Controller>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
case 2:
_upProvider = std::make_unique<MqttBattery>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
case 3:
_upProvider = std::make_unique<VictronSmartShunt>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
Expand All @@ -63,7 +68,6 @@ void BatteryClass::updateSettings()
}
}


void BatteryClass::loop()
{
std::lock_guard<std::mutex> lock(_mutex);
Expand Down
2 changes: 2 additions & 0 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ 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;

JsonObject huawei = doc.createNestedObject("huawei");
huawei["enabled"] = config.Huawei.Enabled;
Expand Down Expand Up @@ -435,6 +436,7 @@ 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));

JsonObject huawei = doc["huawei"];
config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED;
Expand Down
65 changes: 65 additions & 0 deletions src/MqttBattery.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#include <functional>

#include "Configuration.h"
#include "MqttBattery.h"
#include "MqttSettings.h"
#include "MessageOutput.h"

bool MqttBattery::init(bool verboseLogging)
{
_verboseLogging = verboseLogging;

auto const& config = Configuration.get();
_socTopic = config.Battery.MqttTopic;

if (_socTopic.isEmpty()) { return false; }

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'\r\n",
_socTopic.c_str());
}

return true;
}

void MqttBattery::deinit()
{
if (_socTopic.isEmpty()) { return; }
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);

try {
soc = std::stof(value);
}
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;
}

if (soc < 0 || soc > 100) {
MessageOutput.printf("MqttBattery: Implausible SoC '%.2f' in topic '%s'\r\n",
soc, topic);
return;
}

_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);
}
}
2 changes: 2 additions & 0 deletions src/WebApi_battery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request)
root[F("provider")] = config.Battery.Provider;
root[F("jkbms_interface")] = config.Battery.JkBmsInterface;
root[F("jkbms_polling_interval")] = config.Battery.JkBmsPollingInterval;
root[F("mqtt_topic")] = config.Battery.MqttTopic;

response->setLength();
request->send(response);
Expand Down Expand Up @@ -106,6 +107,7 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request)
config.Battery.Provider = root[F("provider")].as<uint8_t>();
config.Battery.JkBmsInterface = root[F("jkbms_interface")].as<uint8_t>();
config.Battery.JkBmsPollingInterval = root[F("jkbms_polling_interval")].as<uint8_t>();
strlcpy(config.Battery.MqttTopic, root[F("mqtt_topic")].as<String>().c_str(), sizeof(config.Battery.MqttTopic));
Configuration.write();

retMsg[F("type")] = F("success");
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/components/BatteryView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</div>
</div>

<div v-else>
<div v-else-if="'values' in batteryData"> <!-- suppress the card for MQTT battery provider -->
<div class="row gy-3">
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
<div class="card">
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,10 @@
"Provider": "Datenanbieter",
"ProviderPylontechCan": "Pylontech per CAN-Bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
"ProviderMqtt": "State of Charge (SoC) Wert aus MQTT Broker",
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
"MqttConfiguration": "MQTT Einstellungen",
"MqttTopic": "SoC-Wert Topic",
"JkBmsConfiguration": "JK BMS Einstellungen",
"JkBmsInterface": "Schnittstellentyp",
"JkBmsInterfaceUart": "TTL-UART an der MCU",
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,10 @@
"Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "State of Charge (SoC) value from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"MqttConfiguration": "MQTT Settings",
"MqttTopic": "SoC value topic",
"JkBmsConfiguration": "JK BMS Settings",
"JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU",
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,10 @@
"Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderMqtt": "State of Charge (SoC) value from MQTT broker",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"MqttConfiguration": "MQTT Settings",
"MqttTopic": "SoC value topic",
"JkBmsConfiguration": "JK BMS Settings",
"JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU",
Expand Down
1 change: 1 addition & 0 deletions webapp/src/types/BatteryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface BatteryConfig {
provider: number;
jkbms_interface: number;
jkbms_polling_interval: number;
mqtt_topic: string;
}
15 changes: 15 additions & 0 deletions webapp/src/views/BatteryAdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@
type="number" min="2" max="90" step="1" :postfix="$t('batteryadmin.Seconds')"/>
</CardElement>

<CardElement v-show="batteryConfigList.enabled && batteryConfigList.provider == 2"
: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') }}
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" v-model="batteryConfigList.mqtt_topic" />
</div>
</div>
</div>
</CardElement>

<FormFooter @reload="getBatteryConfig"/>
</form>
</BasePage>
Expand Down Expand Up @@ -82,6 +96,7 @@ export default defineComponent({
providerTypeList: [
{ key: 0, value: 'PylontechCan' },
{ key: 1, value: 'JkBmsSerial' },
{ key: 2, value: 'Mqtt' },
{ key: 3, value: 'Victron' },
],
jkBmsInterfaceTypeList: [
Expand Down

0 comments on commit e7a0058

Please sign in to comment.