// SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (C) 2022 Thomas Basler and others */ #include "Battery.h" #include "PowerMeter.h" #include "PowerLimiter.h" #include "Configuration.h" #include "MqttSettings.h" #include "NetworkSettings.h" #include "Huawei_can.h" #include #include "MessageOutput.h" #include #include #include PowerLimiterClass PowerLimiter; #define POWER_LIMITER_DEBUG void PowerLimiterClass::init() { } std::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status status) { 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::WaitingForValidTimestamp, "waiting for valid date and time to be available" }, { 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::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" }, { Status::InverterPowerCmdPending, "waiting for a start/stop/restart command to complete" }, { Status::InverterDevInfoPending, "waiting for inverter device information to be available" }, { 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::Stable, "the system is stable, the last power limit is still valid" }, { 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; } // 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()); _lastStatus = status; _lastStatusPrinted = millis(); } void PowerLimiterClass::shutdown(PowerLimiterClass::Status status) { announceStatus(status); if (_inverter == nullptr || !_inverter->isProducing() || !_inverter->isReachable()) { _inverter = nullptr; _shutdownInProgress = false; return; } _shutdownInProgress = true; auto lastLimitCommandState = _inverter->SystemConfigPara()->getLastLimitCommandSuccess(); if (CMD_PENDING == lastLimitCommandState) { return; } auto lastPowerCommandState = _inverter->PowerCommand()->getLastPowerCommandSuccess(); if (CMD_PENDING == lastPowerCommandState) { return; } CONFIG_T& config = Configuration.get(); commitPowerLimit(_inverter, config.PowerLimiter_LowerPowerLimit, false); } void PowerLimiterClass::loop() { CONFIG_T& config = Configuration.get(); // we know that the Hoymiles library refuses to send any message to any // inverter until the system has valid time information. until then we can // do nothing, not even shutdown the inverter. struct tm timeinfo; if (!getLocalTime(&timeinfo, 5)) { return announceStatus(Status::WaitingForValidTimestamp); } 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. return shutdown(_lastStatus); } if (!config.PowerLimiter_Enabled) { return shutdown(Status::DisabledByConfig); } if (PL_MODE_FULL_DISABLE == _mode) { return shutdown(Status::DisabledByMqtt); } std::shared_ptr currentInverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); // 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; // data polling is disabled or the inverter is deemed offline if (!_inverter->isReachable()) { 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) { 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); } // a calculated power limit will always be limited to the reported // device's max power. that upper limit is only known after the first // DevInfoSimpleCommand succeeded. if (_inverter->DevInfo()->getMaxPower() <= 0) { return announceStatus(Status::InverterDevInfoPending); } 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( _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); } // 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 // Check if next inverter restart time is reached if ((_nextInverterRestart > 1) && (_nextInverterRestart <= millis())) { MessageOutput.println("[PowerLimiterClass::loop] send inverter restart"); _inverter->sendRestartControlRequest(); calcNextInverterRestart(); } // Check if NTP time is set and next inverter restart not calculated yet if ((config.PowerLimiter_RestartHour >= 0) && (_nextInverterRestart == 0) ) { // check every 5 seconds if (_nextCalculateCheck < millis()) { struct tm timeinfo; if (getLocalTime(&timeinfo, 5)) { calcNextInverterRestart(); } else { MessageOutput.println("[PowerLimiterClass::loop] inverter restart calculation: NTP not ready"); _nextCalculateCheck += 5000; } } } // Printout some stats if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) { 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()); } // Battery charging cycle conditions // First we always disable discharge if the battery is empty 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)) { _batteryDischargeEnabled = true; } // UI: Solar Passthrough Enabled -> true && EMPTY_AT_NIGHT if (config.PowerLimiter_SolarPassThroughEnabled && config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) { if(isStartThresholdReached(_inverter)) { // In this case we should only discharge the battery as long it is above startThreshold _batteryDischargeEnabled = true; } else { // In this case we should only discharge the battery when there is no sunshine _batteryDischargeEnabled = !canUseDirectSolarPower(); } } // 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) { _batteryDischargeEnabled = true; } } // Calculate and set Power Limit int32_t newPowerLimit = calcPowerLimit(_inverter, canUseDirectSolarPower(), _batteryDischargeEnabled); 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); // the inverter might have been reset to nullptr due to a shutdown if (_inverter != nullptr) { MessageOutput.printf("[PowerLimiterClass::loop] Status: StartTH %i, StopTH: %i, loadCorrectedV %f\r\n", 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())); #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 = _calculationBackoffMsDefault; } /** * 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; // account for losses between solar charger and inverter (cables, junctions...) float lossesFactor = 1.00 - static_cast(config.PowerLimiter_SolarPassThroughLosses)/100; return dcPower * inverterEfficiencyFactor * lossesFactor; } /** * 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; } if (_inverter->isProducing() && _batteryDischargeEnabled) { return PL_UI_STATE_USE_SOLAR_AND_BATTERY; } if (_inverter->isProducing() && !_batteryDischargeEnabled) { return PL_UI_STATE_USE_SOLAR_ONLY; } if(!_inverter->isProducing()) { return PL_UI_STATE_CHARGING; } return PL_UI_STATE_INACTIVE; } int32_t PowerLimiterClass::getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; } bool PowerLimiterClass::getMode() { return _mode; } void PowerLimiterClass::setMode(uint8_t mode) { _mode = mode; } bool PowerLimiterClass::canUseDirectSolarPower() { CONFIG_T& config = Configuration.get(); if (!config.PowerLimiter_SolarPassThroughEnabled || !config.Vedirect_Enabled || !VeDirect.isDataValid()) { return false; } return VeDirect.veFrame.PPV >= 20; // enough power? } // Logic table // | Case # | batteryDischargeEnabled | solarPowerEnabled | useFullSolarPassthrough | Result | // | 1 | false | false | doesn't matter | PL = 0 | // | 2 | false | true | doesn't matter | PL = Victron Power | // | 3 | true | doesn't matter | false | PL = PowerMeter value (Battery can supply unlimited energy) | // | 4 | true | false | true | PL = PowerMeter value | // | 5 | true | true | true | PL = max(PowerMeter value, Victron Power) | int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, bool solarPowerEnabled, bool batteryDischargeEnabled) { CONFIG_T& config = Configuration.get(); int32_t acPower = 0; int32_t newPowerLimit = round(PowerMeter.getPowerTotal()); if (!solarPowerEnabled && !batteryDischargeEnabled) { // Case 1 - No energy sources available return 0; } if (config.PowerLimiter_IsInverterBehindPowerMeter) { // If the inverter the behind the power meter (part of measurement), // the produced power of this inverter has also to be taken into account. // We don't use FLD_PAC from the statistics, because that // data might be too old and unreliable. acPower = static_cast(inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC)); newPowerLimit += acPower; } // We're not trying to hit 0 exactly but take an offset into account // This means we never fully compensate the used power with the inverter // Case 3 newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; // At this point we've calculated the required energy to compensate for household consumption. // 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 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)) { // Case 5 newPowerLimit = newPowerLimit > adjustedVictronChargePower ? newPowerLimit : adjustedVictronChargePower; } else { // We check if the PSU is on and disable the Power Limiter in this case. // The PSU should reduce power or shut down first before the Power Limiter kicks in // The only case where this is not desired is if the battery is over the Full Solar Passthrough Threshold // In this case the Power Limiter should start. The PSU will shutdown when the Power Limiter is active if (HuaweiCan.getAutoPowerStatus()) { return 0; } } // We should use Victron solar power only (corrected by efficiency factor) if (solarPowerEnabled && !batteryDischargeEnabled) { // Case 2 - Limit power to solar power only MessageOutput.printf("[PowerLimiterClass::loop] Consuming Solar Power Only -> adjustedVictronChargePower: %d, powerConsumption: %d \r\n", adjustedVictronChargePower, newPowerLimit); newPowerLimit = std::min(newPowerLimit, adjustedVictronChargePower); } MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit); return newPowerLimit; } void PowerLimiterClass::commitPowerLimit(std::shared_ptr inverter, int32_t limit, bool enablePowerProduction) { // disable power production as soon as possible. // setting the power limit is less important. if (!enablePowerProduction && inverter->isProducing()) { MessageOutput.println("[PowerLimiterClass::commitPowerLimit] Stopping inverter..."); inverter->sendPowerControlRequest(false); } inverter->sendActivePowerControlRequest(static_cast(limit), PowerLimitControlType::AbsolutNonPersistent); _lastRequestedPowerLimit = limit; // enable power production only after setting the desired limit, // such that an older, greater limit will not cause power spikes. if (enablePowerProduction && !inverter->isProducing()) { MessageOutput.println("[PowerLimiterClass::commitPowerLimit] Starting up inverter..."); inverter->sendPowerControlRequest(true); } } /** * 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. returns true if a limit update was * committed, false otherwise. */ 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) { shutdown(Status::LowerLimitUndercut); return true; } // enforce configured upper power limit int32_t effPowerLimit = std::min(newPowerLimit, config.PowerLimiter_UpperPowerLimit); // scale the power limit by the amount of all inverter channels devided by // the amount of producing inverter channels. the inverters limit each of // the n channels to 1/n of the total power limit. scaling the power limit // ensures the total inverter output is what we are asking for. std::list dcChnls = inverter->Statistics()->getChannelsByType(TYPE_DC); int dcProdChnls = 0, dcTotalChnls = dcChnls.size(); for (auto& c : dcChnls) { if (inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) > 2.0) { dcProdChnls++; } } if ((dcProdChnls > 0) && (dcProdChnls != dcTotalChnls)) { MessageOutput.printf("[PowerLimiterClass::setNewPowerLimit] %d channels total, %d producing channels, scaling power limit\r\n", dcTotalChnls, dcProdChnls); effPowerLimit = round(effPowerLimit * static_cast(dcTotalChnls) / dcProdChnls); } effPowerLimit = std::min(effPowerLimit, inverter->DevInfo()->getMaxPower()); // Check if the new value is within the limits of the hysteresis auto diff = std::abs(effPowerLimit - _lastRequestedPowerLimit); 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 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() { if (!canUseDirectSolarPower()) { return 0; } return VeDirect.veFrame.V * VeDirect.veFrame.I; } float PowerLimiterClass::getLoadCorrectedVoltage(std::shared_ptr inverter) { CONFIG_T& config = Configuration.get(); float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); if (dcVoltage <= 0.0) { return 0.0; } return dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); } bool PowerLimiterClass::isStartThresholdReached(std::shared_ptr inverter) { CONFIG_T& config = Configuration.get(); // Check if the Battery interface is enabled and the SOC start threshold is reached if (config.Battery_Enabled && config.PowerLimiter_BatterySocStartThreshold > 0.0 && (millis() - Battery.stateOfChargeLastUpdate) < 60000 && Battery.stateOfCharge >= config.PowerLimiter_BatterySocStartThreshold) { return true; } // Otherwise we use the voltage threshold if (config.PowerLimiter_VoltageStartThreshold <= 0.0) { return false; } float correctedDcVoltage = getLoadCorrectedVoltage(inverter); return correctedDcVoltage >= config.PowerLimiter_VoltageStartThreshold; } bool PowerLimiterClass::isStopThresholdReached(std::shared_ptr inverter) { CONFIG_T& config = Configuration.get(); // Check if the Battery interface is enabled and the SOC stop threshold is reached if (config.Battery_Enabled && config.PowerLimiter_BatterySocStopThreshold > 0.0 && (millis() - Battery.stateOfChargeLastUpdate) < 60000 && Battery.stateOfCharge <= config.PowerLimiter_BatterySocStopThreshold) { return true; } // Otherwise we use the voltage threshold if (config.PowerLimiter_VoltageStopThreshold <= 0.0) { return false; } float correctedDcVoltage = getLoadCorrectedVoltage(inverter); return correctedDcVoltage <= config.PowerLimiter_VoltageStopThreshold; } /// @brief calculate next inverter restart in millis void PowerLimiterClass::calcNextInverterRestart() { CONFIG_T& config = Configuration.get(); // first check if restart is configured at all if (config.PowerLimiter_RestartHour < 0) { _nextInverterRestart = 1; MessageOutput.println("[PowerLimiterClass::calcNextInverterRestart] _nextInverterRestart disabled"); return; } // read time from timeserver, if time is not synced then return struct tm timeinfo; if (getLocalTime(&timeinfo, 5)) { // calculation first step is offset to next restart in minutes uint16_t dayMinutes = timeinfo.tm_hour * 60 + timeinfo.tm_min; uint16_t targetMinutes = config.PowerLimiter_RestartHour * 60; if (config.PowerLimiter_RestartHour > timeinfo.tm_hour) { // next restart is on the same day _nextInverterRestart = targetMinutes - dayMinutes; } else { // next restart is on next day _nextInverterRestart = 1440 - dayMinutes + targetMinutes; } #ifdef POWER_LIMITER_DEBUG MessageOutput.printf("[PowerLimiterClass::calcNextInverterRestart] Localtime read %d %d / configured RestartHour %d\r\n", timeinfo.tm_hour, timeinfo.tm_min, config.PowerLimiter_RestartHour); MessageOutput.printf("[PowerLimiterClass::calcNextInverterRestart] dayMinutes %d / targetMinutes %d\r\n", dayMinutes, targetMinutes); MessageOutput.printf("[PowerLimiterClass::calcNextInverterRestart] next inverter restart in %d minutes\r\n", _nextInverterRestart); #endif // then convert unit for next restart to milliseconds and add current uptime millis() _nextInverterRestart *= 60000; _nextInverterRestart += millis(); } else { MessageOutput.println("[PowerLimiterClass::calcNextInverterRestart] getLocalTime not successful, no calculation"); _nextInverterRestart = 0; } MessageOutput.printf("[PowerLimiterClass::calcNextInverterRestart] _nextInverterRestart @ %d millis\r\n", _nextInverterRestart); } bool PowerLimiterClass::useFullSolarPassthrough(std::shared_ptr inverter) { CONFIG_T& config = Configuration.get(); // We only do full solar PT if general solar PT is enabled if(!config.PowerLimiter_SolarPassThroughEnabled) { return false; } // Check if the Battery interface is enabled and the SOC stop threshold is reached if (config.Battery_Enabled && config.PowerLimiter_FullSolarPassThroughSoc > 0.0 && (millis() - Battery.stateOfChargeLastUpdate) < 60000 && Battery.stateOfCharge >= config.PowerLimiter_FullSolarPassThroughSoc) { return true; } // Otherwise we use the voltage threshold if (config.PowerLimiter_FullSolarPassThroughStartVoltage <= 0.0 || config.PowerLimiter_FullSolarPassThroughStopVoltage <= 0.0) { return false; } float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); #ifdef POWER_LIMITER_DEBUG MessageOutput.printf("[PowerLimiterClass::loop] useFullSolarPassthrough: FullSolarPT Start %f, FullSolarPT Stop: %f, dcVoltage: %f\r\n", config.PowerLimiter_FullSolarPassThroughStartVoltage, config.PowerLimiter_FullSolarPassThroughStopVoltage, dcVoltage); #endif if (dcVoltage <= 0.0) { return false; } if (dcVoltage >= config.PowerLimiter_FullSolarPassThroughStartVoltage) { _fullSolarPassThroughEnabled = true; } if (dcVoltage <= config.PowerLimiter_FullSolarPassThroughStopVoltage) { _fullSolarPassThroughEnabled = false; } return _fullSolarPassThroughEnabled; }