Feature: DPL: add support for multiple inverters

This commit is contained in:
Bernhard Kirchen 2024-09-22 20:56:36 +02:00 committed by Bernhard Kirchen
parent 7e19f19655
commit 4524c0405d
22 changed files with 1992 additions and 1272 deletions

View File

@ -132,6 +132,43 @@ struct POWERMETER_HTTP_SML_CONFIG_T {
}; };
using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T; using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T;
struct POWERLIMITER_INVERTER_CONFIG_T {
uint64_t Serial;
bool IsGoverned;
bool IsBehindPowerMeter;
bool IsSolarPowered;
bool UseOverscalingToCompensateShading;
uint16_t LowerPowerLimit;
uint16_t UpperPowerLimit;
};
using PowerLimiterInverterConfig = struct POWERLIMITER_INVERTER_CONFIG_T;
struct POWERLIMITER_CONFIG_T {
bool Enabled;
bool VerboseLogging;
bool SolarPassThroughEnabled;
uint8_t SolarPassThroughLosses;
bool BatteryAlwaysUseAtNight;
int16_t TargetPowerConsumption;
uint16_t TargetPowerConsumptionHysteresis;
uint16_t BaseLoadLimit;
bool IgnoreSoc;
uint16_t BatterySocStartThreshold;
uint16_t BatterySocStopThreshold;
float VoltageStartThreshold;
float VoltageStopThreshold;
float VoltageLoadCorrectionFactor;
uint16_t FullSolarPassThroughSoc;
float FullSolarPassThroughStartVoltage;
float FullSolarPassThroughStopVoltage;
uint64_t InverterSerialForDcVoltage;
uint8_t InverterChannelIdForDcVoltage;
int8_t RestartHour;
uint16_t TotalUpperPowerLimit;
PowerLimiterInverterConfig Inverters[INV_MAX_COUNT];
};
using PowerLimiterConfig = struct POWERLIMITER_CONFIG_T;
enum BatteryVoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 }; enum BatteryVoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 };
enum BatteryAmperageUnit { Amps = 0, MilliAmps = 1 }; enum BatteryAmperageUnit { Amps = 0, MilliAmps = 1 };
@ -284,34 +321,7 @@ struct CONFIG_T {
PowerMeterHttpSmlConfig HttpSml; PowerMeterHttpSmlConfig HttpSml;
} PowerMeter; } PowerMeter;
struct { PowerLimiterConfig PowerLimiter;
bool Enabled;
bool VerboseLogging;
bool SolarPassThroughEnabled;
uint8_t SolarPassThroughLosses;
bool BatteryAlwaysUseAtNight;
uint32_t Interval;
bool IsInverterBehindPowerMeter;
bool IsInverterSolarPowered;
bool UseOverscalingToCompensateShading;
uint64_t InverterId;
uint8_t InverterChannelId;
int32_t TargetPowerConsumption;
int32_t TargetPowerConsumptionHysteresis;
int32_t LowerPowerLimit;
int32_t BaseLoadLimit;
int32_t UpperPowerLimit;
bool IgnoreSoc;
uint32_t BatterySocStartThreshold;
uint32_t BatterySocStopThreshold;
float VoltageStartThreshold;
float VoltageStopThreshold;
float VoltageLoadCorrectionFactor;
int8_t RestartHour;
uint32_t FullSolarPassThroughSoc;
float FullSolarPassThroughStartVoltage;
float FullSolarPassThroughStopVoltage;
} PowerLimiter;
BatteryConfig Battery; BatteryConfig Battery;
@ -365,6 +375,7 @@ public:
static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target); static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target);
static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target); static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target);
static void serializeBatteryConfig(BatteryConfig const& source, JsonObject& target); static void serializeBatteryConfig(BatteryConfig const& source, JsonObject& target);
static void serializePowerLimiterConfig(PowerLimiterConfig const& source, JsonObject& target);
static void deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target); static void deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target);
static void deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target); static void deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target);
@ -372,6 +383,7 @@ public:
static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target); static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target);
static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target); static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target);
static void deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target); static void deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target);
static void deserializePowerLimiterConfig(JsonObject const& source, PowerLimiterConfig& target);
private: private:
void loop(); void loop();

View File

@ -2,9 +2,11 @@
#pragma once #pragma once
#include "Configuration.h" #include "Configuration.h"
#include "PowerLimiterInverter.h"
#include <espMqttClient.h> #include <espMqttClient.h>
#include <Arduino.h> #include <Arduino.h>
#include <Hoymiles.h> #include <atomic>
#include <deque>
#include <memory> #include <memory>
#include <functional> #include <functional>
#include <optional> #include <optional>
@ -18,6 +20,8 @@
class PowerLimiterClass { class PowerLimiterClass {
public: public:
PowerLimiterClass() = default;
enum class Status : unsigned { enum class Status : unsigned {
Initializing, Initializing,
DisabledByConfig, DisabledByConfig,
@ -25,25 +29,19 @@ public:
WaitingForValidTimestamp, WaitingForValidTimestamp,
PowerMeterPending, PowerMeterPending,
InverterInvalid, InverterInvalid,
InverterChanged, InverterCmdPending,
InverterOffline, ConfigReload,
InverterCommandsDisabled,
InverterLimitPending,
InverterPowerCmdPending,
InverterDevInfoPending,
InverterStatsPending, InverterStatsPending,
CalculatedLimitBelowMinLimit, FullSolarPassthrough,
UnconditionalSolarPassthrough, UnconditionalSolarPassthrough,
NoVeDirect,
NoEnergy,
HuaweiPsu,
Stable, Stable,
}; };
void init(Scheduler& scheduler); void init(Scheduler& scheduler);
uint8_t getInverterUpdateTimeouts() const { return _inverterUpdateTimeouts; } void triggerReloadingConfig() { _reloadConfigFlag = true; }
uint8_t getInverterUpdateTimeouts() const;
uint8_t getPowerLimiterState(); uint8_t getPowerLimiterState();
int32_t getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; } int32_t getInverterOutput() { return _lastExpectedInverterOutput; }
bool getFullSolarPassThroughEnabled() const { return _fullSolarPassThroughEnabled; } bool getFullSolarPassThroughEnabled() const { return _fullSolarPassThroughEnabled; }
enum class Mode : unsigned { enum class Mode : unsigned {
@ -54,6 +52,8 @@ public:
void setMode(Mode m) { _mode = m; } void setMode(Mode m) { _mode = m; }
Mode getMode() const { return _mode; } Mode getMode() const { return _mode; }
bool usesBatteryPoweredInverter();
bool isGovernedInverterProducing();
void calcNextInverterRestart(); void calcNextInverterRestart();
private: private:
@ -61,47 +61,46 @@ private:
Task _loopTask; Task _loopTask;
int32_t _lastRequestedPowerLimit = 0; std::atomic<bool> _reloadConfigFlag = true;
bool _shutdownPending = false; uint16_t _lastExpectedInverterOutput = 0;
std::optional<uint32_t> _oInverterStatsMillis = std::nullopt;
std::optional<uint32_t> _oUpdateStartMillis = std::nullopt;
std::optional<int32_t> _oTargetPowerLimitWatts = std::nullopt;
std::optional<bool> _oTargetPowerState = std::nullopt;
Status _lastStatus = Status::Initializing; Status _lastStatus = Status::Initializing;
uint32_t _lastStatusPrinted = 0; uint32_t _lastStatusPrinted = 0;
uint32_t _lastCalculation = 0; uint32_t _lastCalculation = 0;
static constexpr uint32_t _calculationBackoffMsDefault = 128; static constexpr uint32_t _calculationBackoffMsDefault = 128;
uint32_t _calculationBackoffMs = _calculationBackoffMsDefault; uint32_t _calculationBackoffMs = _calculationBackoffMsDefault;
Mode _mode = Mode::Normal; Mode _mode = Mode::Normal;
std::shared_ptr<InverterAbstract> _inverter = nullptr;
std::deque<std::unique_ptr<PowerLimiterInverter>> _inverters;
bool _batteryDischargeEnabled = false; bool _batteryDischargeEnabled = false;
bool _nighttimeDischarging = false; bool _nighttimeDischarging = false;
uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis() uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis()
uint32_t _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart uint32_t _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart
bool _fullSolarPassThroughEnabled = false; bool _fullSolarPassThroughEnabled = false;
bool _verboseLogging = true; bool _verboseLogging = true;
uint8_t _inverterUpdateTimeouts = 0;
frozen::string const& getStatusText(Status status); frozen::string const& getStatusText(Status status);
void announceStatus(Status status); void announceStatus(Status status);
bool shutdown(Status status); bool shutdown(Status status);
bool shutdown() { return shutdown(_lastStatus); } void reloadConfig();
std::pair<float, char const*> getInverterDcVoltage();
float getBatteryVoltage(bool log = false); float getBatteryVoltage(bool log = false);
int32_t inverterPowerDcToAc(std::shared_ptr<InverterAbstract> inverter, int32_t dcPower); uint16_t solarDcToInverterAc(uint16_t dcPower);
void unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter); void fullSolarPassthrough(PowerLimiterClass::Status reason);
bool canUseDirectSolarPower(); int16_t calcHouseholdConsumption();
bool calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t solarPower, int32_t batteryPowerLimit, bool batteryPower); using inverter_filter_t = std::function<bool(PowerLimiterInverter const&)>;
bool updateInverter(); uint16_t updateInverterLimits(uint16_t powerRequested, inverter_filter_t filter, std::string const& filterExpression);
bool setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit); uint16_t calcBatteryAllowance(uint16_t powerRequested);
int32_t getSolarPower(); bool updateInverters();
int32_t getBatteryDischargeLimit(); uint16_t getSolarPassthroughPower();
std::optional<uint16_t> getBatteryDischargeLimit();
float getBatteryInvertersOutputAcWatts();
float getLoadCorrectedVoltage(); float getLoadCorrectedVoltage();
bool testThreshold(float socThreshold, float voltThreshold, bool testThreshold(float socThreshold, float voltThreshold,
std::function<bool(float, float)> compare); std::function<bool(float, float)> compare);
bool isStartThresholdReached(); bool isStartThresholdReached();
bool isStopThresholdReached(); bool isStopThresholdReached();
bool isBelowStopThreshold(); bool isBelowStopThreshold();
bool useFullSolarPassthrough(); bool isFullSolarPassthroughActive();
}; };
extern PowerLimiterClass PowerLimiter; extern PowerLimiterClass PowerLimiter;

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "PowerLimiterInverter.h"
class PowerLimiterBatteryInverter : public PowerLimiterInverter {
public:
PowerLimiterBatteryInverter(bool verboseLogging, PowerLimiterInverterConfig const& config);
uint16_t getMaxReductionWatts(bool allowStandby) const final;
uint16_t getMaxIncreaseWatts() const final;
uint16_t applyReduction(uint16_t reduction, bool allowStandby) final;
uint16_t applyIncrease(uint16_t increase) final;
uint16_t standby() final;
bool isSolarPowered() const final { return false; }
private:
void setAcOutput(uint16_t expectedOutputWatts) final;
};

View File

@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <Hoymiles.h>
#include <optional>
#include <memory>
class PowerLimiterInverter {
public:
static std::unique_ptr<PowerLimiterInverter> create(bool verboseLogging, PowerLimiterInverterConfig const& config);
// send command(s) to inverter to reach desired target state (limit and
// production). return true if an update is pending, i.e., if the target
// state is NOT yet reached, false otherwise.
bool update();
// returns the timestamp of the oldest stats received for this inverter
// *after* its last command completed. return std::nullopt if new stats
// are pending after the last command completed.
std::optional<uint32_t> getLatestStatsMillis() const;
// the amount of times an update command issued to the inverter timed out
uint8_t getUpdateTimeouts() const { return _updateTimeouts; }
// maximum amount of AC power the inverter is able to produce
// (not regarding the configured upper power limit)
uint16_t getInverterMaxPowerWatts() const;
// maximum amount of AC power the inverter is allowed to produce as per
// upper power limit (additionally restricted by inverter's absolute max)
uint16_t getConfiguredMaxPowerWatts() const;
uint16_t getCurrentOutputAcWatts() const;
// this differs from current output power if new limit was assigned
uint16_t getExpectedOutputAcWatts() const;
// the maximum reduction of power output the inverter
// can achieve with or withouth going into standby.
virtual uint16_t getMaxReductionWatts(bool allowStandby) const = 0;
// the maximum increase of power output the inverter can achieve
// (is expected to achieve), possibly coming out of standby.
virtual uint16_t getMaxIncreaseWatts() const = 0;
// change the target limit such that the requested change becomes effective
// on the expected AC power output. returns the change in the range
// [0..reduction] that will become effective (once update() returns false).
virtual uint16_t applyReduction(uint16_t reduction, bool allowStandby) = 0;
virtual uint16_t applyIncrease(uint16_t increase) = 0;
// stop producing AC power. returns the change in power output
// that will become effective (once update() returns false).
virtual uint16_t standby() = 0;
// wake the inverter from standby and set it to produce
// as much power as permissible by its upper power limit.
void setMaxOutput();
void restart();
float getDcVoltage(uint8_t input);
bool isSendingCommandsEnabled() const { return _spInverter->getEnableCommands(); }
bool isReachable() const { return _spInverter->isReachable(); }
bool isProducing() const { return _spInverter->isProducing(); }
uint64_t getSerial() const { return _config.Serial; }
char const* getSerialStr() const { return _serialStr; }
bool isBehindPowerMeter() const { return _config.IsBehindPowerMeter; }
virtual bool isSolarPowered() const = 0;
void debug() const;
protected:
PowerLimiterInverter(bool verboseLogging, PowerLimiterInverterConfig const& config);
// returns false if the inverter cannot participate
// in achieving the requested change in power output
bool isEligible() const;
uint16_t getCurrentLimitWatts() const;
void setTargetPowerLimitWatts(uint16_t power) { _oTargetPowerLimitWatts = power; }
void setTargetPowerState(bool enable) { _oTargetPowerState = enable; }
void setExpectedOutputAcWatts(uint16_t power) { _expectedOutputAcWatts = power; }
// copied to avoid races with web UI
PowerLimiterInverterConfig _config;
// Hoymiles lib inverter instance
std::shared_ptr<InverterAbstract> _spInverter = nullptr;
bool _verboseLogging;
char _logPrefix[32];
private:
virtual void setAcOutput(uint16_t expectedOutputWatts) = 0;
char _serialStr[16];
// track (target) state
uint8_t _updateTimeouts = 0;
std::optional<uint32_t> _oUpdateStartMillis = std::nullopt;
std::optional<uint16_t> _oTargetPowerLimitWatts = std::nullopt;
std::optional<bool> _oTargetPowerState = std::nullopt;
mutable std::optional<uint32_t> _oStatsMillis = std::nullopt;
// the expected AC output (possibly is different from the target limit)
uint16_t _expectedOutputAcWatts = 0;
};

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "PowerLimiterInverter.h"
class PowerLimiterSolarInverter : public PowerLimiterInverter {
public:
PowerLimiterSolarInverter(bool verboseLogging, PowerLimiterInverterConfig const& config);
uint16_t getMaxReductionWatts(bool allowStandby) const final;
uint16_t getMaxIncreaseWatts() const final;
uint16_t applyReduction(uint16_t reduction, bool allowStandby) final;
uint16_t applyIncrease(uint16_t increase) final;
uint16_t standby() final;
bool isSolarPowered() const final { return true; }
private:
uint16_t scaleLimit(uint16_t expectedOutputWatts);
void setAcOutput(uint16_t expectedOutputWatts) final;
};

View File

@ -130,18 +130,16 @@
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true #define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3 #define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
#define POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT false #define POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT false
#define POWERLIMITER_INTERVAL 10
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true #define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
#define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false #define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false
#define POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING false #define POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING false
#define POWERLIMITER_INVERTER_ID 0ULL
#define POWERLIMITER_INVERTER_CHANNEL_ID 0 #define POWERLIMITER_INVERTER_CHANNEL_ID 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0 #define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0 #define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
#define POWERLIMITER_LOWER_POWER_LIMIT 10 #define POWERLIMITER_LOWER_POWER_LIMIT 10
#define POWERLIMITER_BASE_LOAD_LIMIT 100 #define POWERLIMITER_BASE_LOAD_LIMIT 100
#define POWERLIMITER_UPPER_POWER_LIMIT 800 #define POWERLIMITER_UPPER_POWER_LIMIT 800
#define POWERLIMITER_IGNORE_SOC false #define POWERLIMITER_IGNORE_SOC true
#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80 #define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80
#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20 #define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20
#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0 #define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0

View File

@ -105,6 +105,66 @@ void ConfigurationClass::serializeBatteryConfig(BatteryConfig const& source, Jso
target["mqtt_amperage_unit"] = config.Battery.MqttAmperageUnit; target["mqtt_amperage_unit"] = config.Battery.MqttAmperageUnit;
} }
void ConfigurationClass::serializePowerLimiterConfig(PowerLimiterConfig const& source, JsonObject& target)
{
char serialBuffer[sizeof(uint64_t) * 8 + 1];
auto serialStr = [&serialBuffer](uint64_t const& serial) -> String {
snprintf(serialBuffer, sizeof(serialBuffer), "%0x%08x",
static_cast<uint32_t>((serial >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(serial & 0xFFFFFFFF));
return String(serialBuffer);
};
// we want a representation of our floating-point value in the JSON that
// uses the least amount of decimal digits possible to convey the value that
// is actually represented by the float. this is no easy task. ArduinoJson
// does this for us, however, it does it as expected only for variables of
// type double. this is probably because it assumes all floating-point
// values to have the precision of a double (64 bits), so it prints the
// respective number of siginificant decimals, which are too many if the
// actual value is a float (32 bits).
auto roundedFloat = [](float val) -> double {
return static_cast<int>(val * 100 + (val > 0 ? 0.5 : -0.5)) / 100.0;
};
target["enabled"] = source.Enabled;
target["verbose_logging"] = source.VerboseLogging;
target["solar_passthrough_enabled"] = source.SolarPassThroughEnabled;
target["solar_passthrough_losses"] = source.SolarPassThroughLosses;
target["battery_always_use_at_night"] = source.BatteryAlwaysUseAtNight;
target["target_power_consumption"] = source.TargetPowerConsumption;
target["target_power_consumption_hysteresis"] = source.TargetPowerConsumptionHysteresis;
target["base_load_limit"] = source.BaseLoadLimit;
target["ignore_soc"] = source.IgnoreSoc;
target["battery_soc_start_threshold"] = source.BatterySocStartThreshold;
target["battery_soc_stop_threshold"] = source.BatterySocStopThreshold;
target["voltage_start_threshold"] = roundedFloat(source.VoltageStartThreshold);
target["voltage_stop_threshold"] = roundedFloat(source.VoltageStopThreshold);
target["voltage_load_correction_factor"] = source.VoltageLoadCorrectionFactor;
target["full_solar_passthrough_soc"] = source.FullSolarPassThroughSoc;
target["full_solar_passthrough_start_voltage"] = roundedFloat(source.FullSolarPassThroughStartVoltage);
target["full_solar_passthrough_stop_voltage"] = roundedFloat(source.FullSolarPassThroughStopVoltage);
target["inverter_serial_for_dc_voltage"] = serialStr(source.InverterSerialForDcVoltage);
target["inverter_channel_id_for_dc_voltage"] = source.InverterChannelIdForDcVoltage;
target["inverter_restart_hour"] = source.RestartHour;
target["total_upper_power_limit"] = source.TotalUpperPowerLimit;
JsonArray inverters = target["inverters"].to<JsonArray>();
for (size_t i = 0; i < INV_MAX_COUNT; ++i) {
PowerLimiterInverterConfig const& s = source.Inverters[i];
if (s.Serial == 0ULL) { break; }
JsonObject t = inverters.add<JsonObject>();
t["serial"] = serialStr(s.Serial);
t["is_governed"] = s.IsGoverned;
t["is_behind_power_meter"] = s.IsBehindPowerMeter;
t["is_solar_powered"] = s.IsSolarPowered;
t["use_overscaling_to_compensate_shading"] = s.UseOverscalingToCompensateShading;
t["lower_power_limit"] = s.LowerPowerLimit;
t["upper_power_limit"] = s.UpperPowerLimit;
}
}
bool ConfigurationClass::write() bool ConfigurationClass::write()
{ {
File f = LittleFS.open(CONFIG_FILENAME, "w"); File f = LittleFS.open(CONFIG_FILENAME, "w");
@ -259,32 +319,7 @@ bool ConfigurationClass::write()
serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, powermeter_http_sml); serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, powermeter_http_sml);
JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>(); JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>();
powerlimiter["enabled"] = config.PowerLimiter.Enabled; serializePowerLimiterConfig(config.PowerLimiter, powerlimiter);
powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging;
powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
powerlimiter["solar_passthrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
powerlimiter["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight;
powerlimiter["interval"] = config.PowerLimiter.Interval;
powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
powerlimiter["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered;
powerlimiter["use_overscaling_to_compensate_shading"] = config.PowerLimiter.UseOverscalingToCompensateShading;
powerlimiter["inverter_id"] = config.PowerLimiter.InverterId;
powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
powerlimiter["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit;
powerlimiter["base_load_limit"] = config.PowerLimiter.BaseLoadLimit;
powerlimiter["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit;
powerlimiter["ignore_soc"] = config.PowerLimiter.IgnoreSoc;
powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold;
powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter.BatterySocStopThreshold;
powerlimiter["voltage_start_threshold"] = config.PowerLimiter.VoltageStartThreshold;
powerlimiter["voltage_stop_threshold"] = config.PowerLimiter.VoltageStopThreshold;
powerlimiter["voltage_load_correction_factor"] = config.PowerLimiter.VoltageLoadCorrectionFactor;
powerlimiter["inverter_restart_hour"] = config.PowerLimiter.RestartHour;
powerlimiter["full_solar_passthrough_soc"] = config.PowerLimiter.FullSolarPassThroughSoc;
powerlimiter["full_solar_passthrough_start_voltage"] = config.PowerLimiter.FullSolarPassThroughStartVoltage;
powerlimiter["full_solar_passthrough_stop_voltage"] = config.PowerLimiter.FullSolarPassThroughStopVoltage;
JsonObject battery = doc["battery"].to<JsonObject>(); JsonObject battery = doc["battery"].to<JsonObject>();
serializeBatteryConfig(config.Battery, battery); serializeBatteryConfig(config.Battery, battery);
@ -322,7 +357,7 @@ void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source,
JsonObject source_http_config = source["http_request"]; JsonObject source_http_config = source["http_request"];
// http request parameters of HTTP/JSON power meter were previously stored // http request parameters of HTTP/JSON power meter were previously stored
// alongside other settings. TODO(schlimmchen): remove in early 2025. // alongside other settings. TODO(schlimmchen): remove in mid 2025.
if (source_http_config.isNull()) { source_http_config = source; } if (source_http_config.isNull()) { source_http_config = source; }
strlcpy(target.Url, source_http_config["url"] | "", sizeof(target.Url)); strlcpy(target.Url, source_http_config["url"] | "", sizeof(target.Url));
@ -402,6 +437,49 @@ void ConfigurationClass::deserializeBatteryConfig(JsonObject const& source, Batt
target.MqttAmperageUnit = source["mqtt_amperage_unit"] | BatteryAmperageUnit::Amps; target.MqttAmperageUnit = source["mqtt_amperage_unit"] | BatteryAmperageUnit::Amps;
} }
void ConfigurationClass::deserializePowerLimiterConfig(JsonObject const& source, PowerLimiterConfig& target)
{
auto serialBin = [](String const& input) -> uint64_t {
return strtoll(input.c_str(), NULL, 16);
};
target.Enabled = source["enabled"] | POWERLIMITER_ENABLED;
target.VerboseLogging = source["verbose_logging"] | VERBOSE_LOGGING;
target.SolarPassThroughEnabled = source["solar_passthrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED;
target.SolarPassThroughLosses = source["solar_passthrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES;
target.BatteryAlwaysUseAtNight = source["battery_always_use_at_night"] | POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT;
target.TargetPowerConsumption = source["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION;
target.TargetPowerConsumptionHysteresis = source["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS;
target.BaseLoadLimit = source["base_load_limit"] | POWERLIMITER_BASE_LOAD_LIMIT;
target.IgnoreSoc = source["ignore_soc"] | POWERLIMITER_IGNORE_SOC;
target.BatterySocStartThreshold = source["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD;
target.BatterySocStopThreshold = source["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD;
target.VoltageStartThreshold = source["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD;
target.VoltageStopThreshold = source["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD;
target.VoltageLoadCorrectionFactor = source["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR;
target.FullSolarPassThroughSoc = source["full_solar_passthrough_soc"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC;
target.FullSolarPassThroughStartVoltage = source["full_solar_passthrough_start_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE;
target.FullSolarPassThroughStopVoltage = source["full_solar_passthrough_stop_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE;
target.InverterSerialForDcVoltage = serialBin(source["inverter_serial_for_dc_voltage"] | String("0"));
target.InverterChannelIdForDcVoltage = source["inverter_channel_id_for_dc_voltage"] | POWERLIMITER_INVERTER_CHANNEL_ID;
target.RestartHour = source["inverter_restart_hour"] | POWERLIMITER_RESTART_HOUR;
target.TotalUpperPowerLimit = source["total_upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
JsonArray inverters = source["inverters"].as<JsonArray>();
for (size_t i = 0; i < INV_MAX_COUNT; ++i) {
PowerLimiterInverterConfig& inv = target.Inverters[i];
JsonObject s = inverters[i];
inv.Serial = serialBin(s["serial"] | String("0")); // 0 marks inverter slot as unused
inv.IsGoverned = s["is_governed"] | false;
inv.IsBehindPowerMeter = s["is_behind_power_meter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
inv.IsSolarPowered = s["is_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED;
inv.UseOverscalingToCompensateShading = s["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING;
inv.LowerPowerLimit = s["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT;
inv.UpperPowerLimit = s["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
}
}
bool ConfigurationClass::read() bool ConfigurationClass::read()
{ {
File f = LittleFS.open(CONFIG_FILENAME, "r", false); File f = LittleFS.open(CONFIG_FILENAME, "r", false);
@ -591,7 +669,7 @@ bool ConfigurationClass::read()
deserializePowerMeterMqttConfig(powermeter["mqtt"], config.PowerMeter.Mqtt); deserializePowerMeterMqttConfig(powermeter["mqtt"], config.PowerMeter.Mqtt);
// process settings from legacy config if they are present // process settings from legacy config if they are present
// TODO(schlimmchen): remove in early 2025. // TODO(schlimmchen): remove in mid 2025.
if (!powermeter["mqtt_topic_powermeter_1"].isNull()) { if (!powermeter["mqtt_topic_powermeter_1"].isNull()) {
auto& values = config.PowerMeter.Mqtt.Values; auto& values = config.PowerMeter.Mqtt.Values;
strlcpy(values[0].Topic, powermeter["mqtt_topic_powermeter_1"], sizeof(values[0].Topic)); strlcpy(values[0].Topic, powermeter["mqtt_topic_powermeter_1"], sizeof(values[0].Topic));
@ -602,7 +680,7 @@ bool ConfigurationClass::read()
deserializePowerMeterSerialSdmConfig(powermeter["serial_sdm"], config.PowerMeter.SerialSdm); deserializePowerMeterSerialSdmConfig(powermeter["serial_sdm"], config.PowerMeter.SerialSdm);
// process settings from legacy config if they are present // process settings from legacy config if they are present
// TODO(schlimmchen): remove in early 2025. // TODO(schlimmchen): remove in mid 2025.
if (!powermeter["sdmaddress"].isNull()) { if (!powermeter["sdmaddress"].isNull()) {
config.PowerMeter.SerialSdm.Address = powermeter["sdmaddress"]; config.PowerMeter.SerialSdm.Address = powermeter["sdmaddress"];
} }
@ -614,7 +692,7 @@ bool ConfigurationClass::read()
deserializePowerMeterHttpSmlConfig(powermeter_sml, config.PowerMeter.HttpSml); deserializePowerMeterHttpSmlConfig(powermeter_sml, config.PowerMeter.HttpSml);
// process settings from legacy config if they are present // process settings from legacy config if they are present
// TODO(schlimmchen): remove in early 2025. // TODO(schlimmchen): remove in mid 2025.
if (!powermeter["http_phases"].isNull()) { if (!powermeter["http_phases"].isNull()) {
auto& target = config.PowerMeter.HttpJson; auto& target = config.PowerMeter.HttpJson;
@ -634,33 +712,48 @@ bool ConfigurationClass::read()
} }
JsonObject powerlimiter = doc["powerlimiter"]; JsonObject powerlimiter = doc["powerlimiter"];
config.PowerLimiter.Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED; deserializePowerLimiterConfig(powerlimiter, config.PowerLimiter);
config.PowerLimiter.VerboseLogging = powerlimiter["verbose_logging"] | VERBOSE_LOGGING;
config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED; if (powerlimiter["battery_drain_strategy"].as<uint8_t>() == 1) {
config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passthrough_losses"] | powerlimiter["solar_passtrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES; // solar_passthrough_losses was previously saved as solar_passtrough_losses. Be nice and also try mistyped key. config.PowerLimiter.BatteryAlwaysUseAtNight = true; // convert legacy setting
config.PowerLimiter.BatteryAlwaysUseAtNight = powerlimiter["battery_always_use_at_night"] | POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT; }
if (powerlimiter["battery_drain_strategy"].as<uint8_t>() == 1) { config.PowerLimiter.BatteryAlwaysUseAtNight = true; } // convert legacy setting
config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL; if (!powerlimiter["solar_passtrough_enabled"].isNull()) {
config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; // solar_passthrough_enabled was previously saved as
config.PowerLimiter.IsInverterSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED; // solar_passtrough_enabled. be nice and also try misspelled key.
config.PowerLimiter.UseOverscalingToCompensateShading = powerlimiter["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING; config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"].as<bool>();
config.PowerLimiter.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID; }
config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;
config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION; if (!powerlimiter["solar_passtrough_losses"].isNull()) {
config.PowerLimiter.TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS; // solar_passthrough_losses was previously saved as
config.PowerLimiter.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT; // solar_passtrough_losses. be nice and also try misspelled key.
config.PowerLimiter.BaseLoadLimit = powerlimiter["base_load_limit"] | POWERLIMITER_BASE_LOAD_LIMIT; config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passtrough_losses"].as<uint8_t>();
config.PowerLimiter.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT; }
config.PowerLimiter.IgnoreSoc = powerlimiter["ignore_soc"] | POWERLIMITER_IGNORE_SOC;
config.PowerLimiter.BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD; // process settings from legacy config if they are present
config.PowerLimiter.BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD; // TODO(schlimmchen): remove in mid 2025.
config.PowerLimiter.VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD; if (!powerlimiter["inverter_id"].isNull()) {
config.PowerLimiter.VoltageStopThreshold = powerlimiter["voltage_stop_threshold"] | POWERLIMITER_VOLTAGE_STOP_THRESHOLD; config.PowerLimiter.InverterChannelIdForDcVoltage = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;
config.PowerLimiter.VoltageLoadCorrectionFactor = powerlimiter["voltage_load_correction_factor"] | POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR;
config.PowerLimiter.RestartHour = powerlimiter["inverter_restart_hour"] | POWERLIMITER_RESTART_HOUR; auto& inv = config.PowerLimiter.Inverters[0];
config.PowerLimiter.FullSolarPassThroughSoc = powerlimiter["full_solar_passthrough_soc"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC; uint64_t previousInverterSerial = powerlimiter["inverter_id"].as<uint64_t>();
config.PowerLimiter.FullSolarPassThroughStartVoltage = powerlimiter["full_solar_passthrough_start_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE; if (previousInverterSerial < INV_MAX_COUNT) {
config.PowerLimiter.FullSolarPassThroughStopVoltage = powerlimiter["full_solar_passthrough_stop_voltage"] | POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE; // we previously had an index (not a serial) saved as inverter_id.
previousInverterSerial = config.Inverter[inv.Serial].Serial; // still 0 if no inverters configured
}
inv.Serial = previousInverterSerial;
config.PowerLimiter.InverterSerialForDcVoltage = previousInverterSerial;
inv.IsGoverned = true;
inv.IsBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
inv.IsSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED;
inv.UseOverscalingToCompensateShading = powerlimiter["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING;
inv.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT;
inv.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT;
config.PowerLimiter.TotalUpperPowerLimit = inv.UpperPowerLimit;
config.PowerLimiter.Inverters[1].Serial = 0;
}
deserializeBatteryConfig(doc["battery"], config.Battery); deserializeBatteryConfig(doc["battery"], config.Battery);

View File

@ -356,26 +356,13 @@ void HuaweiCanClass::loop()
_autoPowerEnabledCounter = 10; _autoPowerEnabledCounter = 10;
} }
if (PowerLimiter.isGovernedInverterProducing()) {
// Check if inverter used by the power limiter is active
std::shared_ptr<InverterAbstract> inverter =
Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId);
if (inverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) {
// we previously had an index saved as InverterId. fall back to the
// respective positional lookup if InverterId is not a known serial.
inverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
}
if (inverter != nullptr) {
if(inverter->isProducing()) {
_setValue(0.0, HUAWEI_ONLINE_CURRENT); _setValue(0.0, HUAWEI_ONLINE_CURRENT);
// Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus // Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus
_autoModeBlockedTillMillis = millis() + 1000; _autoModeBlockedTillMillis = millis() + 1000;
MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n"); MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n");
return; return;
} }
}
if (PowerMeter.getLastUpdate() > _lastPowerMeterUpdateReceivedMillis && if (PowerMeter.getLastUpdate() > _lastPowerMeterUpdateReceivedMillis &&
_autoPowerEnabledCounter > 0) { _autoPowerEnabledCounter > 0) {

View File

@ -88,14 +88,14 @@ void MqttHandlePowerLimiterClass::loop()
auto val = static_cast<unsigned>(PowerLimiter.getMode()); auto val = static_cast<unsigned>(PowerLimiter.getMode());
MqttSettings.publish("powerlimiter/status/mode", String(val)); MqttSettings.publish("powerlimiter/status/mode", String(val));
MqttSettings.publish("powerlimiter/status/upper_power_limit", String(config.PowerLimiter.UpperPowerLimit)); MqttSettings.publish("powerlimiter/status/upper_power_limit", String(config.PowerLimiter.TotalUpperPowerLimit));
MqttSettings.publish("powerlimiter/status/target_power_consumption", String(config.PowerLimiter.TargetPowerConsumption)); MqttSettings.publish("powerlimiter/status/target_power_consumption", String(config.PowerLimiter.TargetPowerConsumption));
MqttSettings.publish("powerlimiter/status/inverter_update_timeouts", String(PowerLimiter.getInverterUpdateTimeouts())); MqttSettings.publish("powerlimiter/status/inverter_update_timeouts", String(PowerLimiter.getInverterUpdateTimeouts()));
// no thresholds are relevant for setups without a battery // no thresholds are relevant for setups without a battery
if (config.PowerLimiter.IsInverterSolarPowered) { return; } if (!PowerLimiter.usesBatteryPoweredInverter()) { return; }
MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold)); MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold));
MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold)); MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold));
@ -195,9 +195,9 @@ void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, con
config.PowerLimiter.FullSolarPassThroughStopVoltage = payload_val; config.PowerLimiter.FullSolarPassThroughStopVoltage = payload_val;
break; break;
case MqttPowerLimiterCommand::UpperPowerLimit: case MqttPowerLimiterCommand::UpperPowerLimit:
if (config.PowerLimiter.UpperPowerLimit == intValue) { return; } if (config.PowerLimiter.TotalUpperPowerLimit == intValue) { return; }
MessageOutput.printf("Setting upper power limit to: %d W\r\n", intValue); MessageOutput.printf("Setting total upper power limit to: %d W\r\n", intValue);
config.PowerLimiter.UpperPowerLimit = intValue; config.PowerLimiter.TotalUpperPowerLimit = intValue;
break; break;
case MqttPowerLimiterCommand::TargetPowerConsumption: case MqttPowerLimiterCommand::TargetPowerConsumption:
if (config.PowerLimiter.TargetPowerConsumption == intValue) { return; } if (config.PowerLimiter.TargetPowerConsumption == intValue) { return; }

View File

@ -9,6 +9,7 @@
#include "NetworkSettings.h" #include "NetworkSettings.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "Utils.h" #include "Utils.h"
#include "PowerLimiter.h"
#include "__compiled_constants.h" #include "__compiled_constants.h"
MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass; MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass;
@ -64,7 +65,7 @@ void MqttHandlePowerLimiterHassClass::publishConfig()
publishSelect("DPL Mode", "mdi:gauge", "config", "mode", "mode"); publishSelect("DPL Mode", "mdi:gauge", "config", "mode", "mode");
if (config.PowerLimiter.IsInverterSolarPowered) { if (!PowerLimiter.usesBatteryPoweredInverter()) {
return; return;
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
#include "PowerLimiterBatteryInverter.h"
PowerLimiterBatteryInverter::PowerLimiterBatteryInverter(bool verboseLogging, PowerLimiterInverterConfig const& config)
: PowerLimiterInverter(verboseLogging, config) { }
uint16_t PowerLimiterBatteryInverter::getMaxReductionWatts(bool allowStandby) const
{
if (!isEligible()) { return 0; }
if (!isProducing()) { return 0; }
if (allowStandby) { return getCurrentOutputAcWatts(); }
if (getCurrentOutputAcWatts() <= _config.LowerPowerLimit) { return 0; }
return getCurrentOutputAcWatts() - _config.LowerPowerLimit;
}
uint16_t PowerLimiterBatteryInverter::getMaxIncreaseWatts() const
{
if (!isEligible()) { return 0; }
if (!isProducing()) {
return getConfiguredMaxPowerWatts();
}
// this should not happen for battery-powered inverters, but we want to
// be robust in case something else set a limit on the inverter (or in
// case we did something wrong...).
if (getCurrentLimitWatts() >= getConfiguredMaxPowerWatts()) { return 0; }
// we must not substract the current AC output here, but the current
// limit value, so we avoid trying to produce even more even if the
// inverter is already at the maximum limit value (the actual AC
// output may be less than the inverter's current power limit).
return getConfiguredMaxPowerWatts() - getCurrentLimitWatts();
}
uint16_t PowerLimiterBatteryInverter::applyReduction(uint16_t reduction, bool allowStandby)
{
if (!isEligible()) { return 0; }
if (reduction == 0) { return 0; }
auto low = std::min(getCurrentLimitWatts(), getCurrentOutputAcWatts());
if (low <= _config.LowerPowerLimit) {
if (allowStandby) {
standby();
return std::min(reduction, getCurrentOutputAcWatts());
}
return 0;
}
if ((getCurrentLimitWatts() - _config.LowerPowerLimit) >= reduction) {
setAcOutput(getCurrentLimitWatts() - reduction);
return reduction;
}
if (allowStandby) {
standby();
return std::min(reduction, getCurrentOutputAcWatts());
}
setAcOutput(_config.LowerPowerLimit);
return getCurrentOutputAcWatts() - _config.LowerPowerLimit;
}
uint16_t PowerLimiterBatteryInverter::applyIncrease(uint16_t increase)
{
if (!isEligible()) { return 0; }
if (increase == 0) { return 0; }
// do not wake inverter up if it would produce too much power
if (!isProducing() && _config.LowerPowerLimit > increase) { return 0; }
auto baseline = getCurrentLimitWatts();
// battery-powered inverters in standby can have an arbitrary limit, yet
// the baseline is 0 in case we are about to wake it up from standby.
if (!isProducing()) { baseline = 0; }
auto actualIncrease = std::min(increase, getMaxIncreaseWatts());
setAcOutput(baseline + actualIncrease);
return actualIncrease;
}
uint16_t PowerLimiterBatteryInverter::standby()
{
setTargetPowerState(false);
setExpectedOutputAcWatts(0);
return getCurrentOutputAcWatts();
}
void PowerLimiterBatteryInverter::setAcOutput(uint16_t expectedOutputWatts)
{
// make sure to enforce the lower and upper bounds
expectedOutputWatts = std::min(expectedOutputWatts, getConfiguredMaxPowerWatts());
expectedOutputWatts = std::max(expectedOutputWatts, _config.LowerPowerLimit);
setExpectedOutputAcWatts(expectedOutputWatts);
setTargetPowerLimitWatts(expectedOutputWatts);
setTargetPowerState(true);
}

View File

@ -0,0 +1,312 @@
#include "RestartHelper.h"
#include "MessageOutput.h"
#include "PowerLimiterInverter.h"
#include "PowerLimiterBatteryInverter.h"
#include "PowerLimiterSolarInverter.h"
#include "inverters/HMS_4CH.h"
std::unique_ptr<PowerLimiterInverter> PowerLimiterInverter::create(
bool verboseLogging, PowerLimiterInverterConfig const& config)
{
std::unique_ptr<PowerLimiterInverter> upInverter;
if (config.IsSolarPowered) {
upInverter = std::make_unique<PowerLimiterSolarInverter>(verboseLogging, config);
}
else {
upInverter = std::make_unique<PowerLimiterBatteryInverter>(verboseLogging, config);
}
if (nullptr == upInverter->_spInverter) { return nullptr; }
return std::move(upInverter);
}
PowerLimiterInverter::PowerLimiterInverter(bool verboseLogging, PowerLimiterInverterConfig const& config)
: _config(config)
, _verboseLogging(verboseLogging)
{
_spInverter = Hoymiles.getInverterBySerial(config.Serial);
if (!_spInverter) { return; }
snprintf(_serialStr, sizeof(_serialStr), "%0x%08x",
static_cast<uint32_t>((config.Serial >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(config.Serial & 0xFFFFFFFF));
snprintf(_logPrefix, sizeof(_logPrefix), "[DPL inverter %s]:", _serialStr);
}
bool PowerLimiterInverter::isEligible() const
{
if (!isReachable() || !isSendingCommandsEnabled()) { return false; }
// after startup, the limit effective at the inverter is not known. the
// respective message to request this info is only sent after a significant
// backoff (4 minutes). this is to avoid error messages to appear in the
// inverter's event log. we will wait until the current limit is known.
if (getCurrentLimitWatts() == 0) { return false; }
// the model-dependent maximum AC power output is only known after the
// first DevInfoSimpleCommand succeeded. we desperately need this info, so
// the inverter is not eligible until this value is known.
if (getInverterMaxPowerWatts() == 0) { return false; }
return true;
}
bool PowerLimiterInverter::update()
{
auto reset = [this]() -> bool {
_oTargetPowerState = std::nullopt;
_oTargetPowerLimitWatts = std::nullopt;
_oUpdateStartMillis = std::nullopt;
return false;
};
// do not reset _updateTimeouts below if no state change requested
if (!_oTargetPowerState.has_value() && !_oTargetPowerLimitWatts.has_value()) {
return reset();
}
if (!_oUpdateStartMillis.has_value()) {
_oUpdateStartMillis = millis();
}
if ((millis() - *_oUpdateStartMillis) > 30 * 1000) {
++_updateTimeouts;
MessageOutput.printf("%s timeout (%d in succession), "
"state transition pending: %s, limit pending: %s\r\n",
_logPrefix, _updateTimeouts,
(_oTargetPowerState.has_value()?"yes":"no"),
(_oTargetPowerLimitWatts.has_value()?"yes":"no"));
// NOTE that this is not always 5 minutes, since this counts timeouts,
// not absolute time. after any timeout, an update cycle ends. a new
// timeout can only happen after starting a new update cycle, which in
// turn is only started if the DPL did calculate a new limit, which in
// turn does not happen while the inverter is unreachable, no matter
// how long (a whole night) that might be.
if (_updateTimeouts >= 10) {
MessageOutput.printf("%s issuing restart command after update "
"timed out repeatedly\r\n", _logPrefix);
_spInverter->sendRestartControlRequest();
}
if (_updateTimeouts >= 20) {
MessageOutput.printf("%s restarting system since inverter is "
"unresponsive\r\n", _logPrefix);
RestartHelper.triggerRestart();
}
return reset();
}
auto constexpr halfOfAllMillis = std::numeric_limits<uint32_t>::max() / 2;
auto switchPowerState = [this](bool transitionOn) -> bool {
// no power state transition requested at all
if (!_oTargetPowerState.has_value()) { return false; }
// the transition that may be started is not the one which is requested
if (transitionOn != *_oTargetPowerState) { return false; }
// wait for pending power command(s) to complete
auto lastPowerCommandState = _spInverter->PowerCommand()->getLastPowerCommandSuccess();
if (CMD_PENDING == lastPowerCommandState) {
return true;
}
// we need to wait for statistics that are more recent than
// the last power update command to reliably use isProducing()
auto lastPowerCommandMillis = _spInverter->PowerCommand()->getLastUpdateCommand();
auto lastStatisticsMillis = _spInverter->Statistics()->getLastUpdate();
if ((lastStatisticsMillis - lastPowerCommandMillis) > halfOfAllMillis) { return true; }
if (isProducing() != *_oTargetPowerState) {
MessageOutput.printf("%s %s inverter...\r\n", _logPrefix,
((*_oTargetPowerState)?"Starting":"Stopping"));
_spInverter->sendPowerControlRequest(*_oTargetPowerState);
return true;
}
_oTargetPowerState = std::nullopt; // target power state reached
return false;
};
// we use a lambda function here to be able to use return statements,
// which allows to avoid if-else-indentions and improves code readability
auto updateLimit = [this]() -> bool {
// no limit update requested at all
if (!_oTargetPowerLimitWatts.has_value()) { return false; }
// wait for pending limit command(s) to complete
auto lastLimitCommandState = _spInverter->SystemConfigPara()->getLastLimitCommandSuccess();
if (CMD_PENDING == lastLimitCommandState) {
return true;
}
float newRelativeLimit = static_cast<float>(*_oTargetPowerLimitWatts * 100) / getInverterMaxPowerWatts();
// if no limit command is pending, the SystemConfigPara does report the
// current limit, as the answer by the inverter to a limit command is
// the canonical source that updates the known current limit.
auto currentRelativeLimit = _spInverter->SystemConfigPara()->getLimitPercent();
// we assume having exclusive control over the inverter. if the last
// limit command was successful and sent after we started the last
// update cycle, we should assume *our* requested limit was set.
uint32_t lastLimitCommandMillis = _spInverter->SystemConfigPara()->getLastUpdateCommand();
if ((lastLimitCommandMillis - *_oUpdateStartMillis) < halfOfAllMillis &&
CMD_OK == lastLimitCommandState) {
MessageOutput.printf("%s actual limit is %.1f %% (%.0f W "
"respectively), effective %d ms after update started, "
"requested were %.1f %%\r\n",
_logPrefix, currentRelativeLimit,
(currentRelativeLimit * getInverterMaxPowerWatts() / 100),
(lastLimitCommandMillis - *_oUpdateStartMillis),
newRelativeLimit);
if (std::abs(newRelativeLimit - currentRelativeLimit) > 2.0) {
MessageOutput.printf("%s NOTE: expected limit of %.1f %% "
"and actual limit of %.1f %% mismatch by more than 2 %%, "
"is the DPL in exclusive control over the inverter?\r\n",
_logPrefix, newRelativeLimit, currentRelativeLimit);
}
_oTargetPowerLimitWatts = std::nullopt;
return false;
}
MessageOutput.printf("%s sending limit of %.1f %% (%.0f W "
"respectively), max output is %d W\r\n", _logPrefix,
newRelativeLimit, (newRelativeLimit * getInverterMaxPowerWatts() / 100),
getInverterMaxPowerWatts());
_spInverter->sendActivePowerControlRequest(newRelativeLimit,
PowerLimitControlType::RelativNonPersistent);
return true;
};
// disable power production as soon as possible.
// setting the power limit is less important once the inverter is off.
if (switchPowerState(false)) { return true; }
if (updateLimit()) { return true; }
// enable power production only after setting the desired limit
if (switchPowerState(true)) { return true; }
_updateTimeouts = 0;
return reset();
}
std::optional<uint32_t> PowerLimiterInverter::getLatestStatsMillis() const
{
uint32_t now = millis();
// concerns both power limits and start/stop/restart commands and is
// only updated if a respective response was received from the inverter
auto lastUpdateCmdAge = std::min(
now - _spInverter->SystemConfigPara()->getLastUpdateCommand(),
now - _spInverter->PowerCommand()->getLastUpdateCommand()
);
// we use _oStatsMillis to persist a stats update timestamp, as we are
// looking for the single oldest inverter stats which is still younger than
// the last update command. we shall not just return the actual youngest
// stats timestamp if newer stats arrived while no update command was sent
// in the meantime.
if (_oStatsMillis && lastUpdateCmdAge < (now - *_oStatsMillis)) {
_oStatsMillis.reset();
}
if (!_oStatsMillis) {
auto lastStatsMillis = _spInverter->Statistics()->getLastUpdate();
auto lastStatsAge = now - lastStatsMillis;
if (lastStatsAge > lastUpdateCmdAge) {
return std::nullopt;
}
_oStatsMillis = lastStatsMillis;
}
return _oStatsMillis;
}
uint16_t PowerLimiterInverter::getInverterMaxPowerWatts() const
{
return _spInverter->DevInfo()->getMaxPower();
}
uint16_t PowerLimiterInverter::getConfiguredMaxPowerWatts() const
{
return std::min(getInverterMaxPowerWatts(), _config.UpperPowerLimit);
}
uint16_t PowerLimiterInverter::getCurrentOutputAcWatts() const
{
return _spInverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC);
}
uint16_t PowerLimiterInverter::getExpectedOutputAcWatts() const
{
if (!_oTargetPowerLimitWatts && !_oTargetPowerState) {
// the inverter's output will not change due to commands being sent
return getCurrentOutputAcWatts();
}
return _expectedOutputAcWatts;
}
void PowerLimiterInverter::setMaxOutput()
{
_oTargetPowerState = true;
setAcOutput(getConfiguredMaxPowerWatts());
}
void PowerLimiterInverter::restart()
{
_spInverter->sendRestartControlRequest();
}
float PowerLimiterInverter::getDcVoltage(uint8_t input)
{
return _spInverter->Statistics()->getChannelFieldValue(TYPE_DC,
static_cast<ChannelNum_t>(input), FLD_UDC);
}
uint16_t PowerLimiterInverter::getCurrentLimitWatts() const
{
auto currentLimitPercent = _spInverter->SystemConfigPara()->getLimitPercent();
return static_cast<uint16_t>(currentLimitPercent * getInverterMaxPowerWatts() / 100);
}
void PowerLimiterInverter::debug() const
{
if (!_verboseLogging) { return; }
MessageOutput.printf(
"%s\r\n"
" %s-powered, %s %d W\r\n"
" lower/current/upper limit: %d/%d/%d W, output capability: %d W\r\n"
" sending commands %s, %s, %s\r\n"
" max reduction production/standby: %d/%d W, max increase: %d W\r\n"
" target limit/output/state: %i W (%s)/%d W/%s, %d update timeouts\r\n",
_logPrefix,
(isSolarPowered()?"solar":"battery"),
(isProducing()?"producing":"standing by at"), getCurrentOutputAcWatts(),
_config.LowerPowerLimit, getCurrentLimitWatts(), _config.UpperPowerLimit,
getInverterMaxPowerWatts(),
(isSendingCommandsEnabled()?"enabled":"disabled"),
(isReachable()?"reachable":"offline"),
(isEligible()?"eligible":"disqualified"),
getMaxReductionWatts(false), getMaxReductionWatts(true), getMaxIncreaseWatts(),
(_oTargetPowerLimitWatts.has_value()?*_oTargetPowerLimitWatts:-1),
(_oTargetPowerLimitWatts.has_value()?"update":"unchanged"),
getExpectedOutputAcWatts(),
(_oTargetPowerState.has_value()?(*_oTargetPowerState?"production":"standby"):"unchanged"),
getUpdateTimeouts()
);
}

View File

@ -0,0 +1,189 @@
#include "MessageOutput.h"
#include "PowerLimiterSolarInverter.h"
#include "inverters/HMS_4CH.h"
PowerLimiterSolarInverter::PowerLimiterSolarInverter(bool verboseLogging, PowerLimiterInverterConfig const& config)
: PowerLimiterInverter(verboseLogging, config) { }
uint16_t PowerLimiterSolarInverter::getMaxReductionWatts(bool) const
{
if (!isEligible()) { return 0; }
auto low = std::min(getCurrentLimitWatts(), getCurrentOutputAcWatts());
if (low <= _config.LowerPowerLimit) { return 0; }
return getCurrentOutputAcWatts() - _config.LowerPowerLimit;
}
uint16_t PowerLimiterSolarInverter::getMaxIncreaseWatts() const
{
if (!isEligible()) { return 0; }
// TODO(schlimmchen): left for the author of the scaling method: @AndreasBoehm
return std::min(getConfiguredMaxPowerWatts() - getCurrentOutputAcWatts(), 100);
}
uint16_t PowerLimiterSolarInverter::applyReduction(uint16_t reduction, bool)
{
if (!isEligible()) { return 0; }
if (reduction == 0) { return 0; }
if ((getCurrentOutputAcWatts() - _config.LowerPowerLimit) >= reduction) {
setAcOutput(getCurrentOutputAcWatts() - reduction);
return reduction;
}
setAcOutput(_config.LowerPowerLimit);
return getCurrentOutputAcWatts() - _config.LowerPowerLimit;
}
uint16_t PowerLimiterSolarInverter::applyIncrease(uint16_t increase)
{
if (!isEligible()) { return 0; }
if (increase == 0) { return 0; }
// do not wake inverter up if it would produce too much power
if (!isProducing() && _config.LowerPowerLimit > increase) { return 0; }
// the limit for solar-powered inverters might be scaled, so we use the
// current output as the baseline. solar-powered inverters in standby have
// no output (baseline is zero).
auto baseline = getCurrentOutputAcWatts();
auto actualIncrease = std::min(increase, getMaxIncreaseWatts());
setAcOutput(baseline + actualIncrease);
return actualIncrease;
}
uint16_t PowerLimiterSolarInverter::standby()
{
// solar-powered inverters are never actually put into standby (by the
// DPL), but only set to the configured lower power limit instead.
setAcOutput(_config.LowerPowerLimit);
return getCurrentOutputAcWatts() - _config.LowerPowerLimit;
}
uint16_t PowerLimiterSolarInverter::scaleLimit(uint16_t expectedOutputWatts)
{
// prevent scaling if inverter is not producing, as input channels are not
// producing energy and hence are detected as not-producing, causing
// unreasonable scaling.
if (!isProducing()) { return expectedOutputWatts; }
auto pStats = _spInverter->Statistics();
std::list<ChannelNum_t> dcChnls = pStats->getChannelsByType(TYPE_DC);
size_t dcTotalChnls = dcChnls.size();
// according to the upstream projects README (table with supported devs),
// every 2 channel inverter has 2 MPPTs. then there are the HM*S* 4 channel
// models which have 4 MPPTs. all others have a different number of MPPTs
// than inputs. those are not supported by the current scaling mechanism.
bool supported = dcTotalChnls == 2;
supported |= dcTotalChnls == 4 && HMS_4CH::isValidSerial(getSerial());
if (!supported) { return expectedOutputWatts; }
// test for a reasonable power limit that allows us to assume that an input
// channel with little energy is actually not producing, rather than
// producing very little due to the very low limit.
if (getCurrentLimitWatts() < dcTotalChnls * 10) { return expectedOutputWatts; }
// overscalling allows us to compensate for shaded panels by increasing the
// total power limit, if the inverter is solar powered.
if (_config.UseOverscalingToCompensateShading) {
auto inverterOutputAC = pStats->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC);
float inverterEfficiencyFactor = pStats->getChannelFieldValue(TYPE_INV, CH0, FLD_EFF);
// fall back to hoymiles peak efficiency as per datasheet if inverter
// is currently not producing (efficiency is zero in that case)
inverterEfficiencyFactor = (inverterEfficiencyFactor > 0) ? inverterEfficiencyFactor/100 : 0.967;
// 98% of the expected power is good enough
auto expectedAcPowerPerChannel = (getCurrentLimitWatts() / dcTotalChnls) * 0.98;
if (_verboseLogging) {
MessageOutput.printf("%s expected AC power per channel %f W\r\n",
_logPrefix, expectedAcPowerPerChannel);
}
size_t dcShadedChnls = 0;
auto shadedChannelACPowerSum = 0.0;
for (auto& c : dcChnls) {
auto channelPowerAC = pStats->getChannelFieldValue(TYPE_DC, c, FLD_PDC) * inverterEfficiencyFactor;
if (channelPowerAC < expectedAcPowerPerChannel) {
dcShadedChnls++;
shadedChannelACPowerSum += channelPowerAC;
}
if (_verboseLogging) {
MessageOutput.printf("%s ch %d AC power %f W\r\n",
_logPrefix, c, channelPowerAC);
}
}
// no shading or the shaded channels provide more power than what
// we currently need.
if (dcShadedChnls == 0 || shadedChannelACPowerSum >= expectedOutputWatts) {
return expectedOutputWatts;
}
if (dcShadedChnls == dcTotalChnls) {
// keep the currentLimit when:
// - all channels are shaded
// - currentLimit >= expectedOutputWatts
// - we get the expected AC power or less and
if (getCurrentLimitWatts() >= expectedOutputWatts &&
inverterOutputAC <= expectedOutputWatts) {
if (_verboseLogging) {
MessageOutput.printf("%s all channels are shaded, "
"keeping the current limit of %d W\r\n",
_logPrefix, getCurrentLimitWatts());
}
return getCurrentLimitWatts();
} else {
return expectedOutputWatts;
}
}
size_t dcNonShadedChnls = dcTotalChnls - dcShadedChnls;
uint16_t overScaledLimit = (expectedOutputWatts - shadedChannelACPowerSum) / dcNonShadedChnls * dcTotalChnls;
if (overScaledLimit <= expectedOutputWatts) { return expectedOutputWatts; }
if (_verboseLogging) {
MessageOutput.printf("%s %d/%d channels are shaded, scaling %d W\r\n",
_logPrefix, dcShadedChnls, dcTotalChnls, overScaledLimit);
}
return overScaledLimit;
}
size_t dcProdChnls = 0;
for (auto& c : dcChnls) {
if (pStats->getChannelFieldValue(TYPE_DC, c, FLD_PDC) > 2.0) {
dcProdChnls++;
}
}
if (dcProdChnls == 0 || dcProdChnls == dcTotalChnls) { return expectedOutputWatts; }
uint16_t scaled = expectedOutputWatts / dcProdChnls * dcTotalChnls;
MessageOutput.printf("%s %d/%d channels are producing, scaling from %d to "
"%d W\r\n", _logPrefix, dcProdChnls, dcTotalChnls,
expectedOutputWatts, scaled);
return scaled;
}
void PowerLimiterSolarInverter::setAcOutput(uint16_t expectedOutputWatts)
{
setExpectedOutputAcWatts(expectedOutputWatts);
setTargetPowerLimitWatts(scaleLimit(expectedOutputWatts));
setTargetPowerState(true);
}

View File

@ -5,6 +5,7 @@
#include "WebApi_inverter.h" #include "WebApi_inverter.h"
#include "Configuration.h" #include "Configuration.h"
#include "MqttHandleHass.h" #include "MqttHandleHass.h"
#include "PowerLimiter.h"
#include "WebApi.h" #include "WebApi.h"
#include "WebApi_errors.h" #include "WebApi_errors.h"
#include "defaults.h" #include "defaults.h"
@ -150,6 +151,8 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request)
} }
MqttHandleHass.forceUpdate(); MqttHandleHass.forceUpdate();
PowerLimiter.triggerReloadingConfig();
} }
void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
@ -274,6 +277,8 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
} }
MqttHandleHass.forceUpdate(); MqttHandleHass.forceUpdate();
PowerLimiter.triggerReloadingConfig();
} }
void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
@ -316,6 +321,8 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
MqttHandleHass.forceUpdate(); MqttHandleHass.forceUpdate();
PowerLimiter.triggerReloadingConfig();
} }
void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)

View File

@ -12,6 +12,7 @@
#include "WebApi.h" #include "WebApi.h"
#include "helper.h" #include "helper.h"
#include "WebApi_errors.h" #include "WebApi_errors.h"
#include "Configuration.h"
void WebApiPowerLimiterClass::init(AsyncWebServer& server, Scheduler& scheduler) void WebApiPowerLimiterClass::init(AsyncWebServer& server, Scheduler& scheduler)
{ {
@ -32,35 +33,9 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
} }
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot(); auto root = response->getRoot().as<JsonObject>();
auto const& config = Configuration.get(); auto const& config = Configuration.get();
ConfigurationClass::serializePowerLimiterConfig(config.PowerLimiter, root);
root["enabled"] = config.PowerLimiter.Enabled;
root["verbose_logging"] = config.PowerLimiter.VerboseLogging;
root["solar_passthrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
root["solar_passthrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
root["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight;
root["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter;
root["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered;
root["use_overscaling_to_compensate_shading"] = config.PowerLimiter.UseOverscalingToCompensateShading;
root["inverter_serial"] = String(config.PowerLimiter.InverterId);
root["inverter_channel_id"] = config.PowerLimiter.InverterChannelId;
root["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption;
root["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis;
root["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit;
root["base_load_limit"] = config.PowerLimiter.BaseLoadLimit;
root["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit;
root["ignore_soc"] = config.PowerLimiter.IgnoreSoc;
root["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold;
root["battery_soc_stop_threshold"] = config.PowerLimiter.BatterySocStopThreshold;
root["voltage_start_threshold"] = static_cast<int>(config.PowerLimiter.VoltageStartThreshold * 100 +0.5) / 100.0;
root["voltage_stop_threshold"] = static_cast<int>(config.PowerLimiter.VoltageStopThreshold * 100 +0.5) / 100.0;;
root["voltage_load_correction_factor"] = config.PowerLimiter.VoltageLoadCorrectionFactor;
root["inverter_restart_hour"] = config.PowerLimiter.RestartHour;
root["full_solar_passthrough_soc"] = config.PowerLimiter.FullSolarPassThroughSoc;
root["full_solar_passthrough_start_voltage"] = static_cast<int>(config.PowerLimiter.FullSolarPassThroughStartVoltage * 100 + 0.5) / 100.0;
root["full_solar_passthrough_stop_voltage"] = static_cast<int>(config.PowerLimiter.FullSolarPassThroughStopVoltage * 100 + 0.5) / 100.0;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
} }
@ -70,11 +45,6 @@ void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request)
auto const& config = Configuration.get(); auto const& config = Configuration.get();
size_t invAmount = 0;
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial != 0) { ++invAmount; }
}
AsyncJsonResponse* response = new AsyncJsonResponse(); AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot(); auto& root = response->getRoot();
@ -82,31 +52,24 @@ void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request)
root["battery_enabled"] = config.Battery.Enabled; root["battery_enabled"] = config.Battery.Enabled;
root["charge_controller_enabled"] = config.Vedirect.Enabled; root["charge_controller_enabled"] = config.Vedirect.Enabled;
JsonObject inverters = root["inverters"].to<JsonObject>(); JsonArray inverters = root["inverters"].to<JsonArray>();
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial == 0) { continue; } auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial);
if (!inv) { continue; }
// we use the integer (base 10) representation of the inverter serial, JsonObject obj = inverters.add<JsonObject>();
// rather than the hex represenation as used when handling the inverter obj["serial"] = inv->serialString();
// serial elsewhere in the web application, because in this case, the
// serial is actually not displayed but only used as a value/index.
JsonObject obj = inverters[String(config.Inverter[i].Serial)].to<JsonObject>();
obj["pos"] = i; obj["pos"] = i;
obj["name"] = String(config.Inverter[i].Name); obj["name"] = String(config.Inverter[i].Name);
obj["poll_enable"] = config.Inverter[i].Poll_Enable; obj["poll_enable"] = config.Inverter[i].Poll_Enable;
obj["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night; obj["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night;
obj["command_enable"] = config.Inverter[i].Command_Enable; obj["command_enable"] = config.Inverter[i].Command_Enable;
obj["command_enable_night"] = config.Inverter[i].Command_Enable_Night; obj["command_enable_night"] = config.Inverter[i].Command_Enable_Night;
obj["max_power"] = inv->DevInfo()->getMaxPower(); // okay if zero/unknown
obj["type"] = "Unknown";
obj["channels"] = 1;
auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial);
if (inv != nullptr) {
obj["type"] = inv->typeName(); obj["type"] = inv->typeName();
auto channels = inv->Statistics()->getChannelsByType(TYPE_DC); auto channels = inv->Statistics()->getChannelsByType(TYPE_DC);
obj["channels"] = channels.size(); obj["channels"] = channels.size();
} }
}
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
} }
@ -134,62 +97,10 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
auto& retMsg = response->getRoot(); auto& retMsg = response->getRoot();
// we were not actually checking for all the keys we (unconditionally)
// access below for a long time, and it is technically not needed if users
// use the web application to submit settings. the web app will always
// submit all keys. users who send HTTP requests manually need to beware
// anyways to always include the keys accessed below. if we wanted to
// support a simpler API, like only sending the "enabled" key which only
// changes that key, we need to refactor all of the code below.
if (!root["enabled"].is<bool>()) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
return;
}
{ {
auto guard = Configuration.getWriteGuard(); auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig(); auto& config = guard.getConfig();
config.PowerLimiter.Enabled = root["enabled"].as<bool>(); ConfigurationClass::deserializePowerLimiterConfig(root.as<JsonObject>(), config.PowerLimiter);
PowerLimiter.setMode(PowerLimiterClass::Mode::Normal); // User input sets PL to normal operation
config.PowerLimiter.VerboseLogging = root["verbose_logging"].as<bool>();
if (config.Vedirect.Enabled) {
config.PowerLimiter.SolarPassThroughEnabled = root["solar_passthrough_enabled"].as<bool>();
config.PowerLimiter.SolarPassThroughLosses = root["solar_passthrough_losses"].as<uint8_t>();
config.PowerLimiter.FullSolarPassThroughStartVoltage = static_cast<int>(root["full_solar_passthrough_start_voltage"].as<float>() * 100) / 100.0;
config.PowerLimiter.FullSolarPassThroughStopVoltage = static_cast<int>(root["full_solar_passthrough_stop_voltage"].as<float>() * 100) / 100.0;
}
config.PowerLimiter.IsInverterBehindPowerMeter = root["is_inverter_behind_powermeter"].as<bool>();
config.PowerLimiter.IsInverterSolarPowered = root["is_inverter_solar_powered"].as<bool>();
config.PowerLimiter.BatteryAlwaysUseAtNight = root["battery_always_use_at_night"].as<bool>();
config.PowerLimiter.UseOverscalingToCompensateShading = root["use_overscaling_to_compensate_shading"].as<bool>();
config.PowerLimiter.InverterId = root["inverter_serial"].as<uint64_t>();
config.PowerLimiter.InverterChannelId = root["inverter_channel_id"].as<uint8_t>();
config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as<int32_t>();
config.PowerLimiter.TargetPowerConsumptionHysteresis = root["target_power_consumption_hysteresis"].as<int32_t>();
config.PowerLimiter.LowerPowerLimit = root["lower_power_limit"].as<int32_t>();
config.PowerLimiter.BaseLoadLimit = root["base_load_limit"].as<int32_t>();
config.PowerLimiter.UpperPowerLimit = root["upper_power_limit"].as<int32_t>();
if (config.Battery.Enabled) {
config.PowerLimiter.IgnoreSoc = root["ignore_soc"].as<bool>();
config.PowerLimiter.BatterySocStartThreshold = root["battery_soc_start_threshold"].as<uint32_t>();
config.PowerLimiter.BatterySocStopThreshold = root["battery_soc_stop_threshold"].as<uint32_t>();
if (config.Vedirect.Enabled) {
config.PowerLimiter.FullSolarPassThroughSoc = root["full_solar_passthrough_soc"].as<uint32_t>();
}
}
config.PowerLimiter.VoltageStartThreshold = root["voltage_start_threshold"].as<float>();
config.PowerLimiter.VoltageStartThreshold = static_cast<int>(config.PowerLimiter.VoltageStartThreshold * 100) / 100.0;
config.PowerLimiter.VoltageStopThreshold = root["voltage_stop_threshold"].as<float>();
config.PowerLimiter.VoltageStopThreshold = static_cast<int>(config.PowerLimiter.VoltageStopThreshold * 100) / 100.0;
config.PowerLimiter.VoltageLoadCorrectionFactor = root["voltage_load_correction_factor"].as<float>();
config.PowerLimiter.RestartHour = root["inverter_restart_hour"].as<int8_t>();
} }
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);
@ -197,6 +108,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
response->setLength(); response->setLength();
request->send(response); request->send(response);
PowerLimiter.triggerReloadingConfig();
PowerLimiter.calcNextInverterRestart(); PowerLimiter.calcNextInverterRestart();
// potentially make thresholds auto-discoverable // potentially make thresholds auto-discoverable

View File

@ -154,7 +154,7 @@ void WebApiWsVedirectLiveClass::generateCommonJsonResponse(JsonVariant& root, bo
root["dpl"]["PLSTATE"] = -1; root["dpl"]["PLSTATE"] = -1;
if (Configuration.get().PowerLimiter.Enabled) if (Configuration.get().PowerLimiter.Enabled)
root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState();
root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); root["dpl"]["PLLIMIT"] = PowerLimiter.getInverterOutput();
} }
void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) { void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) {

View File

@ -659,11 +659,12 @@
"ConfigHintsIntro": "Folgende Hinweise zur Konfiguration des Dynamic Power Limiter (DPL) sollen beachtet werden:", "ConfigHintsIntro": "Folgende Hinweise zur Konfiguration des Dynamic Power Limiter (DPL) sollen beachtet werden:",
"ConfigHintPowerMeterDisabled": "Der DPL stellt ohne Stromzählerschnittstelle lediglich die konfigurierte Grundlast als Limit am Wechselrichter ein (Ausnahme: (Full) Solar-Passthrough).", "ConfigHintPowerMeterDisabled": "Der DPL stellt ohne Stromzählerschnittstelle lediglich die konfigurierte Grundlast als Limit am Wechselrichter ein (Ausnahme: (Full) Solar-Passthrough).",
"ConfigHintNoInverter": "Vor dem Festlegen von Einstellungen des DPL muss mindestens ein Inverter konfiguriert sein.", "ConfigHintNoInverter": "Vor dem Festlegen von Einstellungen des DPL muss mindestens ein Inverter konfiguriert sein.",
"ConfigHintInverterCommunication": "Das Abrufen von Daten und Senden von Kommandos muss für den zu regelnden Wechselrichter aktiviert sein.", "ConfigHintInverterCommunication": "Das Abrufen von Daten und Senden von Kommandos muss für jeden zu regelnden Wechselrichter aktiviert sein.",
"ConfigHintNoChargeController": "Die Solar-Passthrough Funktion kann nur mit aktivierter VE.Direct Schnittstelle genutzt werden.", "ConfigHintNoChargeController": "Die Solar-Passthrough Funktion kann nur mit aktivierter VE.Direct Schnittstelle genutzt werden.",
"ConfigHintNoBatteryInterface": "SoC-basierte Schwellwerte können nur mit konfigurierter Batteriekommunikationsschnittstelle genutzt werden.", "ConfigHintNoBatteryInterface": "SoC-basierte Schwellwerte können nur mit konfigurierter Batteriekommunikationsschnittstelle genutzt werden.",
"General": "Allgemein", "General": "Allgemein",
"Enable": "Aktiviert", "Enable": "Aktiviert",
"GovernInverter": "Steuere Wechselrichter \"{name}\"",
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"SolarPassthrough": "Solar-Passthrough", "SolarPassthrough": "Solar-Passthrough",
"EnableSolarPassthrough": "Aktiviere Solar-Passthrough", "EnableSolarPassthrough": "Aktiviere Solar-Passthrough",
@ -671,34 +672,51 @@
"SolarPassthroughLossesInfo": "<b>Hinweis:</b> Bei der Übertragung von Energie vom Solarladeregler zum Inverter sind Leitungsverluste zu erwarten. Um eine schleichende Entladung der Batterie im (Full) Solar-Passthrough Modus zu unterbinden, können diese Verluste berücksichtigt werden. Das am Inverter einzustellende Power Limit wird nach Berücksichtigung von dessen Effizienz zusätzlich um diesen Faktor verringert.", "SolarPassthroughLossesInfo": "<b>Hinweis:</b> Bei der Übertragung von Energie vom Solarladeregler zum Inverter sind Leitungsverluste zu erwarten. Um eine schleichende Entladung der Batterie im (Full) Solar-Passthrough Modus zu unterbinden, können diese Verluste berücksichtigt werden. Das am Inverter einzustellende Power Limit wird nach Berücksichtigung von dessen Effizienz zusätzlich um diesen Faktor verringert.",
"BatteryDischargeAtNight": "Batterie nachts sogar teilweise geladen nutzen", "BatteryDischargeAtNight": "Batterie nachts sogar teilweise geladen nutzen",
"SolarpassthroughInfo": "Diese Funktion ermöglicht den unmittelbaren Verbauch der verfügbaren Solarleistung. Dazu wird die aktuell vom Laderegler gemeldete Solarleistung am Wechselrichter als Limit eingestellt, selbst wenn sich die Batterie in einem Ladezyklus befindet. Somit wird eine unnötige Speicherung vermieden, die verlustbehaftet wäre.", "SolarpassthroughInfo": "Diese Funktion ermöglicht den unmittelbaren Verbauch der verfügbaren Solarleistung. Dazu wird die aktuell vom Laderegler gemeldete Solarleistung am Wechselrichter als Limit eingestellt, selbst wenn sich die Batterie in einem Ladezyklus befindet. Somit wird eine unnötige Speicherung vermieden, die verlustbehaftet wäre.",
"InverterSettings": "Wechselrichter", "InverterSettings": "Wechselrichtereinstellungen",
"Inverter": "Zu regelnder Wechselrichter",
"SelectInverter": "Inverter auswählen...", "SelectInverter": "Inverter auswählen...",
"InverterForDcVoltage": "Wechselrichter für Spannungsmessungen",
"InverterChannelId": "Eingang für Spannungsmessungen", "InverterChannelId": "Eingang für Spannungsmessungen",
"TargetPowerConsumption": "Angestrebter Netzbezug", "TargetPowerConsumption": "Angestrebter Netzbezug",
"TargetPowerConsumptionHint": "Angestrebter erlaubter Stromverbrauch aus dem Netz. Wert darf negativ sein.", "TargetPowerConsumptionHint": "Angestrebter Stromverbrauch aus dem Netz. Wert darf negativ sein.",
"TargetPowerConsumptionHysteresis": "Hysterese", "TargetPowerConsumptionHysteresis": "Hysterese",
"TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den Inverter senden, wenn es vom zurückgemeldeten Limit um mindestens diesen Betrag abweicht.", "TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den jeweiligen Inverter senden, wenn es vom zurückgemeldeten Limit um mindestens diesen Betrag abweicht.",
"LowerPowerLimit": "Minimales Leistungslimit", "LowerPowerLimit": "Minimales Leistungslimit",
"LowerPowerLimitHint": "Dieser Wert muss so gewählt werden, dass ein stabiler Betrieb mit diesem Limit möglich ist. Falls der Wechselrichter nur mit einem kleineren Limit betrieben werden könnte, wird er stattdessen in Standby versetzt.", "LowerPowerLimitHint": "Dieser Wert muss so gewählt werden, dass ein stabiler Betrieb mit diesem Limit möglich ist. Falls der Wechselrichter nur mit einem kleineren Limit betrieben werden könnte, wird er stattdessen in Standby versetzt.",
"BaseLoadLimit": "Grundlast", "BaseLoadLimit": "Grundlast",
"BaseLoadLimitHint": "Relevant beim Betrieb ohne oder beim Ausfall des Stromzählers. Solange es die sonstigen Bedinungen zulassen (insb. Batterieladung) wird dieses Limit am Wechselrichter eingestellt.", "BaseLoadLimitHint": "Relevant beim Betrieb ohne oder beim Ausfall des Stromzählers. Solange es die sonstigen Bedinungen zulassen (insb. Batterieladung), wird diese Leistung auf die Wechselrichter verteilt.",
"TotalUpperPowerLimit": "Maximale Gesamtausgangsleistung",
"TotalUpperPowerLimitHint": "Die Wechselrichter werden so eingestellt, dass sie in Summe höchstens diese Leistung erbringen.",
"ManagedInverters": "Verwaltete Wechselrichter",
"AddInverter": "Wechselrichter Hinzufügen",
"NoManagedInverters": "Der Dynamic Power Limiter verwaltet zur Zeit keine Wechselrichter. Um Fortzufahren, wähle einen Wechselrichter aus der Auswahlliste oben und füge ihn hinzu.",
"InverterLabel": "Name (Typ)",
"PowerSource": "Energiequelle",
"PowerSourceBattery": "Batterie",
"PowerSourceSolarPanels": "Solarmodul(e)",
"EditInverter": "Wechselrichter Bearbeiten",
"EditInverterLabel": "Betrifft Wechselrichter",
"Apply": "Übernehmen",
"Cancel": "@:base.Cancel",
"Delete": "Entfernen",
"DeleteInverter": "Entfernen Bestätigen",
"DeleteInverterMsg": "Soll Wechselrichter {label} mit Seriennummer {serial} von der Liste vom Dynamic Power Limiter verwalteter Wechselrichter entfernt werden?",
"UpperPowerLimit": "Maximales Leistungslimit", "UpperPowerLimit": "Maximales Leistungslimit",
"UpperPowerLimitHint": "Der Wechselrichter wird stets so eingestellt, dass höchstens diese Ausgangsleistung erreicht wird. Dieser Wert muss so gewählt werden, dass die Strombelastbarkeit der AC-Anschlussleitungen eingehalten wird.", "UpperPowerLimitHint": "Der Wechselrichter wird stets so eingestellt, dass höchstens diese Ausgangsleistung erreicht wird. Dieser Wert muss so gewählt werden, dass die Strombelastbarkeit der AC-Anschlussleitungen eingehalten wird.",
"SocThresholds": "Batterie State of Charge (SoC) Schwellwerte", "SocThresholds": "Batterie State of Charge (SoC) Schwellwerte",
"IgnoreSoc": "Batterie SoC ignorieren", "IgnoreSoc": "Nur Spannungs-Schwellwerte nutzen",
"IgnoreSocHint": "Falls aktiviert werden nur die Spannungs-Schwellwerte berücksichtigt. Deaktiviere diesen Schalter, um Batterie State of Charge (SoC) Schwellwerte zu konfigurieren (nicht empfohlen, da der SoC-Wert häufig ungenau ist).",
"StartThreshold": "Batterienutzung Start-Schwellwert", "StartThreshold": "Batterienutzung Start-Schwellwert",
"StopThreshold": "Batterienutzung Stop-Schwellwert", "StopThreshold": "Batterienutzung Stop-Schwellwert",
"FullSolarPassthroughStartThreshold": "Full-Solar-Passthrough Start-Schwellwert", "FullSolarPassthroughStartThreshold": "Full-Solar-Passthrough Start-Schwellwert",
"FullSolarPassthroughStartThresholdHint": "Oberhalb dieses Schwellwertes wird die Inverterleistung der Victron-MPPT-Leistung gleichgesetzt (abzüglich Effizienzkorrekturfaktor). Kann verwendet werden um überschüssige Solarleistung an das Netz zu liefern wenn die Batterie voll ist.", "FullSolarPassthroughStartThresholdHint": "Oberhalb dieses Schwellwertes wird die Inverterleistung der Victron-MPPT-Leistung gleichgesetzt (abzüglich Effizienzkorrekturfaktor). Kann verwendet werden um überschüssige Solarleistung an das Netz zu liefern wenn die Batterie voll ist.",
"VoltageSolarPassthroughStopThreshold": "Full-Solar-Passthrough Stop-Schwellwert", "VoltageSolarPassthroughStopThreshold": "Full-Solar-Passthrough Stop-Schwellwert",
"VoltageLoadCorrectionFactor": "Lastkorrekturfaktor", "VoltageLoadCorrectionFactor": "Lastkorrekturfaktor",
"BatterySocInfo": "<b>Hinweis:</b> Die Akku SoC (State of Charge) Werte werden nur benutzt, wenn die Batterie-Kommunikationsschnittstelle innerhalb der letzten Minute gültige Werte geschickt hat. Andernfalls werden als Fallback-Option die Spannungseinstellungen verwendet.", "BatterySocInfo": "<b>Hinweis:</b> Die Batterie State of Charge (SoC) Schwellwerte werden bevorzugt herangezogen. Sie werden allerdings nur benutzt, wenn die Batterie-Kommunikationsschnittstelle innerhalb der letzten Minute gültige Werte verarbeitet hat. Andernfalls werden ersatzweise die Spannungs-Schwellwerte verwendet.",
"InverterIsBehindPowerMeter": "Stromzählermessung beinhaltet Wechselrichter", "InverterIsBehindPowerMeter": "Stromzählermessung beinhaltet Wechselrichter",
"InverterIsBehindPowerMeterHint": "Aktivieren falls sich der Stromzähler-Messwert um die Ausgangsleistung des Wechselrichters verringert, wenn dieser Strom produziert. Normalerweise ist das zutreffend.", "InverterIsBehindPowerMeterHint": "Aktivieren falls sich der Stromzähler-Messwert um die Ausgangsleistung des Wechselrichters verringert, wenn dieser Strom produziert. Normalerweise ist das zutreffend.",
"InverterIsSolarPowered": "Wechselrichter wird von Solarmodulen gespeist", "InverterIsSolarPowered": "Wechselrichter wird von Solarmodulen gespeist",
"UseOverscalingToCompensateShading": "Verschattung durch Überskalierung ausgleichen", "UseOverscalingToCompensateShading": "Verschattung durch Überskalierung ausgleichen",
"UseOverscalingToCompensateShadingHint": "Erlaubt das Überskalieren des Wechselrichter-Limits, um Verschattung eines oder mehrerer Eingänge auszugleichen", "UseOverscalingToCompensateShadingHint": "Erlaubt das Überskalieren des Wechselrichter-Limits, um Verschattung eines oder mehrerer Eingänge auszugleichen.",
"VoltageThresholds": "Batterie Spannungs-Schwellwerte ", "VoltageThresholds": "Batterie Spannungs-Schwellwerte ",
"VoltageLoadCorrectionInfo": "<b>Hinweis:</b> Wenn Leistung von der Batterie abgegeben wird, bricht ihre Spannung etwas ein. Der Spannungseinbruch skaliert mit dem Entladestrom. Damit nicht vorzeitig der Wechselrichter ausgeschaltet wird sobald der Stop-Schwellenwert unterschritten wurde, wird der hier angegebene Korrekturfaktor mit einberechnet um die Spannung zu errechnen die der Akku in Ruhe hätte. Korrigierte Spannung = DC Spannung + (Aktuelle Leistung (W) * Korrekturfaktor).", "VoltageLoadCorrectionInfo": "<b>Hinweis:</b> Wenn Leistung von der Batterie abgegeben wird, bricht ihre Spannung etwas ein. Der Spannungseinbruch skaliert mit dem Entladestrom. Damit nicht vorzeitig der Wechselrichter ausgeschaltet wird sobald der Stop-Schwellenwert unterschritten wurde, wird der hier angegebene Korrekturfaktor mit einberechnet um die Spannung zu errechnen die der Akku in Ruhe hätte. Korrigierte Spannung = DC Spannung + (Aktuelle Leistung (W) * Korrekturfaktor).",
"InverterRestartHour": "Uhrzeit für geplanten Neustart", "InverterRestartHour": "Uhrzeit für geplanten Neustart",

View File

@ -661,11 +661,12 @@
"ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", "ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:",
"ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).", "ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).",
"ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.", "ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.",
"ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.", "ConfigHintInverterCommunication": "Polling data from and sending commands to all managed inverters must be enabled.",
"ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.", "ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.",
"ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.", "ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.",
"General": "General", "General": "General",
"Enable": "Enable", "Enable": "Enable",
"GovernInverter": "Govern Inverter \"{name}\"",
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"SolarPassthrough": "Solar-Passthrough", "SolarPassthrough": "Solar-Passthrough",
"EnableSolarPassthrough": "Enable Solar-Passthrough", "EnableSolarPassthrough": "Enable Solar-Passthrough",
@ -673,34 +674,51 @@
"SolarPassthroughLossesInfo": "<b>Hint:</b> Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar-passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.", "SolarPassthroughLossesInfo": "<b>Hint:</b> Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar-passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.",
"BatteryDischargeAtNight": "Use battery at night even if only partially charged", "BatteryDischargeAtNight": "Use battery at night even if only partially charged",
"SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.", "SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.",
"InverterSettings": "Inverter", "InverterSettings": "Inverter Settings",
"Inverter": "Target Inverter",
"SelectInverter": "Select an inverter...", "SelectInverter": "Select an inverter...",
"InverterForDcVoltage": "Inverter used for voltage measurements",
"InverterChannelId": "Input used for voltage measurements", "InverterChannelId": "Input used for voltage measurements",
"TargetPowerConsumption": "Target Grid Consumption", "TargetPowerConsumption": "Target Grid Consumption",
"TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", "TargetPowerConsumptionHint": "Grid power consumption the dynamic power limiter tries to achieve. Value may be negative.",
"TargetPowerConsumptionHysteresis": "Hysteresis", "TargetPowerConsumptionHysteresis": "Hysteresis",
"TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last reported power limit exceeds this amount.", "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the respective inverter if the absolute difference to the last reported power limit exceeds this amount.",
"LowerPowerLimit": "Minimum Power Limit", "LowerPowerLimit": "Minimum Power Limit",
"LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.", "LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.",
"BaseLoadLimit": "Base Load", "BaseLoadLimit": "Base Load",
"BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.", "BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (battery charge in particular), the inverters are configured to output this amount of power in total.",
"TotalUpperPowerLimit": "Maximum Total Output",
"TotalUpperPowerLimitHint": "The inverters are configured to output this maximum amount of power in total.",
"ManagedInverters": "Managed Inverters",
"AddInverter": "Add Inverter",
"NoManagedInverters": "The Dynamic Power Limiter is currently not managing any inverters. To continue, select an inverter in the dropdown list above and add it.",
"InverterLabel": "Name (Type)",
"PowerSource": "Power Source",
"PowerSourceBattery": "Battery",
"PowerSourceSolarPanels": "Solar Panel(s)",
"EditInverter": "Edit Inverter",
"EditInverterLabel": "Concerns Inverter",
"Apply": "Apply",
"Cancel": "@:base.Cancel",
"Delete": "Delete",
"DeleteInverter": "Confirm Removal",
"DeleteInverterMsg": "Should inverter {label} with serial number {serial} be removed from the list of inverters managed by the Dynamic Power Limiter?",
"UpperPowerLimit": "Maximum Power Limit", "UpperPowerLimit": "Maximum Power Limit",
"UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.", "UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.",
"SocThresholds": "Battery State of Charge (SoC) Thresholds", "SocThresholds": "Battery State of Charge (SoC) Thresholds",
"IgnoreSoc": "Ignore Battery SoC", "IgnoreSoc": "Use Voltage Threshols Only",
"IgnoreSocHint": "When enabled, only voltage thresholds are considered. Disable this switch to configure and use battery State of Charge (SoC) thresholds (not recommended as the SoC value is often inaccurate).",
"StartThreshold": "Start Threshold for Battery Discharging", "StartThreshold": "Start Threshold for Battery Discharging",
"StopThreshold": "Stop Threshold for Battery Discharging", "StopThreshold": "Stop Threshold for Battery Discharging",
"FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold", "FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold",
"FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.", "FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.",
"VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold", "VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold",
"VoltageLoadCorrectionFactor": "Load correction factor", "VoltageLoadCorrectionFactor": "Load correction factor",
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.", "BatterySocInfo": "<b>Hint:</b> The use of battery State of Charge (SoC) thresholds is prioritized. However, SoC thresholds are only used if the battery communication interface has processed valid SoC values in the last minute. Otherwise, the voltage thresholds will be used as fallback.",
"InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output", "InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output",
"InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.", "InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.",
"InverterIsSolarPowered": "Inverter is powered by solar modules", "InverterIsSolarPowered": "Inverter is powered by solar modules",
"UseOverscalingToCompensateShading": "Compensate for shading", "UseOverscalingToCompensateShading": "Compensate for shading",
"UseOverscalingToCompensateShadingHint": "Allow to overscale the inverter limit to compensate for shading of one or multiple inputs", "UseOverscalingToCompensateShadingHint": "Allow to overscale the inverter limit to compensate for shading of one or multiple inputs.",
"VoltageThresholds": "Battery Voltage Thresholds", "VoltageThresholds": "Battery Voltage Thresholds",
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor).", "VoltageLoadCorrectionInfo": "<b>Hint:</b> When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor).",
"InverterRestartHour": "Automatic Restart Time", "InverterRestartHour": "Automatic Restart Time",

View File

@ -727,11 +727,12 @@
"ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", "ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:",
"ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).", "ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).",
"ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.", "ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.",
"ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.", "ConfigHintInverterCommunication": "Polling data from and sending commands to all managed inverters must be enabled.",
"ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.", "ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.",
"ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.", "ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.",
"General": "General", "General": "General",
"Enable": "Enable", "Enable": "Enable",
"GovernInverter": "Govern Inverter \"{name}\"",
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"SolarPassthrough": "Solar-Passthrough", "SolarPassthrough": "Solar-Passthrough",
"EnableSolarPassthrough": "Enable Solar-Passthrough", "EnableSolarPassthrough": "Enable Solar-Passthrough",
@ -740,8 +741,8 @@
"BatteryDischargeAtNight": "Use battery at night even if only partially charged", "BatteryDischargeAtNight": "Use battery at night even if only partially charged",
"SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.", "SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.",
"InverterSettings": "Inverter", "InverterSettings": "Inverter",
"Inverter": "Target Inverter",
"SelectInverter": "Select an inverter...", "SelectInverter": "Select an inverter...",
"InverterForDcVoltage": "Inverter used for voltage measurements",
"InverterChannelId": "Input used for voltage measurements", "InverterChannelId": "Input used for voltage measurements",
"TargetPowerConsumption": "Target Grid Consumption", "TargetPowerConsumption": "Target Grid Consumption",
"TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", "TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.",
@ -751,22 +752,44 @@
"LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.", "LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.",
"BaseLoadLimit": "Base Load", "BaseLoadLimit": "Base Load",
"BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.", "BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.",
"TotalUpperPowerLimit": "Maximum Total Output",
"TotalUpperPowerLimitHint": "The inverters are configured to output this maximum amount of power in total.",
"ManagedInverters": "Managed Inverters",
"AddInverter": "Add Inverter",
"NoManagedInverters": "The Dynamic Power Limiter is currently not managing any inverters. To continue, select an inverter in the dropdown list above and add it.",
"InverterLabel": "Name (Type)",
"PowerSource": "Power Source",
"PowerSourceBattery": "Battery",
"PowerSourceSolarPanels": "Solar Panel(s)",
"EditInverter": "Edit Inverter",
"EditInverterLabel": "Concerns Inverter",
"Apply": "Apply",
"Cancel": "@:base.Cancel",
"Delete": "Delete",
"DeleteInverter": "Confirm Removal",
"DeleteInverterMsg": "Should inverter {label} with serial number {serial} be removed from the list of inverters managed by the Dynamic Power Limiter?",
"UpperPowerLimit": "Maximum Power Limit", "UpperPowerLimit": "Maximum Power Limit",
"UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.", "UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.",
"SocThresholds": "Battery State of Charge (SoC) Thresholds", "SocThresholds": "Battery State of Charge (SoC) Thresholds",
"IgnoreSoc": "Ignore Battery SoC", "IgnoreSoc": "Use Voltage Threshols Only",
"IgnoreSocHint": "When enabled, only voltage thresholds are considered. Disable this switch to configure and use battery State of Charge (SoC) thresholds (not recommended as the SoC value is often inaccurate).",
"StartThreshold": "Start Threshold for Battery Discharging", "StartThreshold": "Start Threshold for Battery Discharging",
"StopThreshold": "Stop Threshold for Battery Discharging", "StopThreshold": "Stop Threshold for Battery Discharging",
"FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold", "FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold",
"FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.", "FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.",
"VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold", "VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold",
"VoltageLoadCorrectionFactor": "Load correction factor", "VoltageLoadCorrectionFactor": "Load correction factor",
"BatterySocInfo": "<b>Hint:</b> The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.", "BatterySocInfo": "<b>Hint:</b> The use of battery State of Charge (SoC) thresholds is prioritized. However, SoC thresholds are only used if the battery communication interface has processed valid SoC values in the last minute. Otherwise, the voltage thresholds will be used as fallback.",
"InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output", "InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output",
"InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.", "InverterIsBehindPowerMeterHint": "Enable this option if the power meter reading is reduced by the inverter's output when it produces power. This is typically true.",
"InverterIsSolarPowered": "Inverter is powered by solar modules", "InverterIsSolarPowered": "Inverter is powered by solar modules",
"UseOverscalingToCompensateShading": "Compensate for shading",
"UseOverscalingToCompensateShadingHint": "Allow to overscale the inverter limit to compensate for shading of one or multiple inputs.",
"VoltageThresholds": "Battery Voltage Thresholds", "VoltageThresholds": "Battery Voltage Thresholds",
"VoltageLoadCorrectionInfo": "<b>Hint:</b> When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor)." "VoltageLoadCorrectionInfo": "<b>Hint:</b> When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor).",
"InverterRestartHour": "Automatic Restart Time",
"InverterRestartDisabled": "Do not execute automatic restart",
"InverterRestartHint": "The daily yield of the inverter is usually reset at night when the inverter turns off due to lack of light. To reset the daily yield even though the inverter is continuously powered by the battery, the inverter can be automatically restarted daily at the desired time."
}, },
"login": { "login": {
"Login": "Connexion", "Login": "Connexion",

View File

@ -1,10 +1,12 @@
export interface PowerLimiterInverterInfo { export interface PowerLimiterInverterInfo {
serial: string;
pos: number; pos: number;
name: string; name: string;
poll_enable: boolean; poll_enable: boolean;
poll_enable_night: boolean; poll_enable_night: boolean;
command_enable: boolean; command_enable: boolean;
command_enable_night: boolean; command_enable_night: boolean;
max_power: number;
type: string; type: string;
channels: number; channels: number;
} }
@ -15,7 +17,17 @@ export interface PowerLimiterMetaData {
power_meter_enabled: boolean; power_meter_enabled: boolean;
battery_enabled: boolean; battery_enabled: boolean;
charge_controller_enabled: boolean; charge_controller_enabled: boolean;
inverters: { [key: string]: PowerLimiterInverterInfo }; inverters: PowerLimiterInverterInfo[];
}
export interface PowerLimiterInverterConfig {
serial: string;
is_governed: boolean;
is_behind_power_meter: boolean;
is_solar_powered: boolean;
use_overscaling_to_compensate_shading: boolean;
lower_power_limit: number;
upper_power_limit: number;
} }
export interface PowerLimiterConfig { export interface PowerLimiterConfig {
@ -24,16 +36,9 @@ export interface PowerLimiterConfig {
solar_passthrough_enabled: boolean; solar_passthrough_enabled: boolean;
solar_passthrough_losses: number; solar_passthrough_losses: number;
battery_always_use_at_night: boolean; battery_always_use_at_night: boolean;
is_inverter_behind_powermeter: boolean;
is_inverter_solar_powered: boolean;
use_overscaling_to_compensate_shading: boolean;
inverter_serial: string;
inverter_channel_id: number;
target_power_consumption: number; target_power_consumption: number;
target_power_consumption_hysteresis: number; target_power_consumption_hysteresis: number;
lower_power_limit: number;
base_load_limit: number; base_load_limit: number;
upper_power_limit: number;
ignore_soc: boolean; ignore_soc: boolean;
battery_soc_start_threshold: number; battery_soc_start_threshold: number;
battery_soc_stop_threshold: number; battery_soc_stop_threshold: number;
@ -44,4 +49,9 @@ export interface PowerLimiterConfig {
full_solar_passthrough_soc: number; full_solar_passthrough_soc: number;
full_solar_passthrough_start_voltage: number; full_solar_passthrough_start_voltage: number;
full_solar_passthrough_stop_voltage: number; full_solar_passthrough_stop_voltage: number;
inverter_serial_for_dc_voltage: string;
inverter_channel_id_for_dc_voltage: number;
restart_hour: number;
total_upper_power_limit: number;
inverters: PowerLimiterInverterConfig[];
} }

View File

@ -30,7 +30,11 @@
</CardElement> </CardElement>
<form @submit="savePowerLimiterConfig" v-if="!configAlert"> <form @submit="savePowerLimiterConfig" v-if="!configAlert">
<CardElement :text="$t('powerlimiteradmin.General')" textVariant="text-bg-primary" add-space> <CardElement
:text="$t('powerlimiteradmin.General')"
textVariant="text-bg-primary"
:add-space="getConfigHints().length > 0"
>
<InputElement <InputElement
:label="$t('powerlimiteradmin.Enable')" :label="$t('powerlimiteradmin.Enable')"
v-model="powerLimiterConfigList.enabled" v-model="powerLimiterConfigList.enabled"
@ -38,8 +42,19 @@
wide wide
/> />
<template v-if="powerLimiterConfigList.enabled">
<InputElement
v-for="(inv, idx) in powerLimiterConfigList.inverters"
:key="idx"
:label="$t('powerlimiteradmin.GovernInverter', { name: inverterName(inv.serial) })"
v-model="powerLimiterConfigList.inverters[idx].is_governed"
type="checkbox"
wide
/>
</template>
<template v-if="isEnabled">
<InputElement <InputElement
v-show="isEnabled()"
:label="$t('powerlimiteradmin.VerboseLogging')" :label="$t('powerlimiteradmin.VerboseLogging')"
v-model="powerLimiterConfigList.verbose_logging" v-model="powerLimiterConfigList.verbose_logging"
type="checkbox" type="checkbox"
@ -47,7 +62,7 @@
/> />
<InputElement <InputElement
v-show="isEnabled() && hasPowerMeter()" v-if="hasPowerMeter"
:label="$t('powerlimiteradmin.TargetPowerConsumption')" :label="$t('powerlimiteradmin.TargetPowerConsumption')"
:tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHint')" :tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHint')"
v-model="powerLimiterConfigList.target_power_consumption" v-model="powerLimiterConfigList.target_power_consumption"
@ -56,112 +71,76 @@
wide wide
/> />
<InputElement
v-show="isEnabled()"
:label="$t('powerlimiteradmin.TargetPowerConsumptionHysteresis')"
:tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHysteresisHint')"
v-model="powerLimiterConfigList.target_power_consumption_hysteresis"
postfix="W"
type="number"
wide
/>
</CardElement>
<CardElement
:text="$t('powerlimiteradmin.InverterSettings')"
textVariant="text-bg-primary"
add-space
v-if="isEnabled()"
>
<div class="row mb-3">
<label for="inverter_serial" class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.Inverter') }}
</label>
<div class="col-sm-8">
<select
id="inverter_serial"
class="form-select"
v-model="powerLimiterConfigList.inverter_serial"
required
>
<option value="" disabled hidden selected>
{{ $t('powerlimiteradmin.SelectInverter') }}
</option>
<option
v-for="(inv, serial) in powerLimiterMetaData.inverters"
:key="serial"
:value="serial"
>
{{ inv.name }} ({{ inv.type }})
</option>
</select>
</div>
</div>
<InputElement
:label="$t('powerlimiteradmin.InverterIsSolarPowered')"
v-model="powerLimiterConfigList.is_inverter_solar_powered"
type="checkbox"
wide
/>
<InputElement
v-show="canUseBatteryDischargeAtNight()"
:label="$t('powerlimiteradmin.BatteryDischargeAtNight')"
v-model="powerLimiterConfigList.battery_always_use_at_night"
type="checkbox"
wide
/>
<InputElement
v-show="canUseOverscaling()"
:label="$t('powerlimiteradmin.UseOverscalingToCompensateShading')"
:tooltip="$t('powerlimiteradmin.UseOverscalingToCompensateShadingHint')"
v-model="powerLimiterConfigList.use_overscaling_to_compensate_shading"
type="checkbox"
wide
/>
<div class="row mb-3" v-if="needsChannelSelection()">
<label for="inverter_channel" class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.InverterChannelId') }}
</label>
<div class="col-sm-8">
<select
id="inverter_channel"
class="form-select"
v-model="powerLimiterConfigList.inverter_channel_id"
>
<option
v-for="channel in range(
powerLimiterMetaData.inverters[powerLimiterConfigList.inverter_serial].channels
)"
:key="channel"
:value="channel"
>
{{ channel + 1 }}
</option>
</select>
</div>
</div>
<InputElement
:label="$t('powerlimiteradmin.LowerPowerLimit')"
:tooltip="$t('powerlimiteradmin.LowerPowerLimitHint')"
v-model="powerLimiterConfigList.lower_power_limit"
placeholder="50"
min="10"
postfix="W"
type="number"
wide
/>
<InputElement <InputElement
:label="$t('powerlimiteradmin.BaseLoadLimit')" :label="$t('powerlimiteradmin.BaseLoadLimit')"
:tooltip="$t('powerlimiteradmin.BaseLoadLimitHint')" :tooltip="$t('powerlimiteradmin.BaseLoadLimitHint')"
v-model="powerLimiterConfigList.base_load_limit" v-model="powerLimiterConfigList.base_load_limit"
placeholder="200" placeholder="200"
:min="(powerLimiterConfigList.lower_power_limit + 1).toString()" postfix="W"
type="number"
min="0"
wide
/>
<InputElement
:label="$t('powerlimiteradmin.TargetPowerConsumptionHysteresis')"
:tooltip="$t('powerlimiteradmin.TargetPowerConsumptionHysteresisHint')"
v-model="powerLimiterConfigList.target_power_consumption_hysteresis"
postfix="W"
type="number"
min="1"
wide
/>
<InputElement
:label="$t('powerlimiteradmin.TotalUpperPowerLimit')"
:tooltip="$t('powerlimiteradmin.TotalUpperPowerLimitHint')"
v-model="powerLimiterConfigList.total_upper_power_limit"
postfix="W"
type="number"
min="1"
wide
/>
</template>
</CardElement>
<template v-if="isEnabled">
<template v-for="(inv, idx) in powerLimiterConfigList.inverters" :key="idx">
<CardElement
v-if="inv.is_governed"
:text="inverterLabel(inv.serial)"
textVariant="text-bg-primary"
add-space
>
<InputElement
v-if="hasPowerMeter"
:label="$t('powerlimiteradmin.InverterIsBehindPowerMeter')"
v-model="powerLimiterConfigList.inverters[idx].is_behind_power_meter"
:tooltip="$t('powerlimiteradmin.InverterIsBehindPowerMeterHint')"
type="checkbox"
wide
/>
<InputElement
:label="$t('powerlimiteradmin.InverterIsSolarPowered')"
v-model="powerLimiterConfigList.inverters[idx].is_solar_powered"
type="checkbox"
wide
/>
<InputElement
v-if="powerLimiterConfigList.inverters[idx].is_solar_powered"
:label="$t('powerlimiteradmin.UseOverscalingToCompensateShading')"
:tooltip="$t('powerlimiteradmin.UseOverscalingToCompensateShadingHint')"
v-model="powerLimiterConfigList.inverters[idx].use_overscaling_to_compensate_shading"
type="checkbox"
wide
/>
<InputElement
:label="$t('powerlimiteradmin.LowerPowerLimit')"
:tooltip="$t('powerlimiteradmin.LowerPowerLimitHint')"
v-model="powerLimiterConfigList.inverters[idx].lower_power_limit"
postfix="W" postfix="W"
type="number" type="number"
wide wide
@ -169,25 +148,77 @@
<InputElement <InputElement
:label="$t('powerlimiteradmin.UpperPowerLimit')" :label="$t('powerlimiteradmin.UpperPowerLimit')"
v-model="powerLimiterConfigList.upper_power_limit" v-model="powerLimiterConfigList.inverters[idx].upper_power_limit"
:tooltip="$t('powerlimiteradmin.UpperPowerLimitHint')" :tooltip="$t('powerlimiteradmin.UpperPowerLimitHint')"
placeholder="800"
:min="(powerLimiterConfigList.base_load_limit + 1).toString()"
postfix="W" postfix="W"
type="number" type="number"
wide wide
/> />
</CardElement>
</template>
<CardElement
:text="$t('powerlimiteradmin.InverterSettings')"
textVariant="text-bg-primary"
add-space
v-if="governingBatteryPoweredInverters"
>
<InputElement <InputElement
v-show="hasPowerMeter()" :label="$t('powerlimiteradmin.BatteryDischargeAtNight')"
:label="$t('powerlimiteradmin.InverterIsBehindPowerMeter')" v-model="powerLimiterConfigList.battery_always_use_at_night"
v-model="powerLimiterConfigList.is_inverter_behind_powermeter"
:tooltip="$t('powerlimiteradmin.InverterIsBehindPowerMeterHint')"
type="checkbox" type="checkbox"
wide wide
/> />
<div class="row mb-3" v-if="!powerLimiterConfigList.is_inverter_solar_powered"> <div class="row mb-3">
<label for="inverter_serial_for_dc_voltage" class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.InverterForDcVoltage') }}
</label>
<div class="col-sm-8">
<select
id="inverter_serial_for_dc_voltage"
class="form-select"
v-model="powerLimiterConfigList.inverter_serial_for_dc_voltage"
required
>
<option value="" disabled hidden selected>
{{ $t('powerlimiteradmin.SelectInverter') }}
</option>
<option
v-for="inv in governedBatteryPoweredInverters"
:key="inv.serial"
:value="inv.serial"
>
{{ inverterLabel(inv.serial) }}
</option>
</select>
</div>
</div>
<div class="row mb-3" v-if="needsChannelSelection()">
<label for="inverter_channel" class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.InverterChannelId') }}
</label>
<div class="col-sm-8">
<select
id="inverter_channel"
class="form-select"
v-model="powerLimiterConfigList.inverter_channel_id_for_dc_voltage"
>
<option
v-for="channel in range(
getInverterInfo(powerLimiterConfigList.inverter_serial_for_dc_voltage).channels
)"
:key="channel"
:value="channel"
>
{{ channel + 1 }}
</option>
</select>
</div>
</div>
<div class="row mb-3">
<label for="inverter_restart" class="col-sm-4 col-form-label"> <label for="inverter_restart" class="col-sm-4 col-form-label">
{{ $t('powerlimiteradmin.InverterRestartHour') }} {{ $t('powerlimiteradmin.InverterRestartHour') }}
<BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.InverterRestartHint')" /> <BIconInfoCircle v-tooltip :title="$t('powerlimiteradmin.InverterRestartHint')" />
@ -213,7 +244,7 @@
:text="$t('powerlimiteradmin.SolarPassthrough')" :text="$t('powerlimiteradmin.SolarPassthrough')"
textVariant="text-bg-primary" textVariant="text-bg-primary"
add-space add-space
v-if="canUseSolarPassthrough()" v-if="canUseSolarPassthrough"
> >
<div <div
class="alert alert-secondary" class="alert alert-secondary"
@ -249,68 +280,20 @@
</CardElement> </CardElement>
<CardElement <CardElement
:text="$t('powerlimiteradmin.SocThresholds')" :text="$t('powerlimiteradmin.VoltageThresholds')"
textVariant="text-bg-primary" textVariant="text-bg-primary"
add-space add-space
v-if="canUseSoCThresholds()" v-if="canUseVoltageThresholds"
> >
<InputElement <InputElement
v-if="hasBatteryInterface"
:label="$t('powerlimiteradmin.IgnoreSoc')" :label="$t('powerlimiteradmin.IgnoreSoc')"
:tooltip="$t('powerlimiteradmin.IgnoreSocHint')"
v-model="powerLimiterConfigList.ignore_soc" v-model="powerLimiterConfigList.ignore_soc"
type="checkbox" type="checkbox"
wide wide
/> />
<template v-if="!powerLimiterConfigList.ignore_soc">
<div
class="alert alert-secondary"
role="alert"
v-html="$t('powerlimiteradmin.BatterySocInfo')"
></div>
<InputElement
:label="$t('powerlimiteradmin.StartThreshold')"
v-model="powerLimiterConfigList.battery_soc_start_threshold"
placeholder="80"
min="0"
max="100"
postfix="%"
type="number"
wide
/>
<InputElement
:label="$t('powerlimiteradmin.StopThreshold')"
v-model="powerLimiterConfigList.battery_soc_stop_threshold"
placeholder="20"
min="0"
max="100"
postfix="%"
type="number"
wide
/>
<InputElement
:label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')"
:tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')"
v-model="powerLimiterConfigList.full_solar_passthrough_soc"
v-if="isSolarPassthroughEnabled()"
placeholder="80"
min="0"
max="100"
postfix="%"
type="number"
wide
/>
</template>
</CardElement>
<CardElement
:text="$t('powerlimiteradmin.VoltageThresholds')"
textVariant="text-bg-primary"
add-space
v-if="canUseVoltageThresholds()"
>
<InputElement <InputElement
:label="$t('powerlimiteradmin.StartThreshold')" :label="$t('powerlimiteradmin.StartThreshold')"
v-model="powerLimiterConfigList.voltage_start_threshold" v-model="powerLimiterConfigList.voltage_start_threshold"
@ -335,7 +318,7 @@
wide wide
/> />
<template v-if="isSolarPassthroughEnabled()"> <template v-if="isSolarPassthroughEnabled">
<InputElement <InputElement
:label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')" :label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')"
:tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')" :tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')"
@ -379,7 +362,56 @@
></div> ></div>
</CardElement> </CardElement>
<FormFooter @reload="getAllData" /> <CardElement
:text="$t('powerlimiteradmin.SocThresholds')"
textVariant="text-bg-primary"
add-space
v-if="canUseSoCThresholds"
>
<InputElement
:label="$t('powerlimiteradmin.StartThreshold')"
v-model="powerLimiterConfigList.battery_soc_start_threshold"
placeholder="80"
min="0"
max="100"
postfix="%"
type="number"
wide
/>
<InputElement
:label="$t('powerlimiteradmin.StopThreshold')"
v-model="powerLimiterConfigList.battery_soc_stop_threshold"
placeholder="20"
min="0"
max="100"
postfix="%"
type="number"
wide
/>
<InputElement
:label="$t('powerlimiteradmin.FullSolarPassthroughStartThreshold')"
:tooltip="$t('powerlimiteradmin.FullSolarPassthroughStartThresholdHint')"
v-model="powerLimiterConfigList.full_solar_passthrough_soc"
v-if="isSolarPassthroughEnabled"
placeholder="80"
min="0"
max="100"
postfix="%"
type="number"
wide
/>
<div
class="alert alert-secondary"
role="alert"
v-html="$t('powerlimiteradmin.BatterySocInfo')"
></div>
</CardElement>
</template>
<FormFooter @reload="getMetaData" />
</form> </form>
</BasePage> </BasePage>
</template> </template>
@ -393,7 +425,12 @@ import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue'; import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import { BIconInfoCircle } from 'bootstrap-icons-vue'; import { BIconInfoCircle } from 'bootstrap-icons-vue';
import type { PowerLimiterConfig, PowerLimiterMetaData } from '@/types/PowerLimiterConfig'; import type {
PowerLimiterConfig,
PowerLimiterInverterConfig,
PowerLimiterMetaData,
PowerLimiterInverterInfo,
} from '@/types/PowerLimiterConfig';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -416,42 +453,70 @@ export default defineComponent({
}; };
}, },
created() { created() {
this.getAllData(); this.getMetaData();
}, },
watch: { watch: {
'powerLimiterConfigList.inverter_serial'(newVal) { governedInverters() {
if (
!this.governedInverters.some(
(inv: PowerLimiterInverterConfig) =>
inv.serial == this.powerLimiterConfigList.inverter_serial_for_dc_voltage
)
) {
// marks serial as invalid, selects placeholder option
this.powerLimiterConfigList.inverter_serial_for_dc_voltage = '';
}
},
},
computed: {
governedInverters(): PowerLimiterInverterConfig[] {
const inverters = this.powerLimiterConfigList?.inverters || [];
return inverters.filter((inv: PowerLimiterInverterConfig) => inv.is_governed) || [];
},
governedBatteryPoweredInverters(): PowerLimiterInverterConfig[] {
return this.governedInverters.filter((inv: PowerLimiterInverterConfig) => !inv.is_solar_powered);
},
governingBatteryPoweredInverters(): boolean {
return this.governedBatteryPoweredInverters.length > 0;
},
isEnabled(): boolean {
const cfg = this.powerLimiterConfigList;
return cfg.enabled && this.governedInverters.length > 0;
},
isSolarPassthroughEnabled(): boolean {
return (
this.powerLimiterMetaData.charge_controller_enabled &&
this.powerLimiterConfigList.solar_passthrough_enabled
);
},
hasPowerMeter(): boolean {
return this.powerLimiterMetaData.power_meter_enabled;
},
canUseSolarPassthrough(): boolean {
const meta = this.powerLimiterMetaData;
return meta.charge_controller_enabled && this.governingBatteryPoweredInverters;
},
canUseVoltageThresholds(): boolean {
return this.governingBatteryPoweredInverters;
},
canUseSoCThresholds(): boolean {
const cfg = this.powerLimiterConfigList; const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData; const meta = this.powerLimiterMetaData;
return meta.battery_enabled && this.governingBatteryPoweredInverters && !cfg.ignore_soc;
if (newVal === '') { },
return; hasBatteryInterface(): boolean {
} // do not try to convert the placeholder value const meta = this.powerLimiterMetaData;
return meta.battery_enabled && this.governingBatteryPoweredInverters;
if (meta.inverters[newVal] !== undefined) {
return;
}
for (const [serial, inverter] of Object.entries(meta.inverters)) {
// cfg.inverter_serial might be too large to parse as a 32 bit
// int, so we make sure to only try to parse two characters. if
// cfg.inverter_serial is indeed an old position based index,
// it is only one character.
if (inverter.pos == Number(cfg.inverter_serial.substr(0, 2))) {
// inverter_serial uses the old position-based
// value to identify the inverter. convert to serial.
cfg.inverter_serial = serial;
return;
}
}
// previously selected inverter was deleted. marks serial as
// invalid, selects placeholder option.
cfg.inverter_serial = '';
}, },
}, },
methods: { methods: {
getConfigHints() { getInverterInfo(serial: string): PowerLimiterInverterInfo {
const cfg = this.powerLimiterConfigList; return (
this.powerLimiterMetaData.inverters?.find((inv: PowerLimiterInverterInfo) => inv.serial === serial) ||
({} as PowerLimiterInverterInfo)
);
},
getConfigHints(): { severity: string; subject: string }[] {
const meta = this.powerLimiterMetaData; const meta = this.powerLimiterMetaData;
const hints = []; const hints = [];
@ -459,20 +524,26 @@ export default defineComponent({
hints.push({ severity: 'optional', subject: 'PowerMeterDisabled' }); hints.push({ severity: 'optional', subject: 'PowerMeterDisabled' });
} }
if (typeof meta.inverters === 'undefined' || Object.keys(meta.inverters).length == 0) { if (typeof meta.inverters === 'undefined' || meta.inverters.length == 0) {
hints.push({ severity: 'requirement', subject: 'NoInverter' }); hints.push({ severity: 'requirement', subject: 'NoInverter' });
this.configAlert = true; this.configAlert = true;
} else { } else {
const inv = meta.inverters[cfg.inverter_serial]; for (const inv of this.powerLimiterMetaData.inverters) {
if ( if (
inv !== undefined && !this.powerLimiterConfigList.inverters.some(
!(inv.poll_enable && inv.command_enable && inv.poll_enable_night && inv.command_enable_night) (i: PowerLimiterInverterConfig) => i.serial == inv.serial
)
) { ) {
continue;
}
if (!(inv.poll_enable && inv.command_enable && inv.poll_enable_night && inv.command_enable_night)) {
hints.push({ severity: 'requirement', subject: 'InverterCommunication' }); hints.push({ severity: 'requirement', subject: 'InverterCommunication' });
break;
}
} }
} }
if (!cfg.is_inverter_solar_powered) { if (this.governingBatteryPoweredInverters) {
if (!meta.charge_controller_enabled) { if (!meta.charge_controller_enabled) {
hints.push({ severity: 'optional', subject: 'NoChargeController' }); hints.push({ severity: 'optional', subject: 'NoChargeController' });
} }
@ -484,81 +555,103 @@ export default defineComponent({
return hints; return hints;
}, },
isEnabled() {
return this.powerLimiterConfigList.enabled;
},
hasPowerMeter() {
return this.powerLimiterMetaData.power_meter_enabled;
},
canUseOverscaling() {
const cfg = this.powerLimiterConfigList;
return cfg.is_inverter_solar_powered;
},
canUseBatteryDischargeAtNight() {
const cfg = this.powerLimiterConfigList;
return !cfg.is_inverter_solar_powered;
},
canUseSolarPassthrough() {
const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData;
return this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered;
},
canUseSoCThresholds() {
const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData;
return this.isEnabled() && meta.battery_enabled && !cfg.is_inverter_solar_powered;
},
canUseVoltageThresholds() {
const cfg = this.powerLimiterConfigList;
return this.isEnabled() && !cfg.is_inverter_solar_powered;
},
isSolarPassthroughEnabled() {
return this.powerLimiterConfigList.solar_passthrough_enabled;
},
range(end: number) { range(end: number) {
return Array.from(Array(end).keys()); return Array.from(Array(end).keys());
}, },
inverterName(serial: string) {
if (serial === undefined) {
return 'undefined';
}
const meta = this.powerLimiterMetaData;
if (meta === undefined) {
return 'metadata pending';
}
const inv = this.getInverterInfo(serial);
if (inv === undefined) {
return 'not found';
}
return inv.name;
},
inverterLabel(serial: string) {
if (serial === undefined) {
return 'undefined';
}
const meta = this.powerLimiterMetaData;
if (meta === undefined) {
return 'metadata pending';
}
const inv = this.getInverterInfo(serial);
if (inv === undefined) {
return 'not found';
}
return inv.name + ' (' + inv.type + ')';
},
needsChannelSelection() { needsChannelSelection() {
const cfg = this.powerLimiterConfigList; const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData;
const reset = function () { const reset = function () {
cfg.inverter_channel_id = 0; cfg.inverter_channel_id_for_dc_voltage = 0;
return false; return false;
}; };
if (cfg.inverter_serial === '') { if (!this.governingBatteryPoweredInverters) {
return reset(); return reset();
} }
if (cfg.is_inverter_solar_powered) { if (cfg.inverter_serial_for_dc_voltage === '') {
return reset(); return reset();
} }
const inverter = meta.inverters[cfg.inverter_serial]; const inverter = this.getInverterInfo(cfg.inverter_serial_for_dc_voltage);
if (inverter === undefined) { if (cfg.inverter_channel_id_for_dc_voltage >= inverter.channels) {
return reset(); cfg.inverter_channel_id_for_dc_voltage = 0;
}
if (cfg.inverter_channel_id >= inverter.channels) {
reset();
} }
return inverter.channels > 1; return inverter.channels > 1;
}, },
getAllData() { getMetaData() {
this.dataLoading = true; this.dataLoading = true;
fetch('/api/powerlimiter/metadata', { headers: authHeader() }) fetch('/api/powerlimiter/metadata', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
this.powerLimiterMetaData = data; this.powerLimiterMetaData = data;
this.getConfigData();
});
},
getConfigData() {
fetch('/api/powerlimiter/config', { headers: authHeader() }) fetch('/api/powerlimiter/config', { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => { .then((data) => {
data.inverters = this.tidyUpInverterConfigs(data.inverters);
this.powerLimiterConfigList = data; this.powerLimiterConfigList = data;
this.dataLoading = false; this.dataLoading = false;
}); });
}); },
tidyUpInverterConfigs(inverters: PowerLimiterInverterConfig[]): PowerLimiterInverterConfig[] {
const metaInverters = this.powerLimiterMetaData?.inverters || [];
// remove power limiter inverter config if no such inverter exists
inverters = inverters.filter((cfgInv: PowerLimiterInverterConfig) =>
metaInverters.some((metaInv) => metaInv.serial === cfgInv.serial)
);
// add default power limiter inverter config for new inverters
for (const metaInv of metaInverters) {
const known = inverters.some((cfgInv: PowerLimiterInverterConfig) => cfgInv.serial === metaInv.serial);
if (known) {
continue;
}
const newInv = {} as PowerLimiterInverterConfig;
newInv.serial = metaInv.serial;
newInv.is_governed = false;
newInv.is_behind_power_meter = true;
newInv.lower_power_limit = 10 * metaInv.channels;
newInv.upper_power_limit = Math.max(metaInv.max_power, 300);
inverters.push(newInv);
}
return inverters;
}, },
savePowerLimiterConfig(e: Event) { savePowerLimiterConfig(e: Event) {
e.preventDefault(); e.preventDefault();
@ -576,6 +669,7 @@ export default defineComponent({
this.alertMessage = response.message; this.alertMessage = response.message;
this.alertType = response.type; this.alertType = response.type;
this.showAlert = true; this.showAlert = true;
window.scrollTo(0, 0);
}); });
}, },
}, },