From c93001876458aa43e24b37832503e80b809da86e Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sat, 17 Feb 2024 18:29:11 +0100 Subject: [PATCH] Feature: DPL: use best available voltage value the DPL is interested in the battery's voltage to make decisions about draining the battery or letting it charge (if the user opts to use voltage thresholds rather than SoC thresholds). using the DC input voltage reported by the inverter under control has disadvantages: * the data might be quite old due to the communication protocol implementation. more inverters being polled means even more lag. the connection being wireless makes this even worse, due to the need to retry the occasional lost packet, etc. * the data is not very accurate, since the DC input of the inverter is actually some cabling and a couple of junctions away from the actual battery. this voltage drop can mostly only be estimated and is worse with higher load. the load correction factor is there to mitigate this, but it has its own problems and is cumbersome to calibrate. instead, this change aims to use more accurate battery voltage readings, if possible. the DPL now prefers the voltage as reported by the BMS, since it is for sure the closest to the battery of all measuring points and measures its voltage accurately regardless of the load (the voltage reading will still drop with higher loads, but this will be only due to the battery's internal resistance, not that of cabling or junctions). if no BMS voltage reading is available, the DPL will instead use the charge controller's voltage reading, as it is available with much higher frequency and is assumed to be more accurate as it offers a resolution of 10mV. only if none of these two sources can be used, the inverter DC input voltage is assumed as the battery voltage. closes #655. --- include/BatteryStats.h | 1 + include/PowerLimiter.h | 1 + include/VictronMppt.h | 3 +++ src/PowerLimiter.cpp | 45 +++++++++++++++++++++++++++++++++++++++--- src/VictronMppt.cpp | 13 ++++++++++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 916bf311..e4bf4144 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -33,6 +33,7 @@ class BatteryStats { virtual uint32_t getMqttFullPublishIntervalMs() const; bool isSoCValid() const { return _lastUpdateSoC > 0; } + bool isVoltageValid() const { return _lastUpdateVoltage > 0; } protected: virtual void mqttPublish() const; diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index af6ed28f..32260150 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -88,6 +88,7 @@ private: void announceStatus(Status status); bool shutdown(Status status); bool shutdown() { return shutdown(_lastStatus); } + float getBatteryVoltage(bool log = false); int32_t inverterPowerDcToAc(std::shared_ptr inverter, int32_t dcPower); void unconditionalSolarPassthrough(std::shared_ptr inverter); bool canUseDirectSolarPower(); diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 091ddb00..12d6bdf7 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -35,6 +35,9 @@ public: // sum of today's yield of all MPPT charge controllers in kWh double getYieldDay() const; + // minimum of all MPPT charge controllers' output voltages in V + double getOutputVoltage() const; + private: void loop(); VictronMpptClass(VictronMpptClass const& other) = delete; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 68564e37..b4c2229a 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -298,7 +298,7 @@ void PowerLimiterClass::loop() Battery.getStats()->getSoCAgeSeconds(), (config.PowerLimiter.IgnoreSoc?"yes":"no")); - float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t)config.PowerLimiter.InverterChannelId, FLD_UDC); + auto dcVoltage = getBatteryVoltage(true/*log voltages only once per DPL loop*/); MessageOutput.printf("[DPL::loop] dcVoltage: %.2f V, loadCorrectedVoltage: %.2f V, StartTH: %.2f V, StopTH: %.2f V\r\n", dcVoltage, getLoadCorrectedVoltage(), config.PowerLimiter.VoltageStartThreshold, @@ -340,6 +340,46 @@ void PowerLimiterClass::loop() _calculationBackoffMs = _calculationBackoffMsDefault; } +/** + * determines the battery's voltage, trying multiple data providers. the most + * accurate data is expected to be delivered by a BMS, if it's available. more + * accurate and more recent than the inverter's voltage reading is the volage + * at the charge controller's output, if it's available. only as a fallback + * the voltage reported by the inverter is used. + */ +float PowerLimiterClass::getBatteryVoltage(bool log) { + if (!_inverter) { + // there should be no need to call this method if no target inverter is known + MessageOutput.println("DPL getBatteryVoltage: no inverter (programmer error)"); + return 0.0; + } + + auto const& config = Configuration.get(); + auto channel = static_cast(config.PowerLimiter.InverterChannelId); + float inverterVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, channel, FLD_UDC); + float res = inverterVoltage; + + float chargeControllerVoltage = -1; + if (VictronMppt.isDataValid()) { + res = chargeControllerVoltage = static_cast(VictronMppt.getOutputVoltage()); + } + + float bmsVoltage = -1; + auto stats = Battery.getStats(); + if (config.Battery.Enabled + && stats->isVoltageValid() + && stats->getVoltageAgeSeconds() < 60) { + res = bmsVoltage = stats->getVoltage(); + } + + if (log) { + MessageOutput.printf("[DPL::getBatteryVoltage] BMS: %.2f V, MPPT: %.2f V, inverter: %.2f V, returning: %.2fV\r\n", + bmsVoltage, chargeControllerVoltage, inverterVoltage, res); + } + + return res; +} + /** * 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 @@ -593,9 +633,8 @@ float PowerLimiterClass::getLoadCorrectedVoltage() CONFIG_T& config = Configuration.get(); - auto channel = static_cast(config.PowerLimiter.InverterChannelId); float acPower = _inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC); - float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, channel, FLD_UDC); + float dcVoltage = getBatteryVoltage(); if (dcVoltage <= 0.0) { return 0.0; diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index fd1073a7..c4dd0bd5 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -137,3 +137,16 @@ double VictronMpptClass::getYieldDay() const return sum; } + +double VictronMpptClass::getOutputVoltage() const +{ + double min = -1; + + for (const auto& upController : _controllers) { + double volts = upController->getData()->V; + if (min == -1) { min = volts; } + min = std::min(min, volts); + } + + return min; +}