Merge branch 'pr/MalteSchm/172' into development
This commit is contained in:
commit
c337df605c
@ -196,6 +196,12 @@ Topics for 3 phases of a power meter is configurable. Given is an example for th
|
|||||||
| huawei/output_temp | R | Output air temperature | °C |
|
| huawei/output_temp | R | Output air temperature | °C |
|
||||||
| huawei/efficiency | R | Efficiency | Percentage |
|
| huawei/efficiency | R | Efficiency | Percentage |
|
||||||
|
|
||||||
|
## Power Limiter topics
|
||||||
|
| Topic | R / W | Description | Value / Unit |
|
||||||
|
| --------------------------------------- | ----- | ---------------------------------------------------- | -------------------------- |
|
||||||
|
| powerlimiter/cmd/disable | W | Power Limiter disable override for external PL control | 0 / 1 |
|
||||||
|
| powerlimiter/status/disabled | R | Power Limiter disable override status | 0 / 1 |
|
||||||
|
|
||||||
## Currently supported Inverters
|
## Currently supported Inverters
|
||||||
|
|
||||||
| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases |
|
| Model | Required RF Module | DC Inputs | MPP-Tracker | AC Phases |
|
||||||
|
|||||||
20
include/MqttHandlePowerLimiter.h
Normal file
20
include/MqttHandlePowerLimiter.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Configuration.h"
|
||||||
|
#include <espMqttClient.h>
|
||||||
|
|
||||||
|
class MqttHandlePowerLimiterClass {
|
||||||
|
public:
|
||||||
|
void init();
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total);
|
||||||
|
|
||||||
|
uint32_t _lastPublishStats;
|
||||||
|
uint32_t _lastPublish;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
|
||||||
@ -8,10 +8,8 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
STATE_DISCOVER = 0,
|
SHUTDOWN = 0,
|
||||||
STATE_OFF,
|
ACTIVE
|
||||||
STATE_CONSUME_SOLAR_POWER_ONLY,
|
|
||||||
STATE_NORMAL_OPERATION
|
|
||||||
} plStates;
|
} plStates;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@ -26,13 +24,16 @@ public:
|
|||||||
void loop();
|
void loop();
|
||||||
plStates getPowerLimiterState();
|
plStates getPowerLimiterState();
|
||||||
int32_t getLastRequestedPowewrLimit();
|
int32_t getLastRequestedPowewrLimit();
|
||||||
|
void setDisable(bool disable);
|
||||||
|
bool getDisable();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint32_t _lastCommandSent = 0;
|
|
||||||
uint32_t _lastLoop = 0;
|
uint32_t _lastLoop = 0;
|
||||||
int32_t _lastRequestedPowerLimit = 0;
|
int32_t _lastRequestedPowerLimit = 0;
|
||||||
uint32_t _lastLimitSetTime = 0;
|
uint32_t _lastLimitSetTime = 0;
|
||||||
plStates _plState = STATE_DISCOVER;
|
plStates _plState = ACTIVE;
|
||||||
|
bool _disabled = false;
|
||||||
|
bool _batteryDischargeEnabled = false;
|
||||||
|
|
||||||
float _powerMeter1Power;
|
float _powerMeter1Power;
|
||||||
float _powerMeter2Power;
|
float _powerMeter2Power;
|
||||||
|
|||||||
87
src/MqttHandlePowerLimiter.cpp
Normal file
87
src/MqttHandlePowerLimiter.cpp
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Thomas Basler, Malte Schmidt and others
|
||||||
|
*/
|
||||||
|
#include "MessageOutput.h"
|
||||||
|
#include "MqttSettings.h"
|
||||||
|
#include "MqttHandlePowerLimiter.h"
|
||||||
|
#include "PowerLimiter.h"
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
#define TOPIC_SUB_POWER_LIMITER "disable"
|
||||||
|
|
||||||
|
MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterClass::init()
|
||||||
|
{
|
||||||
|
using std::placeholders::_1;
|
||||||
|
using std::placeholders::_2;
|
||||||
|
using std::placeholders::_3;
|
||||||
|
using std::placeholders::_4;
|
||||||
|
using std::placeholders::_5;
|
||||||
|
using std::placeholders::_6;
|
||||||
|
|
||||||
|
String topic = MqttSettings.getPrefix();
|
||||||
|
MqttSettings.subscribe(String(topic + "powerlimiter/cmd/" + TOPIC_SUB_POWER_LIMITER).c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6));
|
||||||
|
|
||||||
|
_lastPublish = millis();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterClass::loop()
|
||||||
|
{
|
||||||
|
if (!MqttSettings.getConnected() ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
|
if ((millis() - _lastPublish) > (config.Mqtt_PublishInterval * 1000) ) {
|
||||||
|
MqttSettings.publish("powerlimiter/status/disabled", String(PowerLimiter.getDisable()));
|
||||||
|
|
||||||
|
yield();
|
||||||
|
_lastPublish = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void MqttHandlePowerLimiterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total)
|
||||||
|
{
|
||||||
|
const CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
|
char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics
|
||||||
|
strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char*
|
||||||
|
|
||||||
|
char* setting;
|
||||||
|
char* rest = &token_topic[strlen(config.Mqtt_Topic)];
|
||||||
|
|
||||||
|
strtok_r(rest, "/", &rest); // Remove "powerlimiter"
|
||||||
|
strtok_r(rest, "/", &rest); // Remove "cmd"
|
||||||
|
|
||||||
|
setting = strtok_r(rest, "/", &rest);
|
||||||
|
|
||||||
|
if (setting == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* str = new char[len + 1];
|
||||||
|
memcpy(str, payload, len);
|
||||||
|
str[len] = '\0';
|
||||||
|
uint8_t payload_val = atoi(str);
|
||||||
|
delete[] str;
|
||||||
|
|
||||||
|
if (!strcmp(setting, TOPIC_SUB_POWER_LIMITER)) {
|
||||||
|
if(payload_val == 1) {
|
||||||
|
MessageOutput.println("Power limiter disabled");
|
||||||
|
PowerLimiter.setDisable(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(payload_val == 0) {
|
||||||
|
MessageOutput.println("Power limiter enabled");
|
||||||
|
PowerLimiter.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MessageOutput.println("Power limiter enable / disable - unknown command received. Please use 0 or 1");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,13 +23,10 @@ void PowerLimiterClass::loop()
|
|||||||
{
|
{
|
||||||
CONFIG_T& config = Configuration.get();
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
if (!config.PowerLimiter_Enabled
|
// Run inital checks to make sure we have met the basic conditions
|
||||||
|| !config.PowerMeter_Enabled
|
if (!config.PowerMeter_Enabled
|
||||||
|| !Hoymiles.isAllRadioIdle()
|
|| !Hoymiles.isAllRadioIdle()
|
||||||
|| (millis() - _lastCommandSent) < (config.PowerLimiter_Interval * 1000)
|
|
||||||
|| (millis() - _lastLoop) < (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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,106 +37,64 @@ void PowerLimiterClass::loop()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float dcVoltage = inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_UDC);
|
// Make sure inverter is turned off if PL is disabled by user/MQTT
|
||||||
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC);
|
// Make sure inverter is turned off when low battery threshold is reached
|
||||||
float correctedDcVoltage = dcVoltage + (acPower * config.PowerLimiter_VoltageLoadCorrectionFactor);
|
if (((!config.PowerLimiter_Enabled || _disabled) && _plState != SHUTDOWN)
|
||||||
|
|| isStopThresholdReached(inverter)) {
|
||||||
|
if (inverter->isProducing()) {
|
||||||
|
MessageOutput.printf("PL initiated inverter shutdown.\r\n");
|
||||||
|
inverter->sendPowerControlRequest(false);
|
||||||
|
} else {
|
||||||
|
_plState = SHUTDOWN;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if power limiter is disabled
|
||||||
|
if (!config.PowerLimiter_Enabled || _disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// At this point the PL is enabled but we could still be in the shutdown state
|
||||||
|
_plState = ACTIVE;
|
||||||
|
|
||||||
// If the last inverter update is too old, don't do anything.
|
// If the last inverter update is too old, don't do anything.
|
||||||
// If the last inverter update was before the last limit updated, don't do anything.
|
// If the last inverter update was before the last limit updated, don't do anything.
|
||||||
// Also give the Power meter 3 seconds time to recognize power changes because of the last set limit
|
// Also give the Power meter 3 seconds time to recognize power changes after the last set limit
|
||||||
// and also because the Hoymiles MPPT might not react immediately.
|
// as the Hoymiles MPPT might not react immediately.
|
||||||
if ((millis() - inverter->Statistics()->getLastUpdate()) > 10000
|
if ((millis() - inverter->Statistics()->getLastUpdate()) > 10000
|
||||||
|| inverter->Statistics()->getLastUpdate() <= _lastLimitSetTime
|
|| inverter->Statistics()->getLastUpdate() <= _lastLimitSetTime
|
||||||
|| PowerMeter.getLastPowerMeterUpdate() <= (_lastLimitSetTime + 3000)) {
|
|| PowerMeter.getLastPowerMeterUpdate() <= (_lastLimitSetTime + 3000)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Printout some stats
|
||||||
if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)) {
|
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",
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
setNewPowerLimit(inverter, -1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// do nothing if battery is empty
|
// Battery charging cycle conditions
|
||||||
if (isStopThresholdReached(inverter))
|
// The battery can only be discharged after a full charge in the
|
||||||
return;
|
// EMPTY_WHEN_FULL case
|
||||||
// check for possible state changes
|
|
||||||
if (canUseDirectSolarPower()) {
|
|
||||||
_plState = STATE_CONSUME_SOLAR_POWER_ONLY;
|
|
||||||
}
|
|
||||||
if (isStartThresholdReached(inverter)) {
|
|
||||||
_plState = STATE_NORMAL_OPERATION;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
break;
|
|
||||||
case STATE_CONSUME_SOLAR_POWER_ONLY: {
|
|
||||||
int32_t newPowerLimit = calcPowerLimit(inverter, true);
|
|
||||||
if (isStopThresholdReached(inverter)) {
|
if (isStopThresholdReached(inverter)) {
|
||||||
_plState = STATE_OFF;
|
// Disable battery discharge when empty
|
||||||
break;
|
_batteryDischargeEnabled = false;
|
||||||
}
|
} else if (!canUseDirectSolarPower() ||
|
||||||
if (isStartThresholdReached(inverter)) {
|
config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT) {
|
||||||
_plState = STATE_NORMAL_OPERATION;
|
// Enable battery discharge
|
||||||
break;
|
_batteryDischargeEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!canUseDirectSolarPower()) {
|
// This checks if the battery discharge start conditions are met for the EMPTY_WHEN_FULL case
|
||||||
if (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT)
|
if (isStartThresholdReached(inverter) && config.PowerLimiter_BatteryDrainStategy == EMPTY_WHEN_FULL) {
|
||||||
_plState = STATE_NORMAL_OPERATION;
|
_batteryDischargeEnabled = true;
|
||||||
else
|
|
||||||
_plState = STATE_OFF;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate and set Power Limit
|
||||||
|
int32_t newPowerLimit = calcPowerLimit(inverter, !_batteryDischargeEnabled);
|
||||||
setNewPowerLimit(inverter, newPowerLimit);
|
setNewPowerLimit(inverter, newPowerLimit);
|
||||||
return;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case STATE_NORMAL_OPERATION: {
|
|
||||||
int32_t newPowerLimit = calcPowerLimit(inverter, false);
|
|
||||||
if (isStopThresholdReached(inverter)) {
|
|
||||||
_plState = STATE_OFF;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!isStartThresholdReached(inverter) && canUseDirectSolarPower() && (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGHT)) {
|
|
||||||
_plState = STATE_CONSUME_SOLAR_POWER_ONLY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if grid power consumption is not within the upper and lower threshold of the target consumption
|
|
||||||
if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
|
|
||||||
newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
|
|
||||||
_lastRequestedPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
|
|
||||||
_lastRequestedPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setNewPowerLimit(inverter, newPowerLimit);;
|
|
||||||
return;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
plStates PowerLimiterClass::getPowerLimiterState() {
|
plStates PowerLimiterClass::getPowerLimiterState() {
|
||||||
@ -150,6 +105,14 @@ int32_t PowerLimiterClass::getLastRequestedPowewrLimit() {
|
|||||||
return _lastRequestedPowerLimit;
|
return _lastRequestedPowerLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PowerLimiterClass::getDisable() {
|
||||||
|
return _disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerLimiterClass::setDisable(bool disable) {
|
||||||
|
_disabled = disable;
|
||||||
|
}
|
||||||
|
|
||||||
bool PowerLimiterClass::canUseDirectSolarPower()
|
bool PowerLimiterClass::canUseDirectSolarPower()
|
||||||
{
|
{
|
||||||
CONFIG_T& config = Configuration.get();
|
CONFIG_T& config = Configuration.get();
|
||||||
@ -167,67 +130,95 @@ bool PowerLimiterClass::canUseDirectSolarPower()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, bool consumeSolarPowerOnly)
|
int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, bool consumeSolarPowerOnly)
|
||||||
{
|
{
|
||||||
CONFIG_T& config = Configuration.get();
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
int32_t newPowerLimit = round(PowerMeter.getPowerTotal());
|
int32_t newPowerLimit = round(PowerMeter.getPowerTotal());
|
||||||
|
|
||||||
float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF);
|
// Safety check, return on too old power meter values
|
||||||
int32_t victronChargePower = this->getDirectSolarPower();
|
if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000)
|
||||||
int32_t adjustedVictronChargePower = victronChargePower * (efficency > 0.0 ? (efficency / 100.0) : 1.0); // if inverter is off, use 1.0
|
&& (millis() - inverter->Statistics()->getLastUpdate()) > (config.Dtu_PollInterval * 10 * 1000)) {
|
||||||
|
// If the power meter values are older than 30 seconds,
|
||||||
|
// and the Inverter Stats are older then 10x the poll interval
|
||||||
|
// set the limit to 0W for safety reasons.
|
||||||
|
MessageOutput.println("[PowerLimiterClass::loop] Power Meter/Inverter values too old. Using 0W (i.e. disable inverter)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n",
|
|
||||||
victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit);
|
|
||||||
|
|
||||||
// Safety check: Are the power meter values not too old?
|
|
||||||
// Are the reported inverter data not too old?
|
|
||||||
if (millis() - PowerMeter.getLastPowerMeterUpdate() < (30 * 1000)
|
|
||||||
&& millis() - inverter->Statistics()->getLastUpdate() < (15 * 1000)) {
|
|
||||||
if (config.PowerLimiter_IsInverterBehindPowerMeter) {
|
if (config.PowerLimiter_IsInverterBehindPowerMeter) {
|
||||||
// If the inverter the behind the power meter (part of measurement),
|
// If the inverter the behind the power meter (part of measurement),
|
||||||
// the produced power of this inverter has also to be taken into account.
|
// 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.
|
||||||
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC);
|
float acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_PAC);
|
||||||
newPowerLimit += static_cast<int>(acPower);
|
newPowerLimit += static_cast<int>(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
|
||||||
newPowerLimit -= config.PowerLimiter_TargetPowerConsumption;
|
newPowerLimit -= config.PowerLimiter_TargetPowerConsumption;
|
||||||
|
|
||||||
int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit;
|
// Check if the new value is within the limits of the hysteresis and
|
||||||
if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) {
|
// if we're not limited to Solar Power only (i.e. we can discharge the battery)
|
||||||
// Battery voltage too low, use Victron solar power (corrected by efficency factor) only
|
// If things did not change much we just use the old setting
|
||||||
upperPowerLimit = adjustedVictronChargePower;
|
if (newPowerLimit >= (-config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
|
||||||
|
newPowerLimit <= (+config.PowerLimiter_TargetPowerConsumptionHysteresis) &&
|
||||||
|
!consumeSolarPowerOnly ) {
|
||||||
|
MessageOutput.println("[PowerLimiterClass::loop] reusing old limit");
|
||||||
|
return _lastRequestedPowerLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPowerLimit > upperPowerLimit)
|
// We should use Victron solar power only (corrected by efficiency factor)
|
||||||
newPowerLimit = upperPowerLimit;
|
if (consumeSolarPowerOnly) {
|
||||||
} else {
|
float efficiency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF);
|
||||||
// If the power meter values are older than 30 seconds,
|
int32_t victronChargePower = this->getDirectSolarPower();
|
||||||
// set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons.
|
int32_t adjustedVictronChargePower = victronChargePower * (efficiency > 0.0 ? (efficiency / 100.0) : 1.0); // if inverter is off, use 1.0
|
||||||
newPowerLimit = config.PowerLimiter_LowerPowerLimit;
|
|
||||||
|
MessageOutput.printf("[PowerLimiterClass::loop] Consuming Solar Power Only -> victronChargePower: %d, efficiency: %.2f, powerConsumption: %d \r\n",
|
||||||
|
victronChargePower, efficiency, newPowerLimit);
|
||||||
|
|
||||||
|
// Limit power to solar power only
|
||||||
|
if (adjustedVictronChargePower < newPowerLimit)
|
||||||
|
newPowerLimit = adjustedVictronChargePower;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Respect power limit
|
||||||
|
if (newPowerLimit > config.PowerLimiter_UpperPowerLimit)
|
||||||
|
newPowerLimit = config.PowerLimiter_UpperPowerLimit;
|
||||||
|
|
||||||
MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit);
|
MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit);
|
||||||
return newPowerLimit;
|
return newPowerLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit)
|
void PowerLimiterClass::setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit)
|
||||||
{
|
{
|
||||||
if(_lastRequestedPowerLimit != newPowerLimit) {
|
|
||||||
CONFIG_T& config = Configuration.get();
|
CONFIG_T& config = Configuration.get();
|
||||||
|
|
||||||
// if limit too low turn inverter offf
|
// Start the inverter in case it's inactive and if the requested power is high enough
|
||||||
|
if (!inverter->isProducing() && newPowerLimit > config.PowerLimiter_LowerPowerLimit) {
|
||||||
|
MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter...");
|
||||||
|
inverter->sendPowerControlRequest(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the inverter if limit is below threshold.
|
||||||
|
// We'll also set the power limit to the lower value in this case
|
||||||
if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) {
|
if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) {
|
||||||
if (inverter->isProducing()) {
|
if (inverter->isProducing()) {
|
||||||
MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter...");
|
MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter...");
|
||||||
inverter->sendPowerControlRequest(false);
|
inverter->sendPowerControlRequest(false);
|
||||||
_lastCommandSent = millis();
|
|
||||||
}
|
}
|
||||||
newPowerLimit = config.PowerLimiter_LowerPowerLimit;
|
newPowerLimit = config.PowerLimiter_LowerPowerLimit;
|
||||||
} else if (!inverter->isProducing()) {
|
|
||||||
MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter...");
|
|
||||||
inverter->sendPowerControlRequest(true);
|
|
||||||
_lastCommandSent = millis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the actual limit. We'll only do this is if the limit is in the right range
|
||||||
|
// and differs from the last requested value
|
||||||
|
if( _lastRequestedPowerLimit != newPowerLimit &&
|
||||||
|
/* newPowerLimit > config.PowerLimiter_LowerPowerLimit && --> This will always be true given the check above, kept for code readability */
|
||||||
|
newPowerLimit <= config.PowerLimiter_UpperPowerLimit ) {
|
||||||
MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit);
|
MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit);
|
||||||
|
|
||||||
int32_t effPowerLimit = newPowerLimit;
|
int32_t effPowerLimit = newPowerLimit;
|
||||||
|
|||||||
@ -119,6 +119,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
|
|||||||
|
|
||||||
CONFIG_T& config = Configuration.get();
|
CONFIG_T& config = Configuration.get();
|
||||||
config.PowerLimiter_Enabled = root[F("enabled")].as<bool>();
|
config.PowerLimiter_Enabled = root[F("enabled")].as<bool>();
|
||||||
|
PowerLimiter.setDisable(false); // User input clears the PL internal disable flag
|
||||||
config.PowerLimiter_SolarPassThroughEnabled = root[F("solar_passtrough_enabled")].as<bool>();
|
config.PowerLimiter_SolarPassThroughEnabled = root[F("solar_passtrough_enabled")].as<bool>();
|
||||||
config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as<uint8_t>();
|
config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as<uint8_t>();
|
||||||
config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as<bool>();
|
config.PowerLimiter_IsInverterBehindPowerMeter = root[F("is_inverter_behind_powermeter")].as<bool>();
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
#include "MqttHandleInverterTotal.h"
|
#include "MqttHandleInverterTotal.h"
|
||||||
#include "MqttHandleVedirect.h"
|
#include "MqttHandleVedirect.h"
|
||||||
#include "MqttHandleHuawei.h"
|
#include "MqttHandleHuawei.h"
|
||||||
|
#include "MqttHandlePowerLimiter.h"
|
||||||
#include "MqttSettings.h"
|
#include "MqttSettings.h"
|
||||||
#include "NetworkSettings.h"
|
#include "NetworkSettings.h"
|
||||||
#include "NtpSettings.h"
|
#include "NtpSettings.h"
|
||||||
@ -106,6 +107,7 @@ void setup()
|
|||||||
MqttHandleHass.init();
|
MqttHandleHass.init();
|
||||||
MqttHandleVedirectHass.init();
|
MqttHandleVedirectHass.init();
|
||||||
MqttHandleHuawei.init();
|
MqttHandleHuawei.init();
|
||||||
|
MqttHandlePowerLimiter.init();
|
||||||
MessageOutput.println("done");
|
MessageOutput.println("done");
|
||||||
|
|
||||||
// Initialize WebApi
|
// Initialize WebApi
|
||||||
@ -216,6 +218,8 @@ void loop()
|
|||||||
yield();
|
yield();
|
||||||
MqttHandleHuawei.loop();
|
MqttHandleHuawei.loop();
|
||||||
yield();
|
yield();
|
||||||
|
MqttHandlePowerLimiter.loop();
|
||||||
|
yield();
|
||||||
WebApi.loop();
|
WebApi.loop();
|
||||||
yield();
|
yield();
|
||||||
Display.loop();
|
Display.loop();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user