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.
This commit is contained in:
Bernhard Kirchen 2023-08-04 12:35:37 +02:00 committed by GitHub
parent 5335ec1bde
commit aff7924411
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 105 additions and 72 deletions

View File

@ -6,6 +6,7 @@
#include <Arduino.h>
#include <Hoymiles.h>
#include <memory>
#include <functional>
#define PL_UI_STATE_INACTIVE 0
#define PL_UI_STATE_CHARGING 1
@ -81,9 +82,12 @@ private:
void commitPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t limit, bool enablePowerProduction);
bool setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit);
int32_t getSolarChargePower();
float getLoadCorrectedVoltage(std::shared_ptr<InverterAbstract> inverter);
bool isStartThresholdReached(std::shared_ptr<InverterAbstract> inverter);
bool isStopThresholdReached(std::shared_ptr<InverterAbstract> inverter);
float getLoadCorrectedVoltage();
bool testThreshold(float socThreshold, float voltThreshold,
std::function<bool(float, float)> compare);
bool isStartThresholdReached();
bool isStopThresholdReached();
bool isBelowStopThreshold();
bool useFullSolarPassthrough(std::shared_ptr<InverterAbstract> inverter);
};

View File

@ -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<int32_t>(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<InverterAbstract> 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<InverterAbstract> 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<InverterAbstract> 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<InverterAbstract> 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<float>(dcTotalChnls) / dcProdChnls);
}
@ -542,14 +554,14 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> 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<InverterAbstract> 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<ChannelNum_t>(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<InverterAbstrac
return dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor);
}
bool PowerLimiterClass::isStartThresholdReached(std::shared_ptr<InverterAbstract> inverter)
bool PowerLimiterClass::testThreshold(float socThreshold, float voltThreshold,
std::function<bool(float, float)> 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; }
return compare(getLoadCorrectedVoltage(), voltThreshold);
}
float correctedDcVoltage = getLoadCorrectedVoltage(inverter);
return correctedDcVoltage >= config.PowerLimiter_VoltageStartThreshold;
}
bool PowerLimiterClass::isStopThresholdReached(std::shared_ptr<InverterAbstract> 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; }
);
}
float correctedDcVoltage = getLoadCorrectedVoltage(inverter);
return correctedDcVoltage <= config.PowerLimiter_VoltageStopThreshold;
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<InverterAbstract> inverter)
@ -684,7 +713,7 @@ bool PowerLimiterClass::useFullSolarPassthrough(std::shared_ptr<InverterAbstract
float dcVoltage = inverter->Statistics()->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);
}