From 3595725f8aac83e42d26a4dc1cd26dbf66f9ec77 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sat, 17 Feb 2024 12:25:07 +0100 Subject: [PATCH] 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. --- include/BatteryStats.h | 2 + include/Configuration.h | 3 +- include/MqttBattery.h | 7 +- src/Configuration.cpp | 6 +- src/MqttBattery.cpp | 99 ++++++++++++++++++++------- src/WebApi_battery.cpp | 8 ++- webapp/src/locales/de.json | 5 +- webapp/src/locales/en.json | 5 +- webapp/src/locales/fr.json | 5 +- webapp/src/types/BatteryConfig.ts | 3 +- webapp/src/views/BatteryAdminView.vue | 14 +++- 11 files changed, 116 insertions(+), 41 deletions(-) diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 47c22e72..12d6330d 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -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. diff --git a/include/Configuration.h b/include/Configuration.h index f4a4a4a9..5fdd21c7 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -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 { diff --git a/include/MqttBattery.h b/include/MqttBattery.h index 83ff412d..61df0450 100644 --- a/include/MqttBattery.h +++ b/include/MqttBattery.h @@ -1,5 +1,6 @@ #pragma once +#include #include "Battery.h" #include @@ -15,8 +16,12 @@ class MqttBattery : public BatteryProvider { private: bool _verboseLogging = false; String _socTopic; + String _voltageTopic; std::shared_ptr _stats = std::make_shared(); - void onMqttMessage(espMqttClientTypes::MessageProperties const& properties, + std::optional 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); }; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 50968d40..441d2dac 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -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; @@ -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; diff --git a/src/MqttBattery.cpp b/src/MqttBattery.cpp index 9e199242..3d303454 100644 --- a/src/MqttBattery.cpp +++ b/src/MqttBattery.cpp @@ -10,20 +10,35 @@ 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; @@ -31,35 +46,69 @@ bool MqttBattery::init(bool verboseLogging) 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(payload), len); +std::optional 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(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(soc)); + _stats->setSoC(static_cast(*soc)); if (_verboseLogging) { MessageOutput.printf("MqttBattery: Updated SoC to %d from '%s'\r\n", - static_cast(soc), topic); + static_cast(*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(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); } } diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index b96eb2cf..9e2230c4 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -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); @@ -103,8 +104,9 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) config.Battery.Provider = root["provider"].as(); config.Battery.JkBmsInterface = root["jkbms_interface"].as(); config.Battery.JkBmsPollingInterval = root["jkbms_polling_interval"].as(); - strlcpy(config.Battery.MqttTopic, root["mqtt_topic"].as().c_str(), sizeof(config.Battery.MqttTopic)); - + strlcpy(config.Battery.MqttSocTopic, root["mqtt_soc_topic"].as().c_str(), sizeof(config.Battery.MqttSocTopic)); + strlcpy(config.Battery.MqttVoltageTopic, root["mqtt_voltage_topic"].as().c_str(), sizeof(config.Battery.MqttVoltageTopic)); + WebApi.writeConfig(retMsg); response->setLength(); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index ddd458fb..b4ff3333 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -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", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 57cb9b05..7286e280 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -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", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 4fbdc8d7..4cdb5b6a 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -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", diff --git a/webapp/src/types/BatteryConfig.ts b/webapp/src/types/BatteryConfig.ts index fc83e84d..4399c211 100644 --- a/webapp/src/types/BatteryConfig.ts +++ b/webapp/src/types/BatteryConfig.ts @@ -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; } diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue index 88b67df4..de938bb3 100644 --- a/webapp/src/views/BatteryAdminView.vue +++ b/webapp/src/views/BatteryAdminView.vue @@ -53,11 +53,21 @@ :text="$t('batteryadmin.MqttConfiguration')" textVariant="text-bg-primary" addSpace>
- + +
+
+
+
+ +
+
+