diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index d23608cb2..27b8e15d3 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 @@ class PowerLimiterClass { 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 b84cecc14..f76729ef7 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(); + + return testThreshold( + config.PowerLimiter_BatterySocStopThreshold, + config.PowerLimiter_VoltageStopThreshold, + [](float a, float b) -> bool { return a <= b; } + ); +} + +bool PowerLimiterClass::isBelowStopThreshold() +{ + 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; } + ); } /// @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); }