Feature: extend battery discharge limit support (#1245)
* implements UI to configure battery discharge limit * adds support for discharge limit to MQTT battery provider * add option to hide `issues` section from battery live view (for MQTT battery)
This commit is contained in:
parent
6318ab4a8b
commit
a6e7007f4b
@ -21,6 +21,8 @@ public:
|
||||
void init(Scheduler&);
|
||||
void updateSettings();
|
||||
|
||||
float getDischargeCurrentLimit();
|
||||
|
||||
std::shared_ptr<BatteryStats const> getStats() const;
|
||||
|
||||
private:
|
||||
|
||||
@ -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; }
|
||||
};
|
||||
|
||||
@ -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 @@ public:
|
||||
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;
|
||||
|
||||
@ -17,6 +17,7 @@ private:
|
||||
bool _verboseLogging = false;
|
||||
String _socTopic;
|
||||
String _voltageTopic;
|
||||
String _dischargeCurrentLimitTopic;
|
||||
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();
|
||||
|
||||
void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
|
||||
@ -25,4 +26,7 @@ private:
|
||||
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);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -89,9 +89,32 @@ void BatteryStats::getLiveViewData(JsonVariant& root) const
|
||||
}
|
||||
root["data_age"] = getAgeSeconds();
|
||||
|
||||
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));
|
||||
|
||||
@ -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<JsonObject>();
|
||||
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<JsonObject>();
|
||||
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;
|
||||
|
||||
@ -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<float>("MqttBattery",
|
||||
std::string(reinterpret_cast<const char*>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -32,22 +32,12 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request)
|
||||
}
|
||||
|
||||
AsyncJsonResponse* response = new AsyncJsonResponse();
|
||||
auto& root = response->getRoot();
|
||||
const CONFIG_T& config = Configuration.get();
|
||||
auto root = response->getRoot().as<JsonObject>();
|
||||
auto& 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;
|
||||
ConfigurationClass::serializeBatteryConfig(config.Battery, root);
|
||||
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
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<bool>();
|
||||
config.Battery.VerboseLogging = root["verbose_logging"].as<bool>();
|
||||
config.Battery.Provider = root["provider"].as<uint8_t>();
|
||||
config.Battery.JkBmsInterface = root["jkbms_interface"].as<uint8_t>();
|
||||
config.Battery.JkBmsPollingInterval = root["jkbms_polling_interval"].as<uint8_t>();
|
||||
strlcpy(config.Battery.MqttSocTopic, root["mqtt_soc_topic"].as<String>().c_str(), sizeof(config.Battery.MqttSocTopic));
|
||||
strlcpy(config.Battery.MqttSocJsonPath, root["mqtt_soc_json_path"].as<String>().c_str(), sizeof(config.Battery.MqttSocJsonPath));
|
||||
strlcpy(config.Battery.MqttVoltageTopic, root["mqtt_voltage_topic"].as<String>().c_str(), sizeof(config.Battery.MqttVoltageTopic));
|
||||
strlcpy(config.Battery.MqttVoltageJsonPath, root["mqtt_voltage_json_path"].as<String>().c_str(), sizeof(config.Battery.MqttVoltageJsonPath));
|
||||
config.Battery.MqttVoltageUnit = static_cast<BatteryVoltageUnit>(root["mqtt_voltage_unit"].as<uint8_t>());
|
||||
auto& config = Configuration.get();
|
||||
ConfigurationClass::deserializeBatteryConfig(root.as<JsonObject>(), config.Battery);
|
||||
|
||||
WebApi.writeConfig(retMsg);
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
</div>
|
||||
|
||||
<div v-else-if="'values' in batteryData">
|
||||
<!-- suppress the card for MQTT battery provider -->
|
||||
<div class="row gy-3 mt-0">
|
||||
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
|
||||
<div class="card">
|
||||
@ -89,7 +88,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="col order-1" v-show="batteryData.showIssues">
|
||||
<div class="card">
|
||||
<div :class="{ 'card-header': true, 'border-bottom-0': maxIssueValue === 0 }">
|
||||
<div class="d-flex flex-row justify-content-between align-items-baseline">
|
||||
|
||||
@ -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": "<b>Hinweis:</b> 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",
|
||||
|
||||
@ -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": "<b>Hint:</b> 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",
|
||||
|
||||
@ -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": "<b>Hint:</b> 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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -14,5 +14,6 @@ export interface Battery {
|
||||
hwversion: string;
|
||||
data_age: number;
|
||||
values: BatteryData[];
|
||||
showIssues: boolean;
|
||||
issues: number[];
|
||||
}
|
||||
|
||||
@ -120,6 +120,78 @@
|
||||
</CardElement>
|
||||
</template>
|
||||
|
||||
<CardElement
|
||||
:text="$t('batteryadmin.DischargeCurrentLimitConfiguration')"
|
||||
textVariant="text-bg-primary"
|
||||
addSpace
|
||||
>
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.LimitDischargeCurrent')"
|
||||
v-model="batteryConfigList.enable_discharge_current_limit"
|
||||
type="checkbox"
|
||||
/>
|
||||
|
||||
<template v-if="batteryConfigList.enable_discharge_current_limit">
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.DischargeCurrentLimit')"
|
||||
v-model="batteryConfigList.discharge_current_limit"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
postfix="A"
|
||||
/>
|
||||
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.UseBatteryReportedDischargeCurrentLimit')"
|
||||
v-model="batteryConfigList.use_battery_reported_discharge_current_limit"
|
||||
type="checkbox"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="batteryConfigList.enable_discharge_current_limit && batteryConfigList.use_battery_reported_discharge_current_limit">
|
||||
<div
|
||||
class="alert alert-secondary"
|
||||
role="alert"
|
||||
v-html="$t('batteryadmin.BatteryReportedDischargeCurrentLimitInfo')"
|
||||
></div>
|
||||
|
||||
<template v-if="batteryConfigList.provider == 2">
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.MqttDischargeCurrentTopic')"
|
||||
v-model="batteryConfigList.mqtt_discharge_current_topic"
|
||||
type="text"
|
||||
maxlength="256"
|
||||
/>
|
||||
|
||||
<InputElement
|
||||
:label="$t('batteryadmin.MqttJsonPath')"
|
||||
v-model="batteryConfigList.mqtt_discharge_current_json_path"
|
||||
type="text"
|
||||
maxlength="128"
|
||||
:tooltip="$t('batteryadmin.MqttJsonPathDescription')"
|
||||
/>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label for="mqtt_amperage_unit" class="col-sm-2 col-form-label">
|
||||
{{ $t('batteryadmin.MqttAmperageUnit') }}
|
||||
</label>
|
||||
|
||||
<div class="col-sm-10">
|
||||
<select
|
||||
id="mqtt_amperage_unit"
|
||||
class="form-select"
|
||||
v-model="batteryConfigList.mqtt_amperage_unit"
|
||||
>
|
||||
<option v-for="u in amperageUnitTypeList" :key="u.key" :value="u.key">
|
||||
{{ u.value }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</CardElement>
|
||||
|
||||
<FormFooter @reload="getBatteryConfig" />
|
||||
</form>
|
||||
</BasePage>
|
||||
@ -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() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user