Skip to content

Commit

Permalink
Feature: extend battery discharge limit support (#1245)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
AndreasBoehm authored Sep 13, 2024
1 parent 6318ab4 commit a6e7007
Show file tree
Hide file tree
Showing 20 changed files with 338 additions and 87 deletions.
2 changes: 2 additions & 0 deletions include/Battery.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class BatteryClass {
void init(Scheduler&);
void updateSettings();

float getDischargeCurrentLimit();

std::shared_ptr<BatteryStats const> getStats() const;

private:
Expand Down
27 changes: 18 additions & 9 deletions include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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 = "";
Expand All @@ -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 {
Expand All @@ -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;

Expand Down Expand Up @@ -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; }
Expand All @@ -154,7 +164,6 @@ class PytesBatteryStats : public BatteryStats {
float _chargeVoltageLimit;
float _chargeCurrentLimit;
float _dischargeVoltageLimit;
float _dischargeCurrentLimit;

uint16_t _stateOfHealth;

Expand Down Expand Up @@ -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; }
};
37 changes: 25 additions & 12 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
4 changes: 4 additions & 0 deletions include/MqttBattery.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class MqttBattery : public BatteryProvider {
bool _verboseLogging = false;
String _socTopic;
String _voltageTopic;
String _dischargeCurrentLimitTopic;
std::shared_ptr<MqttBatteryStats> _stats = std::make_shared<MqttBatteryStats>();

void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties,
Expand All @@ -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);
};
3 changes: 3 additions & 0 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions src/Battery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
40 changes: 33 additions & 7 deletions src/BatteryStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand All @@ -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));
Expand Down Expand Up @@ -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));
Expand Down
Loading

0 comments on commit a6e7007

Please sign in to comment.