Skip to content
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
8 changes: 6 additions & 2 deletions include/PowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ class PowerLimiterClass {

std::deque<std::unique_ptr<PowerLimiterInverter>> _inverters;
std::deque<std::unique_ptr<PowerLimiterInverter>> _retirees;
bool _batteryDischargeEnabled = false;
bool _nighttimeDischarging = false;

enum class BatteryState : uint8_t { STOP = 0, NO_DISCHARGE = 1, DISCHARGE_ALLOWED = 2, DISCHARGE_NIGHT = 3 };
BatteryState _batteryState = BatteryState::STOP;
bool _fromStart = false;
bool _oneStopPerNightDone = false;

std::pair<bool, uint32_t> _nextInverterRestart = { false, 0 };
bool _fullSolarPassThroughActive = false;
float _loadCorrectedVoltage = 0.0f;
Expand Down
104 changes: 70 additions & 34 deletions src/PowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,38 +237,67 @@ void PowerLimiterClass::loop()

autoRestartInverters();

auto getBatteryPower = [this,&config]() -> bool {
if (!usesBatteryPoweredInverter()) { return false; }

auto isDayPeriod = SunPosition.isDayPeriod();

if (_nighttimeDischarging && isDayPeriod) {
_nighttimeDischarging = false;
return isStartThresholdReached();
auto getBatteryState = [this,&config]() -> BatteryState {

// State machine for the battery
// Conditions: we use 'Below Stop Threshold', 'Above Start Threshold', 'Solar-Passthrough', 'Use Battery at night',
// 'Night/Day' and 'From which direction did we enter the stop-start zone' to determine the state.
//
// states description
// --------------------------------------------------------------------------------------------------------------------------------
// STOP: we must stop the inverter, because the battery is below the stop threshold
// NO_DISCHARGE: we can use the inverter, but we do not allow to discharge the battery, A requirement from 'Solar-Passthrough'
// DISCHARGE_ALLOWED: we can use the inverter and we allow discharging of the battery
// DISCHARGE_NIGHT: we can use the inverter and we allow discharging of a partial charged battery at night.
// A requirement from 'Use Battery at night'
//
// Notes: The combination of 'Use Battery at night' and use of 'voltage thresholds' can leads to oscillation between the states
// STOP and DISCHARGE_NIGHT. To avoid this problem, we allow only one transmission from STOP to DISCHARGE_NIGHT per night.
// In case of restart or power-cycle, we accept that the inverter may start discharging at night once again.
// Start-Up can be tricky, because data from the battery provider may not be available. As fallback we use the not very
// accurate inverter voltage and this can lead to the wrong state.

// check if we have a battery powered inverter
if (!usesBatteryPoweredInverter()) { return BatteryState::STOP; }

// check the stop condition
auto day = SunPosition.isDayPeriod();
if (isStopThresholdReached()) {
_fromStart = false;
_oneStopPerNightDone = day ? false : true;
return BatteryState::STOP;
}

if (isStopThresholdReached()) { return false; }

if (isStartThresholdReached()) { return true; }

// start a nighttime discharge cycle on a partially charged battery if
// 1. the respective switch/setting is enabled
// 2. it is now after sunset, i.e., it is nighttime
// 3. we are not already in a discharge cycle
// 4. we did not start a nighttime discharge cycle on a partially
// charged battery already (the _nighttimeDischarging flag will
// only be reset at sunrise, see above)
if (config.PowerLimiter.BatteryAlwaysUseAtNight &&
!isDayPeriod &&
!_batteryDischargeEnabled &&
!_nighttimeDischarging) {
_nighttimeDischarging = true;
return true;
// check the start condition
if (isStartThresholdReached()) {
_fromStart = true;
return BatteryState::DISCHARGE_ALLOWED;
}

// we are between start and stop threshold and keep the state that was
// last triggered, either charging or discharging.
return _batteryDischargeEnabled;
// all of the following conditions mean that we are in the "stop-start zone",
// and we must use the buffered information 'From which direction did we enter the stop-start zone'.

// if we come from start we always allow discharging of the battery
if (_fromStart) { return BatteryState::DISCHARGE_ALLOWED; }

// if we reach this line we come from stop and have to consider the 'Solar-Passthrough' and the 'Use Battery at night' settings.
auto solarPassThroughEnabled = isSolarPassThroughEnabled();
auto isBatteryAlwaysUseAtNightEnabled = config.PowerLimiter.BatteryAlwaysUseAtNight;

// When `Use Battery at night` is disabled or when its day, battery should not be discharged
if (!isBatteryAlwaysUseAtNightEnabled || day) {
_oneStopPerNightDone = false;

// Only allow inverters to be active if we are in solar pass-through mode.
// Otherwise we stop the battery inverters.
if (solarPassThroughEnabled) { return BatteryState::NO_DISCHARGE; }
return BatteryState::STOP;
}

// When `Use Battery at night` is enabled, and its night and we have already stopped the battery once per night, we keep the STOP state.
// Otherwise we allow discharging of a partially charged battery.
if (_oneStopPerNightDone) { return BatteryState::STOP; }
return BatteryState::DISCHARGE_NIGHT;
};

auto getFullSolarPassthrough = [this,&config]() -> bool {
Expand Down Expand Up @@ -302,9 +331,9 @@ void PowerLimiterClass::loop()
return dcVoltage + (acPower * config.PowerLimiter.VoltageLoadCorrectionFactor);
};

_batteryDischargeEnabled = getBatteryPower();
_fullSolarPassThroughActive = getFullSolarPassthrough();
_loadCorrectedVoltage = getLoadCorrectedVoltage();
_batteryState = getBatteryState();
_fullSolarPassThroughActive = getFullSolarPassthrough();

DTU_LOGD("up %lu s, it is %s, next inverter restart at %d s (set to %d)",
millis()/1000,
Expand All @@ -327,7 +356,8 @@ void PowerLimiterClass::loop()
config.PowerLimiter.VoltageLoadCorrectionFactor);

DTU_LOGD("battery discharge %s, start %.2f V or %u %%, stop %.2f V or %u %%",
(_batteryDischargeEnabled?"allowed":"restricted"),
(((_batteryState == BatteryState::DISCHARGE_ALLOWED) || (_batteryState == BatteryState::DISCHARGE_NIGHT))?"allowed":
(_batteryState == BatteryState::NO_DISCHARGE)?"restricted":"stopped"),
config.PowerLimiter.VoltageStartThreshold,
config.PowerLimiter.BatterySocStartThreshold,
config.PowerLimiter.VoltageStopThreshold,
Expand All @@ -346,7 +376,7 @@ void PowerLimiterClass::loop()
(isStopThresholdReached()?"":"NOT "),
(isSolarPassThroughEnabled()?"en":"dis"),
(config.PowerLimiter.BatteryAlwaysUseAtNight?"en":"dis"),
(_nighttimeDischarging?"active":"dormant"));
((_batteryState == BatteryState::DISCHARGE_NIGHT)?"active":"dormant"));

DTU_LOGD("total max AC power is %u W, conduction losses are %u %%",
config.PowerLimiter.TotalUpperPowerLimit,
Expand Down Expand Up @@ -518,7 +548,8 @@ uint8_t PowerLimiterClass::getPowerLimiterState() const
return PL_UI_STATE_CHARGING;
}

return _batteryDischargeEnabled ? PL_UI_STATE_USE_SOLAR_AND_BATTERY : PL_UI_STATE_USE_SOLAR_ONLY;
return ((_batteryState == BatteryState::DISCHARGE_ALLOWED || _batteryState == BatteryState::DISCHARGE_NIGHT))
? PL_UI_STATE_USE_SOLAR_AND_BATTERY : PL_UI_STATE_USE_SOLAR_ONLY;
}

uint16_t PowerLimiterClass::calcTargetOutput() const
Expand Down Expand Up @@ -705,6 +736,11 @@ uint16_t PowerLimiterClass::calcPowerBusUsage(uint16_t powerRequested) const
return 0;
}

if (_batteryState == BatteryState::STOP) {
DTU_LOGD("DC power bus usage blocked by battery below the stop threshold");
return 0;
}

auto solarOutputDc = getSolarPassthroughPower();
auto solarOutputAc = dcPowerBusToInverterAc(solarOutputDc);
if (isFullSolarPassthroughActive() && solarOutputAc > powerRequested) {
Expand Down Expand Up @@ -785,7 +821,7 @@ float PowerLimiterClass::getBatteryInvertersOutputAcWatts() const

std::optional<uint16_t> PowerLimiterClass::getBatteryDischargeLimit() const
{
if (!_batteryDischargeEnabled) { return 0; }
if ((_batteryState == BatteryState::STOP) || (_batteryState == BatteryState::NO_DISCHARGE)) { return 0; }

auto currentLimit = Battery.getDischargeCurrentLimit();
if (currentLimit == FLT_MAX) { return std::nullopt; }
Expand Down