diff --git a/include/Configuration.h b/include/Configuration.h index a925d74d..d514f343 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -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; diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 42f397bd..28463654 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -24,7 +24,6 @@ public: DisabledByMqtt, WaitingForValidTimestamp, PowerMeterDisabled, - PowerMeterTimeout, PowerMeterPending, InverterInvalid, InverterChanged, diff --git a/include/PowerMeter.h b/include/PowerMeter.h index f2b2042c..0ce38f61 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -31,6 +31,7 @@ public: void init(Scheduler& scheduler); float getPowerTotal(bool forceUpdate = true); uint32_t getLastPowerMeterUpdate(); + bool isDataValid(); private: void loop(); diff --git a/include/defaults.h b/include/defaults.h index 7ea99fbd..487c3df6 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -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 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index e99f6914..f4a5a239 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -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; @@ -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; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 138076a8..8dd21404 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -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 texts = { + static const frozen::map 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" }, @@ -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" }, @@ -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) { @@ -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(); } @@ -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( @@ -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); } @@ -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) | @@ -431,37 +419,51 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte return shutdown(Status::HuaweiPsu); } - auto powerMeter = static_cast(PowerMeter.getPowerTotal()); + auto meterValid = PowerMeter.isDataValid(); + auto meterValue = static_cast(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(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; + } + + newPowerLimit -= targetConsumption; } - // 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; - // Case 2: if (!batteryPower) { newPowerLimit = std::min(newPowerLimit, solarPowerAC); @@ -490,7 +492,7 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr 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); } @@ -693,12 +695,18 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr 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 diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index 72526b7d..63cce421 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -136,6 +136,23 @@ uint32_t PowerMeterClass::getLastPowerMeterUpdate() return _lastPowerMeterUpdate; } +bool PowerMeterClass::isDataValid() +{ + auto const& config = Configuration.get(); + + std::lock_guard 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; } diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 81987a23..4c1ebc28 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -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; @@ -188,6 +189,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as(); config.PowerLimiter.TargetPowerConsumptionHysteresis = root["target_power_consumption_hysteresis"].as(); config.PowerLimiter.LowerPowerLimit = root["lower_power_limit"].as(); + config.PowerLimiter.BaseLoadLimit = root["base_load_limit"].as(); config.PowerLimiter.UpperPowerLimit = root["upper_power_limit"].as(); if (config.Battery.Enabled) { diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 00966647..3d7c68f7 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -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", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 47727645..afc036b6 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -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", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 7bf30bbe..67bb9d27 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -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", diff --git a/webapp/src/types/PowerLimiterConfig.ts b/webapp/src/types/PowerLimiterConfig.ts index c6edde8d..0d7ad388 100644 --- a/webapp/src/types/PowerLimiterConfig.ts +++ b/webapp/src/types/PowerLimiterConfig.ts @@ -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; diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index 0a58b4a9..46da5def 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -82,13 +82,21 @@ + +