From e7a005839b997f09a1e8bc191a7276e80e4beffc Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 4 Jan 2024 15:42:10 +0100 Subject: [PATCH] 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. --- include/BatteryStats.h | 13 ++++++ include/Configuration.h | 1 + include/MqttBattery.h | 22 +++++++++ src/Battery.cpp | 6 ++- src/Configuration.cpp | 2 + src/MqttBattery.cpp | 65 +++++++++++++++++++++++++++ src/WebApi_battery.cpp | 2 + webapp/src/components/BatteryView.vue | 2 +- webapp/src/locales/de.json | 3 ++ webapp/src/locales/en.json | 3 ++ webapp/src/locales/fr.json | 3 ++ webapp/src/types/BatteryConfig.ts | 1 + webapp/src/views/BatteryAdminView.vue | 15 +++++++ 13 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 include/MqttBattery.h create mode 100644 src/MqttBattery.cpp diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 5d7067208..8ff129f41 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -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(); } +}; diff --git a/include/Configuration.h b/include/Configuration.h index 38b915bd1..66049fd08 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -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 { diff --git a/include/MqttBattery.h b/include/MqttBattery.h new file mode 100644 index 000000000..83ff412d3 --- /dev/null +++ b/include/MqttBattery.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Battery.h" +#include + +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 getStats() const final { return _stats; } + + private: + bool _verboseLogging = false; + String _socTopic; + std::shared_ptr _stats = std::make_shared(); + + void onMqttMessage(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); +}; diff --git a/src/Battery.cpp b/src/Battery.cpp index 720d056f0..9fdc6e273 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -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(); if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; + case 2: + _upProvider = std::make_unique(); + if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } + break; case 3: _upProvider = std::make_unique(); if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } @@ -63,7 +68,6 @@ void BatteryClass::updateSettings() } } - void BatteryClass::loop() { std::lock_guard lock(_mutex); diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 485d03519..9c166ab00 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -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; diff --git a/src/MqttBattery.cpp b/src/MqttBattery.cpp new file mode 100644 index 000000000..9e1992429 --- /dev/null +++ b/src/MqttBattery.cpp @@ -0,0 +1,65 @@ +#include + +#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(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(soc)); + + if (_verboseLogging) { + MessageOutput.printf("MqttBattery: Updated SoC to %d from '%s'\r\n", + static_cast(soc), topic); + } +} diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index 05897840a..1718dd6b6 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -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(); config.Battery.JkBmsInterface = root[F("jkbms_interface")].as(); config.Battery.JkBmsPollingInterval = root[F("jkbms_polling_interval")].as(); + strlcpy(config.Battery.MqttTopic, root[F("mqtt_topic")].as().c_str(), sizeof(config.Battery.MqttTopic)); Configuration.write(); retMsg[F("type")] = F("success"); diff --git a/webapp/src/components/BatteryView.vue b/webapp/src/components/BatteryView.vue index b1fbcfe24..fe539d9bf 100644 --- a/webapp/src/components/BatteryView.vue +++ b/webapp/src/components/BatteryView.vue @@ -5,7 +5,7 @@ -
+
diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 0ca28ec2b..ac0714002 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -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", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 7f3fd59e1..c819fea44 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -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", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index e6d236833..8f1e88efa 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -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", diff --git a/webapp/src/types/BatteryConfig.ts b/webapp/src/types/BatteryConfig.ts index 8bd05000a..fc83e84d9 100644 --- a/webapp/src/types/BatteryConfig.ts +++ b/webapp/src/types/BatteryConfig.ts @@ -4,4 +4,5 @@ export interface BatteryConfig { provider: number; jkbms_interface: number; jkbms_polling_interval: number; + mqtt_topic: string; } diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue index e599425b3..88b67df4b 100644 --- a/webapp/src/views/BatteryAdminView.vue +++ b/webapp/src/views/BatteryAdminView.vue @@ -49,6 +49,20 @@ type="number" min="2" max="90" step="1" :postfix="$t('batteryadmin.Seconds')"/> + +
+ +
+
+ +
+
+
+
+ @@ -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: [