Skip to content

Commit

Permalink
DPL hysteresis fix and refactor of setNewPowerLimit() (#264)
Browse files Browse the repository at this point in the history
  • Loading branch information
schlimmchen committed Jun 26, 2023
1 parent 7d73ae3 commit 07bb0b0
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 66 deletions.
3 changes: 2 additions & 1 deletion README_onBattery.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ The dynamic power limiter is responsible for automatic inverter power adjustment
Other settings are:
* The inverter ID configures the inverter that is controlled by the power limiter. The power limiter can only control a single inverter at this point in time.
* Channel ID is the inverter input channel ID that is used for battery voltage readings.
* Target power consumption and hysteresis set the power range that can be consumed from the grid.
* Target power consumption specifies the power to be either consumed from the grid (when set to a positive value) or fed back into the grid (when set to a negative value).
* The hysteresis value helps optimize communication with the inverter by skipping unnecessary power limit updates. An update is only sent if the absolute difference between the newly computed power limit and the previously set limit matches or exceeds the hysteresis value. This approach can conserve both airtime and CPU resources.
* Power limits control the min / max limits of the inverter
* Inverter is behind power meter. Select this if your inverter power is measured by the power meter. This is typically the case.
* Battery start and stop threshold can be configured using voltage and / or state of charge values. Stage of charge values requires a Pylontech battery at this point.
Expand Down
1 change: 1 addition & 0 deletions include/PowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class PowerLimiterClass {

bool canUseDirectSolarPower();
int32_t calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, bool solarPowerEnabled, bool batteryDischargeEnabled);
void commitPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t limit, bool enablePowerProduction);
void setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit);
int32_t getSolarChargePower();
float getLoadCorrectedVoltage(std::shared_ptr<InverterAbstract> inverter);
Expand Down
129 changes: 70 additions & 59 deletions src/PowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <VeDirectFrameHandler.h>
#include "MessageOutput.h"
#include <ctime>
#include <cmath>

PowerLimiterClass PowerLimiter;

Expand Down Expand Up @@ -78,9 +79,7 @@ void PowerLimiterClass::loop()
if (((!config.PowerLimiter_Enabled || _mode == PL_MODE_FULL_DISABLE) && _plState != SHUTDOWN)) {
if (inverter->isProducing()) {
MessageOutput.printf("PL initiated inverter shutdown.\r\n");
inverter->sendActivePowerControlRequest(static_cast<float>(config.PowerLimiter_LowerPowerLimit), PowerLimitControlType::AbsolutNonPersistent);
_lastRequestedPowerLimit = config.PowerLimiter_LowerPowerLimit;
inverter->sendPowerControlRequest(false);
commitPowerLimit(inverter, config.PowerLimiter_LowerPowerLimit, false);
} else {
_plState = SHUTDOWN;
}
Expand All @@ -104,10 +103,8 @@ void PowerLimiterClass::loop()
// If the power meter values are older than 30 seconds,
// or the Inverter Stats are older then 10x the poll interval
// set the limit to lower power limit for safety reasons.
MessageOutput.println("[PowerLimiterClass::loop] Power Meter/Inverter values too old. Using 0W (i.e. disable inverter)");
inverter->sendActivePowerControlRequest(static_cast<float>(config.PowerLimiter_LowerPowerLimit), PowerLimitControlType::AbsolutNonPersistent);
_lastRequestedPowerLimit = config.PowerLimiter_LowerPowerLimit;
inverter->sendPowerControlRequest(false);
MessageOutput.println("[PowerLimiterClass::loop] Power Meter/Inverter values too old, shutting down inverter");
commitPowerLimit(inverter, config.PowerLimiter_LowerPowerLimit, false);
#ifdef POWER_LIMITER_DEBUG
MessageOutput.printf("[PowerLimiterClass::loop] ******************* PL safety shutdown, update times exceeded PM: %li, Inverter: %li \r\n", millis() - PowerMeter.getLastPowerMeterUpdate(), millis() - inverter->Statistics()->getLastUpdate());
#endif
Expand Down Expand Up @@ -266,16 +263,6 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inve
// Case 3
newPowerLimit -= config.PowerLimiter_TargetPowerConsumption;

// Check if the new value is within the limits of the hysteresis and
// if we can discharge the battery
// If things did not change much we just use the old setting
if ((newPowerLimit - acPower) >= (-config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
(newPowerLimit - acPower) <= (+config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
batteryDischargeEnabled ) {
MessageOutput.println("[PowerLimiterClass::loop] reusing old limit");
return _lastRequestedPowerLimit;
}

// At this point we've calculated the required energy to compensate for household consumption.
// If the battery is enabled this can always be supplied since we assume that the battery can supply unlimited power
// The next step is to determine if the Solar power as provided by the Victron charger
Expand Down Expand Up @@ -304,64 +291,88 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inve
newPowerLimit = adjustedVictronChargePower;
}

// Respect power limit
if (newPowerLimit > config.PowerLimiter_UpperPowerLimit)
newPowerLimit = config.PowerLimiter_UpperPowerLimit;

MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit);
return newPowerLimit;
}

void PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit)
void PowerLimiterClass::commitPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t limit, bool enablePowerProduction)
{
CONFIG_T& config = Configuration.get();
// disable power production as soon as possible.
// setting the power limit is less important.
if (!enablePowerProduction && inverter->isProducing()) {
MessageOutput.println("[PowerLimiterClass::commitPowerLimit] Stopping inverter...");
inverter->sendPowerControlRequest(false);
}

// Start the inverter in case it's inactive and if the requested power is high enough
if (!inverter->isProducing() && newPowerLimit > config.PowerLimiter_LowerPowerLimit) {
MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter...");
inverter->sendActivePowerControlRequest(static_cast<float>(limit),
PowerLimitControlType::AbsolutNonPersistent);

_lastRequestedPowerLimit = limit;
_lastLimitSetTime = millis();

// 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...");
inverter->sendPowerControlRequest(true);
}
}

/**
* enforces limits and a hystersis on the requested power limit, after scaling
* the power limit to the ratio of total and producing inverter channels.
* commits the sanitized power limit.
*/
void PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit)
{
CONFIG_T& config = Configuration.get();

// Stop the inverter if limit is below threshold.
// We'll also set the power limit to the lower value in this case
if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) {
if (inverter->isProducing()) {
MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter...");
inverter->sendActivePowerControlRequest(static_cast<float>(config.PowerLimiter_LowerPowerLimit), PowerLimitControlType::AbsolutNonPersistent);
_lastRequestedPowerLimit = config.PowerLimiter_LowerPowerLimit;
inverter->sendPowerControlRequest(false);
}
newPowerLimit = config.PowerLimiter_LowerPowerLimit;
}

// Set the actual limit. We'll only do this is if the limit is in the right range
// and differs from the last requested value
if( _lastRequestedPowerLimit != newPowerLimit &&
/* newPowerLimit > config.PowerLimiter_LowerPowerLimit && --> This will always be true given the check above, kept for code readability */
newPowerLimit <= config.PowerLimiter_UpperPowerLimit ) {
MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit);

int32_t effPowerLimit = newPowerLimit;
std::list<ChannelNum_t> dcChnls = inverter->Statistics()->getChannelsByType(TYPE_DC);
int dcProdChnls = 0, dcTotalChnls = dcChnls.size();
for (auto& c : dcChnls) {
if (inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) > 1.0) {
dcProdChnls++;
}
if (!inverter->isProducing()) { return; }

MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] requested power limit %d is smaller than lower power limit %d\r\n",
newPowerLimit, config.PowerLimiter_LowerPowerLimit);
return commitPowerLimit(inverter, config.PowerLimiter_LowerPowerLimit, false);
}

// enforce configured upper power limit
int32_t effPowerLimit = std::min(newPowerLimit, config.PowerLimiter_UpperPowerLimit);


// scale the power limit by the amount of all inverter channels devided by
// the amount of producing inverter channels. the inverters limit each of
// the n channels to 1/n of the total power limit. scaling the power limit
// ensures the total inverter output is what we are asking for.
std::list<ChannelNum_t> dcChnls = inverter->Statistics()->getChannelsByType(TYPE_DC);
int dcProdChnls = 0, dcTotalChnls = dcChnls.size();
for (auto& c : dcChnls) {
if (inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) > 1.0) {
dcProdChnls++;
}
if (dcProdChnls > 0) {
effPowerLimit = round(newPowerLimit * static_cast<float>(dcTotalChnls) / dcProdChnls);
uint16_t inverterMaxPower = inverter->DevInfo()->getMaxPower();
if (effPowerLimit > inverterMaxPower) {
effPowerLimit = inverterMaxPower;
}
}
if (dcProdChnls > 0) {
MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] %d channels total, %d producing channels, scaling power limit\r\n",
dcTotalChnls, dcProdChnls);
effPowerLimit = round(newPowerLimit * static_cast<float>(dcTotalChnls) / dcProdChnls);
if (effPowerLimit > inverter->DevInfo()->getMaxPower()) {
effPowerLimit = inverter->DevInfo()->getMaxPower();
}
}

inverter->sendActivePowerControlRequest(static_cast<float>(effPowerLimit), PowerLimitControlType::AbsolutNonPersistent);
_lastRequestedPowerLimit = effPowerLimit;
// wait for the next inverter update (+ 3 seconds to make sure the limit got applied)
_lastLimitSetTime = millis();
// Check if the new value is within the limits of the hysteresis
auto diff = std::abs(effPowerLimit - _lastRequestedPowerLimit);
if ( diff < config.PowerLimiter_TargetPowerConsumptionHysteresis) {
MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] reusing old limit: %d W, diff: %d, hysteresis: %d\r\n",
_lastRequestedPowerLimit, diff, config.PowerLimiter_TargetPowerConsumptionHysteresis);
return;
}

MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] using new limit: %d W, requested power limit: %d\r\n",
effPowerLimit, newPowerLimit);

commitPowerLimit(inverter, effPowerLimit, true);
}

int32_t PowerLimiterClass::getSolarChargePower()
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -543,8 +543,8 @@
"InverterChannelIdHint": "Wähle den Kanal an dem die Batterie hängt.",
"TargetPowerConsumption": "Angestrebter Netzbezung",
"TargetPowerConsumptionHint": "Angestrebter erlaubter Stromverbrauch aus dem Netz.",
"TargetPowerConsumptionHysteresis": "Hysterese für den angestrebten Netzbezug",
"TargetPowerConsumptionHysteresisHint": "Wert um den der angestrebte Netzbezug schwanken darf, ohne dass nachgeregelt wird.",
"TargetPowerConsumptionHysteresis": "Hysterese für das berechnete Limit",
"TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den Inverter senden, wenn es vom zuletzt gesendeten Limit um mindestens diesen Betrag abweicht.",
"LowerPowerLimit": "Unteres Leistungslimit",
"UpperPowerLimit": "Oberes Leistungslimit",
"PowerMeters": "Leistungsmesser",
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -544,8 +544,8 @@
"InverterChannelIdHint": "Select proper channel where battery is connected to.",
"TargetPowerConsumption": "Target power consumption from grid",
"TargetPowerConsumptionHint": "Set the grid power consumption the limiter tries to achieve.",
"TargetPowerConsumptionHysteresis": "Hysteresis for power consumption from grid",
"TargetPowerConsumptionHysteresisHint": "Value around which the target grid power consumption fluctuates without readjustment.",
"TargetPowerConsumptionHysteresis": "Hysteresis for calculated power limit",
"TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.",
"LowerPowerLimit": "Lower power limit",
"UpperPowerLimit": "Upper power limit",
"PowerMeters": "Power meter",
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -566,8 +566,8 @@
"InverterChannelIdHint": "Select proper channel where battery is connected to.",
"TargetPowerConsumption": "Target power consumption from grid",
"TargetPowerConsumptionHint": "Set the grid power consumption the limiter tries to achieve.",
"TargetPowerConsumptionHysteresis": "Hysteresis for power consumption from grid",
"TargetPowerConsumptionHysteresisHint": "Value around which the target grid power consumption fluctuates without readjustment.",
"TargetPowerConsumptionHysteresis": "Hysteresis for calculated power limit",
"TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.",
"LowerPowerLimit": "Lower power limit",
"UpperPowerLimit": "Upper power limit",
"PowerMeters": "Power meter",
Expand Down

0 comments on commit 07bb0b0

Please sign in to comment.