From 6318ab4a8bbbda67031fd54fbb12ba3ea51c2773 Mon Sep 17 00:00:00 2001 From: ranma Date: Fri, 13 Sep 2024 20:36:16 +0200 Subject: [PATCH] Feature: DPL: Honor battery-provided discharge power limit (#1198) When the BMS provides a discharge current limit, apply this limit in the DPL to the inverter power target when running from battery. --- include/BatteryStats.h | 3 +++ include/PowerLimiter.h | 3 ++- src/PowerLimiter.cpp | 53 +++++++++++++++++++++++++++++++++--------- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 32c4af5a8..68a908137 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -46,6 +46,7 @@ class BatteryStats { virtual bool getImmediateChargingRequest() const { return false; }; virtual float getChargeCurrentLimitation() const { return FLT_MAX; }; + virtual float getDischargeCurrentLimitation() const { return FLT_MAX; }; protected: virtual void mqttPublish() const; @@ -98,6 +99,7 @@ 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; } @@ -136,6 +138,7 @@ class PytesBatteryStats : public BatteryStats { void getLiveViewData(JsonVariant& root) const final; void mqttPublish() const final; float getChargeCurrentLimitation() const { return _chargeCurrentLimit; } ; + float getDischargeCurrentLimitation() const { return _dischargeCurrentLimit; } ; private: void setLastUpdate(uint32_t ts) { _lastUpdate = ts; } diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index db69a303b..c55d1d4bb 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -90,10 +90,11 @@ class PowerLimiterClass { int32_t inverterPowerDcToAc(std::shared_ptr inverter, int32_t dcPower); void unconditionalSolarPassthrough(std::shared_ptr inverter); bool canUseDirectSolarPower(); - bool calcPowerLimit(std::shared_ptr inverter, int32_t solarPower, bool batteryPower); + bool calcPowerLimit(std::shared_ptr inverter, int32_t solarPower, int32_t batteryPowerLimit, bool batteryPower); bool updateInverter(); bool setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit); int32_t getSolarPower(); + int32_t getBatteryDischargeLimit(); float getLoadCorrectedVoltage(); bool testThreshold(float socThreshold, float voltThreshold, std::function compare); diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index c4f5382ea..a3b836e4c 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -288,7 +288,7 @@ void PowerLimiterClass::loop() }; // Calculate and set Power Limit (NOTE: might reset _inverter to nullptr!) - bool limitUpdated = calcPowerLimit(_inverter, getSolarPower(), _batteryDischargeEnabled); + bool limitUpdated = calcPowerLimit(_inverter, getSolarPower(), getBatteryDischargeLimit(), _batteryDischargeEnabled); _lastCalculation = millis(); @@ -423,17 +423,17 @@ uint8_t PowerLimiterClass::getPowerLimiterState() { } // Logic table ("PowerMeter value" can be "base load setting" as a fallback) -// | Case # | batteryPower | solarPower | useFullSolarPassthrough | Resulting inverter limit | -// | 1 | false | < 20 W | doesn't matter | 0 (inverter off) | -// | 2 | false | >= 20 W | doesn't matter | min(PowerMeter value, solarPower) | -// | 3 | true | doesn't matter | false | PowerMeter value (Battery can supply unlimited energy) | -// | 4 | true | fully passed | true | max(PowerMeter value, solarPower) | +// | Case # | batteryPower | solarPower | batteryLimit | useFullSolarPassthrough | Resulting inverter limit | +// | 1 | false | < 20 W | doesn't matter | doesn't matter | 0 (inverter off) | +// | 2 | false | >= 20 W | doesn't matter | doesn't matter | min(PowerMeter value, solarPower) | +// | 3 | true | fully passed | applied | false | min(PowerMeter value batteryLimit+solarPower) | +// | 4 | true | fully passed | doesn't matter | true | max(PowerMeter value, solarPower) | -bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, int32_t solarPowerDC, bool batteryPower) +bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, int32_t solarPowerDC, int32_t batteryPowerLimitDC, bool batteryPower) { if (_verboseLogging) { - MessageOutput.printf("[DPL::calcPowerLimit] battery use %s, solar power (DC): %d W\r\n", - (batteryPower?"allowed":"prevented"), solarPowerDC); + MessageOutput.printf("[DPL::calcPowerLimit] battery use %s, solar power (DC): %d W, battery limit (DC): %d W\r\n", + (batteryPower?"allowed":"prevented"), solarPowerDC, batteryPowerLimitDC); } // Case 1: @@ -458,6 +458,7 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte // old and unreliable. TODO(schlimmchen): is this comment outdated? auto inverterOutput = static_cast(inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC)); + auto batteryPowerLimitAC = inverterPowerDcToAc(inverter, batteryPowerLimitDC); auto solarPowerAC = inverterPowerDcToAc(inverter, solarPowerDC); auto const& config = Configuration.get(); @@ -473,11 +474,12 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte (meterIncludesInv?"":"NOT ")); MessageOutput.printf("[DPL::calcPowerLimit] power meter value: %d W, " - "power meter valid: %s, inverter output: %d W, solar power (AC): %d W\r\n", + "power meter valid: %s, inverter output: %d W, solar power (AC): %d W, battery limit (AC): %d W\r\n", meterValue, (meterValid?"yes":"no"), inverterOutput, - solarPowerAC); + solarPowerAC, + batteryPowerLimitAC); } auto newPowerLimit = baseLoad; @@ -507,6 +509,15 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte } return setNewPowerLimit(inverter, newPowerLimit); + } else { // on batteryPower + // Apply battery-provided discharge power limit. + if (newPowerLimit > batteryPowerLimitAC + solarPowerAC) { + newPowerLimit = batteryPowerLimitAC + solarPowerAC; + if (_verboseLogging) { + MessageOutput.printf("[DPL::calcPowerLimit] limited by battery to: %d W\r\n", + newPowerLimit); + } + } } // Case 4: @@ -888,6 +899,26 @@ int32_t PowerLimiterClass::getSolarPower() return solarPower; } +int32_t PowerLimiterClass::getBatteryDischargeLimit() +{ + auto currentLimit = Battery.getStats()->getDischargeCurrentLimitation(); + + if (currentLimit == FLT_MAX) { + // the returned value is arbitrary, as long as it's + // greater than the inverters max DC power consumption. + return 10 * 1000; + } + + // This uses inverter voltage since there is a voltage drop between + // battery and inverter, so since we are regulating the inverter + // power we should use its voltage. + auto const& config = Configuration.get(); + auto channel = static_cast(config.PowerLimiter.InverterChannelId); + float inverterVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, channel, FLD_UDC); + + return static_cast(inverterVoltage * currentLimit); +} + float PowerLimiterClass::getLoadCorrectedVoltage() { if (!_inverter) {