From 802a47cf4080fe69f26cb8b895b098cb82901c49 Mon Sep 17 00:00:00 2001 From: Tobias Diedrich Date: Sun, 25 Aug 2024 10:29:32 +0200 Subject: [PATCH] DPL: Honor battery-provided discharge power limit When the BMC provides a discharge current limit, apply the this limit in the DPL to the inverter power target when running from battery. In my test this worked well, when the limit dropped from 0.5C (25A) to 0.25C (12.5A) at 20% SoC, the actual measured current draw dropped to 12.3A. See https://github.com/helgeerbe/OpenDTU-OnBattery/issues/657 --- 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 94da35d78..49cb610de 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; @@ -96,6 +97,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 setManufacturer(String&& m) { _manufacturer = std::move(m); } @@ -135,6 +137,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 setManufacturer(String&& m) { _manufacturer = std::move(m); } 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 93f027b07..05258a8c0 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) {