Skip to content

Commit

Permalink
Feature: Refactor/Simplify DPL settings
Browse files Browse the repository at this point in the history
this changeset refactors the web application's DPL settings view. the
DPL settings can be complex, and they shall be presented in a way that
allows users to comprehend their meaning. irrelevant settings are now
hidden or displayed dynamically based on the influencing settings.

* group SoC thresholds into their own card

* hide battery SoC thresholds if battery disabled. if the user did not
  even enable the battery interface, battery SoC values will not be used
  for DPL decisions. in that case we completely hide the respective
  settings from the DPL admin view. this reduces the amount of settings
  for new users and especially users who don't even have a battery in
  their setup or have no BMS connected.

* group voltage thresholds and improve label texts

* fix load correction factor unit

* fix header (wording)

* group solar-passthrough settings in new card

* group inverter-related settings

* hide solar passthrough settings if VE.Direct is disabled. closes #662.

* completely disable form if any requirement is not met

* list available inverters by name and type. this makes it much more
  convenient to select the right inverter, especially since the order of
  the inverters in the web UI is decoupled from their position in the
  internal array, which was used to select them previously. care was
  taken that old configs select the same inverter after an update.
  when editing the DPL settings, the selects an inverter from the newly
  created drow-down list, and the respective old inverter is
  pre-selected.

* disable form if no inverter is configured (config alert)

* make inverter input selection dynamic. adjust selection to actual
  amount of channels for selected inverter. skip selection altogether if
  inverter has only one channel, or if it is solar powered.

* web app: wording adjustments

* group meta data into new property and exclude from submission. saves
  memory when evaluating the submitted settings.

* hide irrelevant settings if inverter is solar-powered

* move restart hour setting to inverter card. translate setting which
  disabled automatic restart.

* simplify "drain strategy" setting into an on/off toggle. care was
  taken that existing configs work the same after an upgrade. the
  respective drain strategy is translated into the new setting when
  reading the config. once the config is written, the new setting is
  persisted and the old is not part of the config any more.

* show more configuration hints, depending on actual configuration

* replace inputs by InputElement components where possible
  • Loading branch information
schlimmchen committed Mar 17, 2024
1 parent 7d6b725 commit 13bc943
Show file tree
Hide file tree
Showing 11 changed files with 481 additions and 412 deletions.
4 changes: 2 additions & 2 deletions include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,11 @@ struct CONFIG_T {
bool VerboseLogging;
bool SolarPassThroughEnabled;
uint8_t SolarPassThroughLosses;
uint8_t BatteryDrainStategy;
bool BatteryAlwaysUseAtNight;
uint32_t Interval;
bool IsInverterBehindPowerMeter;
bool IsInverterSolarPowered;
uint8_t InverterId;
uint64_t InverterId;
uint8_t InverterChannelId;
int32_t TargetPowerConsumption;
int32_t TargetPowerConsumptionHysteresis;
Expand Down
4 changes: 2 additions & 2 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,11 @@
#define POWERLIMITER_ENABLED false
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3
#define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0
#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_INVERTER_ID 0
#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
5 changes: 3 additions & 2 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ bool ConfigurationClass::write()
powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging;
powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled;
powerlimiter["solar_passtrough_losses"] = config.PowerLimiter.SolarPassThroughLosses;
powerlimiter["battery_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy;
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;
Expand Down Expand Up @@ -429,7 +429,8 @@ bool ConfigurationClass::read()
config.PowerLimiter.VerboseLogging = powerlimiter["verbose_logging"] | VERBOSE_LOGGING;
config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED;
config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passthrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES;
config.PowerLimiter.BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY;
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;
config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER;
config.PowerLimiter.IsInverterSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED;
Expand Down
8 changes: 7 additions & 1 deletion src/Huawei_can.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,13 @@ void HuaweiCanClass::loop()

// Check if inverter used by the power limiter is active
std::shared_ptr<InverterAbstract> inverter =
Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
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()) {
Expand Down
16 changes: 11 additions & 5 deletions src/PowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,13 @@ void PowerLimiterClass::loop()
}

std::shared_ptr<InverterAbstract> currentInverter =
Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId);

if (currentInverter == 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.
currentInverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId);
}

// in case of (newly) broken configuration, shut down
// the last inverter we worked with (if any)
Expand Down Expand Up @@ -242,14 +248,14 @@ void PowerLimiterClass::loop()

if (isStartThresholdReached()) { return true; }

// with solar passthrough, and the respective drain strategy, we
// with solar passthrough, and the respective switch enabled, we
// may start discharging the battery when it is nighttime. we also
// stop the discharge cycle if it becomes daytime again.
// TODO(schlimmchen): should be supported by sunrise and sunset, such
// that a thunderstorm or other events that drastically lower the solar
// power do not cause the start of a discharge cycle during the day.
if (config.PowerLimiter.SolarPassThroughEnabled &&
config.PowerLimiter.BatteryDrainStategy == EMPTY_AT_NIGHT) {
config.PowerLimiter.BatteryAlwaysUseAtNight) {
return getSolarPower() == 0;
}

Expand Down Expand Up @@ -287,10 +293,10 @@ void PowerLimiterClass::loop()
(isStopThresholdReached()?"yes":"no"),
(_inverter->isProducing()?"is":"is NOT"));

MessageOutput.printf("[DPL::loop] battery discharging %s, SolarPT %s, Drain Strategy: %i\r\n",
MessageOutput.printf("[DPL::loop] battery discharging %s, SolarPT %s, use at night: %i\r\n",
(_batteryDischargeEnabled?"allowed":"prevented"),
(config.PowerLimiter.SolarPassThroughEnabled?"enabled":"disabled"),
config.PowerLimiter.BatteryDrainStategy);
config.PowerLimiter.BatteryAlwaysUseAtNight);
};

if (_verboseLogging) { logging(); }
Expand Down
91 changes: 69 additions & 22 deletions src/WebApi_powerlimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,24 @@ void WebApiPowerLimiterClass::init(AsyncWebServer& server, Scheduler& scheduler)

void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
{
AsyncJsonResponse* response = new AsyncJsonResponse();
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(false, 1024 + 384 * invAmount);
auto& root = response->getRoot();
const CONFIG_T& config = Configuration.get();

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_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy;
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["inverter_id"] = config.PowerLimiter.InverterId;
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;
Expand All @@ -54,6 +60,37 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request)
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;

JsonObject metadata = root.createNestedObject("metadata");
metadata["power_meter_enabled"] = config.PowerMeter.Enabled;
metadata["battery_enabled"] = config.Battery.Enabled;
metadata["charge_controller_enabled"] = config.Vedirect.Enabled;

JsonObject inverters = metadata.createNestedObject("inverters");
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial == 0) { continue; }

// we use the integer (base 10) representation of the inverter serial,
// rather than the hex represenation as used when handling the inverter
// 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.createNestedObject(String(config.Inverter[i].Serial));
obj["pos"] = i;
obj["name"] = String(config.Inverter[i].Name);
obj["poll_enable"] = config.Inverter[i].Poll_Enable;
obj["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night;
obj["command_enable"] = config.Inverter[i].Command_Enable;
obj["command_enable_night"] = config.Inverter[i].Command_Enable_Night;

obj["type"] = "Unknown";
obj["channels"] = 1;
auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial);
if (inv != nullptr) {
obj["type"] = inv->typeName();
auto channels = inv->Statistics()->getChannelsByType(TYPE_DC);
obj["channels"] = channels.size();
}
}

response->setLength();
request->send(response);
}
Expand Down Expand Up @@ -103,48 +140,58 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
return;
}

if (!(root.containsKey("enabled")
&& root.containsKey("lower_power_limit")
&& root.containsKey("inverter_id")
&& root.containsKey("inverter_channel_id")
&& root.containsKey("target_power_consumption")
&& root.containsKey("target_power_consumption_hysteresis")
)) {
// 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.containsKey("enabled")) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
return;
}


CONFIG_T& config = Configuration.get();
config.PowerLimiter.Enabled = root["enabled"].as<bool>();
PowerLimiter.setMode(PowerLimiterClass::Mode::Normal); // User input sets PL to normal operation
config.PowerLimiter.VerboseLogging = root["verbose_logging"].as<bool>();
config.PowerLimiter.SolarPassThroughEnabled = root["solar_passthrough_enabled"].as<bool>();
config.PowerLimiter.SolarPassThroughLosses = root["solar_passthrough_losses"].as<uint8_t>();
config.PowerLimiter.BatteryDrainStategy= root["battery_drain_strategy"].as<uint8_t>();

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.BatteryAlwaysUseAtNight= root["battery_always_use_at_night"].as<bool>();
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.InverterId = root["inverter_id"].as<uint8_t>();
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.UpperPowerLimit = root["upper_power_limit"].as<int32_t>();
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.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>();
config.PowerLimiter.FullSolarPassThroughSoc = root["full_solar_passthrough_soc"].as<uint32_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;

WebApi.writeConfig(retMsg);

Expand Down
Loading

0 comments on commit 13bc943

Please sign in to comment.