Skip to content

Commit

Permalink
Feature: DPL: add support for multiple inverters
Browse files Browse the repository at this point in the history
  • Loading branch information
schlimmchen committed Nov 17, 2024
1 parent 7e19f19 commit 4524c04
Show file tree
Hide file tree
Showing 22 changed files with 1,936 additions and 1,216 deletions.
68 changes: 40 additions & 28 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,43 @@ 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 BatteryAmperageUnit { Amps = 0, MilliAmps = 1 };
Expand Down Expand Up @@ -284,34 +321,7 @@ struct CONFIG_T {
PowerMeterHttpSmlConfig HttpSml;
} PowerMeter;

struct {
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;
PowerLimiterConfig PowerLimiter;

BatteryConfig Battery;

Expand Down Expand Up @@ -365,13 +375,15 @@ class ConfigurationClass {
static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target);
static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig 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 deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target);
static void deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target);
static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target);
static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target);
static void deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target);
static void deserializePowerLimiterConfig(JsonObject const& source, PowerLimiterConfig& target);

private:
void loop();
Expand Down
61 changes: 30 additions & 31 deletions include/PowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
#pragma once

#include "Configuration.h"
#include "PowerLimiterInverter.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <Hoymiles.h>
#include <atomic>
#include <deque>
#include <memory>
#include <functional>
#include <optional>
Expand All @@ -18,32 +20,28 @@

class PowerLimiterClass {
public:
PowerLimiterClass() = default;

enum class Status : unsigned {
Initializing,
DisabledByConfig,
DisabledByMqtt,
WaitingForValidTimestamp,
PowerMeterPending,
InverterInvalid,
InverterChanged,
InverterOffline,
InverterCommandsDisabled,
InverterLimitPending,
InverterPowerCmdPending,
InverterDevInfoPending,
InverterCmdPending,
ConfigReload,
InverterStatsPending,
CalculatedLimitBelowMinLimit,
FullSolarPassthrough,
UnconditionalSolarPassthrough,
NoVeDirect,
NoEnergy,
HuaweiPsu,
Stable,
};

void init(Scheduler& scheduler);
uint8_t getInverterUpdateTimeouts() const { return _inverterUpdateTimeouts; }
void triggerReloadingConfig() { _reloadConfigFlag = true; }
uint8_t getInverterUpdateTimeouts() const;
uint8_t getPowerLimiterState();
int32_t getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; }
int32_t getInverterOutput() { return _lastExpectedInverterOutput; }
bool getFullSolarPassThroughEnabled() const { return _fullSolarPassThroughEnabled; }

enum class Mode : unsigned {
Expand All @@ -54,54 +52,55 @@ class PowerLimiterClass {

void setMode(Mode m) { _mode = m; }
Mode getMode() const { return _mode; }
bool usesBatteryPoweredInverter();
bool isGovernedInverterProducing();
void calcNextInverterRestart();

private:
void loop();

Task _loopTask;

int32_t _lastRequestedPowerLimit = 0;
bool _shutdownPending = false;
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;
std::atomic<bool> _reloadConfigFlag = true;
uint16_t _lastExpectedInverterOutput = 0;
Status _lastStatus = Status::Initializing;
uint32_t _lastStatusPrinted = 0;
uint32_t _lastCalculation = 0;
static constexpr uint32_t _calculationBackoffMsDefault = 128;
uint32_t _calculationBackoffMs = _calculationBackoffMsDefault;
Mode _mode = Mode::Normal;
std::shared_ptr<InverterAbstract> _inverter = nullptr;

std::deque<std::unique_ptr<PowerLimiterInverter>> _inverters;
bool _batteryDischargeEnabled = 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 _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart
bool _fullSolarPassThroughEnabled = false;
bool _verboseLogging = true;
uint8_t _inverterUpdateTimeouts = 0;

frozen::string const& getStatusText(Status status);
void announceStatus(Status status);
bool shutdown(Status status);
bool shutdown() { return shutdown(_lastStatus); }
void reloadConfig();
std::pair<float, char const*> getInverterDcVoltage();
float getBatteryVoltage(bool log = false);
int32_t inverterPowerDcToAc(std::shared_ptr<InverterAbstract> inverter, int32_t dcPower);
void unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter);
bool canUseDirectSolarPower();
bool calcPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t solarPower, int32_t batteryPowerLimit, bool batteryPower);
bool updateInverter();
bool setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit);
int32_t getSolarPower();
int32_t getBatteryDischargeLimit();
uint16_t solarDcToInverterAc(uint16_t dcPower);
void fullSolarPassthrough(PowerLimiterClass::Status reason);
int16_t calcHouseholdConsumption();
using inverter_filter_t = std::function<bool(PowerLimiterInverter const&)>;
uint16_t updateInverterLimits(uint16_t powerRequested, inverter_filter_t filter, std::string const& filterExpression);
uint16_t calcBatteryAllowance(uint16_t powerRequested);
bool updateInverters();
uint16_t getSolarPassthroughPower();
std::optional<uint16_t> getBatteryDischargeLimit();
float getBatteryInvertersOutputAcWatts();
float getLoadCorrectedVoltage();
bool testThreshold(float socThreshold, float voltThreshold,
std::function<bool(float, float)> compare);
bool isStartThresholdReached();
bool isStopThresholdReached();
bool isBelowStopThreshold();
bool useFullSolarPassthrough();
bool isFullSolarPassthroughActive();
};

extern PowerLimiterClass PowerLimiter;
19 changes: 19 additions & 0 deletions include/PowerLimiterBatteryInverter.h
Original file line number Diff line number Diff line change
@@ -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;
};
111 changes: 111 additions & 0 deletions include/PowerLimiterInverter.h
Original file line number Diff line number Diff line change
@@ -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;
};
20 changes: 20 additions & 0 deletions include/PowerLimiterSolarInverter.h
Original file line number Diff line number Diff line change
@@ -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;
};
4 changes: 1 addition & 3 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,16 @@
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
#define POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT false
#define POWERLIMITER_INTERVAL 10
#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true
#define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false
#define POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING false
#define POWERLIMITER_INVERTER_ID 0ULL
#define POWERLIMITER_INVERTER_CHANNEL_ID 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0
#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0
#define POWERLIMITER_LOWER_POWER_LIMIT 10
#define POWERLIMITER_BASE_LOAD_LIMIT 100
#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_STOP_THRESHOLD 20
#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0
Expand Down
Loading

0 comments on commit 4524c04

Please sign in to comment.