From 6d2d85bf41fea8e86accb0b4c5b49d56aef0c50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= Date: Sat, 8 Feb 2025 19:41:55 +0100 Subject: [PATCH] Feature: DPL support for smart-buffer-batteries (#1606) * Feature: DPL support for smart-buffer-batteries Change the settings to pick a power source (battery, solar, smart-buffer) and treat smart-buffers like a mix of battery and solar. Smart-buffer-powered (Marstek B2500, Anker Solix, Zendure, etc.) inverters can always increase to the max limit without any checks, support overscaling, can be put in standby and restarted. * log uptime and inverter restart also when governing buffered inverters --------- Co-authored-by: Bernhard Kirchen --- include/Configuration.h | 6 +- include/PowerLimiter.h | 1 + include/PowerLimiterBatteryInverter.h | 1 - include/PowerLimiterInverter.h | 5 +- include/PowerLimiterOverscalingInverter.h | 17 +++ include/PowerLimiterSmartBufferInverter.h | 14 ++ include/PowerLimiterSolarInverter.h | 10 +- include/defaults.h | 1 - src/Configuration.cpp | 27 +++- src/PowerLimiter.cpp | 35 +++-- src/PowerLimiterInverter.cpp | 18 ++- src/PowerLimiterOverscalingInverter.cpp | 143 +++++++++++++++++++++ src/PowerLimiterSmartBufferInverter.cpp | 75 +++++++++++ src/PowerLimiterSolarInverter.cpp | 134 +------------------ webapp/src/locales/de.json | 5 +- webapp/src/locales/en.json | 5 +- webapp/src/locales/fr.json | 1 - webapp/src/types/PowerLimiterConfig.ts | 2 +- webapp/src/views/PowerLimiterAdminView.vue | 84 +++++++----- 19 files changed, 388 insertions(+), 196 deletions(-) create mode 100644 include/PowerLimiterOverscalingInverter.h create mode 100644 include/PowerLimiterSmartBufferInverter.h create mode 100644 src/PowerLimiterOverscalingInverter.cpp create mode 100644 src/PowerLimiterSmartBufferInverter.cpp diff --git a/include/Configuration.h b/include/Configuration.h index cdfd5eb54..c08a56bae 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -10,7 +10,7 @@ #define CONFIG_FILENAME "/config.json" #define CONFIG_VERSION 0x00011d00 // 0.1.29 // make sure to clean all after change -#define CONFIG_VERSION_ONBATTERY 4 +#define CONFIG_VERSION_ONBATTERY 5 #define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -136,11 +136,13 @@ struct POWERLIMITER_INVERTER_CONFIG_T { uint64_t Serial; bool IsGoverned; bool IsBehindPowerMeter; - bool IsSolarPowered; bool UseOverscaling; uint16_t LowerPowerLimit; uint16_t UpperPowerLimit; uint8_t ScalingThreshold; + + enum InverterPowerSource { Battery = 0, Solar = 1, SmartBuffer = 2 }; + InverterPowerSource PowerSource; }; using PowerLimiterInverterConfig = struct POWERLIMITER_INVERTER_CONFIG_T; diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 134d32d1b..8b3ec7f8d 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -52,6 +52,7 @@ class PowerLimiterClass { void setMode(Mode m) { _mode = m; } Mode getMode() const { return _mode; } bool usesBatteryPoweredInverter(); + bool usesSmartBufferPoweredInverter(); bool isGovernedInverterProducing(); private: diff --git a/include/PowerLimiterBatteryInverter.h b/include/PowerLimiterBatteryInverter.h index 348a41f95..9602ef4d9 100644 --- a/include/PowerLimiterBatteryInverter.h +++ b/include/PowerLimiterBatteryInverter.h @@ -12,7 +12,6 @@ class PowerLimiterBatteryInverter : public PowerLimiterInverter { uint16_t applyReduction(uint16_t reduction, bool allowStandby) final; uint16_t applyIncrease(uint16_t increase) final; uint16_t standby() final; - bool isSolarPowered() const final { return false; } private: void setAcOutput(uint16_t expectedOutputWatts) final; diff --git a/include/PowerLimiterInverter.h b/include/PowerLimiterInverter.h index adff68091..1ea78bd15 100644 --- a/include/PowerLimiterInverter.h +++ b/include/PowerLimiterInverter.h @@ -68,7 +68,10 @@ class PowerLimiterInverter { uint64_t getSerial() const { return _config.Serial; } char const* getSerialStr() const { return _serialStr; } bool isBehindPowerMeter() const { return _config.IsBehindPowerMeter; } - virtual bool isSolarPowered() const = 0; + + bool isBatteryPowered() const { return _config.PowerSource == PowerLimiterInverterConfig::InverterPowerSource::Battery; } + bool isSolarPowered() const { return _config.PowerSource == PowerLimiterInverterConfig::InverterPowerSource::Solar; } + bool isSmartBufferPowered() const { return _config.PowerSource == PowerLimiterInverterConfig::InverterPowerSource::SmartBuffer; } void debug() const; diff --git a/include/PowerLimiterOverscalingInverter.h b/include/PowerLimiterOverscalingInverter.h new file mode 100644 index 000000000..374793611 --- /dev/null +++ b/include/PowerLimiterOverscalingInverter.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "PowerLimiterInverter.h" + +class PowerLimiterOverscalingInverter : public PowerLimiterInverter { +public: + PowerLimiterOverscalingInverter(bool verboseLogging, PowerLimiterInverterConfig const& config); + + uint16_t applyIncrease(uint16_t increase) final; + +protected: + void setAcOutput(uint16_t expectedOutputWatts) final; + +private: + uint16_t scaleLimit(uint16_t expectedOutputWatts); +}; diff --git a/include/PowerLimiterSmartBufferInverter.h b/include/PowerLimiterSmartBufferInverter.h new file mode 100644 index 000000000..62923a791 --- /dev/null +++ b/include/PowerLimiterSmartBufferInverter.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "PowerLimiterOverscalingInverter.h" + +class PowerLimiterSmartBufferInverter : public PowerLimiterOverscalingInverter { +public: + PowerLimiterSmartBufferInverter(bool verboseLogging, PowerLimiterInverterConfig const& config); + + uint16_t getMaxReductionWatts(bool allowStandby) const final; + uint16_t getMaxIncreaseWatts() const final; + uint16_t applyReduction(uint16_t reduction, bool allowStandby) final; + uint16_t standby() final; +}; diff --git a/include/PowerLimiterSolarInverter.h b/include/PowerLimiterSolarInverter.h index 72023211c..af0d622dc 100644 --- a/include/PowerLimiterSolarInverter.h +++ b/include/PowerLimiterSolarInverter.h @@ -1,20 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "PowerLimiterInverter.h" +#include "PowerLimiterOverscalingInverter.h" -class PowerLimiterSolarInverter : public PowerLimiterInverter { +class PowerLimiterSolarInverter : public PowerLimiterOverscalingInverter { public: PowerLimiterSolarInverter(bool verboseLogging, PowerLimiterInverterConfig const& config); uint16_t getMaxReductionWatts(bool allowStandby) const final; uint16_t getMaxIncreaseWatts() const final; uint16_t applyReduction(uint16_t reduction, bool allowStandby) final; - uint16_t applyIncrease(uint16_t increase) final; uint16_t standby() final; - bool isSolarPowered() const final { return true; } - -private: - uint16_t scaleLimit(uint16_t expectedOutputWatts); - void setAcOutput(uint16_t expectedOutputWatts) final; }; diff --git a/include/defaults.h b/include/defaults.h index 3028a2f79..c096d152e 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -131,7 +131,6 @@ #define POWERLIMITER_CONDUCTION_LOSSES 3 #define POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT false #define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true -#define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false #define POWERLIMITER_USE_OVERSCALING false #define POWERLIMITER_INVERTER_CHANNEL_ID 0 #define POWERLIMITER_TARGET_POWER_CONSUMPTION 0 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 3c9dfb27b..500618854 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -181,7 +181,7 @@ void ConfigurationClass::serializePowerLimiterConfig(PowerLimiterConfig const& s t["serial"] = serialStr(s.Serial); t["is_governed"] = s.IsGoverned; t["is_behind_power_meter"] = s.IsBehindPowerMeter; - t["is_solar_powered"] = s.IsSolarPowered; + t["power_source"] = s.PowerSource; t["use_overscaling_to_compensate_shading"] = s.UseOverscaling; t["lower_power_limit"] = s.LowerPowerLimit; t["upper_power_limit"] = s.UpperPowerLimit; @@ -521,7 +521,7 @@ void ConfigurationClass::deserializePowerLimiterConfig(JsonObject const& source, inv.Serial = serialBin(s["serial"] | String("0")); // 0 marks inverter slot as unused inv.IsGoverned = s["is_governed"] | false; inv.IsBehindPowerMeter = s["is_behind_power_meter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; - inv.IsSolarPowered = s["is_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED; + inv.PowerSource = s["power_source"] | PowerLimiterInverterConfig::InverterPowerSource::Battery; inv.UseOverscaling = s["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING; inv.LowerPowerLimit = s["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT; inv.UpperPowerLimit = s["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT; @@ -942,7 +942,13 @@ void ConfigurationClass::migrateOnBattery() config.PowerLimiter.InverterSerialForDcVoltage = previousInverterSerial; inv.IsGoverned = true; inv.IsBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; - inv.IsSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED; + + if (powerlimiter["is_inverter_solar_powered"]) { + inv.PowerSource = PowerLimiterInverterConfig::InverterPowerSource::Solar; + } else { + inv.PowerSource = PowerLimiterInverterConfig::InverterPowerSource::Battery; + } + inv.UseOverscaling = powerlimiter["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING; inv.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT; inv.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT; @@ -968,6 +974,21 @@ void ConfigurationClass::migrateOnBattery() config.SolarCharger.PublishUpdatesOnly = vedirect["updates_only"] | SOLAR_CHARGER_PUBLISH_UPDATES_ONLY; } + if (config.Cfg.VersionOnBattery < 5) { + JsonArray inverters = doc["powerlimiter"]["inverters"].as(); + + for (size_t i = 0; i < INV_MAX_COUNT; ++i) { + PowerLimiterInverterConfig& inv = config.PowerLimiter.Inverters[i]; + JsonObject s = inverters[i]; + + if (s["is_solar_powered"]) { + inv.PowerSource = PowerLimiterInverterConfig::InverterPowerSource::Solar; + } else { + inv.PowerSource = PowerLimiterInverterConfig::InverterPowerSource::Battery; + } + } + } + f.close(); config.Cfg.VersionOnBattery = CONFIG_VERSION_ONBATTERY; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index fe47258c2..97428b8d8 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -19,7 +19,7 @@ #include "SunPosition.h" static auto sBatteryPoweredFilter = [](PowerLimiterInverter const& inv) { - return !inv.isSolarPowered(); + return inv.isBatteryPowered(); }; static const char sBatteryPoweredExpression[] = "battery-powered"; @@ -30,6 +30,12 @@ static auto sSolarPoweredFilter = [](PowerLimiterInverter const& inv) { static const char sSolarPoweredExpression[] = "solar-powered"; +static auto sSmartBufferPoweredFilter = [](PowerLimiterInverter const& inv) { + return inv.isSmartBufferPowered(); +}; + +static const char sSmartBufferPoweredExpression[] = "smart-buffer-powered"; + PowerLimiterClass PowerLimiter; void PowerLimiterClass::init(Scheduler& scheduler) @@ -282,13 +288,15 @@ void PowerLimiterClass::loop() // re-calculate load-corrected voltage once (and only once) per DPL loop _oLoadCorrectedVoltage = std::nullopt; - if (_verboseLogging && usesBatteryPoweredInverter()) { + if (_verboseLogging && (usesBatteryPoweredInverter() || usesSmartBufferPoweredInverter())) { MessageOutput.printf("[DPL] up %lu s, %snext inverter restart at %d s (set to %d)\r\n", millis()/1000, (_nextInverterRestart.first?"":"NO "), _nextInverterRestart.second/1000, config.PowerLimiter.RestartHour); + } + if (_verboseLogging && usesBatteryPoweredInverter()) { MessageOutput.printf("[DPL] battery interface %sabled, SoC %.1f %% (%s), age %u s (%s)\r\n", (config.Battery.Enabled?"en":"dis"), Battery.getStats()->getSoC(), @@ -339,8 +347,10 @@ void PowerLimiterClass::loop() inverterTotalPower = std::min(inverterTotalPower, totalAllowance); auto coveredBySolar = updateInverterLimits(inverterTotalPower, sSolarPoweredFilter, sSolarPoweredExpression); - auto remaining = (inverterTotalPower >= coveredBySolar) ? inverterTotalPower - coveredBySolar : 0; - auto powerBusUsage = calcPowerBusUsage(remaining); + auto remainingAfterSolar = (inverterTotalPower >= coveredBySolar) ? inverterTotalPower - coveredBySolar : 0; + auto coveredBySmartBuffer = updateInverterLimits(remainingAfterSolar, sSmartBufferPoweredFilter, sSmartBufferPoweredExpression); + auto remainingAfterSmartBuffer = (remainingAfterSolar >= coveredBySmartBuffer) ? remainingAfterSolar - coveredBySmartBuffer : 0; + auto powerBusUsage = calcPowerBusUsage(remainingAfterSmartBuffer); auto coveredByBattery = updateInverterLimits(powerBusUsage, sBatteryPoweredFilter, sBatteryPoweredExpression); if (_verboseLogging) { @@ -566,6 +576,8 @@ uint16_t PowerLimiterClass::updateInverterLimits(uint16_t powerRequested, matchingInverters.push_back(upInv.get()); } + if (matchingInverters.empty()) { return 0; } + int32_t diff = powerRequested - producing; auto const& config = Configuration.get(); @@ -579,8 +591,6 @@ uint16_t PowerLimiterClass::updateInverterLimits(uint16_t powerRequested, (plural?"s":""), producing, diff, hysteresis); } - if (matchingInverters.empty()) { return 0; } - if (std::abs(diff) < static_cast(hysteresis)) { return producing; } uint16_t covered = 0; @@ -723,7 +733,7 @@ float PowerLimiterClass::getBatteryInvertersOutputAcWatts() float res = 0; for (auto const& upInv : _inverters) { - if (upInv->isSolarPowered()) { continue; } + if (!upInv->isBatteryPowered()) { continue; } // TODO(schlimmchen): we must use the DC power instead, as the battery // voltage drops proportional to the DC current draw, but the AC power // output does not correlate with the battery current or voltage. @@ -906,7 +916,16 @@ bool PowerLimiterClass::isFullSolarPassthroughActive() bool PowerLimiterClass::usesBatteryPoweredInverter() { for (auto const& upInv : _inverters) { - if (!upInv->isSolarPowered()) { return true; } + if (upInv->isBatteryPowered()) { return true; } + } + + return false; +} + +bool PowerLimiterClass::usesSmartBufferPoweredInverter() +{ + for (auto const& upInv : _inverters) { + if (upInv->isSmartBufferPowered()) { return true; } } return false; diff --git a/src/PowerLimiterInverter.cpp b/src/PowerLimiterInverter.cpp index 947b18205..bfac2ba57 100644 --- a/src/PowerLimiterInverter.cpp +++ b/src/PowerLimiterInverter.cpp @@ -3,17 +3,23 @@ #include "PowerLimiterInverter.h" #include "PowerLimiterBatteryInverter.h" #include "PowerLimiterSolarInverter.h" +#include "PowerLimiterSmartBufferInverter.h" std::unique_ptr PowerLimiterInverter::create( bool verboseLogging, PowerLimiterInverterConfig const& config) { std::unique_ptr upInverter; - if (config.IsSolarPowered) { - upInverter = std::make_unique(verboseLogging, config); - } - else { - upInverter = std::make_unique(verboseLogging, config); + switch (config.PowerSource) { + case PowerLimiterInverterConfig::InverterPowerSource::Battery: + upInverter = std::make_unique(verboseLogging, config); + break; + case PowerLimiterInverterConfig::InverterPowerSource::Solar: + upInverter = std::make_unique(verboseLogging, config); + break; + case PowerLimiterInverterConfig::InverterPowerSource::SmartBuffer: + upInverter = std::make_unique(verboseLogging, config); + break; } if (nullptr == upInverter->_spInverter) { return nullptr; } @@ -336,7 +342,7 @@ void PowerLimiterInverter::debug() const " max reduction production/standby: %d/%d W, max increase: %d W\r\n" " target limit/output/state: %i W (%s)/%d W/%s, %d update timeouts\r\n", _logPrefix, - (isSolarPowered()?"solar":"battery"), + (isSmartBufferPowered()?"smart-buffer":(isSolarPowered()?"solar":"battery")), (isProducing()?"producing":"standing by at"), getCurrentOutputAcWatts(), _config.LowerPowerLimit, getCurrentLimitWatts(), _config.UpperPowerLimit, getInverterMaxPowerWatts(), diff --git a/src/PowerLimiterOverscalingInverter.cpp b/src/PowerLimiterOverscalingInverter.cpp new file mode 100644 index 000000000..84af614c4 --- /dev/null +++ b/src/PowerLimiterOverscalingInverter.cpp @@ -0,0 +1,143 @@ +#include "MessageOutput.h" +#include "PowerLimiterOverscalingInverter.h" + +PowerLimiterOverscalingInverter::PowerLimiterOverscalingInverter(bool verboseLogging, PowerLimiterInverterConfig const& config) + : PowerLimiterInverter(verboseLogging, config) { } + +uint16_t PowerLimiterOverscalingInverter::applyIncrease(uint16_t increase) +{ + if (isEligible() != Eligibility::Eligible) { return 0; } + + if (increase == 0) { return 0; } + + // do not wake inverter up if it would produce too much power + if (!isProducing() && _config.LowerPowerLimit > increase) { return 0; } + + // the limit might be scaled, so we use the + // current output as the baseline. inverters in standby have + // no output (baseline is zero). + auto baseline = getCurrentOutputAcWatts(); + + auto actualIncrease = std::min(increase, getMaxIncreaseWatts()); + setAcOutput(baseline + actualIncrease); + return actualIncrease; +} + +uint16_t PowerLimiterOverscalingInverter::scaleLimit(uint16_t expectedOutputWatts) +{ + // overscalling allows us to compensate for shaded panels by increasing the + // total power limit, if the inverter is solar powered. + // this feature should not be used when homyiles 'Power Distribution Logic' is available + // as the inverter will take care of the power distribution across the MPPTs itself. + // (added in inverter firmware 01.01.12 on supported models (HMS-1600/1800/2000)) + // When disabled we return the expected output. + if (!_config.UseOverscaling || _spInverter->supportsPowerDistributionLogic()) { return expectedOutputWatts; } + + // prevent scaling if inverter is not producing, as input channels are not + // producing energy and hence are detected as not-producing, causing + // unreasonable scaling. + if (!isProducing()) { return expectedOutputWatts; } + + auto pStats = _spInverter->Statistics(); + std::vector dcChnls = _spInverter->getChannelsDC(); + std::vector dcMppts = _spInverter->getMppts(); + size_t dcTotalChnls = dcChnls.size(); + size_t dcTotalMppts = dcMppts.size(); + + // if there is only one MPPT available, there is nothing we can do + if (dcTotalMppts <= 1) { return expectedOutputWatts; } + + // test for a reasonable power limit that allows us to assume that an input + // channel with little energy is actually not producing, rather than + // producing very little due to the very low limit. + if (getCurrentLimitWatts() < dcTotalChnls * 10) { return expectedOutputWatts; } + + auto inverterOutputAC = pStats->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC); + + float inverterEfficiencyFactor = pStats->getChannelFieldValue(TYPE_INV, CH0, FLD_EFF); + + // fall back to hoymiles peak efficiency as per datasheet if inverter + // is currently not producing (efficiency is zero in that case) + inverterEfficiencyFactor = (inverterEfficiencyFactor > 0) ? inverterEfficiencyFactor/100 : 0.967; + + auto scalingThreshold = static_cast(_config.ScalingThreshold) / 100.0; + auto expectedAcPowerPerMppt = (getCurrentLimitWatts() / dcTotalMppts) * scalingThreshold; + + if (_verboseLogging) { + MessageOutput.printf( + "%s\r\n" + " expected AC power per MPPT %.0f W\r\n", + _logPrefix, expectedAcPowerPerMppt); + } + + size_t dcShadedMppts = 0; + auto shadedChannelACPowerSum = 0.0; + + for (auto& m : dcMppts) { + float mpptPowerAC = 0.0; + std::vector mpptChnls = _spInverter->getChannelsDCByMppt(m); + + for (auto& c : mpptChnls) { + mpptPowerAC += pStats->getChannelFieldValue(TYPE_DC, c, FLD_PDC) * inverterEfficiencyFactor; + } + + if (mpptPowerAC < expectedAcPowerPerMppt) { + dcShadedMppts++; + shadedChannelACPowerSum += mpptPowerAC; + } + + if (_verboseLogging) { + MessageOutput.printf(" MPPT-%c AC power %.0f W\r\n", + mpptName(m), mpptPowerAC); + } + } + + // no shading or the shaded channels provide more power than what + // we currently need. + if (dcShadedMppts == 0 || shadedChannelACPowerSum >= expectedOutputWatts) { + return expectedOutputWatts; + } + + if (dcShadedMppts == dcTotalMppts) { + // keep the currentLimit when: + // - all channels are shaded + // - currentLimit >= expectedOutputWatts + // - we get the expected AC power or less and + if (getCurrentLimitWatts() >= expectedOutputWatts && + inverterOutputAC <= expectedOutputWatts) { + if (_verboseLogging) { + MessageOutput.printf(" all mppts are shaded, " + "keeping the current limit of %d W\r\n", + getCurrentLimitWatts()); + } + + return getCurrentLimitWatts(); + + } else { + return expectedOutputWatts; + } + } + + size_t dcNonShadedMppts = dcTotalMppts - dcShadedMppts; + uint16_t overScaledLimit = (expectedOutputWatts - shadedChannelACPowerSum) / dcNonShadedMppts * dcTotalMppts; + + if (overScaledLimit <= expectedOutputWatts) { return expectedOutputWatts; } + + if (_verboseLogging) { + MessageOutput.printf(" %d/%d mppts are not-producing/shaded, scaling %d W\r\n", + dcShadedMppts, dcTotalMppts, overScaledLimit); + } + + return overScaledLimit; +} + +void PowerLimiterOverscalingInverter::setAcOutput(uint16_t expectedOutputWatts) +{ + // make sure to enforce the lower and upper bounds + expectedOutputWatts = std::min(expectedOutputWatts, getConfiguredMaxPowerWatts()); + expectedOutputWatts = std::max(expectedOutputWatts, _config.LowerPowerLimit); + + setExpectedOutputAcWatts(expectedOutputWatts); + setTargetPowerLimitWatts(scaleLimit(expectedOutputWatts)); + setTargetPowerState(true); +} diff --git a/src/PowerLimiterSmartBufferInverter.cpp b/src/PowerLimiterSmartBufferInverter.cpp new file mode 100644 index 000000000..bb9858830 --- /dev/null +++ b/src/PowerLimiterSmartBufferInverter.cpp @@ -0,0 +1,75 @@ +#include "MessageOutput.h" +#include "PowerLimiterSmartBufferInverter.h" + +PowerLimiterSmartBufferInverter::PowerLimiterSmartBufferInverter(bool verboseLogging, PowerLimiterInverterConfig const& config) + : PowerLimiterOverscalingInverter(verboseLogging, config) { } + +uint16_t PowerLimiterSmartBufferInverter::getMaxReductionWatts(bool allowStandby) const +{ + if (isEligible() != Eligibility::Eligible) { return 0; } + + if (!isProducing()) { return 0; } + + if (allowStandby) { return getCurrentOutputAcWatts(); } + + if (getCurrentOutputAcWatts() <= _config.LowerPowerLimit) { return 0; } + + return getCurrentOutputAcWatts() - _config.LowerPowerLimit; +} + +uint16_t PowerLimiterSmartBufferInverter::getMaxIncreaseWatts() const +{ + if (isEligible() != Eligibility::Eligible) { return 0; } + + if (!isProducing()) { + return getConfiguredMaxPowerWatts(); + } + + // when overscaling is in use we must not substract the current limit + // because it might be scaled and higher than the configured max power. + if (_config.UseOverscaling && !_spInverter->supportsPowerDistributionLogic()) { + return getConfiguredMaxPowerWatts() - getCurrentOutputAcWatts(); + } + + // we must not substract the current AC output here, but the current + // limit value, so we avoid trying to produce even more even if the + // inverter is already at the maximum limit value (the actual AC + // output may be less than the inverter's current power limit). + return std::max(0, getConfiguredMaxPowerWatts() - getCurrentLimitWatts()); +} + +uint16_t PowerLimiterSmartBufferInverter::applyReduction(uint16_t reduction, bool allowStandby) +{ + if (isEligible() != Eligibility::Eligible) { return 0; } + + if (reduction == 0) { return 0; } + + auto low = std::min(getCurrentLimitWatts(), getCurrentOutputAcWatts()); + if (low <= _config.LowerPowerLimit) { + if (allowStandby) { + standby(); + return std::min(reduction, getCurrentOutputAcWatts()); + } + return 0; + } + + if ((getCurrentLimitWatts() - _config.LowerPowerLimit) >= reduction) { + setAcOutput(getCurrentLimitWatts() - reduction); + return reduction; + } + + if (allowStandby) { + standby(); + return std::min(reduction, getCurrentOutputAcWatts()); + } + + setAcOutput(_config.LowerPowerLimit); + return getCurrentOutputAcWatts() - _config.LowerPowerLimit; +} + +uint16_t PowerLimiterSmartBufferInverter::standby() +{ + setTargetPowerState(false); + setExpectedOutputAcWatts(0); + return getCurrentOutputAcWatts(); +} diff --git a/src/PowerLimiterSolarInverter.cpp b/src/PowerLimiterSolarInverter.cpp index 58312232e..225427f7a 100644 --- a/src/PowerLimiterSolarInverter.cpp +++ b/src/PowerLimiterSolarInverter.cpp @@ -2,7 +2,7 @@ #include "PowerLimiterSolarInverter.h" PowerLimiterSolarInverter::PowerLimiterSolarInverter(bool verboseLogging, PowerLimiterInverterConfig const& config) - : PowerLimiterInverter(verboseLogging, config) { } + : PowerLimiterOverscalingInverter(verboseLogging, config) { } uint16_t PowerLimiterSolarInverter::getMaxReductionWatts(bool) const { @@ -110,25 +110,6 @@ uint16_t PowerLimiterSolarInverter::applyReduction(uint16_t reduction, bool) return getCurrentOutputAcWatts() - _config.LowerPowerLimit; } -uint16_t PowerLimiterSolarInverter::applyIncrease(uint16_t increase) -{ - if (isEligible() != Eligibility::Eligible) { return 0; } - - if (increase == 0) { return 0; } - - // do not wake inverter up if it would produce too much power - if (!isProducing() && _config.LowerPowerLimit > increase) { return 0; } - - // the limit for solar-powered inverters might be scaled, so we use the - // current output as the baseline. solar-powered inverters in standby have - // no output (baseline is zero). - auto baseline = getCurrentOutputAcWatts(); - - auto actualIncrease = std::min(increase, getMaxIncreaseWatts()); - setAcOutput(baseline + actualIncrease); - return actualIncrease; -} - uint16_t PowerLimiterSolarInverter::standby() { // solar-powered inverters are never actually put into standby (by the @@ -136,116 +117,3 @@ uint16_t PowerLimiterSolarInverter::standby() setAcOutput(_config.LowerPowerLimit); return getCurrentOutputAcWatts() - _config.LowerPowerLimit; } - -uint16_t PowerLimiterSolarInverter::scaleLimit(uint16_t expectedOutputWatts) -{ - // overscalling allows us to compensate for shaded panels by increasing the - // total power limit, if the inverter is solar powered. - // this feature should not be used when homyiles 'Power Distribution Logic' is available - // as the inverter will take care of the power distribution across the MPPTs itself. - // (added in inverter firmware 01.01.12 on supported models (HMS-1600/1800/2000)) - // When disabled we return the expected output. - if (!_config.UseOverscaling || _spInverter->supportsPowerDistributionLogic()) { return expectedOutputWatts; } - - // prevent scaling if inverter is not producing, as input channels are not - // producing energy and hence are detected as not-producing, causing - // unreasonable scaling. - if (!isProducing()) { return expectedOutputWatts; } - - auto pStats = _spInverter->Statistics(); - std::vector dcChnls = _spInverter->getChannelsDC(); - std::vector dcMppts = _spInverter->getMppts(); - size_t dcTotalChnls = dcChnls.size(); - size_t dcTotalMppts = dcMppts.size(); - - // if there is only one MPPT available, there is nothing we can do - if (dcTotalMppts <= 1) { return expectedOutputWatts; } - - // test for a reasonable power limit that allows us to assume that an input - // channel with little energy is actually not producing, rather than - // producing very little due to the very low limit. - if (getCurrentLimitWatts() < dcTotalChnls * 10) { return expectedOutputWatts; } - - auto inverterOutputAC = pStats->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC); - - float inverterEfficiencyFactor = pStats->getChannelFieldValue(TYPE_INV, CH0, FLD_EFF); - - // fall back to hoymiles peak efficiency as per datasheet if inverter - // is currently not producing (efficiency is zero in that case) - inverterEfficiencyFactor = (inverterEfficiencyFactor > 0) ? inverterEfficiencyFactor/100 : 0.967; - - auto scalingThreshold = static_cast(_config.ScalingThreshold) / 100.0; - auto expectedAcPowerPerMppt = (getCurrentLimitWatts() / dcTotalMppts) * scalingThreshold; - - if (_verboseLogging) { - MessageOutput.printf("%s expected AC power per MPPT %.0f W\r\n", - _logPrefix, expectedAcPowerPerMppt); - } - - size_t dcShadedMppts = 0; - auto shadedChannelACPowerSum = 0.0; - - for (auto& m : dcMppts) { - float mpptPowerAC = 0.0; - std::vector mpptChnls = _spInverter->getChannelsDCByMppt(m); - - for (auto& c : mpptChnls) { - mpptPowerAC += pStats->getChannelFieldValue(TYPE_DC, c, FLD_PDC) * inverterEfficiencyFactor; - } - - if (mpptPowerAC < expectedAcPowerPerMppt) { - dcShadedMppts++; - shadedChannelACPowerSum += mpptPowerAC; - } - - if (_verboseLogging) { - MessageOutput.printf("%s MPPT-%c AC power %.0f W\r\n", - _logPrefix, mpptName(m), mpptPowerAC); - } - } - - // no shading or the shaded channels provide more power than what - // we currently need. - if (dcShadedMppts == 0 || shadedChannelACPowerSum >= expectedOutputWatts) { - return expectedOutputWatts; - } - - if (dcShadedMppts == dcTotalMppts) { - // keep the currentLimit when: - // - all channels are shaded - // - currentLimit >= expectedOutputWatts - // - we get the expected AC power or less and - if (getCurrentLimitWatts() >= expectedOutputWatts && - inverterOutputAC <= expectedOutputWatts) { - if (_verboseLogging) { - MessageOutput.printf("%s all mppts are shaded, " - "keeping the current limit of %d W\r\n", - _logPrefix, getCurrentLimitWatts()); - } - - return getCurrentLimitWatts(); - - } else { - return expectedOutputWatts; - } - } - - size_t dcNonShadedMppts = dcTotalMppts - dcShadedMppts; - uint16_t overScaledLimit = (expectedOutputWatts - shadedChannelACPowerSum) / dcNonShadedMppts * dcTotalMppts; - - if (overScaledLimit <= expectedOutputWatts) { return expectedOutputWatts; } - - if (_verboseLogging) { - MessageOutput.printf("%s %d/%d mppts are not-producing/shaded, scaling %d W\r\n", - _logPrefix, dcShadedMppts, dcTotalMppts, overScaledLimit); - } - - return overScaledLimit; -} - -void PowerLimiterSolarInverter::setAcOutput(uint16_t expectedOutputWatts) -{ - setExpectedOutputAcWatts(expectedOutputWatts); - setTargetPowerLimitWatts(scaleLimit(expectedOutputWatts)); - setTargetPowerState(true); -} diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index f0c7da00d..fc3cd008f 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -726,7 +726,10 @@ "ScalingPowerThreshold": "Schwellenwert für Überskalierung", "ScalingPowerThresholdHint": "Minimale Eingangsleistungsschwelle (%). Eingänge unterhalb dieses Prozentsatzes werden als verschattet/ungenutzt bewertet.", "InverterIsBehindPowerMeterHint": "Aktivieren falls sich der Stromzähler-Messwert um die Ausgangsleistung des Wechselrichters verringert, wenn dieser Strom produziert. Normalerweise ist das zutreffend.", - "InverterIsSolarPowered": "Wechselrichter wird von Solarmodulen gespeist", + "PowerSource": "Stromquelle", + "PowerSourceBattery": "Batterie", + "PowerSourceSolar": "Solarmodule", + "PowerSourceSmartBuffer": "Smart Buffer Batterie (Marstek B2500, Anker Solix, Zendure, etc.)", "UseOverscaling": "Verschattetet/Ungenutzte Eingänge ausgleichen", "UseOverscalingHint": "Erlaubt das Überskalieren des Wechselrichter-Limits, um ungenutzte Eingänge oder Verschattung eines oder mehrerer Eingänge auszugleichen.", "VoltageThresholds": "Batterie Spannungs-Schwellwerte ", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 0b403af7c..e8b5249d4 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -728,7 +728,10 @@ "ScalingPowerThreshold": "Overscaling input power threshold", "ScalingPowerThresholdHint": "Set the minimum power input threshold (%). Inputs below this percentage are considered shaded/unused.", "InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.", - "InverterIsSolarPowered": "Inverter is powered by solar modules", + "PowerSource": "Power Source", + "PowerSourceBattery": "Battery", + "PowerSourceSolar": "Solar Panels", + "PowerSourceSmartBuffer": "Smart Buffer Battery (Marstek B2500, Anker Solix, Zendure, etc.)", "UseOverscaling": "Compensate shaded or unused inputs", "UseOverscalingHint": "Allow to overscale the inverter limit to compensate for unused inputs or shading of one or multiple inputs.", "VoltageThresholds": "Battery Voltage Thresholds", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 9ea4ddbfb..673909b0b 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -736,7 +736,6 @@ "ScalingPowerThreshold": "Overscaling input power threshold", "ScalingPowerThresholdHint": "Set the minimum power input threshold (%). Inputs below this percentage are considered shaded/unused.", "InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.", - "InverterIsSolarPowered": "Inverter is powered by solar modules", "UseOverscaling": "Compensate shaded or unused inputs", "UseOverscalingHint": "Allow to overscale the inverter limit to compensate for unused inputs or shading of one or multiple inputs.", "VoltageThresholds": "Battery Voltage Thresholds", diff --git a/webapp/src/types/PowerLimiterConfig.ts b/webapp/src/types/PowerLimiterConfig.ts index 253b001be..91bf98e74 100644 --- a/webapp/src/types/PowerLimiterConfig.ts +++ b/webapp/src/types/PowerLimiterConfig.ts @@ -26,7 +26,7 @@ export interface PowerLimiterInverterConfig { serial: string; is_governed: boolean; is_behind_power_meter: boolean; - is_solar_powered: boolean; + power_source: number; use_overscaling_to_compensate_shading: boolean; lower_power_limit: number; upper_power_limit: number; diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index e5630e56a..c45799316 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -102,6 +102,31 @@ wide /> + +