diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 44160fec0..844bf20ec 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -7,6 +7,14 @@ #include #include +enum PowerLimiterStates { + STATE_DISCOVER = 0, + STATE_OFF, + STATE_CONSUME_SOLAR_POWER_ONLY, + STATE_NORMAL_OPERATION +}; + + class PowerLimiterClass { public: void init(); @@ -18,13 +26,15 @@ class PowerLimiterClass { uint32_t _lastLoop; uint32_t _lastPowerMeterUpdate; uint16_t _lastRequestedPowerLimit; - bool _consumeSolarPowerOnly; + u_int8_t _plState = STATE_DISCOVER; float _powerMeter1Power; float _powerMeter2Power; float _powerMeter3Power; bool canUseDirectSolarPower(); + int32_t calcPowerLimit(std::shared_ptr inverter, bool consumeSolarPowerOnly); + void setNewPowerLimit(std::shared_ptr inverter, uint32_t newPowerLimit); uint16_t getDirectSolarPower(); float getLoadCorrectedVoltage(std::shared_ptr inverter); bool isStartThresholdReached(std::shared_ptr inverter); diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 0e60a1f8f..b550b1866 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -39,7 +39,6 @@ void PowerLimiterClass::init() MqttSettings.subscribe(config.PowerLimiter_MqttTopicPowerMeter3, 0, std::bind(&PowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); } - _consumeSolarPowerOnly = true; _lastCommandSent = 0; _lastLoop = 0; _lastPowerMeterUpdate = 0; @@ -88,140 +87,168 @@ void PowerLimiterClass::loop() } float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC); + float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); + float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); if ((millis() - inverter->Statistics()->getLastUpdate()) > 10000) { return; } - float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); - uint32_t victronChargePower = this->getDirectSolarPower(); - - MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s \r\n", victronChargePower, efficency, _consumeSolarPowerOnly ? "true" : "false"); - if (millis() - _lastPowerMeterUpdate < (30 * 1000)) { 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()); } - int32_t powerMeter = _powerMeter1Power + _powerMeter2Power + _powerMeter3Power; - - if (inverter->isProducing()) { - float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC); - float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor); - - if ((_consumeSolarPowerOnly && isStartThresholdReached(inverter)) - || !canUseDirectSolarPower()) { - // The battery is full enough again, use the full battery power from now on. - _consumeSolarPowerOnly = false; - } else if (!_consumeSolarPowerOnly && !isStopThresholdReached(inverter) && canUseDirectSolarPower()) { - // The battery voltage dropped too low - _consumeSolarPowerOnly = true; - } - - if (isStopThresholdReached(inverter) - || (_consumeSolarPowerOnly && !canUseDirectSolarPower())) { - // DC voltage too low, stop the inverter - MessageOutput.printf("[PowerLimiterClass::loop] DC voltage: %.2f Corrected DC voltage: %.2f...\r\n", - dcVoltage, correctedDcVoltage); - MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter..."); - inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); - - uint16_t newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit; - inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); - _lastRequestedPowerLimit = newPowerLimit; - _lastCommandSent = millis(); - _consumeSolarPowerOnly = false; - - return; - } - } else { - if ((isStartThresholdReached(inverter) || (canUseDirectSolarPower() && (!isStopThresholdReached(inverter)))) - && powerMeter >= config.PowerLimiter_LowerPowerLimit) { - // DC voltage high enough, start the inverter - MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter..."); - _lastCommandSent = millis(); - inverter->sendPowerControlRequest(Hoymiles.getRadio(), true); - - // In this mode, the inverter should consume the current solar power only - // and not drain additional power from the battery - if (!isStartThresholdReached(inverter)) { - _consumeSolarPowerOnly = true; + while(true) { + switch(_plState) { + case STATE_DISCOVER: + if (!inverter->isProducing() || isStopThresholdReached(inverter)) { + _plState = STATE_OFF; + } + else if (canUseDirectSolarPower()) { + _plState = STATE_CONSUME_SOLAR_POWER_ONLY; + } + else { + _plState = STATE_NORMAL_OPERATION; + } + break; + case STATE_OFF: + // if on turn off + if (inverter->isProducing()) { + MessageOutput.printf("[PowerLimiterClass::loop] DC voltage: %.2f Corrected DC voltage: %.2f...\r\n", + dcVoltage, correctedDcVoltage); + MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter..."); + inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); + + uint16_t newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit; + inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); + _lastRequestedPowerLimit = newPowerLimit; + _lastCommandSent = millis(); + } + + // do nothing if battery is empty + if (isStopThresholdReached(inverter)) + return; + // check for possible state changes + if (isStartThresholdReached(inverter) && calcPowerLimit(inverter, false) >= config.PowerLimiter_LowerPowerLimit) { + _plState = STATE_NORMAL_OPERATION; + } + else if (canUseDirectSolarPower() && calcPowerLimit(inverter, true) >= config.PowerLimiter_LowerPowerLimit) { + _plState = STATE_CONSUME_SOLAR_POWER_ONLY; + } + + // inverter on on state change + if (_plState != STATE_OFF) { + // DC voltage high enough, start the inverter + MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter..."); + inverter->sendPowerControlRequest(Hoymiles.getRadio(), true); + _lastCommandSent = millis(); + } + else + return; + break; + case STATE_CONSUME_SOLAR_POWER_ONLY: { + int32_t newPowerLimit = calcPowerLimit(inverter, true); + if (!inverter->isProducing() + || isStopThresholdReached(inverter) + || newPowerLimit < config.PowerLimiter_LowerPowerLimit) { + _plState = STATE_OFF; + break; + } + else if (!canUseDirectSolarPower() || isStartThresholdReached(inverter)) { + _plState = STATE_NORMAL_OPERATION; + break; + } + setNewPowerLimit(inverter, newPowerLimit); + return; + break; } + case STATE_NORMAL_OPERATION: { + int32_t newPowerLimit = calcPowerLimit(inverter, false); + if (!inverter->isProducing() + || isStopThresholdReached(inverter) + || newPowerLimit < config.PowerLimiter_LowerPowerLimit) { + _plState = STATE_OFF; + break; + } + // check if grid power consumption is within the upper an lower threshold of the target consumption + else if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && + newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) { + return; + } + setNewPowerLimit(inverter, newPowerLimit); + return; + break; + } } + } +} - return; +bool PowerLimiterClass::canUseDirectSolarPower() +{ + CONFIG_T& config = Configuration.get(); + + if (!config.PowerLimiter_SolarPassTroughEnabled + || !config.Vedirect_Enabled) { + return false; } - int32_t newPowerLimit = 0; + if (VeDirect.veFrame.PPV < 10.0) { + // Not enough power + return false; + } - if (millis() - _lastPowerMeterUpdate < (30 * 1000)) { - newPowerLimit = powerMeter; - // check if grid power consumption is within the upper an lower threshold of the target consumption - if (!_consumeSolarPowerOnly && - newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && - newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) - return; - else { - 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 unrelieable. - newPowerLimit += _lastRequestedPowerLimit; - } + return true; +} - newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; +int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, bool consumeSolarPowerOnly) +{ + CONFIG_T& config = Configuration.get(); + + int32_t newPowerLimit = _powerMeter1Power + _powerMeter2Power + _powerMeter3Power; - uint16_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; - if (_consumeSolarPowerOnly && (upperPowerLimit > victronChargePower)) { - // Battery voltage too low, use Victron solar power (corrected by efficency factor) only - upperPowerLimit = victronChargePower * (efficency / 100.0); - } + float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); + uint32_t victronChargePower = this->getDirectSolarPower(); + uint32_t adjustedVictronChargePower = victronChargePower * (efficency > 0.0 ? (efficency / 100.0) : 1.0); // if inverter is off, use 1.0 - if (newPowerLimit > upperPowerLimit) - newPowerLimit = upperPowerLimit; - else if (newPowerLimit < (uint16_t)config.PowerLimiter_LowerPowerLimit) { - newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit; - // stop the inverter - MessageOutput.println("[PowerLimiterClass::loop] Power limit below lower power limit. Stopping inverter..."); - inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); + MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s \r\n", victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false"); + + if (millis() - _lastPowerMeterUpdate < (30 * 1000)) { + 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 unrelieable. + newPowerLimit += _lastRequestedPowerLimit; + } - } + newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; - MessageOutput.printf("[PowerLimiterClass::loop] powerMeter: %d W lastRequestedPowerLimit: %d\r\n", - powerMeter, _lastRequestedPowerLimit); + uint16_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; + if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { + // Battery voltage too low, use Victron solar power (corrected by efficency factor) only + upperPowerLimit = adjustedVictronChargePower; } + + if (newPowerLimit > upperPowerLimit) + newPowerLimit = upperPowerLimit; } else { // If the power meter values are older than 30 seconds, // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. newPowerLimit = config.PowerLimiter_LowerPowerLimit; } + return newPowerLimit; +} +void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inverter, uint32_t newPowerLimit) +{ MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit); inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); _lastRequestedPowerLimit = newPowerLimit; - _lastCommandSent = millis(); } -bool PowerLimiterClass::canUseDirectSolarPower() -{ - CONFIG_T& config = Configuration.get(); - - if (!config.PowerLimiter_SolarPassTroughEnabled - || !config.Vedirect_Enabled) { - return false; - } - - if (VeDirect.veFrame.PPV < 10.0) { - // Not enough power - return false; - } - - return true; -} - uint16_t PowerLimiterClass::getDirectSolarPower() { if (!this->canUseDirectSolarPower()) {