powerLimiter with state machine
This commit is contained in:
parent
b70407d7fe
commit
716fc867a1
@ -7,6 +7,14 @@
|
|||||||
#include <Hoymiles.h>
|
#include <Hoymiles.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
enum PowerLimiterStates {
|
||||||
|
STATE_DISCOVER = 0,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_CONSUME_SOLAR_POWER_ONLY,
|
||||||
|
STATE_NORMAL_OPERATION
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
class PowerLimiterClass {
|
class PowerLimiterClass {
|
||||||
public:
|
public:
|
||||||
void init();
|
void init();
|
||||||
@ -18,13 +26,15 @@ private:
|
|||||||
uint32_t _lastLoop;
|
uint32_t _lastLoop;
|
||||||
uint32_t _lastPowerMeterUpdate;
|
uint32_t _lastPowerMeterUpdate;
|
||||||
uint16_t _lastRequestedPowerLimit;
|
uint16_t _lastRequestedPowerLimit;
|
||||||
bool _consumeSolarPowerOnly;
|
u_int8_t _plState = STATE_DISCOVER;
|
||||||
|
|
||||||
float _powerMeter1Power;
|
float _powerMeter1Power;
|
||||||
float _powerMeter2Power;
|
float _powerMeter2Power;
|
||||||
float _powerMeter3Power;
|
float _powerMeter3Power;
|
||||||
|
|
||||||
bool canUseDirectSolarPower();
|
bool canUseDirectSolarPower();
|
||||||
|
int32_t calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, bool consumeSolarPowerOnly);
|
||||||
|
void setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, uint32_t newPowerLimit);
|
||||||
uint16_t getDirectSolarPower();
|
uint16_t getDirectSolarPower();
|
||||||
float getLoadCorrectedVoltage(std::shared_ptr<InverterAbstract> inverter);
|
float getLoadCorrectedVoltage(std::shared_ptr<InverterAbstract> inverter);
|
||||||
bool isStartThresholdReached(std::shared_ptr<InverterAbstract> inverter);
|
bool isStartThresholdReached(std::shared_ptr<InverterAbstract> inverter);
|
||||||
|
|||||||
@ -39,7 +39,6 @@ void PowerLimiterClass::init()
|
|||||||
MqttSettings.subscribe(config.PowerLimiter_MqttTopicPowerMeter3, 0, std::bind(&PowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
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;
|
_lastLoop = 0;
|
||||||
_lastPowerMeterUpdate = 0;
|
_lastPowerMeterUpdate = 0;
|
||||||
@ -88,121 +87,102 @@ void PowerLimiterClass::loop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
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) {
|
if ((millis() - inverter->Statistics()->getLastUpdate()) > 10000) {
|
||||||
return;
|
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)) {
|
if (millis() - _lastPowerMeterUpdate < (30 * 1000)) {
|
||||||
MessageOutput.printf("[PowerLimiterClass::loop] dcVoltage: %.2f Voltage Start Threshold: %.2f Voltage Stop Threshold: %.2f inverter->isProducing(): %d\r\n",
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t powerMeter = _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
|
|
||||||
|
|
||||||
if (inverter->isProducing()) {
|
while(true) {
|
||||||
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC);
|
switch(_plState) {
|
||||||
float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor);
|
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))
|
uint16_t newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit;
|
||||||
|| !canUseDirectSolarPower()) {
|
inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent);
|
||||||
// The battery is full enough again, use the full battery power from now on.
|
_lastRequestedPowerLimit = newPowerLimit;
|
||||||
_consumeSolarPowerOnly = false;
|
_lastCommandSent = millis();
|
||||||
} else if (!_consumeSolarPowerOnly && !isStopThresholdReached(inverter) && canUseDirectSolarPower()) {
|
}
|
||||||
// The battery voltage dropped too low
|
|
||||||
_consumeSolarPowerOnly = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStopThresholdReached(inverter)
|
// do nothing if battery is empty
|
||||||
|| (_consumeSolarPowerOnly && !canUseDirectSolarPower())) {
|
if (isStopThresholdReached(inverter))
|
||||||
// DC voltage too low, stop the inverter
|
return;
|
||||||
MessageOutput.printf("[PowerLimiterClass::loop] DC voltage: %.2f Corrected DC voltage: %.2f...\r\n",
|
// check for possible state changes
|
||||||
dcVoltage, correctedDcVoltage);
|
if (isStartThresholdReached(inverter) && calcPowerLimit(inverter, false) >= config.PowerLimiter_LowerPowerLimit) {
|
||||||
MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter...");
|
_plState = STATE_NORMAL_OPERATION;
|
||||||
inverter->sendPowerControlRequest(Hoymiles.getRadio(), false);
|
}
|
||||||
|
else if (canUseDirectSolarPower() && calcPowerLimit(inverter, true) >= config.PowerLimiter_LowerPowerLimit) {
|
||||||
|
_plState = STATE_CONSUME_SOLAR_POWER_ONLY;
|
||||||
|
}
|
||||||
|
|
||||||
uint16_t newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit;
|
// inverter on on state change
|
||||||
inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent);
|
if (_plState != STATE_OFF) {
|
||||||
_lastRequestedPowerLimit = newPowerLimit;
|
// DC voltage high enough, start the inverter
|
||||||
|
MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter...");
|
||||||
_lastCommandSent = millis();
|
inverter->sendPowerControlRequest(Hoymiles.getRadio(), true);
|
||||||
_consumeSolarPowerOnly = false;
|
_lastCommandSent = millis();
|
||||||
|
}
|
||||||
return;
|
else
|
||||||
}
|
return;
|
||||||
} else {
|
break;
|
||||||
if ((isStartThresholdReached(inverter) || (canUseDirectSolarPower() && (!isStopThresholdReached(inverter))))
|
case STATE_CONSUME_SOLAR_POWER_ONLY: {
|
||||||
&& powerMeter >= config.PowerLimiter_LowerPowerLimit) {
|
int32_t newPowerLimit = calcPowerLimit(inverter, true);
|
||||||
// DC voltage high enough, start the inverter
|
if (!inverter->isProducing()
|
||||||
MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter...");
|
|| isStopThresholdReached(inverter)
|
||||||
_lastCommandSent = millis();
|
|| newPowerLimit < config.PowerLimiter_LowerPowerLimit) {
|
||||||
inverter->sendPowerControlRequest(Hoymiles.getRadio(), true);
|
_plState = STATE_OFF;
|
||||||
|
break;
|
||||||
// In this mode, the inverter should consume the current solar power only
|
}
|
||||||
// and not drain additional power from the battery
|
else if (!canUseDirectSolarPower() || isStartThresholdReached(inverter)) {
|
||||||
if (!isStartThresholdReached(inverter)) {
|
_plState = STATE_NORMAL_OPERATION;
|
||||||
_consumeSolarPowerOnly = true;
|
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()
|
bool PowerLimiterClass::canUseDirectSolarPower()
|
||||||
@ -222,6 +202,53 @@ bool PowerLimiterClass::canUseDirectSolarPower()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> 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<InverterAbstract> 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()
|
uint16_t PowerLimiterClass::getDirectSolarPower()
|
||||||
{
|
{
|
||||||
if (!this->canUseDirectSolarPower()) {
|
if (!this->canUseDirectSolarPower()) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user