Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inhibit solar passthrough while battery below stop threshold #354

Merged
merged 3 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions include/PowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -81,9 +82,12 @@ class PowerLimiterClass {
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);
};

Expand Down
167 changes: 98 additions & 69 deletions src/PowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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;
}
Expand All @@ -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();
Expand Down Expand Up @@ -396,6 +411,7 @@ bool PowerLimiterClass::canUseDirectSolarPower()
CONFIG_T& config = Configuration.get();

if (!config.PowerLimiter_SolarPassThroughEnabled
|| isBelowStopThreshold()
|| !config.Vedirect_Enabled
|| !VeDirect.isDataValid()) {
return false;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}

Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}

Expand All @@ -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;
Expand All @@ -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; }

float correctedDcVoltage = getLoadCorrectedVoltage(inverter);
return correctedDcVoltage >= config.PowerLimiter_VoltageStartThreshold;
return compare(getLoadCorrectedVoltage(), voltThreshold);
}

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; }
);
}

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
Expand All @@ -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;
}

Expand All @@ -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)
Expand All @@ -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);
}

Expand Down
Loading