Skip to content

Commit

Permalink
Feature: DPL: support for multiple inverters
Browse files Browse the repository at this point in the history
  • Loading branch information
schlimmchen committed Sep 1, 2024
1 parent 9ebbc58 commit 96b0689
Show file tree
Hide file tree
Showing 14 changed files with 1,475 additions and 927 deletions.
67 changes: 39 additions & 28 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,42 @@ struct POWERMETER_HTTP_SML_CONFIG_T {
};
using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T;

struct POWERLIMITER_INVERTER_CONFIG_T {
uint64_t Serial;
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 };

struct CONFIG_T {
Expand Down Expand Up @@ -248,34 +284,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;

struct {
bool Enabled;
Expand Down Expand Up @@ -327,12 +336,14 @@ class ConfigurationClass {
static void serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target);
static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target);
static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig 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 deserializePowerLimiterConfig(JsonObject const& source, PowerLimiterConfig& target);
};

extern ConfigurationClass Configuration;
52 changes: 25 additions & 27 deletions include/PowerLimiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
#pragma once

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

class PowerLimiterClass {
public:
PowerLimiterClass() = default;

enum class Status : unsigned {
Initializing,
DisabledByConfig,
DisabledByMqtt,
WaitingForValidTimestamp,
PowerMeterPending,
InverterInvalid,
InverterChanged,
InverterOffline,
InverterCommandsDisabled,
InverterLimitPending,
InverterPowerCmdPending,
InverterDevInfoPending,
SingleSolarPoweredInverter,
InverterCmdPending,
InverterRemoval,
InverterStatsPending,
CalculatedLimitBelowMinLimit,
FullSolarPassthrough,
UnconditionalSolarPassthrough,
NoVeDirect,
NoEnergy,
Expand All @@ -41,9 +41,9 @@ class PowerLimiterClass {
};

void init(Scheduler& scheduler);
uint8_t getInverterUpdateTimeouts() const { return _inverterUpdateTimeouts; }
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,53 +54,51 @@ class PowerLimiterClass {

void setMode(Mode m) { _mode = m; }
Mode getMode() const { return _mode; }
bool usesBatteryPoweredInverter();
bool isManagedInverterProducing();
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;
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<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); }
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, bool batteryPower);
bool updateInverter();
bool setNewPowerLimit(std::shared_ptr<InverterAbstract> inverter, int32_t newPowerLimit);
int32_t getSolarPower();
uint16_t solarDcToInverterAc(uint16_t dcPower);
void fullSolarPassthrough(PowerLimiterClass::Status reason);
uint16_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();
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;
104 changes: 104 additions & 0 deletions include/PowerLimiterInverter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once

#include "Configuration.h"
#include <Hoymiles.h>
#include <optional>

class PowerLimiterInverter {
public:
PowerLimiterInverter(bool verboseLogging, PowerLimiterInverterConfig const& config);

// NOTE: this class is not prepared to be used if isValid() returns false,
// i.e., nullptr exceptions will occur if the instance is used despite
// isValid() being false.
bool isValid() const;

// 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 pendng after the last command completed.
std::optional<uint32_t> getLatestStatsMillis() const;

// the amount of times an update command issues 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.
uint16_t getMaxReductionWatts(bool allowStandby) const;

// the maximum increase of power output the inverter can achieve
// (is expected to achieve), possibly coming out of standby.
uint16_t getMaxIncreaseWatts() const;

// 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).
uint16_t applyReduction(uint16_t reduction, bool allowStandby);
uint16_t applyIncrease(uint16_t increase);

// stop producing AC power. returns the change in power output
// that will become effective (once update() returns false).
uint16_t standby();

// 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; }
bool isSolarPowered() const { return _config.IsSolarPowered; }

void debug() const;

private:
uint16_t getCurrentLimitWatts() const;
uint16_t scaleLimit(uint16_t expectedOutputWatts);
void setAcOutput(uint16_t expectedOutputWatts);

bool _verboseLogging;
char _serialStr[16];
char _logPrefix[32];

// copied to avoid races with web UI
PowerLimiterInverterConfig _config;

// Hoymiles lib inverter instance
std::shared_ptr<InverterAbstract> _spInverter = nullptr;

// 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, which possibly is different
// from the target limit due to scaling
uint16_t _targetAcOutputWatts = 0;
};
2 changes: 0 additions & 2 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,9 @@
#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
Expand Down
Loading

0 comments on commit 96b0689

Please sign in to comment.