From ccbaf558087d2c481e148b9f9b1bce125d3ba07c Mon Sep 17 00:00:00 2001 From: cerise21 <40767429+cerise21@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:25:41 +0200 Subject: [PATCH 1/2] Feature: Set/obtain DPL target power consumption via MQTT --- include/MqttHandlePowerLimiter.h | 3 ++- src/MqttHandlePowerLimiter.cpp | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/include/MqttHandlePowerLimiter.h b/include/MqttHandlePowerLimiter.h index fd35b5798..a3e78ab73 100644 --- a/include/MqttHandlePowerLimiter.h +++ b/include/MqttHandlePowerLimiter.h @@ -24,7 +24,8 @@ class MqttHandlePowerLimiterClass { VoltageStopThreshold, FullSolarPassThroughStartVoltage, FullSolarPassThroughStopVoltage, - UpperPowerLimit + UpperPowerLimit, + TargetPowerConsumption }; void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index dd1c96dd3..ab35fff97 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -45,6 +45,7 @@ void MqttHandlePowerLimiterClass::init(Scheduler& scheduler) subscribe("threshold/voltage/full_solar_passthrough_stop", MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage); subscribe("mode", MqttPowerLimiterCommand::Mode); subscribe("upper_power_limit", MqttPowerLimiterCommand::UpperPowerLimit); + subscribe("target_power_consumption", MqttPowerLimiterCommand::TargetPowerConsumption); _lastPublish = millis(); } @@ -79,6 +80,8 @@ void MqttHandlePowerLimiterClass::loop() MqttSettings.publish("powerlimiter/status/upper_power_limit", String(config.PowerLimiter.UpperPowerLimit)); + MqttSettings.publish("powerlimiter/status/target_power_consumption", String(config.PowerLimiter.TargetPowerConsumption)); + MqttSettings.publish("powerlimiter/status/inverter_update_timeouts", String(PowerLimiter.getInverterUpdateTimeouts())); // no thresholds are relevant for setups without a battery @@ -182,6 +185,11 @@ void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, con MessageOutput.printf("Setting upper power limit to: %d W\r\n", intValue); config.PowerLimiter.UpperPowerLimit = intValue; break; + case MqttPowerLimiterCommand::TargetPowerConsumption: + if (config.PowerLimiter.TargetPowerConsumption == intValue) { return; } + MessageOutput.printf("Setting target power consumption to: %d W\r\n", intValue); + config.PowerLimiter.TargetPowerConsumption = intValue; + break; } // not reached if the value did not change From 83ac15405ecb5ab8fbc17d62b93b9ed76b9f91bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20B=C3=B6hm?= Date: Thu, 20 Jun 2024 13:32:29 +0200 Subject: [PATCH 2/2] Feature: Implement DPL 'overscaling' to compensate shading (#956) --- include/Configuration.h | 1 + include/defaults.h | 1 + src/Configuration.cpp | 2 + src/PowerLimiter.cpp | 72 +++++++++++++++++++--- src/WebApi_powerlimiter.cpp | 2 + webapp/src/locales/de.json | 2 + webapp/src/locales/en.json | 2 + webapp/src/types/PowerLimiterConfig.ts | 1 + webapp/src/views/PowerLimiterAdminView.vue | 10 +++ 9 files changed, 83 insertions(+), 10 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index a78b80626..c93991529 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -210,6 +210,7 @@ struct CONFIG_T { uint32_t Interval; bool IsInverterBehindPowerMeter; bool IsInverterSolarPowered; + bool UseOverscalingToCompensateShading; uint64_t InverterId; uint8_t InverterChannelId; int32_t TargetPowerConsumption; diff --git a/include/defaults.h b/include/defaults.h index 237a94b06..32d1e54cd 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -127,6 +127,7 @@ #define POWERLIMITER_INTERVAL 10 #define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true #define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false +#define POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING false #define POWERLIMITER_INVERTER_ID 0ULL #define POWERLIMITER_INVERTER_CHANNEL_ID 0 #define POWERLIMITER_TARGET_POWER_CONSUMPTION 0 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index d23ac9ba4..1495753d8 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -184,6 +184,7 @@ bool ConfigurationClass::write() powerlimiter["interval"] = config.PowerLimiter.Interval; powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter; powerlimiter["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered; + powerlimiter["use_overscaling_to_compensate_shading"] = config.PowerLimiter.UseOverscalingToCompensateShading; powerlimiter["inverter_id"] = config.PowerLimiter.InverterId; powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId; powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption; @@ -444,6 +445,7 @@ bool ConfigurationClass::read() config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL; config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; config.PowerLimiter.IsInverterSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED; + config.PowerLimiter.UseOverscalingToCompensateShading = powerlimiter["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING; config.PowerLimiter.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID; config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID; config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 7ce0d8bd9..d9e280488 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -20,8 +20,8 @@ PowerLimiterClass PowerLimiter; -void PowerLimiterClass::init(Scheduler& scheduler) -{ +void PowerLimiterClass::init(Scheduler& scheduler) +{ scheduler.addTask(_loopTask); _loopTask.setCallback(std::bind(&PowerLimiterClass::loop, this)); _loopTask.setIterations(TASK_FOREVER); @@ -337,6 +337,16 @@ float PowerLimiterClass::getBatteryVoltage(bool log) { return res; } +static float getInverterEfficiency(std::shared_ptr inverter) +{ + float inverterEfficiencyPercent = inverter->Statistics()->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) + return (inverterEfficiencyPercent > 0) ? inverterEfficiencyPercent/100 : 0.967; +} + /** * calculate the AC output power (limit) to set, such that the inverter uses * 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 { CONFIG_T& config = Configuration.get(); - float inverterEfficiencyPercent = inverter->Statistics()->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) - float inverterEfficiencyFactor = (inverterEfficiencyPercent > 0) ? inverterEfficiencyPercent/100 : 0.967; + float inverterEfficiencyFactor = getInverterEfficiency(inverter); // account for losses between solar charger and inverter (cables, junctions...) float lossesFactor = 1.00 - static_cast(config.PowerLimiter.SolarPassThroughLosses)/100; @@ -687,8 +692,7 @@ bool PowerLimiterClass::updateInverter() * * TODO(schlimmchen): the current implementation is broken and is in need of * refactoring. currently it only works for inverters that provide one MPPT for - * each input. it also does not work as expected if any input produces *some* - * energy, but is limited by its respective solar input. + * each input. */ static int32_t scalePowerLimit(std::shared_ptr inverter, int32_t newLimit, int32_t currentLimitWatts) { @@ -713,6 +717,54 @@ static int32_t scalePowerLimit(std::shared_ptr inverter, int32 // producing very little due to the very low limit. if (currentLimitWatts < dcTotalChnls * 10) { return newLimit; } + auto const& config = Configuration.get(); + auto allowOverscaling = config.PowerLimiter.UseOverscalingToCompensateShading; + auto isInverterSolarPowered = config.PowerLimiter.IsInverterSolarPowered; + + // overscalling allows us to compensate for shaded panels by increasing the + // total power limit, if the inverter is solar powered. + if (allowOverscaling && isInverterSolarPowered) { + auto inverterOutputAC = inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC); + float inverterEfficiencyFactor = getInverterEfficiency(inverter); + + // 98% of the expected power is good enough + auto expectedAcPowerPerChannel = (currentLimitWatts / dcTotalChnls) * 0.98; + + size_t dcShadedChnls = 0; + auto shadedChannelACPowerSum = 0.0; + + for (auto& c : dcChnls) { + auto channelPowerAC = inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) * inverterEfficiencyFactor; + + if (channelPowerAC < expectedAcPowerPerChannel) { + dcShadedChnls++; + shadedChannelACPowerSum += channelPowerAC; + } + } + + // no shading or the shaded channels provide more power than what + // we currently need. + if (dcShadedChnls == 0 || shadedChannelACPowerSum >= newLimit) { return newLimit; } + + // keep the currentLimit when all channels are shaded and we get the + // expected AC power or less. + if (dcShadedChnls == dcTotalChnls && inverterOutputAC <= newLimit) { + MessageOutput.printf("[DPL::scalePowerLimit] all channels are shaded, " + "keeping the current limit of %d W\r\n", currentLimitWatts); + + return currentLimitWatts; + } + + size_t dcNonShadedChnls = dcTotalChnls - dcShadedChnls; + auto overScaledLimit = static_cast((newLimit - shadedChannelACPowerSum) / dcNonShadedChnls * dcTotalChnls); + + if (overScaledLimit <= newLimit) { return newLimit; } + + MessageOutput.printf("[DPL::scalePowerLimit] %d/%d channels are shaded, " + "scaling %d W\r\n", dcShadedChnls, dcTotalChnls, overScaledLimit); + return overScaledLimit; + } + size_t dcProdChnls = 0; for (auto& c : dcChnls) { if (inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) > 2.0) { diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 114ced773..b28380d3c 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -38,6 +38,7 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) root["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight; root["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter; root["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered; + root["use_overscaling_to_compensate_shading"] = config.PowerLimiter.UseOverscalingToCompensateShading; root["inverter_serial"] = String(config.PowerLimiter.InverterId); root["inverter_channel_id"] = config.PowerLimiter.InverterChannelId; root["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption; @@ -159,6 +160,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) config.PowerLimiter.IsInverterBehindPowerMeter = root["is_inverter_behind_powermeter"].as(); config.PowerLimiter.IsInverterSolarPowered = root["is_inverter_solar_powered"].as(); + config.PowerLimiter.UseOverscalingToCompensateShading = root["use_overscaling_to_compensate_shading"].as(); config.PowerLimiter.InverterId = root["inverter_serial"].as(); config.PowerLimiter.InverterChannelId = root["inverter_channel_id"].as(); config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as(); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index ff73380c8..3f992d310 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -637,6 +637,8 @@ "InverterIsBehindPowerMeter": "Stromzählermessung beinhaltet Wechselrichter", "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", + "UseOverscalingToCompensateShading": "Verschattung durch Überskalierung ausgleichen", + "UseOverscalingToCompensateShadingHint": "Erlaubt das Überskalieren des Wechselrichter-Limits, um Verschattung eines oder mehrerer Eingänge auszugleichen", "VoltageThresholds": "Batterie Spannungs-Schwellwerte ", "VoltageLoadCorrectionInfo": "Hinweis: 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).", "InverterRestartHour": "Uhrzeit für geplanten Neustart", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index de7240d76..c89045c1f 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -643,6 +643,8 @@ "InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output", "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", + "UseOverscalingToCompensateShading": "Compensate for shading", + "UseOverscalingToCompensateShadingHint": "Allow to overscale the inverter limit to compensate for shading of one or multiple inputs", "VoltageThresholds": "Battery Voltage Thresholds", "VoltageLoadCorrectionInfo": "Hint: 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).", "InverterRestartHour": "Automatic Restart Time", diff --git a/webapp/src/types/PowerLimiterConfig.ts b/webapp/src/types/PowerLimiterConfig.ts index 0d7ad388d..f8220a7d5 100644 --- a/webapp/src/types/PowerLimiterConfig.ts +++ b/webapp/src/types/PowerLimiterConfig.ts @@ -26,6 +26,7 @@ export interface PowerLimiterConfig { battery_always_use_at_night: boolean; is_inverter_behind_powermeter: boolean; is_inverter_solar_powered: boolean; + use_overscaling_to_compensate_shading: boolean; inverter_serial: string; inverter_channel_id: number; target_power_consumption: number; diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index e6cc0f22e..84cc0dcfc 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -68,6 +68,12 @@ v-model="powerLimiterConfigList.is_inverter_solar_powered" type="checkbox" wide/> + +