Skip to content

Commit

Permalink
powerLimiter with state machine
Browse files Browse the repository at this point in the history
  • Loading branch information
helgeerbe committed Mar 7, 2023
1 parent b70407d commit 716fc86
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 101 deletions.
12 changes: 11 additions & 1 deletion include/PowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
#include <Hoymiles.h>
#include <memory>

enum PowerLimiterStates {
STATE_DISCOVER = 0,
STATE_OFF,
STATE_CONSUME_SOLAR_POWER_ONLY,
STATE_NORMAL_OPERATION
};


class PowerLimiterClass {
public:
void init();
Expand All @@ -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<InverterAbstract> inverter, bool consumeSolarPowerOnly);
void setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, uint32_t newPowerLimit);
uint16_t getDirectSolarPower();
float getLoadCorrectedVoltage(std::shared_ptr<InverterAbstract> inverter);
bool isStartThresholdReached(std::shared_ptr<InverterAbstract> inverter);
Expand Down
227 changes: 127 additions & 100 deletions src/PowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<InverterAbstract> 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<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();
}

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()) {
Expand Down

0 comments on commit 716fc86

Please sign in to comment.