diff --git a/docs/PowerLimiterInverterStates.drawio b/docs/PowerLimiterInverterStates.drawio new file mode 100644 index 00000000..3d547018 --- /dev/null +++ b/docs/PowerLimiterInverterStates.drawio @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/PowerLimiterInverterStates.png b/docs/PowerLimiterInverterStates.png new file mode 100644 index 00000000..3a94dcc1 Binary files /dev/null and b/docs/PowerLimiterInverterStates.png differ diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h index 087ec621..9011b01b 100644 --- a/include/MqttHandleVedirect.h +++ b/include/MqttHandleVedirect.h @@ -18,7 +18,7 @@ public: void init(); void loop(); private: - veStruct _kvFrame; + veStruct _kvFrame{}; uint32_t _lastPublish; }; diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 44160fec..844bf20e 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 @@ private: 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/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index 6ac4c700..fc732c0c 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -37,7 +37,7 @@ typedef struct { double V; // battery voltage in V double I; // battery current in A double VPV; // panel voltage in V - double PPV; // panel power in W + uint16_t PPV; // panel power in W double H19; // yield total kWh double H20; // yield today kWh uint16_t H21; // maximum power today W @@ -61,13 +61,13 @@ public: String getOrAsString(uint32_t offReason); // off reason as string String getMpptAsString(uint8_t mppt); // state of mppt as string - veStruct veFrame; // public map for received name and value pairs + veStruct veFrame{}; // public struct for received name and value pairs private: void setLastUpdate(); // set timestampt after successful frame read void rxData(uint8_t inbyte); // byte of serial data void textRxEvent(char *, char *); - void frameEndEvent(bool); // copy temp map to public map + void frameEndEvent(bool); // copy temp struct to public struct void logE(const char *, const char *); bool hexRxEvent(uint8_t); @@ -77,7 +77,7 @@ private: char * _textPointer; // pointer to the private buffer we're writing to, name or value char _name[VE_MAX_VALUE_LEN]; // buffer for the field name char _value[VE_MAX_VALUE_LEN]; // buffer for the field value - veStruct _tmpFrame; // private struct for received name and value pairs + veStruct _tmpFrame{}; // private struct for received name and value pairs unsigned long _pollInterval; unsigned long _lastPoll; }; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 0b2cac15..c7a4d8c3 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -39,8 +39,7 @@ void PowerLimiterClass::init() MqttSettings.subscribe(config.PowerLimiter_MqttTopicPowerMeter3, 0, std::bind(&PowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); } - _consumeSolarPowerOnly = true; - _lastCommandSent = 0; + _lastCommandSent = 0; _lastLoop = 0; _lastPowerMeterUpdate = 0; _lastRequestedPowerLimit = 0; @@ -74,132 +73,117 @@ void PowerLimiterClass::loop() || !Hoymiles.getRadio()->isIdle() || (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000) || (millis() - _lastLoop) < (config.PowerLimiter_Interval * 1000)) { + if (!config.PowerLimiter_Enabled) + _plState = STATE_DISCOVER; // ensure STATE_DISCOVER is set, if PowerLimiter will be enabled. return; } _lastLoop = millis(); std::shared_ptr inverter = Hoymiles.getInverterByPos(config.PowerLimiter_InverterId); - if (inverter == nullptr || !inverter->isReachable()) { return; } 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); + 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); - if ((_consumeSolarPowerOnly && isStartThresholdReached(inverter))) { - // 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; - } + uint16_t newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit; + inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); + _lastRequestedPowerLimit = newPowerLimit; + _lastCommandSent = millis(); + return; + } - 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); + // 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; + } - 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; + // 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(); + return; + } + 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; } - - int32_t newPowerLimit = 0; - - 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; - } - - newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; - - 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); - } - - 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] powerMeter: %d W lastRequestedPowerLimit: %d\r\n", - powerMeter, _lastRequestedPowerLimit); - } - } 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; - } - - 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() @@ -219,13 +203,60 @@ bool PowerLimiterClass::canUseDirectSolarPower() return true; } +int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, bool consumeSolarPowerOnly) +{ + CONFIG_T& config = Configuration.get(); + + int32_t newPowerLimit = _powerMeter1Power + _powerMeter2Power + _powerMeter3Power; + + 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 + + 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; + + 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(); +} + uint16_t PowerLimiterClass::getDirectSolarPower() { if (!canUseDirectSolarPower()) { return 0; } - return round(VeDirect.veFrame.PPV); + return VeDirect.veFrame.PPV; } float PowerLimiterClass::getLoadCorrectedVoltage(std::shared_ptr inverter)