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 47c22e721..12d6330d2 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 f4a4a4a9b..5fdd21c7f 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 83ff412d3..61df04500 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 50968d40f..441d2dac9 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 9e1992429..3d3034547 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 b96eb2cf6..9e2230c4e 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 ddd458fb3..b4ff3333b 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 57cb9b05a..7286e280a 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 4fbdc8d73..4cdb5b6ac 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 fc83e84d9..4399c211b 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 88b67df4b..de938bb3a 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>
- + +
+
+
+
+ +
+
+