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/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/PowerLimiterSolarInverter.h b/include/PowerLimiterSolarInverter.h index 72023211c..202442455 100644 --- a/include/PowerLimiterSolarInverter.h +++ b/include/PowerLimiterSolarInverter.h @@ -12,7 +12,6 @@ class PowerLimiterSolarInverter : 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 true; } private: uint16_t scaleLimit(uint16_t expectedOutputWatts); 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..608f4dc88 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) @@ -339,8 +345,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(inverterTotalPower, sSmartBufferPoweredFilter, sSmartBufferPoweredExpression); + auto remainingAfterSmartBuffer = (remainingAfterSolar >= coveredBySmartBuffer) ? remainingAfterSolar - coveredBySmartBuffer : 0; + auto powerBusUsage = calcPowerBusUsage(remainingAfterSmartBuffer); auto coveredByBattery = updateInverterLimits(powerBusUsage, sBatteryPoweredFilter, sBatteryPoweredExpression); if (_verboseLogging) { @@ -566,6 +574,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 +589,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 +731,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 +914,7 @@ bool PowerLimiterClass::isFullSolarPassthroughActive() bool PowerLimiterClass::usesBatteryPoweredInverter() { for (auto const& upInv : _inverters) { - if (!upInv->isSolarPowered()) { return true; } + if (upInv->isBatteryPowered()) { return true; } } return false; diff --git a/src/PowerLimiterInverter.cpp b/src/PowerLimiterInverter.cpp index 947b18205..d124ed009 100644 --- a/src/PowerLimiterInverter.cpp +++ b/src/PowerLimiterInverter.cpp @@ -9,11 +9,11 @@ std::unique_ptr PowerLimiterInverter::create( { std::unique_ptr upInverter; - if (config.IsSolarPowered) { - upInverter = std::make_unique(verboseLogging, config); + if (config.PowerSource == PowerLimiterInverterConfig::InverterPowerSource::Battery) { + upInverter = std::make_unique(verboseLogging, config); } else { - upInverter = std::make_unique(verboseLogging, config); + upInverter = std::make_unique(verboseLogging, config); } if (nullptr == upInverter->_spInverter) { return nullptr; } @@ -336,7 +336,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/PowerLimiterSolarInverter.cpp b/src/PowerLimiterSolarInverter.cpp index 58312232e..f0c1f835a 100644 --- a/src/PowerLimiterSolarInverter.cpp +++ b/src/PowerLimiterSolarInverter.cpp @@ -27,6 +27,11 @@ uint16_t PowerLimiterSolarInverter::getMaxIncreaseWatts() const // the maximum increase possible for this inverter int16_t maxTotalIncrease = getConfiguredMaxPowerWatts() - getCurrentOutputAcWatts(); + if (_config.PowerSource == PowerLimiterInverterConfig::InverterPowerSource::SmartBuffer) { + // smart buffer inverters can always increase the power to the configured max + return maxTotalIncrease; + } + auto pStats = _spInverter->Statistics(); std::vector dcMppts = _spInverter->getMppts(); size_t dcTotalMppts = dcMppts.size(); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 0cbf7912f..58d733254 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -725,7 +725,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 ddad495b1..77e3f2866 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -727,7 +727,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 c89a0bf56..0e23cced8 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -121,16 +121,29 @@ wide /> - +
+ +
+ +
+
inv.is_governed) || []; }, governedBatteryPoweredInverters(): PowerLimiterInverterConfig[] { - return this.governedInverters.filter((inv: PowerLimiterInverterConfig) => !inv.is_solar_powered); + return this.governedInverters.filter((inv: PowerLimiterInverterConfig) => inv.power_source == 0); }, governingBatteryPoweredInverters(): boolean { return this.governedBatteryPoweredInverters.length > 0; @@ -689,6 +707,7 @@ export default defineComponent({ newInv.is_behind_power_meter = true; newInv.lower_power_limit = this.getLowerLimitMinimum(newInv); newInv.upper_power_limit = Math.max(metaInv.max_power, 300); + newInv.power_source = 0; // battery inverters.push(newInv); }