From 71079fa0cc930a51a2818b4d074e9696167ff613 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 28 Jun 2023 20:56:21 +0200 Subject: [PATCH] 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); }