diff --git a/include/Battery.h b/include/Battery.h index 5130381cc..1265a8bb2 100644 --- a/include/Battery.h +++ b/include/Battery.h @@ -21,6 +21,8 @@ class BatteryClass { void init(Scheduler&); void updateSettings(); + float getDischargeCurrentLimit(); + std::shared_ptr getStats() const; private: diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 68a908137..380b2dec4 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -28,6 +28,9 @@ class BatteryStats { float getChargeCurrent() const { return _current; }; uint8_t getChargeCurrentPrecision() const { return _currentPrecision; } + float getDischargeCurrentLimit() const { return _dischargeCurrentLimit; }; + uint32_t getDischargeCurrentLimitAgeSeconds() const { return (millis() - _lastUpdateDischargeCurrentLimit) / 1000; } + // convert stats to JSON for web application live view virtual void getLiveViewData(JsonVariant& root) const; @@ -40,13 +43,15 @@ class BatteryStats { bool isSoCValid() const { return _lastUpdateSoC > 0; } bool isVoltageValid() const { return _lastUpdateVoltage > 0; } bool isCurrentValid() const { return _lastUpdateCurrent > 0; } + bool isDischargeCurrentLimitValid() const { return _lastUpdateDischargeCurrentLimit > 0; } // returns true if the battery reached a critically low voltage/SoC, // such that it is in need of charging to prevent degredation. virtual bool getImmediateChargingRequest() const { return false; }; virtual float getChargeCurrentLimitation() const { return FLT_MAX; }; - virtual float getDischargeCurrentLimitation() const { return FLT_MAX; }; + + virtual bool supportsAlarmsAndWarnings() const { return true; }; protected: virtual void mqttPublish() const; @@ -68,6 +73,11 @@ class BatteryStats { _lastUpdateCurrent = _lastUpdate = timestamp; } + void setDischargeCurrentLimit(float dischargeCurrentLimit, uint32_t timestamp) { + _dischargeCurrentLimit = dischargeCurrentLimit; + _lastUpdateDischargeCurrentLimit = _lastUpdate = timestamp; + } + void setManufacturer(const String& m); String _hwversion = ""; @@ -89,6 +99,9 @@ class BatteryStats { float _current = 0; uint8_t _currentPrecision = 0; // decimal places uint32_t _lastUpdateCurrent = 0; + + float _dischargeCurrentLimit = 0; + uint32_t _lastUpdateDischargeCurrentLimit = 0; }; class PylontechBatteryStats : public BatteryStats { @@ -99,14 +112,12 @@ class PylontechBatteryStats : public BatteryStats { void mqttPublish() const final; bool getImmediateChargingRequest() const { return _chargeImmediately; } ; float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ; - float getDischargeCurrentLimitation() const { return _dischargeCurrentLimitation; } ; private: void setLastUpdate(uint32_t ts) { _lastUpdate = ts; } float _chargeVoltage; float _chargeCurrentLimitation; - float _dischargeCurrentLimitation; uint16_t _stateOfHealth; float _temperature; @@ -137,8 +148,7 @@ class PytesBatteryStats : public BatteryStats { public: void getLiveViewData(JsonVariant& root) const final; void mqttPublish() const final; - float getChargeCurrentLimitation() const { return _chargeCurrentLimit; } ; - float getDischargeCurrentLimitation() const { return _dischargeCurrentLimit; } ; + float getChargeCurrentLimitation() const { return _chargeCurrentLimit; }; private: void setLastUpdate(uint32_t ts) { _lastUpdate = ts; } @@ -154,7 +164,6 @@ class PytesBatteryStats : public BatteryStats { float _chargeVoltageLimit; float _chargeCurrentLimit; float _dischargeVoltageLimit; - float _dischargeCurrentLimit; uint16_t _stateOfHealth; @@ -269,7 +278,7 @@ class MqttBatteryStats : public BatteryStats { // we do NOT publish the same data under a different topic. void mqttPublish() const final { } - // we don't need a card in the liveview, since the SoC and - // voltage (if available) is already displayed at the top. - void getLiveViewData(JsonVariant& root) const final { } + void getLiveViewData(JsonVariant& root) const final; + + bool supportsAlarmsAndWarnings() const final { return false; } }; diff --git a/include/Configuration.h b/include/Configuration.h index 9f433faf3..c8ae55e15 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -128,6 +128,28 @@ using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T; enum BatteryVoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 }; +enum BatteryAmperageUnit { Amps = 0, MilliAmps = 1 }; + +struct BATTERY_CONFIG_T { + bool Enabled; + bool VerboseLogging; + uint8_t Provider; + uint8_t JkBmsInterface; + uint8_t JkBmsPollingInterval; + char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char MqttSocJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; + char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char MqttVoltageJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; + BatteryVoltageUnit MqttVoltageUnit; + bool EnableDischargeCurrentLimit; + float DischargeCurrentLimit; + bool UseBatteryReportedDischargeCurrentLimit; + char MqttDischargeCurrentTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char MqttDischargeCurrentJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; + BatteryAmperageUnit MqttAmperageUnit; +}; +using BatteryConfig = struct BATTERY_CONFIG_T; + struct CONFIG_T { struct { uint32_t Version; @@ -277,18 +299,7 @@ struct CONFIG_T { float FullSolarPassThroughStopVoltage; } PowerLimiter; - struct { - bool Enabled; - bool VerboseLogging; - uint8_t Provider; - uint8_t JkBmsInterface; - uint8_t JkBmsPollingInterval; - char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttSocJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; - char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttVoltageJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; - BatteryVoltageUnit MqttVoltageUnit; - } Battery; + BatteryConfig Battery; struct { bool Enabled; @@ -327,12 +338,14 @@ class ConfigurationClass { static void serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target); static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target); static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target); + static void serializeBatteryConfig(BatteryConfig const& source, JsonObject& target); static void deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target); static void deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target); static void deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target); static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target); static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target); + static void deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target); }; extern ConfigurationClass Configuration; diff --git a/include/MqttBattery.h b/include/MqttBattery.h index a230a9d43..7698e4c94 100644 --- a/include/MqttBattery.h +++ b/include/MqttBattery.h @@ -17,6 +17,7 @@ class MqttBattery : public BatteryProvider { bool _verboseLogging = false; String _socTopic; String _voltageTopic; + String _dischargeCurrentLimitTopic; std::shared_ptr _stats = std::make_shared(); void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, @@ -25,4 +26,7 @@ class MqttBattery : public BatteryProvider { void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties, char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, char const* jsonPath); + void onMqttMessageDischargeCurrentLimit(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath); }; diff --git a/include/defaults.h b/include/defaults.h index d92b8645d..f3482cdb8 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -151,6 +151,9 @@ #define BATTERY_PROVIDER 0 // Pylontech CAN receiver #define BATTERY_JKBMS_INTERFACE 0 #define BATTERY_JKBMS_POLLING_INTERVAL 5 +#define BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT false +#define BATTERY_DISCHARGE_CURRENT_LIMIT 0 +#define BATTERY_USE_BATTERY_REPORTED_DISCHARGE_CURRENT_LIMIT false #define HUAWEI_ENABLED false #define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL diff --git a/src/Battery.cpp b/src/Battery.cpp index 79ba70020..029f8ab0a 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -79,3 +79,33 @@ void BatteryClass::loop() _upProvider->getStats()->mqttLoop(); } + +float BatteryClass::getDischargeCurrentLimit() +{ + CONFIG_T& config = Configuration.get(); + + if (!config.Battery.EnableDischargeCurrentLimit) { return FLT_MAX; } + + auto dischargeCurrentLimit = config.Battery.DischargeCurrentLimit; + auto dischargeCurrentValid = dischargeCurrentLimit > 0.0f; + + auto statsCurrentLimit = getStats()->getDischargeCurrentLimit(); + auto statsLimitValid = config.Battery.UseBatteryReportedDischargeCurrentLimit + && statsCurrentLimit >= 0.0f + && getStats()->getDischargeCurrentLimitAgeSeconds() <= 60; + + if (statsLimitValid && dischargeCurrentValid) { + // take the lowest limit + return min(statsCurrentLimit, dischargeCurrentLimit); + } + + if (statsLimitValid) { + return statsCurrentLimit; + } + + if (dischargeCurrentValid) { + return dischargeCurrentValid; + } + + return FLT_MAX; +} diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index e4fe305a5..48c869eac 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -89,9 +89,32 @@ void BatteryStats::getLiveViewData(JsonVariant& root) const } root["data_age"] = getAgeSeconds(); - addLiveViewValue(root, "SoC", _soc, "%", _socPrecision); - addLiveViewValue(root, "voltage", _voltage, "V", 2); - addLiveViewValue(root, "current", _current, "A", _currentPrecision); + if (isSoCValid()) { + addLiveViewValue(root, "SoC", _soc, "%", _socPrecision); + } + + if (isVoltageValid()) { + addLiveViewValue(root, "voltage", _voltage, "V", 2); + } + + if (isCurrentValid()) { + addLiveViewValue(root, "current", _current, "A", _currentPrecision); + } + + if (isDischargeCurrentLimitValid()) { + addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimit, "A", 1); + } + + root["showIssues"] = supportsAlarmsAndWarnings(); +} + +void MqttBatteryStats::getLiveViewData(JsonVariant& root) const +{ + // as we don't want to repeat the data that is already shown in the live data card + // we only add the live view data here when the discharge current limit can be shown + if (isDischargeCurrentLimitValid()) { + BatteryStats::getLiveViewData(root); + } } void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const @@ -101,7 +124,6 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const // values go into the "Status" card of the web application addLiveViewValue(root, "chargeVoltage", _chargeVoltage, "V", 1); addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1); - addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimitation, "A", 1); addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0); addLiveViewValue(root, "temperature", _temperature, "°C", 1); @@ -140,7 +162,6 @@ void PytesBatteryStats::getLiveViewData(JsonVariant& root) const addLiveViewValue(root, "chargeVoltage", _chargeVoltageLimit, "V", 1); addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimit, "A", 1); addLiveViewValue(root, "dischargeVoltageLimitation", _dischargeVoltageLimit, "V", 1); - addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimit, "A", 1); addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0); addLiveViewValue(root, "temperature", _temperature, "°C", 1); @@ -311,15 +332,22 @@ void BatteryStats::mqttPublish() const { MqttSettings.publish("battery/manufacturer", _manufacturer); MqttSettings.publish("battery/dataAge", String(getAgeSeconds())); + if (isSoCValid()) { MqttSettings.publish("battery/stateOfCharge", String(_soc)); } + if (isVoltageValid()) { MqttSettings.publish("battery/voltage", String(_voltage)); } + if (isCurrentValid()) { MqttSettings.publish("battery/current", String(_current)); } + + if (isDischargeCurrentLimitValid()) { + MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimit)); + } } void PylontechBatteryStats::mqttPublish() const @@ -328,7 +356,6 @@ void PylontechBatteryStats::mqttPublish() const MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage)); MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation)); - MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimitation)); MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); MqttSettings.publish("battery/temperature", String(_temperature)); MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge)); @@ -356,7 +383,6 @@ void PytesBatteryStats::mqttPublish() const MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltageLimit)); MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimit)); - MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimit)); MqttSettings.publish("battery/settings/dischargeVoltageLimitation", String(_dischargeVoltageLimit)); MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 9b3c7d4e4..50454594b 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -74,6 +74,26 @@ void ConfigurationClass::serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfi serializeHttpRequestConfig(source.HttpRequest, target); } +void ConfigurationClass::serializeBatteryConfig(BatteryConfig const& source, JsonObject& target) +{ + target["enabled"] = config.Battery.Enabled; + target["verbose_logging"] = config.Battery.VerboseLogging; + target["provider"] = config.Battery.Provider; + target["jkbms_interface"] = config.Battery.JkBmsInterface; + target["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval; + target["mqtt_soc_topic"] = config.Battery.MqttSocTopic; + target["mqtt_soc_json_path"] = config.Battery.MqttSocJsonPath; + target["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic; + target["mqtt_voltage_json_path"] = config.Battery.MqttVoltageJsonPath; + target["mqtt_voltage_unit"] = config.Battery.MqttVoltageUnit; + target["enable_discharge_current_limit"] = config.Battery.EnableDischargeCurrentLimit; + target["discharge_current_limit"] = config.Battery.DischargeCurrentLimit; + target["use_battery_reported_discharge_current_limit"] = config.Battery.UseBatteryReportedDischargeCurrentLimit; + target["mqtt_discharge_current_topic"] = config.Battery.MqttDischargeCurrentTopic; + target["mqtt_discharge_current_json_path"] = config.Battery.MqttDischargeCurrentJsonPath; + target["mqtt_amperage_unit"] = config.Battery.MqttAmperageUnit; +} + bool ConfigurationClass::write() { File f = LittleFS.open(CONFIG_FILENAME, "w"); @@ -251,16 +271,7 @@ bool ConfigurationClass::write() powerlimiter["full_solar_passthrough_stop_voltage"] = config.PowerLimiter.FullSolarPassThroughStopVoltage; JsonObject battery = doc["battery"].to(); - battery["enabled"] = config.Battery.Enabled; - battery["verbose_logging"] = config.Battery.VerboseLogging; - battery["provider"] = config.Battery.Provider; - battery["jkbms_interface"] = config.Battery.JkBmsInterface; - battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval; - battery["mqtt_topic"] = config.Battery.MqttSocTopic; - battery["mqtt_json_path"] = config.Battery.MqttSocJsonPath; - battery["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic; - battery["mqtt_voltage_json_path"] = config.Battery.MqttVoltageJsonPath; - battery["mqtt_voltage_unit"] = config.Battery.MqttVoltageUnit; + serializeBatteryConfig(config.Battery, battery); JsonObject huawei = doc["huawei"].to(); huawei["enabled"] = config.Huawei.Enabled; @@ -353,6 +364,26 @@ void ConfigurationClass::deserializePowerMeterHttpSmlConfig(JsonObject const& so deserializeHttpRequestConfig(source, target.HttpRequest); } +void ConfigurationClass::deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target) +{ + target.Enabled = source["enabled"] | BATTERY_ENABLED; + target.VerboseLogging = source["verbose_logging"] | VERBOSE_LOGGING; + target.Provider = source["provider"] | BATTERY_PROVIDER; + target.JkBmsInterface = source["jkbms_interface"] | BATTERY_JKBMS_INTERFACE; + target.JkBmsPollingInterval = source["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL; + strlcpy(target.MqttSocTopic, source["mqtt_soc_topic"] | source["mqtt_topic"] | "", sizeof(config.Battery.MqttSocTopic)); // mqtt_soc_topic was previously saved as mqtt_topic. Be nice and also try old key. + strlcpy(target.MqttSocJsonPath, source["mqtt_soc_json_path"] | source["mqtt_json_path"] | "", sizeof(config.Battery.MqttSocJsonPath)); // mqtt_soc_json_path was previously saved as mqtt_json_path. Be nice and also try old key. + strlcpy(target.MqttVoltageTopic, source["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic)); + strlcpy(target.MqttVoltageJsonPath, source["mqtt_voltage_json_path"] | "", sizeof(config.Battery.MqttVoltageJsonPath)); + target.MqttVoltageUnit = source["mqtt_voltage_unit"] | BatteryVoltageUnit::Volts; + target.EnableDischargeCurrentLimit = source["enable_discharge_current_limit"] | BATTERY_ENABLE_DISCHARGE_CURRENT_LIMIT; + target.DischargeCurrentLimit = source["discharge_current_limit"] | BATTERY_DISCHARGE_CURRENT_LIMIT; + target.UseBatteryReportedDischargeCurrentLimit = source["use_battery_reported_discharge_current_limit"] | BATTERY_USE_BATTERY_REPORTED_DISCHARGE_CURRENT_LIMIT; + strlcpy(target.MqttDischargeCurrentTopic, source["mqtt_discharge_current_topic"] | "", sizeof(config.Battery.MqttDischargeCurrentTopic)); + strlcpy(target.MqttDischargeCurrentJsonPath, source["mqtt_discharge_current_json_path"] | "", sizeof(config.Battery.MqttDischargeCurrentJsonPath)); + target.MqttAmperageUnit = source["mqtt_amperage_unit"] | BatteryAmperageUnit::Amps; +} + bool ConfigurationClass::read() { File f = LittleFS.open(CONFIG_FILENAME, "r", false); @@ -600,17 +631,7 @@ bool ConfigurationClass::read() config.PowerLimiter.FullSolarPassThroughStartVoltage = powerlimiter["full_solar_passthrough_start_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE; config.PowerLimiter.FullSolarPassThroughStopVoltage = powerlimiter["full_solar_passthrough_stop_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE; - JsonObject battery = doc["battery"]; - config.Battery.Enabled = battery["enabled"] | BATTERY_ENABLED; - config.Battery.VerboseLogging = battery["verbose_logging"] | VERBOSE_LOGGING; - 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.MqttSocTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttSocTopic)); - strlcpy(config.Battery.MqttSocJsonPath, battery["mqtt_json_path"] | "", sizeof(config.Battery.MqttSocJsonPath)); - strlcpy(config.Battery.MqttVoltageTopic, battery["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic)); - strlcpy(config.Battery.MqttVoltageJsonPath, battery["mqtt_voltage_json_path"] | "", sizeof(config.Battery.MqttVoltageJsonPath)); - config.Battery.MqttVoltageUnit = battery["mqtt_voltage_unit"] | BatteryVoltageUnit::Volts; + deserializeBatteryConfig(doc["battery"], config.Battery); JsonObject huawei = doc["huawei"]; config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED; diff --git a/src/MqttBattery.cpp b/src/MqttBattery.cpp index 544ff0322..cd17cf7d2 100644 --- a/src/MqttBattery.cpp +++ b/src/MqttBattery.cpp @@ -9,6 +9,7 @@ bool MqttBattery::init(bool verboseLogging) { _verboseLogging = verboseLogging; + _stats->setManufacturer("MQTT"); auto const& config = Configuration.get(); @@ -44,6 +45,25 @@ bool MqttBattery::init(bool verboseLogging) } } + if (config.Battery.EnableDischargeCurrentLimit && config.Battery.UseBatteryReportedDischargeCurrentLimit) { + _dischargeCurrentLimitTopic = config.Battery.MqttDischargeCurrentTopic; + + if (!_dischargeCurrentLimitTopic.isEmpty()) { + MqttSettings.subscribe(_dischargeCurrentLimitTopic, 0/*QoS*/, + std::bind(&MqttBattery::onMqttMessageDischargeCurrentLimit, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6, + config.Battery.MqttDischargeCurrentJsonPath) + ); + + if (_verboseLogging) { + MessageOutput.printf("MqttBattery: Subscribed to '%s' for discharge current limit readings\r\n", + _dischargeCurrentLimitTopic.c_str()); + } + } + } + return true; } @@ -56,6 +76,10 @@ void MqttBattery::deinit() if (!_socTopic.isEmpty()) { MqttSettings.unsubscribe(_socTopic); } + + if (!_dischargeCurrentLimitTopic.isEmpty()) { + MqttSettings.unsubscribe(_dischargeCurrentLimitTopic); + } } void MqttBattery::onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, @@ -125,3 +149,38 @@ void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties con *voltage, topic); } } + +void MqttBattery::onMqttMessageDischargeCurrentLimit(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) +{ + auto amperage = Utils::getNumericValueFromMqttPayload("MqttBattery", + std::string(reinterpret_cast(payload), len), topic, + jsonPath); + + + if (!amperage.has_value()) { return; } + + auto const& config = Configuration.get(); + using Unit_t = BatteryAmperageUnit; + switch (config.Battery.MqttAmperageUnit) { + case Unit_t::MilliAmps: + *amperage /= 1000; + break; + default: + break; + } + + if (*amperage < 0) { + MessageOutput.printf("MqttBattery: Implausible amperage '%.2f' in topic '%s'\r\n", + *amperage, topic); + return; + } + + _stats->setDischargeCurrentLimit(*amperage, millis()); + + if (_verboseLogging) { + MessageOutput.printf("MqttBattery: Updated amperage to %.2f from '%s'\r\n", + *amperage, topic); + } +} diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index a3b836e4c..6589d7fec 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -901,7 +901,7 @@ int32_t PowerLimiterClass::getSolarPower() int32_t PowerLimiterClass::getBatteryDischargeLimit() { - auto currentLimit = Battery.getStats()->getDischargeCurrentLimitation(); + auto currentLimit = Battery.getDischargeCurrentLimit(); if (currentLimit == FLT_MAX) { // the returned value is arbitrary, as long as it's diff --git a/src/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index d1e7d94c7..f75326895 100644 --- a/src/PylontechCanReceiver.cpp +++ b/src/PylontechCanReceiver.cpp @@ -17,11 +17,11 @@ void PylontechCanReceiver::onMessage(twai_message_t rx_message) case 0x351: { _stats->_chargeVoltage = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1); _stats->_chargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1); - _stats->_dischargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); + _stats->setDischargeCurrentLimit(this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1), millis()); if (_verboseLogging) { MessageOutput.printf("[Pylontech] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f\r\n", - _stats->_chargeVoltage, _stats->_chargeCurrentLimitation, _stats->_dischargeCurrentLimitation); + _stats->_chargeVoltage, _stats->_chargeCurrentLimitation, _stats->getDischargeCurrentLimit()); } break; } @@ -154,7 +154,7 @@ void PylontechCanReceiver::dummyData() _stats->setSoC(42, 0/*precision*/, millis()); _stats->_chargeVoltage = dummyFloat(50); _stats->_chargeCurrentLimitation = dummyFloat(33); - _stats->_dischargeCurrentLimitation = dummyFloat(12); + _stats->setDischargeCurrentLimit(dummyFloat(12), millis()); _stats->_stateOfHealth = 99; _stats->setVoltage(48.67, millis()); _stats->setCurrent(dummyFloat(-1), 1/*precision*/, millis()); diff --git a/src/PytesCanReceiver.cpp b/src/PytesCanReceiver.cpp index e4b22e51a..bcdd0d274 100644 --- a/src/PytesCanReceiver.cpp +++ b/src/PytesCanReceiver.cpp @@ -16,13 +16,13 @@ void PytesCanReceiver::onMessage(twai_message_t rx_message) case 0x351: { _stats->_chargeVoltageLimit = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1); _stats->_chargeCurrentLimit = this->scaleValue(this->readUnsignedInt16(rx_message.data + 2), 0.1); - _stats->_dischargeCurrentLimit = this->scaleValue(this->readUnsignedInt16(rx_message.data + 4), 0.1); + _stats->setDischargeCurrentLimit(this->scaleValue(this->readUnsignedInt16(rx_message.data + 4), 0.1), millis()); _stats->_dischargeVoltageLimit = this->scaleValue(this->readSignedInt16(rx_message.data + 6), 0.1); if (_verboseLogging) { MessageOutput.printf("[Pytes] chargeVoltageLimit: %f chargeCurrentLimit: %f dischargeCurrentLimit: %f dischargeVoltageLimit: %f\r\n", _stats->_chargeVoltageLimit, _stats->_chargeCurrentLimit, - _stats->_dischargeCurrentLimit, _stats->_dischargeVoltageLimit); + _stats->getDischargeCurrentLimit(), _stats->_dischargeVoltageLimit); } break; } diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index aa8040d73..5965caeef 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -32,22 +32,12 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& root = response->getRoot(); - const CONFIG_T& config = Configuration.get(); - - root["enabled"] = config.Battery.Enabled; - root["verbose_logging"] = config.Battery.VerboseLogging; - root["provider"] = config.Battery.Provider; - root["jkbms_interface"] = config.Battery.JkBmsInterface; - root["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval; - root["mqtt_soc_topic"] = config.Battery.MqttSocTopic; - root["mqtt_soc_json_path"] = config.Battery.MqttSocJsonPath; - root["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic; - root["mqtt_voltage_json_path"] = config.Battery.MqttVoltageJsonPath; - root["mqtt_voltage_unit"] = config.Battery.MqttVoltageUnit; - - response->setLength(); - request->send(response); + auto root = response->getRoot().as(); + auto& config = Configuration.get(); + + ConfigurationClass::serializeBatteryConfig(config.Battery, root); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiBatteryClass::onAdminGet(AsyncWebServerRequest* request) @@ -80,17 +70,8 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) return; } - CONFIG_T& config = Configuration.get(); - config.Battery.Enabled = root["enabled"].as(); - config.Battery.VerboseLogging = root["verbose_logging"].as(); - config.Battery.Provider = root["provider"].as(); - config.Battery.JkBmsInterface = root["jkbms_interface"].as(); - config.Battery.JkBmsPollingInterval = root["jkbms_polling_interval"].as(); - strlcpy(config.Battery.MqttSocTopic, root["mqtt_soc_topic"].as().c_str(), sizeof(config.Battery.MqttSocTopic)); - strlcpy(config.Battery.MqttSocJsonPath, root["mqtt_soc_json_path"].as().c_str(), sizeof(config.Battery.MqttSocJsonPath)); - strlcpy(config.Battery.MqttVoltageTopic, root["mqtt_voltage_topic"].as().c_str(), sizeof(config.Battery.MqttVoltageTopic)); - strlcpy(config.Battery.MqttVoltageJsonPath, root["mqtt_voltage_json_path"].as().c_str(), sizeof(config.Battery.MqttVoltageJsonPath)); - config.Battery.MqttVoltageUnit = static_cast(root["mqtt_voltage_unit"].as()); + auto& config = Configuration.get(); + ConfigurationClass::deserializeBatteryConfig(root.as(), config.Battery); WebApi.writeConfig(retMsg); diff --git a/webapp/src/components/BatteryView.vue b/webapp/src/components/BatteryView.vue index d5b180fe5..39d3c84f8 100644 --- a/webapp/src/components/BatteryView.vue +++ b/webapp/src/components/BatteryView.vue @@ -6,7 +6,6 @@
-
@@ -89,7 +88,7 @@
-
+
diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 384959405..91045f22a 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -684,7 +684,14 @@ "JkBmsInterfaceUart": "TTL-UART an der MCU", "JkBmsInterfaceTransceiver": "RS-485 Transceiver an der MCU", "PollingInterval": "Abfrageintervall", - "Seconds": "@:base.Seconds" + "Seconds": "@:base.Seconds", + "DischargeCurrentLimitConfiguration": "Einstellungen Entladestromlimit", + "LimitDischargeCurrent": "Entladestrom limitieren", + "DischargeCurrentLimit": "max. Entladestrom", + "UseBatteryReportedDischargeCurrentLimit": "Von der Batterie übermitteltes Limit verwenden", + "BatteryReportedDischargeCurrentLimitInfo": "Hinweis: Das niedrigste Limit wird angewendet, wobei das von der Batterie übermittelte Entladestromlimit nur verwendet wird, wenn in der letzten Minute ein Update eingegangen ist; andernfalls dient das zuvor festgelegte Limit als Fallback.", + "MqttDischargeCurrentTopic": "Topic für Entladestromlimit", + "MqttAmperageUnit": "Einheit" }, "inverteradmin": { "InverterSettings": "Wechselrichter Einstellungen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index b94ad01b9..a851f7a27 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -687,7 +687,14 @@ "JkBmsInterfaceUart": "TTL-UART on MCU", "JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU", "PollingInterval": "Polling Interval", - "Seconds": "@:base.Seconds" + "Seconds": "@:base.Seconds", + "DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings", + "LimitDischargeCurrent": "Limit Discharge Current", + "DischargeCurrentLimit": "max. Discharge Current", + "UseBatteryReportedDischargeCurrentLimit": "Use Battery-Reported limit", + "BatteryReportedDischargeCurrentLimitInfo": "Hint: The lowest limit will be applied, with the battery-reported discharge current limit used only if an update was received in the last minute; otherwise, the previously specified limit will act as a fallback.", + "MqttDischargeCurrentTopic": "Discharge Current Limit Value Topic", + "MqttAmperageUnit": "Unit" }, "inverteradmin": { "InverterSettings": "Inverter Settings", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 852dc8331..43bfbc4a1 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -610,7 +610,14 @@ "JkBmsInterfaceUart": "TTL-UART on MCU", "JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU", "PollingInterval": "Polling Interval", - "Seconds": "@:base.Seconds" + "Seconds": "@:base.Seconds", + "DischargeCurrentLimitConfiguration": "Discharge Current Limit Settings", + "LimitDischargeCurrent": "Limit Discharge Current", + "DischargeCurrentLimit": "max. Discharge Current", + "UseBatteryReportedDischargeCurrentLimit": "Use Battery-Reported limit", + "BatteryReportedDischargeCurrentLimitInfo": "Hint: The lowest limit will be applied, with the battery-reported discharge current limit used only if an update was received in the last minute; otherwise, the previously specified limit will act as a fallback.", + "MqttDischargeCurrentTopic": "Discharge Current Limit Value Topic", + "MqttAmperageUnit": "Unit" }, "inverteradmin": { "InverterSettings": "Paramètres des onduleurs", diff --git a/webapp/src/types/BatteryConfig.ts b/webapp/src/types/BatteryConfig.ts index 348ed2a32..a781b74a0 100644 --- a/webapp/src/types/BatteryConfig.ts +++ b/webapp/src/types/BatteryConfig.ts @@ -9,4 +9,10 @@ export interface BatteryConfig { mqtt_voltage_topic: string; mqtt_voltage_json_path: string; mqtt_voltage_unit: number; + enable_discharge_current_limit: boolean; + discharge_current_limit: number; + use_battery_reported_discharge_current_limit: boolean; + mqtt_discharge_current_topic: string; + mqtt_discharge_current_json_path: string; + mqtt_amperage_unit: number; } diff --git a/webapp/src/types/BatteryDataStatus.ts b/webapp/src/types/BatteryDataStatus.ts index 964e4858d..402a407cb 100644 --- a/webapp/src/types/BatteryDataStatus.ts +++ b/webapp/src/types/BatteryDataStatus.ts @@ -14,5 +14,6 @@ export interface Battery { hwversion: string; data_age: number; values: BatteryData[]; + showIssues: boolean; issues: number[]; } diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue index bd54fbc08..84852f7aa 100644 --- a/webapp/src/views/BatteryAdminView.vue +++ b/webapp/src/views/BatteryAdminView.vue @@ -120,6 +120,78 @@ + + + + + + + + @@ -167,6 +239,10 @@ export default defineComponent({ { key: 1, value: 'dV' }, { key: 0, value: 'V' }, ], + amperageUnitTypeList: [ + { key: 1, value: 'mA' }, + { key: 0, value: 'A' }, + ], }; }, created() {