Skip to content

Commit

Permalink
fix: DPL support for smart-buffer-batteries
Browse files Browse the repository at this point in the history
change settings to pick a power source (battery, solar, smart-buffer) and treat smart-buffers like a mix of battery and solar-powered.

Smart-buffer-powered (Marstek B2500, Anker Solix, Zendure, etc.) inverters can always increase to the max limit without any checks, support overscaling and can be restarted.
  • Loading branch information
AndreasBoehm committed Feb 4, 2025
1 parent ac14ce5 commit 8c04c93
Show file tree
Hide file tree
Showing 14 changed files with 92 additions and 32 deletions.
6 changes: 4 additions & 2 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down
1 change: 0 additions & 1 deletion include/PowerLimiterBatteryInverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 4 additions & 1 deletion include/PowerLimiterInverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 0 additions & 1 deletion include/PowerLimiterSolarInverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 0 additions & 1 deletion include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 24 additions & 3 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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<JsonArray>();

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;
Expand Down
22 changes: 15 additions & 7 deletions src/PowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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<int32_t>(hysteresis)) { return producing; }

uint16_t covered = 0;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions src/PowerLimiterInverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ std::unique_ptr<PowerLimiterInverter> PowerLimiterInverter::create(
{
std::unique_ptr<PowerLimiterInverter> upInverter;

if (config.IsSolarPowered) {
upInverter = std::make_unique<PowerLimiterSolarInverter>(verboseLogging, config);
if (config.PowerSource == PowerLimiterInverterConfig::InverterPowerSource::Battery) {
upInverter = std::make_unique<PowerLimiterBatteryInverter>(verboseLogging, config);
}
else {
upInverter = std::make_unique<PowerLimiterBatteryInverter>(verboseLogging, config);
upInverter = std::make_unique<PowerLimiterSolarInverter>(verboseLogging, config);
}

if (nullptr == upInverter->_spInverter) { return nullptr; }
Expand Down Expand Up @@ -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(),
Expand Down
5 changes: 5 additions & 0 deletions src/PowerLimiterSolarInverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<MpptNum_t> dcMppts = _spInverter->getMppts();
size_t dcTotalMppts = dcMppts.size();
Expand Down
5 changes: 4 additions & 1 deletion webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ",
Expand Down
5 changes: 4 additions & 1 deletion webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion webapp/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/types/PowerLimiterConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
35 changes: 27 additions & 8 deletions webapp/src/views/PowerLimiterAdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,29 @@
wide
/>

<InputElement
:label="$t('powerlimiteradmin.InverterIsSolarPowered')"
v-model="powerLimiterConfigList.inverters[idx].is_solar_powered"
type="checkbox"
wide
/>
<div class="row mb-3">
<label class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.PowerSource') }}
</label>
<div class="col-sm-8">
<select
class="form-select"
v-model="powerLimiterConfigList.inverters[idx].power_source"
>
<option
v-for="provider in powerSourceList"
:key="provider.key"
:value="provider.key"
>
{{ $t(`powerlimiteradmin.PowerSource` + provider.value) }}
</option>
</select>
</div>
</div>

<InputElement
v-if="
powerLimiterConfigList.inverters[idx].is_solar_powered &&
powerLimiterConfigList.inverters[idx].power_source != 0 &&
inverterSupportsOverscaling(inv.serial)
"
:label="$t('powerlimiteradmin.UseOverscaling')"
Expand Down Expand Up @@ -463,6 +476,11 @@ export default defineComponent({
alertType: 'info',
showAlert: false,
configAlert: false,
powerSourceList: [
{ key: 0, value: 'Battery' },
{ key: 1, value: 'Solar' },
{ key: 2, value: 'SmartBuffer' },
],
};
},
created() {
Expand All @@ -487,7 +505,7 @@ export default defineComponent({
return inverters.filter((inv: PowerLimiterInverterConfig) => 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;
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit 8c04c93

Please sign in to comment.