From 8b233246930de31f3816f3d967f4ac74b64f4fd2 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Tue, 27 Jun 2023 22:01:39 +0200 Subject: [PATCH 01/10] ensure only one PowerCommand and ActivePowerCommand is queued neither a PowerCommand nor an ActivePowerCommand shall be enqueued if another one is already pending. --- lib/Hoymiles/src/inverters/HM_Abstract.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 097fffeb..47acfbba 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -121,6 +121,10 @@ bool HM_Abstract::sendActivePowerControlRequest(float limit, PowerLimitControlTy return false; } + if (CMD_PENDING == SystemConfigPara()->getLastLimitCommandSuccess()) { + return false; + } + if (type == PowerLimitControlType::RelativNonPersistent || type == PowerLimitControlType::RelativPersistent) { limit = min(100, limit); } @@ -147,6 +151,10 @@ bool HM_Abstract::sendPowerControlRequest(bool turnOn) return false; } + if (CMD_PENDING == PowerCommand()->getLastPowerCommandSuccess()) { + return false; + } + if (turnOn) { _powerState = 1; } else { From fd208cf6bb302454a30e16f988586b89880548e9 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Tue, 27 Jun 2023 22:01:15 +0200 Subject: [PATCH 02/10] DPL: remove unused member variables --- include/PowerLimiter.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 24694030..6846d086 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -49,10 +49,6 @@ private: uint32_t _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart bool _fullSolarPassThroughEnabled = false; - float _powerMeter1Power; - float _powerMeter2Power; - float _powerMeter3Power; - bool canUseDirectSolarPower(); int32_t calcPowerLimit(std::shared_ptr inverter, bool solarPowerEnabled, bool batteryDischargeEnabled); void commitPowerLimit(std::shared_ptr inverter, int32_t limit, bool enablePowerProduction); From 0b0bcf1dfb28c058800d05406e6fd4cc6756061d Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 29 Jun 2023 21:52:18 +0200 Subject: [PATCH 03/10] fix typo: getLastRequestedPowe*w*rLimit() --- include/PowerLimiter.h | 2 +- src/PowerLimiter.cpp | 2 +- src/WebApi_ws_vedirect_live.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 6846d086..7eb78fa8 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -33,7 +33,7 @@ public: void init(); void loop(); uint8_t getPowerLimiterState(); - int32_t getLastRequestedPowewrLimit(); + int32_t getLastRequestedPowerLimit(); void setMode(uint8_t mode); bool getMode(); void calcNextInverterRestart(); diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index cbaf80dc..93bd01f3 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -199,7 +199,7 @@ uint8_t PowerLimiterClass::getPowerLimiterState() { return PL_UI_STATE_INACTIVE; } -int32_t PowerLimiterClass::getLastRequestedPowewrLimit() { +int32_t PowerLimiterClass::getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; } diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 1edbe13a..e416ff87 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -145,7 +145,7 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) root["dpl"]["PLSTATE"] = -1; if (Configuration.get().PowerLimiter_Enabled) root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); - root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowewrLimit(); + root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); if (VeDirect.getLastUpdate() > _newestVedirectTimestamp) { _newestVedirectTimestamp = VeDirect.getLastUpdate(); From 18b107666075a2747aaf4782f533b9b2b3d4c6ec Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Tue, 27 Jun 2023 21:44:39 +0200 Subject: [PATCH 04/10] DPL: improve responsiveness this implementation checks all requirements for a new power limit to be calculated, one after the other. if any requirement is not met, a respective status is announced. status messages are communicated on the (serial) console. these can also be displayed easily on the web app in the future. the status texts explain clearly what the DPL is currently doing, which aids understanding how the DPL works. the status is only announced if it changes, or after a fixed interval. as each requirement is checked individually, the code readability is improved as well. previously, all the respective conditions had to be checked as well, but the statements were more complex. the DPL loop is now executed with high frequency, i.e., it does not wait for a fixed timespan to pass before checking requirements. it always aborts on the first unmet requirement. this should improve responsiveness, as the DPL checks all requirements more often. the DPL now waits for all power commands and power limit updates to complete. when that is the case, a settling time elapses. after the settling phase, the DPL waits for a new update from the inverter and from the power meter. now it can be assumed that the values are in sync. it then makes sense to calculate a new power limit immediately, which the DPL then does. --- include/PowerLimiter.h | 38 ++++++-- src/PowerLimiter.cpp | 212 +++++++++++++++++++++++++---------------- 2 files changed, 160 insertions(+), 90 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 7eb78fa8..2b212527 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -16,12 +16,6 @@ #define PL_MODE_FULL_DISABLE 1 #define PL_MODE_SOLAR_PT_ONLY 2 - -typedef enum { - SHUTDOWN = 0, - ACTIVE -} plStates; - typedef enum { EMPTY_WHEN_FULL= 0, EMPTY_AT_NIGHT @@ -30,6 +24,22 @@ typedef enum { class PowerLimiterClass { public: + enum class Status : unsigned { + Initializing, + DisabledByConfig, + DisabledByMqtt, + PowerMeterDisabled, + PowerMeterTimeout, + PowerMeterPending, + InverterInvalid, + InverterOffline, + InverterLimitPending, + InverterPowerCmdPending, + InverterStatsPending, + Settling, + LowerLimitUndercut + }; + void init(); void loop(); uint8_t getPowerLimiterState(); @@ -39,16 +49,26 @@ public: void calcNextInverterRestart(); private: - uint32_t _lastLoop = 0; + enum class plStates : unsigned { + INIT, // looping for the first time after system startup + ACTIVE, // normal operation, sending power limit updates to inverter + SHUTDOWN, // power limiter shuts down inverter + OFF // inverter was shut down, power limiter is NOT active + }; + int32_t _lastRequestedPowerLimit = 0; - uint32_t _lastLimitSetTime = 0; - plStates _plState; + plStates _plState = plStates::INIT; + Status _lastStatus = Status::Initializing; + uint32_t _lastStatusPrinted = 0; uint8_t _mode = PL_MODE_ENABLE_NORMAL_OP; bool _batteryDischargeEnabled = false; uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis() uint32_t _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart bool _fullSolarPassThroughEnabled = false; + std::string const& getStatusText(Status status); + void announceStatus(Status status); + void shutdown(Status status); bool canUseDirectSolarPower(); int32_t calcPowerLimit(std::shared_ptr inverter, bool solarPowerEnabled, bool batteryDischargeEnabled); void commitPowerLimit(std::shared_ptr inverter, int32_t limit, bool enablePowerProduction); diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 93bd01f3..16682b8d 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -13,47 +13,152 @@ #include "MessageOutput.h" #include #include +#include PowerLimiterClass PowerLimiter; #define POWER_LIMITER_DEBUG -void PowerLimiterClass::init() +void PowerLimiterClass::init() { } + +std::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status status) { - CONFIG_T& config = Configuration.get(); - if (config.PowerLimiter_Enabled) { - // We'll start in active state - _plState = ACTIVE; - } else { - _plState = SHUTDOWN; - } + static const std::string missing = "programmer error: missing status text"; + + static const std::map texts = { + { Status::Initializing, "initializing (should not see me)" }, + { Status::DisabledByConfig, "disabled by configuration" }, + { Status::DisabledByMqtt, "disabled by MQTT" }, + { Status::PowerMeterDisabled, "no power meter is configured/enabled" }, + { Status::PowerMeterTimeout, "power meter readings are outdated" }, + { Status::PowerMeterPending, "waiting for sufficiently recent power meter reading" }, + { Status::InverterInvalid, "invalid inverter selection/configuration" }, + { Status::InverterOffline, "inverter is offline (polling enabled? radio okay?)" }, + { Status::InverterLimitPending, "waiting for a power limit command to complete" }, + { Status::InverterPowerCmdPending, "waiting for a start/stop/restart command to complete" }, + { Status::InverterStatsPending, "waiting for sufficiently recent inverter data" }, + { Status::Settling, "waiting for the system to settle" }, + { Status::LowerLimitUndercut, "calculated power limit undercuts configured lower limit" } + }; + + auto iter = texts.find(status); + if (iter == texts.end()) { return missing; } + + return iter->second; +} + +void PowerLimiterClass::announceStatus(PowerLimiterClass::Status status) +{ + // this method is called with high frequency. print the status text if + // the status changed since we last printed the text of another one. + // otherwise repeat the info with a fixed interval. + if (_lastStatus == status && millis() < _lastStatusPrinted + 10 * 1000) { return; } + + MessageOutput.printf("[%11.3f] DPL: %s\r\n", + static_cast(millis())/1000, getStatusText(status).c_str()); + + _lastStatus = status; + _lastStatusPrinted = millis(); +} + +void PowerLimiterClass::shutdown(PowerLimiterClass::Status status) +{ + announceStatus(status); + + if (_plState == plStates::OFF) { return; } + + _plState = plStates::SHUTDOWN; + + CONFIG_T& config = Configuration.get(); + std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); + if (inverter == nullptr || !inverter->isProducing() || !inverter->isReachable()) { + _inverter = nullptr; + _plState = plStates::OFF; + return; + } + + auto lastLimitCommandState = inverter->SystemConfigPara()->getLastLimitCommandSuccess(); + if (CMD_PENDING == lastLimitCommandState) { return; } + + auto lastPowerCommandState = inverter->PowerCommand()->getLastPowerCommandSuccess(); + if (CMD_PENDING == lastPowerCommandState) { return; } + + commitPowerLimit(inverter, config.PowerLimiter_LowerPowerLimit, false); } void PowerLimiterClass::loop() { CONFIG_T& config = Configuration.get(); - // Run inital checks to make sure we have met the basic conditions - if (!config.PowerMeter_Enabled - || !Hoymiles.isAllRadioIdle() - || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { - return; + if (plStates::SHUTDOWN == _plState) { + // we transition from SHUTDOWN to OFF when we know the inverter was + // shut down. until then, we retry shutting it down. in this case we + // preserve the original status that lead to the decision to shut down. + return shutdown(_lastStatus); + } + + if (!config.PowerLimiter_Enabled) { + return shutdown(Status::DisabledByConfig); + } + + if (PL_MODE_FULL_DISABLE == _mode) { + return shutdown(Status::DisabledByMqtt); + } + + // refuse to do anything without a power meter + if (!config.PowerMeter_Enabled) { + return shutdown(Status::PowerMeterDisabled); + } + + if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000)) { + return shutdown(Status::PowerMeterTimeout); + } + + std::shared_ptr inverter = + Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); + + if (inverter == nullptr) { return announceStatus(Status::InverterInvalid); } + + // data polling is disabled or the inverter is deemed offline + if (!inverter->isReachable()) { + return announceStatus(Status::InverterOffline); + } + + // concerns active power commands (power limits) only (also from web app or MQTT) + auto lastLimitCommandState = inverter->SystemConfigPara()->getLastLimitCommandSuccess(); + if (CMD_PENDING == lastLimitCommandState) { + return announceStatus(Status::InverterLimitPending); + } + + // concerns power commands (start, stop, restart) only (also from web app or MQTT) + auto lastPowerCommandState = inverter->PowerCommand()->getLastPowerCommandSuccess(); + if (CMD_PENDING == lastPowerCommandState) { + return announceStatus(Status::InverterPowerCmdPending); + } + + // concerns both power limits and start/stop/restart commands and is + // only updated if a respective response was received from the inverter + auto lastUpdateCmd = std::max( + inverter->SystemConfigPara()->getLastUpdateCommand(), + inverter->PowerCommand()->getLastUpdateCommand()); + + // wait for power meter and inverter stat updates after a settling phase + auto settlingEnd = lastUpdateCmd + 3 * 1000; + + if (millis() < settlingEnd) { return announceStatus(Status::Settling); } + + if (inverter->Statistics()->getLastUpdate() <= settlingEnd) { + return announceStatus(Status::InverterStatsPending); + } + + if (PowerMeter.getLastPowerMeterUpdate() <= settlingEnd) { + return announceStatus(Status::PowerMeterPending); } #ifdef POWER_LIMITER_DEBUG MessageOutput.println("[PowerLimiterClass::loop] ******************* ENTER **********************"); #endif - _lastLoop = millis(); - - std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); - if (inverter == nullptr || !inverter->isReachable()) { -#ifdef POWER_LIMITER_DEBUG - MessageOutput.println("[PowerLimiterClass::loop] ******************* No inverter found"); -#endif - return; - } - // Check if next inverter restart time is reached if ((_nextInverterRestart > 1) && (_nextInverterRestart <= millis())) { MessageOutput.println("[PowerLimiterClass::loop] send inverter restart"); @@ -75,56 +180,6 @@ void PowerLimiterClass::loop() } } - // Make sure inverter is turned off if PL is disabled by user/MQTT - if (((!config.PowerLimiter_Enabled || _mode == PL_MODE_FULL_DISABLE) && _plState != SHUTDOWN)) { - if (inverter->isProducing()) { - MessageOutput.printf("PL initiated inverter shutdown.\r\n"); - commitPowerLimit(inverter, config.PowerLimiter_LowerPowerLimit, false); - } else { - _plState = SHUTDOWN; - } -#ifdef POWER_LIMITER_DEBUG - MessageOutput.printf("[PowerLimiterClass::loop] ******************* PL put into shutdown, _plState = %i\r\n", _plState); -#endif - return; - } - - // Return if power limiter is disabled - if (!config.PowerLimiter_Enabled || _mode == PL_MODE_FULL_DISABLE) { -#ifdef POWER_LIMITER_DEBUG - MessageOutput.printf("[PowerLimiterClass::loop] ******************* PL disabled\r\n"); -#endif - return; - } - - // Safety check, return on too old power meter values - if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000) - || (millis() - inverter->Statistics()->getLastUpdate()) > (config.Dtu_PollInterval * 10 * 1000)) { - // If the power meter values are older than 30 seconds, - // or the Inverter Stats are older then 10x the poll interval - // set the limit to lower power limit for safety reasons. - MessageOutput.println("[PowerLimiterClass::loop] Power Meter/Inverter values too old, shutting down inverter"); - commitPowerLimit(inverter, config.PowerLimiter_LowerPowerLimit, false); -#ifdef POWER_LIMITER_DEBUG - MessageOutput.printf("[PowerLimiterClass::loop] ******************* PL safety shutdown, update times exceeded PM: %li, Inverter: %li \r\n", millis() - PowerMeter.getLastPowerMeterUpdate(), millis() - inverter->Statistics()->getLastUpdate()); -#endif - return; - } - - // At this point the PL is enabled but we could still be in the shutdown state - _plState = ACTIVE; - - // If the last inverter update was before the last limit updated, don't do anything. - // Also give the Power meter 3 seconds time to recognize power changes after the last set limit - // as the Hoymiles MPPT might not react immediately. - if (inverter->Statistics()->getLastUpdate() <= _lastLimitSetTime - || PowerMeter.getLastPowerMeterUpdate() <= (_lastLimitSetTime + 3000)) { -#ifdef POWER_LIMITER_DEBUG - MessageOutput.printf("[PowerLimiterClass::loop] ******************* PL inverter updates PM: %i, Inverter: %i \r\n", PowerMeter.getLastPowerMeterUpdate() - (_lastLimitSetTime + 3000), inverter->Statistics()->getLastUpdate() - _lastLimitSetTime); -#endif - return; - } - // Printout some stats if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) { float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); @@ -308,7 +363,6 @@ void PowerLimiterClass::commitPowerLimit(std::shared_ptr inver PowerLimitControlType::AbsolutNonPersistent); _lastRequestedPowerLimit = limit; - _lastLimitSetTime = millis(); // enable power production only after setting the desired limit, // such that an older, greater limit will not cause power spikes. @@ -328,13 +382,8 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver CONFIG_T& config = Configuration.get(); // Stop the inverter if limit is below threshold. - // We'll also set the power limit to the lower value in this case if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) { - if (!inverter->isProducing()) { return; } - - MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] requested power limit %d is smaller than lower power limit %d\r\n", - newPowerLimit, config.PowerLimiter_LowerPowerLimit); - return commitPowerLimit(inverter, config.PowerLimiter_LowerPowerLimit, false); + return shutdown(Status::LowerLimitUndercut); } // enforce configured upper power limit @@ -372,6 +421,7 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] using new limit: %d W, requested power limit: %d\r\n", effPowerLimit, newPowerLimit); + _plState = plStates::ACTIVE; commitPowerLimit(inverter, effPowerLimit, true); } @@ -522,4 +572,4 @@ bool PowerLimiterClass::useFullSolarPassthrough(std::shared_ptr Date: Tue, 27 Jun 2023 22:32:29 +0200 Subject: [PATCH 05/10] DPL requirement: disabled inverter commands if the inverter is not configured to be sent commands to, the DPL is unable to control it, so the loop is aborted. --- include/PowerLimiter.h | 1 + src/PowerLimiter.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 2b212527..342d6092 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -33,6 +33,7 @@ public: PowerMeterPending, InverterInvalid, InverterOffline, + InverterCommandsDisabled, InverterLimitPending, InverterPowerCmdPending, InverterStatsPending, diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 16682b8d..0304f599 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -34,6 +34,7 @@ std::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status st { Status::PowerMeterPending, "waiting for sufficiently recent power meter reading" }, { Status::InverterInvalid, "invalid inverter selection/configuration" }, { Status::InverterOffline, "inverter is offline (polling enabled? radio okay?)" }, + { Status::InverterCommandsDisabled, "inverter configuration prohibits sending commands" }, { Status::InverterLimitPending, "waiting for a power limit command to complete" }, { Status::InverterPowerCmdPending, "waiting for a start/stop/restart command to complete" }, { Status::InverterStatsPending, "waiting for sufficiently recent inverter data" }, @@ -124,6 +125,11 @@ void PowerLimiterClass::loop() return announceStatus(Status::InverterOffline); } + // sending commands to the inverter is disabled + if (!_inverter->getEnableCommands()) { + return announceStatus(Status::InverterCommandsDisabled); + } + // concerns active power commands (power limits) only (also from web app or MQTT) auto lastLimitCommandState = inverter->SystemConfigPara()->getLastLimitCommandSuccess(); if (CMD_PENDING == lastLimitCommandState) { From 71079fa0cc930a51a2818b4d074e9696167ff613 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 28 Jun 2023 20:56:21 +0200 Subject: [PATCH 06/10] DPL: work on internal copy of pointer to inverter the DPL already took care to shut down the inverter if anything fishy was going on, mainly to make sure that the battery is not drained. however, some cases were missed: * if the configuration changed such that another inverter is now targeted, the one the DPL controlled previously was not shut down. * if the configuration changed such that another inverter (different serial number) was configured at the same index, the previous one was not shut down. this change corrects these problems by making the DPL keep a copy of the shared_ptr to the inverter. the shared_ptr is only released once the DPL shut the respective inverter down. --- include/PowerLimiter.h | 2 ++ src/PowerLimiter.cpp | 76 ++++++++++++++++++++++++------------------ 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 342d6092..78444ba3 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -32,6 +32,7 @@ public: PowerMeterTimeout, PowerMeterPending, InverterInvalid, + InverterChanged, InverterOffline, InverterCommandsDisabled, InverterLimitPending, @@ -62,6 +63,7 @@ private: Status _lastStatus = Status::Initializing; uint32_t _lastStatusPrinted = 0; uint8_t _mode = PL_MODE_ENABLE_NORMAL_OP; + std::shared_ptr _inverter = nullptr; bool _batteryDischargeEnabled = false; uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis() uint32_t _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 0304f599..500a5777 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -33,6 +33,7 @@ std::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status st { Status::PowerMeterTimeout, "power meter readings are outdated" }, { Status::PowerMeterPending, "waiting for sufficiently recent power meter reading" }, { Status::InverterInvalid, "invalid inverter selection/configuration" }, + { Status::InverterChanged, "target inverter changed" }, { Status::InverterOffline, "inverter is offline (polling enabled? radio okay?)" }, { Status::InverterCommandsDisabled, "inverter configuration prohibits sending commands" }, { Status::InverterLimitPending, "waiting for a power limit command to complete" }, @@ -70,21 +71,20 @@ void PowerLimiterClass::shutdown(PowerLimiterClass::Status status) _plState = plStates::SHUTDOWN; - CONFIG_T& config = Configuration.get(); - std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); - if (inverter == nullptr || !inverter->isProducing() || !inverter->isReachable()) { + if (_inverter == nullptr || !_inverter->isProducing() || !_inverter->isReachable()) { _inverter = nullptr; _plState = plStates::OFF; return; } - auto lastLimitCommandState = inverter->SystemConfigPara()->getLastLimitCommandSuccess(); + auto lastLimitCommandState = _inverter->SystemConfigPara()->getLastLimitCommandSuccess(); if (CMD_PENDING == lastLimitCommandState) { return; } - auto lastPowerCommandState = inverter->PowerCommand()->getLastPowerCommandSuccess(); + auto lastPowerCommandState = _inverter->PowerCommand()->getLastPowerCommandSuccess(); if (CMD_PENDING == lastPowerCommandState) { return; } - commitPowerLimit(inverter, config.PowerLimiter_LowerPowerLimit, false); + CONFIG_T& config = Configuration.get(); + commitPowerLimit(_inverter, config.PowerLimiter_LowerPowerLimit, false); } void PowerLimiterClass::loop() @@ -115,13 +115,29 @@ void PowerLimiterClass::loop() return shutdown(Status::PowerMeterTimeout); } - std::shared_ptr inverter = + std::shared_ptr currentInverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); - if (inverter == nullptr) { return announceStatus(Status::InverterInvalid); } + // in case of (newly) broken configuration, shut down + // the last inverter we worked with (if any) + if (currentInverter == nullptr) { + return shutdown(Status::InverterInvalid); + } + + // if the DPL is supposed to manage another inverter now, we first + // shut down the previous one, if any. then we pick up the new one. + if (_inverter != nullptr && _inverter->serial() != currentInverter->serial()) { + return shutdown(Status::InverterChanged); + } + + // update our pointer as the configuration might have changed + _inverter = currentInverter; + + // controlling an inverter means the DPL will shut it down eventually + _plState = plStates::ACTIVE; // data polling is disabled or the inverter is deemed offline - if (!inverter->isReachable()) { + if (!_inverter->isReachable()) { return announceStatus(Status::InverterOffline); } @@ -131,13 +147,13 @@ void PowerLimiterClass::loop() } // concerns active power commands (power limits) only (also from web app or MQTT) - auto lastLimitCommandState = inverter->SystemConfigPara()->getLastLimitCommandSuccess(); + auto lastLimitCommandState = _inverter->SystemConfigPara()->getLastLimitCommandSuccess(); if (CMD_PENDING == lastLimitCommandState) { return announceStatus(Status::InverterLimitPending); } // concerns power commands (start, stop, restart) only (also from web app or MQTT) - auto lastPowerCommandState = inverter->PowerCommand()->getLastPowerCommandSuccess(); + auto lastPowerCommandState = _inverter->PowerCommand()->getLastPowerCommandSuccess(); if (CMD_PENDING == lastPowerCommandState) { return announceStatus(Status::InverterPowerCmdPending); } @@ -145,15 +161,15 @@ void PowerLimiterClass::loop() // concerns both power limits and start/stop/restart commands and is // only updated if a respective response was received from the inverter auto lastUpdateCmd = std::max( - inverter->SystemConfigPara()->getLastUpdateCommand(), - inverter->PowerCommand()->getLastUpdateCommand()); + _inverter->SystemConfigPara()->getLastUpdateCommand(), + _inverter->PowerCommand()->getLastUpdateCommand()); // wait for power meter and inverter stat updates after a settling phase auto settlingEnd = lastUpdateCmd + 3 * 1000; if (millis() < settlingEnd) { return announceStatus(Status::Settling); } - if (inverter->Statistics()->getLastUpdate() <= settlingEnd) { + if (_inverter->Statistics()->getLastUpdate() <= settlingEnd) { return announceStatus(Status::InverterStatsPending); } @@ -168,7 +184,7 @@ void PowerLimiterClass::loop() // Check if next inverter restart time is reached if ((_nextInverterRestart > 1) && (_nextInverterRestart <= millis())) { MessageOutput.println("[PowerLimiterClass::loop] send inverter restart"); - inverter->sendRestartControlRequest(); + _inverter->sendRestartControlRequest(); calcNextInverterRestart(); } @@ -188,26 +204,26 @@ void PowerLimiterClass::loop() // Printout some stats if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) { - float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); + float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); MessageOutput.printf("[PowerLimiterClass::loop] dcVoltage: %.2f Voltage Start Threshold: %.2f Voltage Stop Threshold: %.2f inverter->isProducing(): %d\r\n", - dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, inverter->isProducing()); + dcVoltage, config.PowerLimiter_VoltageStartThreshold, config.PowerLimiter_VoltageStopThreshold, _inverter->isProducing()); } // Battery charging cycle conditions // First we always disable discharge if the battery is empty - if (isStopThresholdReached(inverter)) { + if (isStopThresholdReached(_inverter)) { // Disable battery discharge when empty _batteryDischargeEnabled = false; } else { // UI: Solar Passthrough Enabled -> false // Battery discharge can be enabled when start threshold is reached - if (!config.PowerLimiter_SolarPassThroughEnabled && isStartThresholdReached(inverter)) { + if (!config.PowerLimiter_SolarPassThroughEnabled && isStartThresholdReached(_inverter)) { _batteryDischargeEnabled = true; } // UI: Solar Passthrough Enabled -> true && EMPTY_AT_NIGHT if (config.PowerLimiter_SolarPassThroughEnabled && config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) { - if(isStartThresholdReached(inverter)) { + if(isStartThresholdReached(_inverter)) { // In this case we should only discharge the battery as long it is above startThreshold _batteryDischargeEnabled = true; } @@ -219,18 +235,18 @@ void PowerLimiterClass::loop() // UI: Solar Passthrough Enabled -> true && EMPTY_WHEN_FULL // Battery discharge can be enabled when start threshold is reached - if (config.PowerLimiter_SolarPassThroughEnabled && isStartThresholdReached(inverter) && config.PowerLimiter_BatteryDrainStategy == EMPTY_WHEN_FULL) { + if (config.PowerLimiter_SolarPassThroughEnabled && isStartThresholdReached(_inverter) && config.PowerLimiter_BatteryDrainStategy == EMPTY_WHEN_FULL) { _batteryDischargeEnabled = true; } } // Calculate and set Power Limit - int32_t newPowerLimit = calcPowerLimit(inverter, canUseDirectSolarPower(), _batteryDischargeEnabled); - setNewPowerLimit(inverter, newPowerLimit); + int32_t newPowerLimit = calcPowerLimit(_inverter, canUseDirectSolarPower(), _batteryDischargeEnabled); + setNewPowerLimit(_inverter, newPowerLimit); #ifdef POWER_LIMITER_DEBUG MessageOutput.printf("[PowerLimiterClass::loop] Status: SolarPT enabled %i, Drain Strategy: %i, canUseDirectSolarPower: %i, Batt discharge: %i\r\n", config.PowerLimiter_SolarPassThroughEnabled, config.PowerLimiter_BatteryDrainStategy, canUseDirectSolarPower(), _batteryDischargeEnabled); MessageOutput.printf("[PowerLimiterClass::loop] Status: StartTH %i, StopTH: %i, loadCorrectedV %f\r\n", - isStartThresholdReached(inverter), isStopThresholdReached(inverter), getLoadCorrectedVoltage(inverter)); + isStartThresholdReached(_inverter), isStopThresholdReached(_inverter), getLoadCorrectedVoltage(_inverter)); MessageOutput.printf("[PowerLimiterClass::loop] Status Batt: Ena: %i, SOC: %i, StartTH: %i, StopTH: %i, LastUpdate: %li\r\n", config.Battery_Enabled, Battery.stateOfCharge, config.PowerLimiter_BatterySocStartThreshold, config.PowerLimiter_BatterySocStopThreshold, millis() - Battery.stateOfChargeLastUpdate); MessageOutput.printf("[PowerLimiterClass::loop] ******************* Leaving PL, PL set to: %i, SP: %i, Batt: %i, PM: %f\r\n", newPowerLimit, canUseDirectSolarPower(), _batteryDischargeEnabled, round(PowerMeter.getPowerTotal())); @@ -238,22 +254,19 @@ void PowerLimiterClass::loop() } uint8_t PowerLimiterClass::getPowerLimiterState() { - CONFIG_T& config = Configuration.get(); - - std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); - if (inverter == nullptr || !inverter->isReachable()) { + if (_inverter == nullptr || !_inverter->isReachable()) { return PL_UI_STATE_INACTIVE; } - if (inverter->isProducing() && _batteryDischargeEnabled) { + if (_inverter->isProducing() && _batteryDischargeEnabled) { return PL_UI_STATE_USE_SOLAR_AND_BATTERY; } - if (inverter->isProducing() && !_batteryDischargeEnabled) { + if (_inverter->isProducing() && !_batteryDischargeEnabled) { return PL_UI_STATE_USE_SOLAR_ONLY; } - if(!inverter->isProducing()) { + if(!_inverter->isProducing()) { return PL_UI_STATE_CHARGING; } @@ -427,7 +440,6 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] using new limit: %d W, requested power limit: %d\r\n", effPowerLimit, newPowerLimit); - _plState = plStates::ACTIVE; commitPowerLimit(inverter, effPowerLimit, true); } From b2d58af5e86e6167234862648d3578225244736d Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 28 Jun 2023 22:58:08 +0200 Subject: [PATCH 07/10] DPL: separate unconditional solar passthrough mode the unconditional solar passthrough mode, configured using MQTT, works differently than the normal mode of operation. it is also independent from the power meter reading. if this mode is active, a shortcut is taken to a function that implements the actions for this mode. this is convenient since we don't have to consider special cases in the code that handles normal mode of operation. --- include/PowerLimiter.h | 4 +++ src/PowerLimiter.cpp | 82 +++++++++++++++++++++++++++++++----------- 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 78444ba3..52d6f2cb 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -38,6 +38,8 @@ public: InverterLimitPending, InverterPowerCmdPending, InverterStatsPending, + UnconditionalSolarPassthrough, + NoVeDirect, Settling, LowerLimitUndercut }; @@ -72,6 +74,8 @@ private: std::string const& getStatusText(Status status); void announceStatus(Status status); void shutdown(Status status); + int32_t inverterPowerDcToAc(std::shared_ptr inverter, int32_t dcPower); + void unconditionalSolarPassthrough(std::shared_ptr inverter); bool canUseDirectSolarPower(); int32_t calcPowerLimit(std::shared_ptr inverter, bool solarPowerEnabled, bool batteryDischargeEnabled); void commitPowerLimit(std::shared_ptr inverter, int32_t limit, bool enablePowerProduction); diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 500a5777..622316b7 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -39,6 +39,8 @@ std::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status st { Status::InverterLimitPending, "waiting for a power limit command to complete" }, { Status::InverterPowerCmdPending, "waiting for a start/stop/restart command to complete" }, { Status::InverterStatsPending, "waiting for sufficiently recent inverter data" }, + { Status::UnconditionalSolarPassthrough, "unconditionally passing through all solar power (MQTT override)" }, + { Status::NoVeDirect, "VE.Direct disabled, connection broken, or data outdated" }, { Status::Settling, "waiting for the system to settle" }, { Status::LowerLimitUndercut, "calculated power limit undercuts configured lower limit" } }; @@ -106,15 +108,6 @@ void PowerLimiterClass::loop() return shutdown(Status::DisabledByMqtt); } - // refuse to do anything without a power meter - if (!config.PowerMeter_Enabled) { - return shutdown(Status::PowerMeterDisabled); - } - - if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000)) { - return shutdown(Status::PowerMeterTimeout); - } - std::shared_ptr currentInverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); @@ -158,6 +151,21 @@ void PowerLimiterClass::loop() return announceStatus(Status::InverterPowerCmdPending); } + if (PL_MODE_SOLAR_PT_ONLY == _mode) { + // handle this mode of operation separately + return unconditionalSolarPassthrough(_inverter); + } + + // the normal mode of operation requires a valid + // power meter reading to calculate a power limit + if (!config.PowerMeter_Enabled) { + return shutdown(Status::PowerMeterDisabled); + } + + if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000)) { + return shutdown(Status::PowerMeterTimeout); + } + // concerns both power limits and start/stop/restart commands and is // only updated if a respective response was received from the inverter auto lastUpdateCmd = std::max( @@ -253,6 +261,45 @@ void PowerLimiterClass::loop() #endif } +/** + * 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 + * efficiency. + */ +int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr inverter, int32_t dcPower) +{ + CONFIG_T& config = Configuration.get(); + + float inverterEfficiencyPercent = inverter->Statistics()->getChannelFieldValue( + TYPE_AC, static_cast(config.PowerLimiter_InverterChannelId), 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; + + return dcPower * inverterEfficiencyFactor; +} + +/** + * implements the "unconditional solar passthrough" mode of operation, which + * can currently only be set using MQTT. in this mode of operation, the + * inverter shall behave as if it was connected to the solar panels directly, + * i.e., all solar power (and only solar power) is fed to the AC side, + * independent from the power meter reading. + */ +void PowerLimiterClass::unconditionalSolarPassthrough(std::shared_ptr inverter) +{ + CONFIG_T& config = Configuration.get(); + + if (!config.Vedirect_Enabled || !VeDirect.isDataValid()) { + return shutdown(Status::NoVeDirect); + } + + int32_t solarPower = VeDirect.veFrame.V * VeDirect.veFrame.I; + setNewPowerLimit(inverter, inverterPowerDcToAc(inverter, solarPower)); + announceStatus(Status::UnconditionalSolarPassthrough); +} + uint8_t PowerLimiterClass::getPowerLimiterState() { if (_inverter == nullptr || !_inverter->isReachable()) { return PL_UI_STATE_INACTIVE; @@ -341,13 +388,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve // If the battery is enabled this can always be supplied since we assume that the battery can supply unlimited power // The next step is to determine if the Solar power as provided by the Victron charger // actually constrains or dictates another inverter power value - float inverterEfficiencyPercent = inverter->Statistics()->getChannelFieldValue( - TYPE_AC, static_cast(config.PowerLimiter_InverterChannelId), 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; - int32_t victronChargePower = getSolarChargePower(); - int32_t adjustedVictronChargePower = victronChargePower * inverterEfficiencyFactor; + int32_t adjustedVictronChargePower = inverterPowerDcToAc(inverter, getSolarChargePower()); // Battery can be discharged and we should output max (Victron solar power || power meter value) if(batteryDischargeEnabled && useFullSolarPassthrough(inverter)) { @@ -356,13 +397,12 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve } // We should use Victron solar power only (corrected by efficiency factor) - if ((solarPowerEnabled && !batteryDischargeEnabled) || (_mode == PL_MODE_SOLAR_PT_ONLY)) { + if (solarPowerEnabled && !batteryDischargeEnabled) { // Case 2 - Limit power to solar power only - MessageOutput.printf("[PowerLimiterClass::loop] Consuming Solar Power Only -> victronChargePower: %d, inverter efficiency: %.2f, powerConsumption: %d \r\n", - victronChargePower, inverterEfficiencyFactor, newPowerLimit); + MessageOutput.printf("[PowerLimiterClass::loop] Consuming Solar Power Only -> adjustedVictronChargePower: %d, powerConsumption: %d \r\n", + adjustedVictronChargePower, newPowerLimit); - if ((adjustedVictronChargePower < newPowerLimit) || (_mode == PL_MODE_SOLAR_PT_ONLY)) - newPowerLimit = adjustedVictronChargePower; + newPowerLimit = std::min(newPowerLimit, adjustedVictronChargePower); } MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit); From 9bab740c4361d0fe9666059a60db8029f9d87ac3 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 29 Jun 2023 21:49:10 +0200 Subject: [PATCH 08/10] DPL: replace _plState enum with a simple boolean switch --- include/PowerLimiter.h | 9 +-------- src/PowerLimiter.cpp | 13 ++++--------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 52d6f2cb..6249bb4e 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -53,15 +53,8 @@ public: void calcNextInverterRestart(); private: - enum class plStates : unsigned { - INIT, // looping for the first time after system startup - ACTIVE, // normal operation, sending power limit updates to inverter - SHUTDOWN, // power limiter shuts down inverter - OFF // inverter was shut down, power limiter is NOT active - }; - int32_t _lastRequestedPowerLimit = 0; - plStates _plState = plStates::INIT; + bool _shutdownInProgress; Status _lastStatus = Status::Initializing; uint32_t _lastStatusPrinted = 0; uint8_t _mode = PL_MODE_ENABLE_NORMAL_OP; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 622316b7..3ccf979a 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -69,16 +69,14 @@ void PowerLimiterClass::shutdown(PowerLimiterClass::Status status) { announceStatus(status); - if (_plState == plStates::OFF) { return; } - - _plState = plStates::SHUTDOWN; - if (_inverter == nullptr || !_inverter->isProducing() || !_inverter->isReachable()) { _inverter = nullptr; - _plState = plStates::OFF; + _shutdownInProgress = false; return; } + _shutdownInProgress = true; + auto lastLimitCommandState = _inverter->SystemConfigPara()->getLastLimitCommandSuccess(); if (CMD_PENDING == lastLimitCommandState) { return; } @@ -93,7 +91,7 @@ void PowerLimiterClass::loop() { CONFIG_T& config = Configuration.get(); - if (plStates::SHUTDOWN == _plState) { + if (_shutdownInProgress) { // we transition from SHUTDOWN to OFF when we know the inverter was // shut down. until then, we retry shutting it down. in this case we // preserve the original status that lead to the decision to shut down. @@ -126,9 +124,6 @@ void PowerLimiterClass::loop() // update our pointer as the configuration might have changed _inverter = currentInverter; - // controlling an inverter means the DPL will shut it down eventually - _plState = plStates::ACTIVE; - // data polling is disabled or the inverter is deemed offline if (!_inverter->isReachable()) { return announceStatus(Status::InverterOffline); From 461fce8ff4738214a4edadcb6374a398a4c301c1 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 29 Jun 2023 21:59:31 +0200 Subject: [PATCH 09/10] DPL: do not repeat being disabled --- src/PowerLimiter.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 3ccf979a..262614bf 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -58,6 +58,10 @@ void PowerLimiterClass::announceStatus(PowerLimiterClass::Status status) // otherwise repeat the info with a fixed interval. if (_lastStatus == status && millis() < _lastStatusPrinted + 10 * 1000) { return; } + // after announcing once that the DPL is disabled by configuration, it + // should just be silent while it is disabled. + if (status == Status::DisabledByConfig && _lastStatus == status) { return; } + MessageOutput.printf("[%11.3f] DPL: %s\r\n", static_cast(millis())/1000, getStatusText(status).c_str()); From 9aeb1583b5ef588255b33f5d1777fe2c2a7e13a9 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 30 Jun 2023 19:21:44 +0200 Subject: [PATCH 10/10] DPL: consider the system stable when reusing the old limit a new status is needed to communicate that no update was sent to the inverter because its power limit is still valid. in this case, calculating a new power limit is delayed by an exponentially increasing backoff. the maximum backoff time is ~1s, which is still plenty fast. the backoff is actually necessary for another reason: at least currently, a lot of debug messages are printed to the console. printing all that information in every DPL loop() is too much. --- include/PowerLimiter.h | 5 ++++- src/PowerLimiter.cpp | 30 +++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 6249bb4e..3a59d73c 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -41,6 +41,7 @@ public: UnconditionalSolarPassthrough, NoVeDirect, Settling, + Stable, LowerLimitUndercut }; @@ -57,6 +58,8 @@ private: bool _shutdownInProgress; Status _lastStatus = Status::Initializing; uint32_t _lastStatusPrinted = 0; + uint32_t _lastCalculation = 0; + uint32_t _calculationBackoffMs = 0; uint8_t _mode = PL_MODE_ENABLE_NORMAL_OP; std::shared_ptr _inverter = nullptr; bool _batteryDischargeEnabled = false; @@ -72,7 +75,7 @@ private: bool canUseDirectSolarPower(); int32_t calcPowerLimit(std::shared_ptr inverter, bool solarPowerEnabled, bool batteryDischargeEnabled); void commitPowerLimit(std::shared_ptr inverter, int32_t limit, bool enablePowerProduction); - void setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit); + bool setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit); int32_t getSolarChargePower(); float getLoadCorrectedVoltage(std::shared_ptr inverter); bool isStartThresholdReached(std::shared_ptr inverter); diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 262614bf..dc72e5da 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -42,6 +42,7 @@ std::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status st { Status::UnconditionalSolarPassthrough, "unconditionally passing through all solar power (MQTT override)" }, { Status::NoVeDirect, "VE.Direct disabled, connection broken, or data outdated" }, { Status::Settling, "waiting for the system to settle" }, + { Status::Stable, "the system is stable, the last power limit is still valid" }, { Status::LowerLimitUndercut, "calculated power limit undercuts configured lower limit" } }; @@ -184,6 +185,12 @@ void PowerLimiterClass::loop() return announceStatus(Status::PowerMeterPending); } + // since _lastCalculation and _calculationBackoffMs are initialized to + // zero, this test is passed the first time the condition is checked. + if (millis() < (_lastCalculation + _calculationBackoffMs)) { + return announceStatus(Status::Stable); + } + #ifdef POWER_LIMITER_DEBUG MessageOutput.println("[PowerLimiterClass::loop] ******************* ENTER **********************"); #endif @@ -248,7 +255,7 @@ void PowerLimiterClass::loop() } // Calculate and set Power Limit int32_t newPowerLimit = calcPowerLimit(_inverter, canUseDirectSolarPower(), _batteryDischargeEnabled); - setNewPowerLimit(_inverter, newPowerLimit); + bool limitUpdated = setNewPowerLimit(_inverter, newPowerLimit); #ifdef POWER_LIMITER_DEBUG MessageOutput.printf("[PowerLimiterClass::loop] Status: SolarPT enabled %i, Drain Strategy: %i, canUseDirectSolarPower: %i, Batt discharge: %i\r\n", config.PowerLimiter_SolarPassThroughEnabled, config.PowerLimiter_BatteryDrainStategy, canUseDirectSolarPower(), _batteryDischargeEnabled); @@ -258,6 +265,16 @@ void PowerLimiterClass::loop() config.Battery_Enabled, Battery.stateOfCharge, config.PowerLimiter_BatterySocStartThreshold, config.PowerLimiter_BatterySocStopThreshold, millis() - Battery.stateOfChargeLastUpdate); MessageOutput.printf("[PowerLimiterClass::loop] ******************* Leaving PL, PL set to: %i, SP: %i, Batt: %i, PM: %f\r\n", newPowerLimit, canUseDirectSolarPower(), _batteryDischargeEnabled, round(PowerMeter.getPowerTotal())); #endif + + _lastCalculation = millis(); + + if (!limitUpdated) { + // increase polling backoff if system seems to be stable + _calculationBackoffMs = std::min(1024, _calculationBackoffMs * 2); + return announceStatus(Status::Stable); + } + + _calculationBackoffMs = 128; } /** @@ -433,15 +450,17 @@ void PowerLimiterClass::commitPowerLimit(std::shared_ptr inver /** * enforces limits and a hystersis on the requested power limit, after scaling * the power limit to the ratio of total and producing inverter channels. - * commits the sanitized power limit. + * commits the sanitized power limit. returns true if a limit update was + * committed, false otherwise. */ -void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit) +bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit) { CONFIG_T& config = Configuration.get(); // Stop the inverter if limit is below threshold. if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) { - return shutdown(Status::LowerLimitUndercut); + shutdown(Status::LowerLimitUndercut); + return true; } // enforce configured upper power limit @@ -473,13 +492,14 @@ void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver if ( diff < config.PowerLimiter_TargetPowerConsumptionHysteresis) { MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] reusing old limit: %d W, diff: %d, hysteresis: %d\r\n", _lastRequestedPowerLimit, diff, config.PowerLimiter_TargetPowerConsumptionHysteresis); - return; + return false; } MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] using new limit: %d W, requested power limit: %d\r\n", effPowerLimit, newPowerLimit); commitPowerLimit(inverter, effPowerLimit, true); + return true; } int32_t PowerLimiterClass::getSolarChargePower()