Skip to content

Commit 83ac154

Browse files
authored
Feature: Implement DPL 'overscaling' to compensate shading (#956)
1 parent ccbaf55 commit 83ac154

File tree

9 files changed

+83
-10
lines changed

9 files changed

+83
-10
lines changed

include/Configuration.h

+1
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ struct CONFIG_T {
210210
uint32_t Interval;
211211
bool IsInverterBehindPowerMeter;
212212
bool IsInverterSolarPowered;
213+
bool UseOverscalingToCompensateShading;
213214
uint64_t InverterId;
214215
uint8_t InverterChannelId;
215216
int32_t TargetPowerConsumption;

include/defaults.h

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
#define POWERLIMITER_INTERVAL 10
128128
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
129129
#define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false
130+
#define POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING false
130131
#define POWERLIMITER_INVERTER_ID 0ULL
131132
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
132133
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0

src/Configuration.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ bool ConfigurationClass::write()
184184
powerlimiter["interval"] = config.PowerLimiter.Interval;
185185
powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
186186
powerlimiter["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered;
187+
powerlimiter["use_overscaling_to_compensate_shading"] = config.PowerLimiter.UseOverscalingToCompensateShading;
187188
powerlimiter["inverter_id"] = config.PowerLimiter.InverterId;
188189
powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
189190
powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
@@ -444,6 +445,7 @@ bool ConfigurationClass::read()
444445
config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL;
445446
config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
446447
config.PowerLimiter.IsInverterSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED;
448+
config.PowerLimiter.UseOverscalingToCompensateShading = powerlimiter["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING;
447449
config.PowerLimiter.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID;
448450
config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;
449451
config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION;

src/PowerLimiter.cpp

+62-10
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020

2121
PowerLimiterClass PowerLimiter;
2222

23-
void PowerLimiterClass::init(Scheduler& scheduler)
24-
{
23+
void PowerLimiterClass::init(Scheduler& scheduler)
24+
{
2525
scheduler.addTask(_loopTask);
2626
_loopTask.setCallback(std::bind(&PowerLimiterClass::loop, this));
2727
_loopTask.setIterations(TASK_FOREVER);
@@ -337,6 +337,16 @@ float PowerLimiterClass::getBatteryVoltage(bool log) {
337337
return res;
338338
}
339339

340+
static float getInverterEfficiency(std::shared_ptr<InverterAbstract> inverter)
341+
{
342+
float inverterEfficiencyPercent = inverter->Statistics()->getChannelFieldValue(
343+
TYPE_INV, CH0, FLD_EFF);
344+
345+
// fall back to hoymiles peak efficiency as per datasheet if inverter
346+
// is currently not producing (efficiency is zero in that case)
347+
return (inverterEfficiencyPercent > 0) ? inverterEfficiencyPercent/100 : 0.967;
348+
}
349+
340350
/**
341351
* calculate the AC output power (limit) to set, such that the inverter uses
342352
* the given power on its DC side, i.e., adjust the power for the inverter's
@@ -346,12 +356,7 @@ int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr<InverterAbstract>
346356
{
347357
CONFIG_T& config = Configuration.get();
348358

349-
float inverterEfficiencyPercent = inverter->Statistics()->getChannelFieldValue(
350-
TYPE_INV, CH0, FLD_EFF);
351-
352-
// fall back to hoymiles peak efficiency as per datasheet if inverter
353-
// is currently not producing (efficiency is zero in that case)
354-
float inverterEfficiencyFactor = (inverterEfficiencyPercent > 0) ? inverterEfficiencyPercent/100 : 0.967;
359+
float inverterEfficiencyFactor = getInverterEfficiency(inverter);
355360

356361
// account for losses between solar charger and inverter (cables, junctions...)
357362
float lossesFactor = 1.00 - static_cast<float>(config.PowerLimiter.SolarPassThroughLosses)/100;
@@ -687,8 +692,7 @@ bool PowerLimiterClass::updateInverter()
687692
*
688693
* TODO(schlimmchen): the current implementation is broken and is in need of
689694
* refactoring. currently it only works for inverters that provide one MPPT for
690-
* each input. it also does not work as expected if any input produces *some*
691-
* energy, but is limited by its respective solar input.
695+
* each input.
692696
*/
693697
static int32_t scalePowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newLimit, int32_t currentLimitWatts)
694698
{
@@ -713,6 +717,54 @@ static int32_t scalePowerLimit(std::shared_ptr<InverterAbstract> inverter, int32
713717
// producing very little due to the very low limit.
714718
if (currentLimitWatts < dcTotalChnls * 10) { return newLimit; }
715719

720+
auto const& config = Configuration.get();
721+
auto allowOverscaling = config.PowerLimiter.UseOverscalingToCompensateShading;
722+
auto isInverterSolarPowered = config.PowerLimiter.IsInverterSolarPowered;
723+
724+
// overscalling allows us to compensate for shaded panels by increasing the
725+
// total power limit, if the inverter is solar powered.
726+
if (allowOverscaling && isInverterSolarPowered) {
727+
auto inverterOutputAC = inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC);
728+
float inverterEfficiencyFactor = getInverterEfficiency(inverter);
729+
730+
// 98% of the expected power is good enough
731+
auto expectedAcPowerPerChannel = (currentLimitWatts / dcTotalChnls) * 0.98;
732+
733+
size_t dcShadedChnls = 0;
734+
auto shadedChannelACPowerSum = 0.0;
735+
736+
for (auto& c : dcChnls) {
737+
auto channelPowerAC = inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) * inverterEfficiencyFactor;
738+
739+
if (channelPowerAC < expectedAcPowerPerChannel) {
740+
dcShadedChnls++;
741+
shadedChannelACPowerSum += channelPowerAC;
742+
}
743+
}
744+
745+
// no shading or the shaded channels provide more power than what
746+
// we currently need.
747+
if (dcShadedChnls == 0 || shadedChannelACPowerSum >= newLimit) { return newLimit; }
748+
749+
// keep the currentLimit when all channels are shaded and we get the
750+
// expected AC power or less.
751+
if (dcShadedChnls == dcTotalChnls && inverterOutputAC <= newLimit) {
752+
MessageOutput.printf("[DPL::scalePowerLimit] all channels are shaded, "
753+
"keeping the current limit of %d W\r\n", currentLimitWatts);
754+
755+
return currentLimitWatts;
756+
}
757+
758+
size_t dcNonShadedChnls = dcTotalChnls - dcShadedChnls;
759+
auto overScaledLimit = static_cast<int32_t>((newLimit - shadedChannelACPowerSum) / dcNonShadedChnls * dcTotalChnls);
760+
761+
if (overScaledLimit <= newLimit) { return newLimit; }
762+
763+
MessageOutput.printf("[DPL::scalePowerLimit] %d/%d channels are shaded, "
764+
"scaling %d W\r\n", dcShadedChnls, dcTotalChnls, overScaledLimit);
765+
return overScaledLimit;
766+
}
767+
716768
size_t dcProdChnls = 0;
717769
for (auto& c : dcChnls) {
718770
if (inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) > 2.0) {

src/WebApi_powerlimiter.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
3838
root["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight;
3939
root["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
4040
root["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered;
41+
root["use_overscaling_to_compensate_shading"] = config.PowerLimiter.UseOverscalingToCompensateShading;
4142
root["inverter_serial"] = String(config.PowerLimiter.InverterId);
4243
root["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
4344
root["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
@@ -159,6 +160,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
159160

160161
config.PowerLimiter.IsInverterBehindPowerMeter = root["is_inverter_behind_powermeter"].as<bool>();
161162
config.PowerLimiter.IsInverterSolarPowered = root["is_inverter_solar_powered"].as<bool>();
163+
config.PowerLimiter.UseOverscalingToCompensateShading = root["use_overscaling_to_compensate_shading"].as<bool>();
162164
config.PowerLimiter.InverterId = root["inverter_serial"].as<uint64_t>();
163165
config.PowerLimiter.InverterChannelId = root["inverter_channel_id"].as<uint8_t>();
164166
config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as<int32_t>();

webapp/src/locales/de.json

+2
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,8 @@
637637
"InverterIsBehindPowerMeter": "Stromzählermessung beinhaltet Wechselrichter",
638638
"InverterIsBehindPowerMeterHint": "Aktivieren falls sich der Stromzähler-Messwert um die Ausgangsleistung des Wechselrichters verringert, wenn dieser Strom produziert. Normalerweise ist das zutreffend.",
639639
"InverterIsSolarPowered": "Wechselrichter wird von Solarmodulen gespeist",
640+
"UseOverscalingToCompensateShading": "Verschattung durch Überskalierung ausgleichen",
641+
"UseOverscalingToCompensateShadingHint": "Erlaubt das Überskalieren des Wechselrichter-Limits, um Verschattung eines oder mehrerer Eingänge auszugleichen",
640642
"VoltageThresholds": "Batterie Spannungs-Schwellwerte ",
641643
"VoltageLoadCorrectionInfo": "<b>Hinweis:</b> Wenn Leistung von der Batterie abgegeben wird, bricht ihre Spannung etwas ein. Der Spannungseinbruch skaliert mit dem Entladestrom. Damit nicht vorzeitig der Wechselrichter ausgeschaltet wird sobald der Stop-Schwellenwert unterschritten wurde, wird der hier angegebene Korrekturfaktor mit einberechnet um die Spannung zu errechnen die der Akku in Ruhe hätte. Korrigierte Spannung = DC Spannung + (Aktuelle Leistung (W) * Korrekturfaktor).",
642644
"InverterRestartHour": "Uhrzeit für geplanten Neustart",

webapp/src/locales/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,8 @@
643643
"InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output",
644644
"InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.",
645645
"InverterIsSolarPowered": "Inverter is powered by solar modules",
646+
"UseOverscalingToCompensateShading": "Compensate for shading",
647+
"UseOverscalingToCompensateShadingHint": "Allow to overscale the inverter limit to compensate for shading of one or multiple inputs",
646648
"VoltageThresholds": "Battery Voltage Thresholds",
647649
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor).",
648650
"InverterRestartHour": "Automatic Restart Time",

webapp/src/types/PowerLimiterConfig.ts

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface PowerLimiterConfig {
2626
battery_always_use_at_night: boolean;
2727
is_inverter_behind_powermeter: boolean;
2828
is_inverter_solar_powered: boolean;
29+
use_overscaling_to_compensate_shading: boolean;
2930
inverter_serial: string;
3031
inverter_channel_id: number;
3132
target_power_consumption: number;

webapp/src/views/PowerLimiterAdminView.vue

+10
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@
6868
v-model="powerLimiterConfigList.is_inverter_solar_powered"
6969
type="checkbox" wide/>
7070

71+
<InputElement v-show="canUseOverscaling()"
72+
:label="$t('powerlimiteradmin.UseOverscalingToCompensateShading')"
73+
:tooltip="$t('powerlimiteradmin.UseOverscalingToCompensateShadingHint')"
74+
v-model="powerLimiterConfigList.use_overscaling_to_compensate_shading"
75+
type="checkbox" wide/>
76+
7177
<div class="row mb-3" v-if="needsChannelSelection()">
7278
<label for="inverter_channel" class="col-sm-4 col-form-label">
7379
{{ $t('powerlimiteradmin.InverterChannelId') }}
@@ -309,6 +315,10 @@ export default defineComponent({
309315
hasPowerMeter() {
310316
return this.powerLimiterMetaData.power_meter_enabled;
311317
},
318+
canUseOverscaling() {
319+
const cfg = this.powerLimiterConfigList;
320+
return cfg.is_inverter_solar_powered;
321+
},
312322
canUseSolarPassthrough() {
313323
const cfg = this.powerLimiterConfigList;
314324
const meta = this.powerLimiterMetaData;

0 commit comments

Comments
 (0)