Feature: implement MQTT-driven battery provider (#589)
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.
This commit is contained in:
parent
65319ed07e
commit
e7a005839b
@ -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(); }
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
22
include/MqttBattery.h
Normal file
22
include/MqttBattery.h
Normal file
@ -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);
|
||||
};
|
||||
@ -5,6 +5,7 @@
|
||||
#include "PylontechCanReceiver.h"
|
||||
#include "JkBmsController.h"
|
||||
#include "VictronSmartShunt.h"
|
||||
#include "MqttBattery.h"
|
||||
|
||||
BatteryClass Battery;
|
||||
|
||||
@ -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; }
|
||||
@ -63,7 +68,6 @@ void BatteryClass::updateSettings()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void BatteryClass::loop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
65
src/MqttBattery.cpp
Normal file
65
src/MqttBattery.cpp
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -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");
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -4,4 +4,5 @@ export interface BatteryConfig {
|
||||
provider: number;
|
||||
jkbms_interface: number;
|
||||
jkbms_polling_interval: number;
|
||||
mqtt_topic: string;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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: [
|
||||
|
||||
Loading…
Reference in New Issue
Block a user