From aff7924411597fc651114120445ac7a534cf993b Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 4 Aug 2023 12:35:37 +0200 Subject: [PATCH] Inhibit solar passthrough while battery below stop threshold (#354) * DPL: improve verbose logging * shorten DPL log prefix * canUseDirectSolarPower() was printed two times * _batteryDischargeEnabled was printed two times * convert boolean values to human-readable strings * add units where possible * split messages into block "before calculating new limit" and "after calculating new limit", as the latter cannot rely on _inverter being available. * order messages such that variables whose value is derived from other variables are printed later than their dependencies. * merge output into blocks (one instance near "Printout some stats") * remove more redundant info (produced in functions outside loop()) * print target grid consumption * DPL: inhibit solar passthrough while stop threshold reached * DPL: implement and use isBelowStopThreshold() we only want to inhibit solar passthrough if the SoC is *below* the stop threshold, not if it is equal to the stop threshold. otherwise, when discharging, we would discharge until the battery reached the stop threshold, then we would also inhibit solar passthrough, until the battery is charged to the SoC stop threshold plus one percent. --- include/PowerLimiter.h | 10 ++- src/PowerLimiter.cpp | 167 ++++++++++++++++++++++++----------------- 2 files changed, 105 insertions(+), 72 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index d23608cb..27b8e15d 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -6,6 +6,7 @@ #include #include #include +#include #define PL_UI_STATE_INACTIVE 0 #define PL_UI_STATE_CHARGING 1 @@ -81,9 +82,12 @@ private: void commitPowerLimit(std::shared_ptr inverter, int32_t limit, bool enablePowerProduction); bool setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit); int32_t getSolarChargePower(); - float getLoadCorrectedVoltage(std::shared_ptr inverter); - bool isStartThresholdReached(std::shared_ptr inverter); - bool isStopThresholdReached(std::shared_ptr inverter); + float getLoadCorrectedVoltage(); + bool testThreshold(float socThreshold, float voltThreshold, + std::function compare); + bool isStartThresholdReached(); + bool isStopThresholdReached(); + bool isBelowStopThreshold(); bool useFullSolarPassthrough(std::shared_ptr inverter); }; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index b84cecc1..f76729ef 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -228,12 +228,12 @@ void PowerLimiterClass::loop() } if (_verboseLogging) { - MessageOutput.println("[PowerLimiterClass::loop] ******************* ENTER **********************"); + MessageOutput.println("[DPL::loop] ******************* ENTER **********************"); } // Check if next inverter restart time is reached if ((_nextInverterRestart > 1) && (_nextInverterRestart <= millis())) { - MessageOutput.println("[PowerLimiterClass::loop] send inverter restart"); + MessageOutput.println("[DPL::loop] send inverter restart"); _inverter->sendRestartControlRequest(); calcNextInverterRestart(); } @@ -246,34 +246,27 @@ void PowerLimiterClass::loop() if (getLocalTime(&timeinfo, 5)) { calcNextInverterRestart(); } else { - MessageOutput.println("[PowerLimiterClass::loop] inverter restart calculation: NTP not ready"); + MessageOutput.println("[DPL::loop] inverter restart calculation: NTP not ready"); _nextCalculateCheck += 5000; } } } - // Printout some stats - if (_verboseLogging && millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) { - float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); - MessageOutput.printf("[PowerLimiterClass::loop] dcVoltage: %.2f Voltage Start Threshold: %.2f Voltage Stop Threshold: %.2f inverter->isProducing(): %d\r\n", - dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, _inverter->isProducing()); - } - // Battery charging cycle conditions // First we always disable discharge if the battery is empty - if (isStopThresholdReached(_inverter)) { + if (isStopThresholdReached()) { // Disable battery discharge when empty _batteryDischargeEnabled = false; } else { // UI: Solar Passthrough Enabled -> false // Battery discharge can be enabled when start threshold is reached - if (!config.PowerLimiter_SolarPassThroughEnabled && isStartThresholdReached(_inverter)) { + if (!config.PowerLimiter_SolarPassThroughEnabled && isStartThresholdReached()) { _batteryDischargeEnabled = true; } // UI: Solar Passthrough Enabled -> true && EMPTY_AT_NIGHT if (config.PowerLimiter_SolarPassThroughEnabled && config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) { - if(isStartThresholdReached(_inverter)) { + if(isStartThresholdReached()) { // In this case we should only discharge the battery as long it is above startThreshold _batteryDischargeEnabled = true; } @@ -285,24 +278,46 @@ void PowerLimiterClass::loop() // UI: Solar Passthrough Enabled -> true && EMPTY_WHEN_FULL // Battery discharge can be enabled when start threshold is reached - if (config.PowerLimiter_SolarPassThroughEnabled && isStartThresholdReached(_inverter) && config.PowerLimiter_BatteryDrainStategy == EMPTY_WHEN_FULL) { + if (config.PowerLimiter_SolarPassThroughEnabled && isStartThresholdReached() && config.PowerLimiter_BatteryDrainStategy == EMPTY_WHEN_FULL) { _batteryDischargeEnabled = true; } } - // Calculate and set Power Limit + + if (_verboseLogging) { + MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %li ms\r\n", + (config.Battery_Enabled?"enabled":"disabled"), Battery.stateOfCharge, + config.PowerLimiter_BatterySocStartThreshold, config.PowerLimiter_BatterySocStopThreshold, + millis() - Battery.stateOfChargeLastUpdate); + + float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t)config.PowerLimiter_InverterChannelId, FLD_UDC); + MessageOutput.printf("[DPL::loop] dcVoltage: %.2f V, loadCorrectedVoltage: %.2f V, StartTH: %.2f V, StopTH: %.2f V\r\n", + dcVoltage, getLoadCorrectedVoltage(), + config.PowerLimiter_VoltageStartThreshold, + config.PowerLimiter_VoltageStopThreshold); + + MessageOutput.printf("[DPL::loop] StartTH reached: %s, StopTH reached: %s, inverter %s producing\r\n", + (isStartThresholdReached()?"yes":"no"), + (isStopThresholdReached()?"yes":"no"), + (_inverter->isProducing()?"is":"is NOT")); + + MessageOutput.printf("[DPL::loop] SolarPT %s, Drain Strategy: %i, canUseDirectSolarPower: %s\r\n", + (config.PowerLimiter_SolarPassThroughEnabled?"enabled":"disabled"), + config.PowerLimiter_BatteryDrainStategy, (canUseDirectSolarPower()?"yes":"no")); + + MessageOutput.printf("[DPL::loop] battery discharging %s, PowerMeter: %d W, target consumption: %d W\r\n", + (_batteryDischargeEnabled?"allowed":"prevented"), + static_cast(round(PowerMeter.getPowerTotal())), + config.PowerLimiter_TargetPowerConsumption); + } + + // Calculate and set Power Limit (NOTE: might reset _inverter to nullptr!) int32_t newPowerLimit = calcPowerLimit(_inverter, canUseDirectSolarPower(), _batteryDischargeEnabled); bool limitUpdated = setNewPowerLimit(_inverter, newPowerLimit); + if (_verboseLogging) { - MessageOutput.printf("[PowerLimiterClass::loop] Status: SolarPT enabled %i, Drain Strategy: %i, canUseDirectSolarPower: %i, Batt discharge: %i\r\n", - config.PowerLimiter_SolarPassThroughEnabled, config.PowerLimiter_BatteryDrainStategy, canUseDirectSolarPower(), _batteryDischargeEnabled); - // the inverter might have been reset to nullptr due to a shutdown - if (_inverter != nullptr) { - MessageOutput.printf("[PowerLimiterClass::loop] Status: StartTH %i, StopTH: %i, loadCorrectedV %f\r\n", - isStartThresholdReached(_inverter), isStopThresholdReached(_inverter), getLoadCorrectedVoltage(_inverter)); - } - MessageOutput.printf("[PowerLimiterClass::loop] Status Batt: Ena: %i, SOC: %i, StartTH: %i, StopTH: %i, LastUpdate: %li\r\n", - config.Battery_Enabled, Battery.stateOfCharge, config.PowerLimiter_BatterySocStartThreshold, config.PowerLimiter_BatterySocStopThreshold, millis() - Battery.stateOfChargeLastUpdate); - MessageOutput.printf("[PowerLimiterClass::loop] ******************* Leaving PL, PL set to: %i, SP: %i, Batt: %i, PM: %f\r\n", newPowerLimit, canUseDirectSolarPower(), _batteryDischargeEnabled, round(PowerMeter.getPowerTotal())); + MessageOutput.printf("[DPL::loop] ******************* Leaving PL, calculated limit: %d W, requested limit: %d W (%s)\r\n", + newPowerLimit, _lastRequestedPowerLimit, + (limitUpdated?"updated from calculated":"kept last requested")); } _lastCalculation = millis(); @@ -396,6 +411,7 @@ bool PowerLimiterClass::canUseDirectSolarPower() CONFIG_T& config = Configuration.get(); if (!config.PowerLimiter_SolarPassThroughEnabled + || isBelowStopThreshold() || !config.Vedirect_Enabled || !VeDirect.isDataValid()) { return false; @@ -463,17 +479,13 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve if (solarPowerEnabled && !batteryDischargeEnabled) { // Case 2 - Limit power to solar power only if (_verboseLogging) { - MessageOutput.printf("[PowerLimiterClass::loop] Consuming Solar Power Only -> adjustedVictronChargePower: %d, powerConsumption: %d \r\n", + MessageOutput.printf("[DPL::loop] Consuming Solar Power Only -> adjustedVictronChargePower: %d W, newPowerLimit: %d W\r\n", adjustedVictronChargePower, newPowerLimit); } newPowerLimit = std::min(newPowerLimit, adjustedVictronChargePower); } - if (_verboseLogging) { - MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit); - } - return newPowerLimit; } @@ -482,7 +494,7 @@ void PowerLimiterClass::commitPowerLimit(std::shared_ptr inver // disable power production as soon as possible. // setting the power limit is less important. if (!enablePowerProduction && inverter->isProducing()) { - MessageOutput.println("[PowerLimiterClass::commitPowerLimit] Stopping inverter..."); + MessageOutput.println("[DPL::commitPowerLimit] Stopping inverter..."); inverter->sendPowerControlRequest(false); } @@ -494,7 +506,7 @@ void PowerLimiterClass::commitPowerLimit(std::shared_ptr inver // enable power production only after setting the desired limit, // such that an older, greater limit will not cause power spikes. if (enablePowerProduction && !inverter->isProducing()) { - MessageOutput.println("[PowerLimiterClass::commitPowerLimit] Starting up inverter..."); + MessageOutput.println("[DPL::commitPowerLimit] Starting up inverter..."); inverter->sendPowerControlRequest(true); } } @@ -531,7 +543,7 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver } } if ((dcProdChnls > 0) && (dcProdChnls != dcTotalChnls)) { - MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] %d channels total, %d producing channels, scaling power limit\r\n", + MessageOutput.printf("[DPL::setNewPowerLimit] %d channels total, %d producing channels, scaling power limit\r\n", dcTotalChnls, dcProdChnls); effPowerLimit = round(effPowerLimit * static_cast(dcTotalChnls) / dcProdChnls); } @@ -542,14 +554,14 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver auto diff = std::abs(effPowerLimit - _lastRequestedPowerLimit); if ( diff < config.PowerLimiter_TargetPowerConsumptionHysteresis) { if (_verboseLogging) { - MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] reusing old limit: %d W, diff: %d, hysteresis: %d\r\n", + MessageOutput.printf("[DPL::setNewPowerLimit] reusing old limit: %d W, diff: %d W, hysteresis: %d W\r\n", _lastRequestedPowerLimit, diff, config.PowerLimiter_TargetPowerConsumptionHysteresis); } return false; } if (_verboseLogging) { - MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] using new limit: %d W, requested power limit: %d\r\n", + MessageOutput.printf("[DPL::setNewPowerLimit] using new limit: %d W, requested power limit: %d W\r\n", effPowerLimit, newPowerLimit); } @@ -566,12 +578,19 @@ int32_t PowerLimiterClass::getSolarChargePower() return VeDirect.veFrame.V * VeDirect.veFrame.I; } -float PowerLimiterClass::getLoadCorrectedVoltage(std::shared_ptr inverter) +float PowerLimiterClass::getLoadCorrectedVoltage() { + if (!_inverter) { + // there should be no need to call this method if no target inverter is known + MessageOutput.println(F("DPL getLoadCorrectedVoltage: no inverter (programmer error)")); + return 0.0; + } + CONFIG_T& config = Configuration.get(); - float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); - float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); + auto channel = static_cast(config.PowerLimiter_InverterChannelId); + float acPower = _inverter->Statistics()->getChannelFieldValue(TYPE_AC, channel, FLD_PAC); + float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, channel, FLD_UDC); if (dcVoltage <= 0.0) { return 0.0; @@ -580,44 +599,54 @@ float PowerLimiterClass::getLoadCorrectedVoltage(std::shared_ptr inverter) +bool PowerLimiterClass::testThreshold(float socThreshold, float voltThreshold, + std::function compare) { CONFIG_T& config = Configuration.get(); - // Check if the Battery interface is enabled and the SOC start threshold is reached - if (config.Battery_Enabled - && config.PowerLimiter_BatterySocStartThreshold > 0.0 + // prefer SoC provided through battery interface + if (config.Battery_Enabled && socThreshold > 0.0 && (millis() - Battery.stateOfChargeLastUpdate) < 60000) { - return Battery.stateOfCharge >= config.PowerLimiter_BatterySocStartThreshold; + return compare(Battery.stateOfCharge, socThreshold); } - // Otherwise we use the voltage threshold - if (config.PowerLimiter_VoltageStartThreshold <= 0.0) { - return false; - } + // use voltage threshold as fallback + if (voltThreshold <= 0.0) { return false; } - float correctedDcVoltage = getLoadCorrectedVoltage(inverter); - return correctedDcVoltage >= config.PowerLimiter_VoltageStartThreshold; + return compare(getLoadCorrectedVoltage(), voltThreshold); } -bool PowerLimiterClass::isStopThresholdReached(std::shared_ptr inverter) +bool PowerLimiterClass::isStartThresholdReached() { CONFIG_T& config = Configuration.get(); - // Check if the Battery interface is enabled and the SOC stop threshold is reached - if (config.Battery_Enabled - && config.PowerLimiter_BatterySocStopThreshold > 0.0 - && (millis() - Battery.stateOfChargeLastUpdate) < 60000) { - return Battery.stateOfCharge <= config.PowerLimiter_BatterySocStopThreshold; - } + return testThreshold( + config.PowerLimiter_BatterySocStartThreshold, + config.PowerLimiter_VoltageStartThreshold, + [](float a, float b) -> bool { return a >= b; } + ); +} - // Otherwise we use the voltage threshold - if (config.PowerLimiter_VoltageStopThreshold <= 0.0) { - return false; - } +bool PowerLimiterClass::isStopThresholdReached() +{ + CONFIG_T& config = Configuration.get(); - float correctedDcVoltage = getLoadCorrectedVoltage(inverter); - return correctedDcVoltage <= config.PowerLimiter_VoltageStopThreshold; + return testThreshold( + config.PowerLimiter_BatterySocStopThreshold, + config.PowerLimiter_VoltageStopThreshold, + [](float a, float b) -> bool { return a <= b; } + ); +} + +bool PowerLimiterClass::isBelowStopThreshold() +{ + CONFIG_T& config = Configuration.get(); + + return testThreshold( + config.PowerLimiter_BatterySocStopThreshold, + config.PowerLimiter_VoltageStopThreshold, + [](float a, float b) -> bool { return a < b; } + ); } /// @brief calculate next inverter restart in millis @@ -628,7 +657,7 @@ void PowerLimiterClass::calcNextInverterRestart() // first check if restart is configured at all if (config.PowerLimiter_RestartHour < 0) { _nextInverterRestart = 1; - MessageOutput.println("[PowerLimiterClass::calcNextInverterRestart] _nextInverterRestart disabled"); + MessageOutput.println("[DPL::calcNextInverterRestart] _nextInverterRestart disabled"); return; } @@ -646,18 +675,18 @@ void PowerLimiterClass::calcNextInverterRestart() _nextInverterRestart = 1440 - dayMinutes + targetMinutes; } if (_verboseLogging) { - MessageOutput.printf("[PowerLimiterClass::calcNextInverterRestart] Localtime read %d %d / configured RestartHour %d\r\n", timeinfo.tm_hour, timeinfo.tm_min, config.PowerLimiter_RestartHour); - MessageOutput.printf("[PowerLimiterClass::calcNextInverterRestart] dayMinutes %d / targetMinutes %d\r\n", dayMinutes, targetMinutes); - MessageOutput.printf("[PowerLimiterClass::calcNextInverterRestart] next inverter restart in %d minutes\r\n", _nextInverterRestart); + MessageOutput.printf("[DPL::calcNextInverterRestart] Localtime read %d %d / configured RestartHour %d\r\n", timeinfo.tm_hour, timeinfo.tm_min, config.PowerLimiter_RestartHour); + MessageOutput.printf("[DPL::calcNextInverterRestart] dayMinutes %d / targetMinutes %d\r\n", dayMinutes, targetMinutes); + MessageOutput.printf("[DPL::calcNextInverterRestart] next inverter restart in %d minutes\r\n", _nextInverterRestart); } // then convert unit for next restart to milliseconds and add current uptime millis() _nextInverterRestart *= 60000; _nextInverterRestart += millis(); } else { - MessageOutput.println("[PowerLimiterClass::calcNextInverterRestart] getLocalTime not successful, no calculation"); + MessageOutput.println("[DPL::calcNextInverterRestart] getLocalTime not successful, no calculation"); _nextInverterRestart = 0; } - MessageOutput.printf("[PowerLimiterClass::calcNextInverterRestart] _nextInverterRestart @ %d millis\r\n", _nextInverterRestart); + MessageOutput.printf("[DPL::calcNextInverterRestart] _nextInverterRestart @ %d millis\r\n", _nextInverterRestart); } bool PowerLimiterClass::useFullSolarPassthrough(std::shared_ptr inverter) @@ -684,7 +713,7 @@ bool PowerLimiterClass::useFullSolarPassthrough(std::shared_ptrStatistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); if (_verboseLogging) { - MessageOutput.printf("[PowerLimiterClass::loop] useFullSolarPassthrough: FullSolarPT Start %f, FullSolarPT Stop: %f, dcVoltage: %f\r\n", + MessageOutput.printf("[DPL::loop] useFullSolarPassthrough: FullSolarPT Start %.2f V, FullSolarPT Stop: %.2f V, dcVoltage: %.2f V\r\n", config.PowerLimiter_FullSolarPassThroughStartVoltage, config.PowerLimiter_FullSolarPassThroughStopVoltage, dcVoltage); }