Skip to content

Commit

Permalink
Feature: DPL support for smart-buffer-batteries (#1606)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
AndreasBoehm and schlimmchen authored Feb 8, 2025
1 parent 878c27c commit 6d2d85b
Show file tree
Hide file tree
Showing 19 changed files with 388 additions and 196 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: 1 addition & 0 deletions include/PowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class PowerLimiterClass {
void setMode(Mode m) { _mode = m; }
Mode getMode() const { return _mode; }
bool usesBatteryPoweredInverter();
bool usesSmartBufferPoweredInverter();
bool isGovernedInverterProducing();

private:
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
17 changes: 17 additions & 0 deletions include/PowerLimiterOverscalingInverter.h
Original file line number Diff line number Diff line change
@@ -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);
};
14 changes: 14 additions & 0 deletions include/PowerLimiterSmartBufferInverter.h
Original file line number Diff line number Diff line change
@@ -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;
};
10 changes: 2 additions & 8 deletions include/PowerLimiterSolarInverter.h
Original file line number Diff line number Diff line change
@@ -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;
};
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
35 changes: 27 additions & 8 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 @@ -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(),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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<int32_t>(hysteresis)) { return producing; }

uint16_t covered = 0;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 12 additions & 6 deletions src/PowerLimiterInverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@
#include "PowerLimiterInverter.h"
#include "PowerLimiterBatteryInverter.h"
#include "PowerLimiterSolarInverter.h"
#include "PowerLimiterSmartBufferInverter.h"

std::unique_ptr<PowerLimiterInverter> PowerLimiterInverter::create(
bool verboseLogging, PowerLimiterInverterConfig const& config)
{
std::unique_ptr<PowerLimiterInverter> upInverter;

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

if (nullptr == upInverter->_spInverter) { return nullptr; }
Expand Down Expand Up @@ -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(),
Expand Down
Loading

0 comments on commit 6d2d85b

Please sign in to comment.