Skip to content

Commit

Permalink
Feature: DPL: introduce base load setting
Browse files Browse the repository at this point in the history
on power meter issues (usually a timeout), keep the inverter enabled and
make it produce the configured base load limit if the battery can be
discharged. that should be okay since the base load config value is
expected to be small and a little less than the actual household base
load, i.e., if this amount of power is produced, the household will
consume it in any case and no energy is fed into the grid.
  • Loading branch information
schlimmchen committed Apr 15, 2024
1 parent cf1ea42 commit 7e30711
Show file tree
Hide file tree
Showing 13 changed files with 99 additions and 47 deletions.
1 change: 1 addition & 0 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ struct CONFIG_T {
int32_t TargetPowerConsumption;
int32_t TargetPowerConsumptionHysteresis;
int32_t LowerPowerLimit;
int32_t BaseLoadLimit;
int32_t UpperPowerLimit;
bool IgnoreSoc;
uint32_t BatterySocStartThreshold;
Expand Down
1 change: 0 additions & 1 deletion include/PowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class PowerLimiterClass {
DisabledByMqtt,
WaitingForValidTimestamp,
PowerMeterDisabled,
PowerMeterTimeout,
PowerMeterPending,
InverterInvalid,
InverterChanged,
Expand Down
1 change: 1 addition & 0 deletions include/PowerMeter.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class PowerMeterClass {
void init(Scheduler& scheduler);
float getPowerTotal(bool forceUpdate = true);
uint32_t getLastPowerMeterUpdate();
bool isDataValid();

private:
void loop();
Expand Down
1 change: 1 addition & 0 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
#define POWERLIMITER_LOWER_POWER_LIMIT 10
#define POWERLIMITER_BASE_LOAD_LIMIT 100
#define POWERLIMITER_UPPER_POWER_LIMIT 800
#define POWERLIMITER_IGNORE_SOC false
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
Expand Down
2 changes: 2 additions & 0 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ bool ConfigurationClass::write()
powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
powerlimiter["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit;
powerlimiter["base_load_limit"] = config.PowerLimiter.BaseLoadLimit;
powerlimiter["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit;
powerlimiter["ignore_soc"] = config.PowerLimiter.IgnoreSoc;
powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold;
Expand Down Expand Up @@ -443,6 +444,7 @@ bool ConfigurationClass::read()
config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION;
config.PowerLimiter.TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS;
config.PowerLimiter.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT;
config.PowerLimiter.BaseLoadLimit = powerlimiter["base_load_limit"] | POWERLIMITER_BASE_LOAD_LIMIT;
config.PowerLimiter.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
config.PowerLimiter.IgnoreSoc = powerlimiter["ignore_soc"] | POWERLIMITER_IGNORE_SOC;
config.PowerLimiter.BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD;
Expand Down
86 changes: 47 additions & 39 deletions src/PowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,12 @@ frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status
{
static const frozen::string missing = "programmer error: missing status text";

static const frozen::map<Status, frozen::string, 21> texts = {
static const frozen::map<Status, frozen::string, 20> texts = {
{ Status::Initializing, "initializing (should not see me)" },
{ Status::DisabledByConfig, "disabled by configuration" },
{ Status::DisabledByMqtt, "disabled by MQTT" },
{ Status::WaitingForValidTimestamp, "waiting for valid date and time to be available" },
{ Status::PowerMeterDisabled, "no power meter is configured/enabled" },
{ Status::PowerMeterTimeout, "power meter readings are outdated" },
{ Status::PowerMeterPending, "waiting for sufficiently recent power meter reading" },
{ Status::InverterInvalid, "invalid inverter selection/configuration" },
{ Status::InverterChanged, "target inverter changed" },
Expand All @@ -47,7 +46,7 @@ frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status
{ Status::InverterPowerCmdPending, "waiting for a start/stop/restart command to complete" },
{ Status::InverterDevInfoPending, "waiting for inverter device information to be available" },
{ Status::InverterStatsPending, "waiting for sufficiently recent inverter data" },
{ Status::CalculatedLimitBelowMinLimit, "calculated limit is less than lower power limit" },
{ Status::CalculatedLimitBelowMinLimit, "calculated limit is less than minimum power limit" },
{ Status::UnconditionalSolarPassthrough, "unconditionally passing through all solar power (MQTT override)" },
{ Status::NoVeDirect, "VE.Direct disabled, connection broken, or data outdated" },
{ Status::NoEnergy, "no energy source available to power the inverter from" },
Expand Down Expand Up @@ -82,8 +81,7 @@ void PowerLimiterClass::announceStatus(PowerLimiterClass::Status status)
/**
* returns true if the inverter state was changed or is about to change, i.e.,
* if it is actually in need of a shutdown. returns false otherwise, i.e., the
* inverter is already shut down and the inverter limit is set to the configured
* lower power limit.
* inverter is already shut down.
*/
bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status)
{
Expand All @@ -93,14 +91,6 @@ bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status)

_oTargetPowerState = false;

auto const& config = Configuration.get();
if ( (Status::PowerMeterTimeout == status ||
Status::CalculatedLimitBelowMinLimit == status)
&& config.PowerLimiter.IsInverterSolarPowered) {
_oTargetPowerState = true;
}

_oTargetPowerLimitWatts = config.PowerLimiter.LowerPowerLimit;
return updateInverter();
}

Expand Down Expand Up @@ -191,11 +181,6 @@ void PowerLimiterClass::loop()
return;
}

if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000)) {
shutdown(Status::PowerMeterTimeout);
return;
}

// concerns both power limits and start/stop/restart commands and is
// only updated if a respective response was received from the inverter
auto lastUpdateCmd = std::max(
Expand All @@ -206,7 +191,10 @@ void PowerLimiterClass::loop()
return announceStatus(Status::InverterStatsPending);
}

if (PowerMeter.getLastPowerMeterUpdate() <= lastUpdateCmd) {
// if the power meter is being used, i.e., if its data is valid, we want to
// wait for a new reading after adjusting the inverter limit. otherwise, we
// proceed as we will use a fallback limit independent of the power meter.
if (PowerMeter.isDataValid() && PowerMeter.getLastPowerMeterUpdate() <= lastUpdateCmd) {
return announceStatus(Status::PowerMeterPending);
}

Expand Down Expand Up @@ -403,7 +391,7 @@ uint8_t PowerLimiterClass::getPowerLimiterState() {
return PL_UI_STATE_INACTIVE;
}

// Logic table
// Logic table ("PowerMeter value" can be "base load setting" as a fallback)
// | Case # | batteryPower | solarPower | useFullSolarPassthrough | Resulting inverter limit |
// | 1 | false | < 20 W | doesn't matter | 0 (inverter off) |
// | 2 | false | >= 20 W | doesn't matter | min(PowerMeter value, solarPower) |
Expand Down Expand Up @@ -431,36 +419,50 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inverte
return shutdown(Status::HuaweiPsu);
}

auto powerMeter = static_cast<int32_t>(PowerMeter.getPowerTotal());
auto meterValid = PowerMeter.isDataValid();

auto meterValue = static_cast<int32_t>(PowerMeter.getPowerTotal());

// We don't use FLD_PAC from the statistics, because that data might be too
// old and unreliable. TODO(schlimmchen): is this comment outdated?
auto inverterOutput = static_cast<int32_t>(inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC));

auto solarPowerAC = inverterPowerDcToAc(inverter, solarPowerDC);

auto const& config = Configuration.get();
auto targetConsumption = config.PowerLimiter.TargetPowerConsumption;
auto baseLoad = config.PowerLimiter.BaseLoadLimit;
bool meterIncludesInv = config.PowerLimiter.IsInverterBehindPowerMeter;

if (_verboseLogging) {
MessageOutput.printf("[DPL::calcPowerLimit] power meter: %d W, "
"target consumption: %d W, inverter output: %d W, solar power (AC): %d\r\n",
powerMeter,
config.PowerLimiter.TargetPowerConsumption,
MessageOutput.printf("[DPL::calcPowerLimit] target consumption: %d W, "
"base load: %d W, power meter does %sinclude inverter output\r\n",
targetConsumption,
baseLoad,
(meterIncludesInv?"":"NOT "));

MessageOutput.printf("[DPL::calcPowerLimit] power meter value: %d W, "
"power meter valid: %s, inverter output: %d W, solar power (AC): %d W\r\n",
meterValue,
(meterValid?"yes":"no"),
inverterOutput,
solarPowerAC);
}

auto newPowerLimit = powerMeter;
auto newPowerLimit = baseLoad;

if (config.PowerLimiter.IsInverterBehindPowerMeter) {
// If the inverter the behind the power meter (part of measurement),
// the produced power of this inverter has also to be taken into account.
// We don't use FLD_PAC from the statistics, because that
// data might be too old and unreliable.
newPowerLimit += inverterOutput;
}
if (meterValid) {
newPowerLimit = meterValue;

if (meterIncludesInv) {
// If the inverter is wired behind the power meter, i.e., if its
// output is part of the power meter measurement, the produced
// power of this inverter has to be taken into account.
newPowerLimit += inverterOutput;
}

// We're not trying to hit 0 exactly but take an offset into account
// This means we never fully compensate the used power with the inverter
newPowerLimit -= config.PowerLimiter.TargetPowerConsumption;
newPowerLimit -= targetConsumption;
}

// Case 2:
if (!batteryPower) {
Expand Down Expand Up @@ -490,7 +492,7 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inverte
}

if (_verboseLogging) {
MessageOutput.printf("[DPL::calcPowerLimit] match power meter with limit of %d W\r\n",
MessageOutput.printf("[DPL::calcPowerLimit] match household consumption with limit of %d W\r\n",
newPowerLimit);
}

Expand Down Expand Up @@ -693,12 +695,18 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inver

if (_verboseLogging) {
MessageOutput.printf("[DPL::setNewPowerLimit] input limit: %d W, "
"lower limit: %d W, upper limit: %d W, hysteresis: %d W\r\n",
"min limit: %d W, max limit: %d W, hysteresis: %d W\r\n",
newPowerLimit, lowerLimit, upperLimit, hysteresis);
}

if (newPowerLimit < lowerLimit) {
return shutdown(Status::CalculatedLimitBelowMinLimit);
if (!config.PowerLimiter.IsInverterSolarPowered) {
return shutdown(Status::CalculatedLimitBelowMinLimit);
}

MessageOutput.println("[DPL::setNewPowerLimit] keep solar-powered "
"inverter running at min limit");
newPowerLimit = lowerLimit;
}

// enforce configured upper power limit
Expand Down
17 changes: 17 additions & 0 deletions src/PowerMeter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,23 @@ uint32_t PowerMeterClass::getLastPowerMeterUpdate()
return _lastPowerMeterUpdate;
}

bool PowerMeterClass::isDataValid()
{
auto const& config = Configuration.get();

std::lock_guard<std::mutex> l(_mutex);

bool valid = config.PowerMeter.Enabled &&
_lastPowerMeterUpdate > 0 &&
((millis() - _lastPowerMeterUpdate) < (30 * 1000));

// reset if timed out to avoid glitch once
// (millis() - _lastPowerMeterUpdate) overflows
if (!valid) { _lastPowerMeterUpdate = 0; }

return valid;
}

void PowerMeterClass::mqtt()
{
if (!MqttSettings.getConnected()) { return; }
Expand Down
2 changes: 2 additions & 0 deletions src/WebApi_powerlimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
root["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
root["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
root["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit;
root["base_load_limit"] = config.PowerLimiter.BaseLoadLimit;
root["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit;
root["ignore_soc"] = config.PowerLimiter.IgnoreSoc;
root["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold;
Expand Down Expand Up @@ -188,6 +189,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as<int32_t>();
config.PowerLimiter.TargetPowerConsumptionHysteresis = root["target_power_consumption_hysteresis"].as<int32_t>();
config.PowerLimiter.LowerPowerLimit = root["lower_power_limit"].as<int32_t>();
config.PowerLimiter.BaseLoadLimit = root["base_load_limit"].as<int32_t>();
config.PowerLimiter.UpperPowerLimit = root["upper_power_limit"].as<int32_t>();

if (config.Battery.Enabled) {
Expand Down
8 changes: 6 additions & 2 deletions webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -607,8 +607,12 @@
"TargetPowerConsumptionHint": "Angestrebter erlaubter Stromverbrauch aus dem Netz. Wert darf negativ sein.",
"TargetPowerConsumptionHysteresis": "Hysterese",
"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",
"LowerPowerLimit": "Minmales Leistungslimit",
"LowerPowerLimitHint": "Dieser Wert muss so gewählt werden, dass ein stabiler Betrieb mit diesem Limit möglich ist. Falls der Wechselrichter nur mit einem kleineren Limit betrieben werden könnte, wird er stattdessen in Standby versetzt.",
"BaseLoadLimit": "Grundlast",
"BaseLoadLimitHint": "Relevant beim Betrieb ohne oder beim Ausfall des Stromzählers. Solange es die sonstigen Bedinungen zulassen (insb. Batterieladung) wird dieses Limit am Wechselrichter eingestellt.",
"UpperPowerLimit": "Maximales Leistungslimit",
"UpperPowerLimitHint": "Der Wechselrichter wird stets so eingestellt, dass höchstens diese Ausgangsleistung erreicht wird. Dieser Wert muss so gewählt werden, dass die Strombelastbarkeit der AC-Anschlussleitungen eingehalten wird.",
"SocThresholds": "Batterie State of Charge (SoC) Schwellwerte",
"IgnoreSoc": "Batterie SoC ignorieren",
"StartThreshold": "Batterienutzung Start-Schwellwert",
Expand Down
8 changes: 6 additions & 2 deletions webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -613,8 +613,12 @@
"TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.",
"TargetPowerConsumptionHysteresis": "Hysteresis",
"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",
"LowerPowerLimit": "Minimum Power Limit",
"LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.",
"BaseLoadLimit": "Base Load",
"BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.",
"UpperPowerLimit": "Maximum Power Limit",
"UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.",
"SocThresholds": "Battery State of Charge (SoC) Thresholds",
"IgnoreSoc": "Ignore Battery SoC",
"StartThreshold": "Start Threshold for Battery Discharging",
Expand Down
8 changes: 6 additions & 2 deletions webapp/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -695,8 +695,12 @@
"TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.",
"TargetPowerConsumptionHysteresis": "Hysteresis",
"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",
"LowerPowerLimit": "Minimum Power Limit",
"LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.",
"BaseLoadLimit": "Base Load",
"BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.",
"UpperPowerLimit": "Maximum Power Limit",
"UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.",
"SocThresholds": "Battery State of Charge (SoC) Thresholds",
"IgnoreSoc": "Ignore Battery SoC",
"StartThreshold": "Start Threshold for Battery Discharging",
Expand Down
1 change: 1 addition & 0 deletions webapp/src/types/PowerLimiterConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface PowerLimiterConfig {
target_power_consumption: number;
target_power_consumption_hysteresis: number;
lower_power_limit: number;
base_load_limit: number;
upper_power_limit: number;
ignore_soc: boolean;
battery_soc_start_threshold: number;
Expand Down
10 changes: 9 additions & 1 deletion webapp/src/views/PowerLimiterAdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,21 @@
</div>

<InputElement :label="$t('powerlimiteradmin.LowerPowerLimit')"
:tooltip="$t('powerlimiteradmin.LowerPowerLimitHint')"
v-model="powerLimiterConfigList.lower_power_limit"
placeholder="50" min="10" postfix="W"
type="number" wide/>

<InputElement :label="$t('powerlimiteradmin.BaseLoadLimit')"
:tooltip="$t('powerlimiteradmin.BaseLoadLimitHint')"
v-model="powerLimiterConfigList.base_load_limit"
placeholder="200" :min="(powerLimiterConfigList.lower_power_limit + 1).toString()" postfix="W"
type="number" wide/>

<InputElement :label="$t('powerlimiteradmin.UpperPowerLimit')"
v-model="powerLimiterConfigList.upper_power_limit"
placeholder="800" min="20" postfix="W"
:tooltip="$t('powerlimiteradmin.UpperPowerLimitHint')"
placeholder="800" :min="(powerLimiterConfigList.base_load_limit + 1).toString()" postfix="W"
type="number" wide/>

<InputElement :label="$t('powerlimiteradmin.InverterIsBehindPowerMeter')"
Expand Down

0 comments on commit 7e30711

Please sign in to comment.