Skip to content

Commit

Permalink
Merge branch 'hoylabs:development' into development
Browse files Browse the repository at this point in the history
  • Loading branch information
Snoopy-HSS authored Dec 25, 2024
2 parents bfaf21e + 36fc00a commit e8374b5
Show file tree
Hide file tree
Showing 18 changed files with 169 additions and 97 deletions.
3 changes: 2 additions & 1 deletion include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ struct POWERLIMITER_INVERTER_CONFIG_T {
bool IsGoverned;
bool IsBehindPowerMeter;
bool IsSolarPowered;
bool UseOverscalingToCompensateShading;
bool UseOverscaling;
uint16_t LowerPowerLimit;
uint16_t UpperPowerLimit;
uint8_t ScalingThreshold;
};
using PowerLimiterInverterConfig = struct POWERLIMITER_INVERTER_CONFIG_T;

Expand Down
1 change: 1 addition & 0 deletions include/PowerLimiterSolarInverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ class PowerLimiterSolarInverter : public PowerLimiterInverter {
private:
uint16_t scaleLimit(uint16_t expectedOutputWatts);
void setAcOutput(uint16_t expectedOutputWatts) final;
static char mpptName(MpptNum_t mppt);
};
3 changes: 2 additions & 1 deletion include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,14 @@
#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_TO_COMPENSATE_SHADING false
#define POWERLIMITER_USE_OVERSCALING false
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
#define POWERLIMITER_LOWER_POWER_LIMIT 10
#define POWERLIMITER_BASE_LOAD_LIMIT 100
#define POWERLIMITER_UPPER_POWER_LIMIT 800
#define POWERLIMITER_SCALING_THRESHOLD 98
#define POWERLIMITER_IGNORE_SOC true
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20
Expand Down
7 changes: 7 additions & 0 deletions lib/Hoymiles/src/inverters/HMS_4CH.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,10 @@ uint8_t HMS_4CH::getChannelMetaDataSize() const
{
return sizeof(channelMetaData) / sizeof(channelMetaData[0]);
}

bool HMS_4CH::supportsPowerDistributionLogic()
{
// This feature was added in inverter firmware version 01.01.12 and
// will limit the AC output instead of limiting the DC inputs.
return DevInfo()->getFwBuildVersion() >= 10112U;
};
1 change: 1 addition & 0 deletions lib/Hoymiles/src/inverters/HMS_4CH.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ class HMS_4CH : public HMS_Abstract {
uint8_t getByteAssignmentSize() const;
const channelMetaData_t* getChannelMetaData() const;
uint8_t getChannelMetaDataSize() const;
bool supportsPowerDistributionLogic() final;
};
3 changes: 2 additions & 1 deletion lib/Hoymiles/src/inverters/HM_Abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ class HM_Abstract : public InverterAbstract {
bool sendRestartControlRequest();
bool resendPowerControlRequest();
bool sendGridOnProFileParaRequest();
bool supportsPowerDistributionLogic() override { return false; };

private:
uint8_t _lastAlarmLogCnt = 0;
float _activePowerControlLimit = 0;
PowerLimitControlType _activePowerControlType = PowerLimitControlType::AbsolutNonPersistent;

uint8_t _powerState = 1;
};
};
3 changes: 3 additions & 0 deletions lib/Hoymiles/src/inverters/InverterAbstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ class InverterAbstract {
virtual bool sendChangeChannelRequest();
virtual bool sendGridOnProFileParaRequest() = 0;

// This feature will limit the AC output instead of limiting the DC inputs.
virtual bool supportsPowerDistributionLogic() = 0;

HoymilesRadio* getRadio();

AlarmLogParser* EventLog();
Expand Down
8 changes: 5 additions & 3 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,10 @@ void ConfigurationClass::serializePowerLimiterConfig(PowerLimiterConfig const& s
t["is_governed"] = s.IsGoverned;
t["is_behind_power_meter"] = s.IsBehindPowerMeter;
t["is_solar_powered"] = s.IsSolarPowered;
t["use_overscaling_to_compensate_shading"] = s.UseOverscalingToCompensateShading;
t["use_overscaling_to_compensate_shading"] = s.UseOverscaling;
t["lower_power_limit"] = s.LowerPowerLimit;
t["upper_power_limit"] = s.UpperPowerLimit;
t["scaling_threshold"] = s.ScalingThreshold;
}
}

Expand Down Expand Up @@ -484,9 +485,10 @@ void ConfigurationClass::deserializePowerLimiterConfig(JsonObject const& source,
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.UseOverscalingToCompensateShading = s["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING;
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;
inv.ScalingThreshold = s["scaling_threshold"] | POWERLIMITER_SCALING_THRESHOLD;
}
}

Expand Down Expand Up @@ -916,7 +918,7 @@ void ConfigurationClass::migrateOnBattery()
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;
inv.UseOverscalingToCompensateShading = powerlimiter["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING;
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 Down
159 changes: 80 additions & 79 deletions src/PowerLimiterSolarInverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ uint16_t PowerLimiterSolarInverter::standby()

uint16_t PowerLimiterSolarInverter::scaleLimit(uint16_t expectedOutputWatts)
{
// overscalling allows us to compensate for shaded panels by increasing the
// total power limit, if the inverter is solar powered.
// this feature should not be used when homyiles 'Power Distribution Logic' is available
// as the inverter will take care of the power distribution across the MPPTs itself.
// (added in inverter firmware 01.01.12 on supported models (HMS-1600/1800/2000))
// When disabled we return the expected output.
if (!_config.UseOverscaling || _spInverter->supportsPowerDistributionLogic()) { return expectedOutputWatts; }

// prevent scaling if inverter is not producing, as input channels are not
// producing energy and hence are detected as not-producing, causing
// unreasonable scaling.
Expand All @@ -136,108 +144,81 @@ uint16_t PowerLimiterSolarInverter::scaleLimit(uint16_t expectedOutputWatts)
// producing very little due to the very low limit.
if (getCurrentLimitWatts() < dcTotalChnls * 10) { return expectedOutputWatts; }

// overscalling allows us to compensate for shaded panels by increasing the
// total power limit, if the inverter is solar powered.
if (_config.UseOverscalingToCompensateShading) {
auto inverterOutputAC = pStats->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC);
auto inverterOutputAC = pStats->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC);

float inverterEfficiencyFactor = pStats->getChannelFieldValue(TYPE_INV, CH0, FLD_EFF);
float inverterEfficiencyFactor = pStats->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)
inverterEfficiencyFactor = (inverterEfficiencyFactor > 0) ? inverterEfficiencyFactor/100 : 0.967;
// fall back to hoymiles peak efficiency as per datasheet if inverter
// is currently not producing (efficiency is zero in that case)
inverterEfficiencyFactor = (inverterEfficiencyFactor > 0) ? inverterEfficiencyFactor/100 : 0.967;

// 98% of the expected power is good enough
auto expectedAcPowerPerMppt = (getCurrentLimitWatts() / dcTotalMppts) * 0.98;
auto scalingThreshold = static_cast<float>(_config.ScalingThreshold) / 100.0;
auto expectedAcPowerPerMppt = (getCurrentLimitWatts() / dcTotalMppts) * scalingThreshold;

if (_verboseLogging) {
MessageOutput.printf("%s expected AC power per MPPT %.0f W\r\n",
_logPrefix, expectedAcPowerPerMppt);
}

size_t dcShadedMppts = 0;
auto shadedChannelACPowerSum = 0.0;

for (auto& m : dcMppts) {
float mpptPowerAC = 0.0;
std::vector<ChannelNum_t> mpptChnls = _spInverter->getChannelsDCByMppt(m);
if (_verboseLogging) {
MessageOutput.printf("%s expected AC power per MPPT %.0f W\r\n",
_logPrefix, expectedAcPowerPerMppt);
}

for (auto& c : mpptChnls) {
mpptPowerAC += pStats->getChannelFieldValue(TYPE_DC, c, FLD_PDC) * inverterEfficiencyFactor;
}
size_t dcShadedMppts = 0;
auto shadedChannelACPowerSum = 0.0;

if (mpptPowerAC < expectedAcPowerPerMppt) {
dcShadedMppts++;
shadedChannelACPowerSum += mpptPowerAC;
}
for (auto& m : dcMppts) {
float mpptPowerAC = 0.0;
std::vector<ChannelNum_t> mpptChnls = _spInverter->getChannelsDCByMppt(m);

if (_verboseLogging) {
MessageOutput.printf("%s MPPT-%c AC power %.0f W\r\n",
_logPrefix, m + 'a', mpptPowerAC);
}
for (auto& c : mpptChnls) {
mpptPowerAC += pStats->getChannelFieldValue(TYPE_DC, c, FLD_PDC) * inverterEfficiencyFactor;
}

// no shading or the shaded channels provide more power than what
// we currently need.
if (dcShadedMppts == 0 || shadedChannelACPowerSum >= expectedOutputWatts) {
return expectedOutputWatts;
if (mpptPowerAC < expectedAcPowerPerMppt) {
dcShadedMppts++;
shadedChannelACPowerSum += mpptPowerAC;
}

if (dcShadedMppts == dcTotalMppts) {
// keep the currentLimit when:
// - all channels are shaded
// - currentLimit >= expectedOutputWatts
// - we get the expected AC power or less and
if (getCurrentLimitWatts() >= expectedOutputWatts &&
inverterOutputAC <= expectedOutputWatts) {
if (_verboseLogging) {
MessageOutput.printf("%s all mppts are shaded, "
"keeping the current limit of %d W\r\n",
_logPrefix, getCurrentLimitWatts());
}

return getCurrentLimitWatts();

} else {
return expectedOutputWatts;
}
}

size_t dcNonShadedMppts = dcTotalMppts - dcShadedMppts;
uint16_t overScaledLimit = (expectedOutputWatts - shadedChannelACPowerSum) / dcNonShadedMppts * dcTotalMppts;

if (overScaledLimit <= expectedOutputWatts) { return expectedOutputWatts; }

if (_verboseLogging) {
MessageOutput.printf("%s %d/%d mppts are shaded, scaling %d W\r\n",
_logPrefix, dcShadedMppts, dcTotalMppts, overScaledLimit);
MessageOutput.printf("%s MPPT-%c AC power %.0f W\r\n",
_logPrefix, mpptName(m), mpptPowerAC);
}
}

return overScaledLimit;
// no shading or the shaded channels provide more power than what
// we currently need.
if (dcShadedMppts == 0 || shadedChannelACPowerSum >= expectedOutputWatts) {
return expectedOutputWatts;
}

size_t dcProdMppts = 0;
for (auto& m : dcMppts) {
float dcPowerMppt = 0.0;
std::vector<ChannelNum_t> mpptChnls = _spInverter->getChannelsDCByMppt(m);
if (dcShadedMppts == dcTotalMppts) {
// keep the currentLimit when:
// - all channels are shaded
// - currentLimit >= expectedOutputWatts
// - we get the expected AC power or less and
if (getCurrentLimitWatts() >= expectedOutputWatts &&
inverterOutputAC <= expectedOutputWatts) {
if (_verboseLogging) {
MessageOutput.printf("%s all mppts are shaded, "
"keeping the current limit of %d W\r\n",
_logPrefix, getCurrentLimitWatts());
}

for (auto& c : mpptChnls) {
dcPowerMppt += pStats->getChannelFieldValue(TYPE_DC, c, FLD_PDC);
}
return getCurrentLimitWatts();

if (dcPowerMppt > 2.0 * mpptChnls.size()) {
dcProdMppts++;
} else {
return expectedOutputWatts;
}
}

if (dcProdMppts == 0 || dcProdMppts == dcTotalMppts) { return expectedOutputWatts; }
size_t dcNonShadedMppts = dcTotalMppts - dcShadedMppts;
uint16_t overScaledLimit = (expectedOutputWatts - shadedChannelACPowerSum) / dcNonShadedMppts * dcTotalMppts;

uint16_t scaled = expectedOutputWatts / dcProdMppts * dcTotalMppts;
MessageOutput.printf("%s %d/%d mppts are producing, scaling from %d to "
"%d W\r\n", _logPrefix, dcProdMppts, dcTotalMppts,
expectedOutputWatts, scaled);
if (overScaledLimit <= expectedOutputWatts) { return expectedOutputWatts; }

return scaled;
if (_verboseLogging) {
MessageOutput.printf("%s %d/%d mppts are not-producing/shaded, scaling %d W\r\n",
_logPrefix, dcShadedMppts, dcTotalMppts, overScaledLimit);
}

return overScaledLimit;
}

void PowerLimiterSolarInverter::setAcOutput(uint16_t expectedOutputWatts)
Expand All @@ -246,3 +227,23 @@ void PowerLimiterSolarInverter::setAcOutput(uint16_t expectedOutputWatts)
setTargetPowerLimitWatts(scaleLimit(expectedOutputWatts));
setTargetPowerState(true);
}

char PowerLimiterSolarInverter::mpptName(MpptNum_t mppt)
{
switch (mppt) {
case MpptNum_t::MPPT_A:
return 'a';

case MpptNum_t::MPPT_B:
return 'b';

case MpptNum_t::MPPT_C:
return 'c';

case MpptNum_t::MPPT_D:
return 'd';

default:
return '?';
}
}
1 change: 1 addition & 0 deletions src/WebApi_devinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request)
root["hw_model_name"] = inv->DevInfo()->getHwModelName();
root["max_power"] = inv->DevInfo()->getMaxPower();
root["fw_build_datetime"] = inv->DevInfo()->getFwBuildDateTimeStr();
root["pdl_supported"] = inv->supportsPowerDistributionLogic();
}

WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
Expand Down
1 change: 1 addition & 0 deletions src/WebApi_powerlimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request)
obj["type"] = inv->typeName();
auto channels = inv->Statistics()->getChannelsByType(TYPE_DC);
obj["channels"] = channels.size();
obj["pdl_supported"] = inv->supportsPowerDistributionLogic();
}

WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
Expand Down
5 changes: 5 additions & 0 deletions webapp/src/components/DevInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
<td>{{ $t('devinfo.HardwareVersion') }}</td>
<td>{{ devInfoList.hw_version }}</td>
</tr>
<tr>
<td>{{ $t('devinfo.SupportsPowerDistributionLogic') }}</td>
<td v-if="devInfoList.pdl_supported">{{ $t('devinfo.yes') }}</td>
<td v-else>{{ $t('devinfo.no') }}</td>
</tr>
</tbody>
</table>
</template>
Expand Down
11 changes: 8 additions & 3 deletions webapp/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,10 @@
"FirmwareVersion": "Firmware-Version",
"FirmwareBuildDate": "Firmware-Erstellungsdatum",
"HardwarePartNumber": "Hardware-Teilenummer",
"HardwareVersion": "Hardware-Version"
"HardwareVersion": "Hardware-Version",
"SupportsPowerDistributionLogic": "'Power Distribution Logic' unterstützt",
"yes": "@:base.Yes",
"no": "@:base.No"
},
"gridprofile": {
"NoInfo": "@:devinfo.NoInfo",
Expand Down Expand Up @@ -708,10 +711,12 @@
"VoltageLoadCorrectionFactor": "Lastkorrekturfaktor",
"BatterySocInfo": "<b>Hinweis:</b> Die Batterie State of Charge (SoC) Schwellwerte werden bevorzugt herangezogen. Sie werden allerdings nur benutzt, wenn die Batterie-Kommunikationsschnittstelle innerhalb der letzten Minute gültige Werte verarbeitet hat. Andernfalls werden ersatzweise die Spannungs-Schwellwerte verwendet.",
"InverterIsBehindPowerMeter": "Stromzählermessung beinhaltet Wechselrichter",
"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",
"UseOverscalingToCompensateShading": "Verschattung durch Überskalierung ausgleichen",
"UseOverscalingToCompensateShadingHint": "Erlaubt das Überskalieren des Wechselrichter-Limits, um Verschattung eines oder mehrerer Eingänge auszugleichen.",
"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 ",
"VoltageLoadCorrectionInfo": "<b>Hinweis:</b> Wenn Leistung von der Batterie abgegeben wird, bricht ihre Spannung etwas ein. Der Spannungseinbruch skaliert mit dem Entladestrom. Damit Wechselrichter nicht vorzeitig ausgeschaltet werden 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 automatischen Wechselrichterneustart",
Expand Down
11 changes: 8 additions & 3 deletions webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,10 @@
"FirmwareVersion": "Firmware Version",
"FirmwareBuildDate": "Firmware Build Date",
"HardwarePartNumber": "Hardware Part Number",
"HardwareVersion": "Hardware Version"
"HardwareVersion": "Hardware Version",
"SupportsPowerDistributionLogic": "'Power Distribution Logic' supported",
"yes": "@:base.Yes",
"no": "@:base.No"
},
"gridprofile": {
"NoInfo": "@:devinfo.NoInfo",
Expand Down Expand Up @@ -710,10 +713,12 @@
"VoltageLoadCorrectionFactor": "Load correction factor",
"BatterySocInfo": "<b>Hint:</b> The use of battery State of Charge (SoC) thresholds is prioritized. However, SoC thresholds are only used if the battery communication interface has processed valid SoC values in the last minute. Otherwise, the voltage thresholds will be used as fallback.",
"InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output",
"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",
"UseOverscalingToCompensateShading": "Compensate for shading",
"UseOverscalingToCompensateShadingHint": "Allow to overscale the inverter limit to compensate for shading of one or multiple inputs.",
"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",
"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 inverters 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 Inverter Restart Time",
Expand Down
Loading

0 comments on commit e8374b5

Please sign in to comment.