Skip to content

Commit

Permalink
introduce and use ConfigurationClass::migrateOnBattery()
Browse files Browse the repository at this point in the history
this function does all the conversions from legacy configuration
settings into the current config schema. in the future, we also have
the OpenDTU-OnBattery-specific config version value available to
easily distinguish different config schemas.
  • Loading branch information
schlimmchen committed Nov 30, 2024
1 parent 47cb472 commit fc965c1
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 92 deletions.
5 changes: 4 additions & 1 deletion include/Configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011d00 // 0.1.29 // make sure to clean all after change
#define CONFIG_VERSION_ONBATTERY 1

#define WIFI_MAX_SSID_STRLEN 32
#define WIFI_MAX_PASSWORD_STRLEN 64
Expand Down Expand Up @@ -198,6 +199,7 @@ using BatteryConfig = struct BATTERY_CONFIG_T;
struct CONFIG_T {
struct {
uint32_t Version;
uint32_t VersionOnBattery;
uint32_t SaveCount;
} Cfg;

Expand Down Expand Up @@ -351,6 +353,7 @@ class ConfigurationClass {
bool read();
bool write();
void migrate();
void migrateOnBattery();
CONFIG_T const& get();

class WriteGuard {
Expand All @@ -377,7 +380,7 @@ class ConfigurationClass {
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_http_config, 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);
Expand Down
215 changes: 124 additions & 91 deletions src/Configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ bool ConfigurationClass::write()

JsonObject cfg = doc["cfg"].to<JsonObject>();
cfg["version"] = config.Cfg.Version;
cfg["version_onbattery"] = config.Cfg.VersionOnBattery;
cfg["save_count"] = config.Cfg.SaveCount;

JsonObject wifi = doc["wifi"].to<JsonObject>();
Expand Down Expand Up @@ -352,14 +353,8 @@ bool ConfigurationClass::write()
return true;
}

void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target)
void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source_http_config, HttpRequestConfig& target)
{
JsonObject source_http_config = source["http_request"];

// http request parameters of HTTP/JSON power meter were previously stored
// alongside other settings. TODO(schlimmchen): remove in mid 2025.
if (source_http_config.isNull()) { source_http_config = source; }

strlcpy(target.Url, source_http_config["url"] | "", sizeof(target.Url));
target.AuthType = source_http_config["auth_type"] | HttpRequestConfig::Auth::None;
strlcpy(target.Username, source_http_config["username"] | "", sizeof(target.Username));
Expand Down Expand Up @@ -398,7 +393,7 @@ void ConfigurationClass::deserializePowerMeterHttpJsonConfig(JsonObject const& s
PowerMeterHttpJsonValue& t = target.Values[i];
JsonObject s = values[i];

deserializeHttpRequestConfig(s, t.HttpRequest);
deserializeHttpRequestConfig(s["http_request"], t.HttpRequest);

t.Enabled = s["enabled"] | false;
strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath));
Expand All @@ -412,7 +407,7 @@ void ConfigurationClass::deserializePowerMeterHttpJsonConfig(JsonObject const& s
void ConfigurationClass::deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target)
{
target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL;
deserializeHttpRequestConfig(source, target.HttpRequest);
deserializeHttpRequestConfig(source["http_request"], target.HttpRequest);
}

void ConfigurationClass::deserializeBatteryConfig(JsonObject const& source, BatteryConfig& target)
Expand Down Expand Up @@ -487,9 +482,18 @@ bool ConfigurationClass::read()

JsonDocument doc;

// as OpenDTU-OnBattery was in use a long time without the version marker
// specific to OpenDTU-OnBattery, we must distinguish the cases (1) where a
// valid legacy config.json file was read and (2) where there was no config
// (or an error when reading occured). in the former case we want to
// perform a migration, whereas in the latter there is no need for a
// migration as the config is default-initialized to the current version.
uint32_t version_onbattery = 0;

// Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f);
if (error) {
version_onbattery = CONFIG_VERSION_ONBATTERY;
MessageOutput.println("Failed to read file, using default configuration");
}

Expand All @@ -499,6 +503,7 @@ bool ConfigurationClass::read()

JsonObject cfg = doc["cfg"];
config.Cfg.Version = cfg["version"] | CONFIG_VERSION;
config.Cfg.VersionOnBattery = cfg["version_onbattery"] | version_onbattery;
config.Cfg.SaveCount = cfg["save_count"] | 0;

JsonObject wifi = doc["wifi"];
Expand Down Expand Up @@ -661,92 +666,13 @@ bool ConfigurationClass::read()

deserializePowerMeterMqttConfig(powermeter["mqtt"], config.PowerMeter.Mqtt);

// process settings from legacy config if they are present
// TODO(schlimmchen): remove in mid 2025.
if (!powermeter["mqtt_topic_powermeter_1"].isNull()) {
auto& values = config.PowerMeter.Mqtt.Values;
strlcpy(values[0].Topic, powermeter["mqtt_topic_powermeter_1"], sizeof(values[0].Topic));
strlcpy(values[1].Topic, powermeter["mqtt_topic_powermeter_2"], sizeof(values[1].Topic));
strlcpy(values[2].Topic, powermeter["mqtt_topic_powermeter_3"], sizeof(values[2].Topic));
}

deserializePowerMeterSerialSdmConfig(powermeter["serial_sdm"], config.PowerMeter.SerialSdm);

// process settings from legacy config if they are present
// TODO(schlimmchen): remove in mid 2025.
if (!powermeter["sdmaddress"].isNull()) {
config.PowerMeter.SerialSdm.Address = powermeter["sdmaddress"];
}

JsonObject powermeter_http_json = powermeter["http_json"];
deserializePowerMeterHttpJsonConfig(powermeter_http_json, config.PowerMeter.HttpJson);

JsonObject powermeter_sml = powermeter["http_sml"];
deserializePowerMeterHttpSmlConfig(powermeter_sml, config.PowerMeter.HttpSml);

// process settings from legacy config if they are present
// TODO(schlimmchen): remove in mid 2025.
if (!powermeter["http_phases"].isNull()) {
auto& target = config.PowerMeter.HttpJson;

for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) {
PowerMeterHttpJsonValue& t = target.Values[i];
JsonObject s = powermeter["http_phases"][i];
deserializePowerMeterHttpJsonConfig(powermeter["http_json"], config.PowerMeter.HttpJson);

deserializeHttpRequestConfig(s, t.HttpRequest);
deserializePowerMeterHttpSmlConfig(powermeter["http_sml"], config.PowerMeter.HttpSml);

t.Enabled = s["enabled"] | false;
strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath));
t.PowerUnit = s["unit"] | PowerMeterHttpJsonValue::Unit::Watts;
t.SignInverted = s["sign_inverted"] | false;
}

target.IndividualRequests = powermeter["http_individual_requests"] | false;
}

JsonObject powerlimiter = doc["powerlimiter"];
deserializePowerLimiterConfig(powerlimiter, config.PowerLimiter);

if (powerlimiter["battery_drain_strategy"].as<uint8_t>() == 1) {
config.PowerLimiter.BatteryAlwaysUseAtNight = true; // convert legacy setting
}

if (!powerlimiter["solar_passtrough_enabled"].isNull()) {
// solar_passthrough_enabled was previously saved as
// solar_passtrough_enabled. be nice and also try misspelled key.
config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"].as<bool>();
}

if (!powerlimiter["solar_passtrough_losses"].isNull()) {
// solar_passthrough_losses was previously saved as
// solar_passtrough_losses. be nice and also try misspelled key.
config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passtrough_losses"].as<uint8_t>();
}

// process settings from legacy config if they are present
// TODO(schlimmchen): remove in mid 2025.
if (!powerlimiter["inverter_id"].isNull()) {
config.PowerLimiter.InverterChannelIdForDcVoltage = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;

auto& inv = config.PowerLimiter.Inverters[0];
uint64_t previousInverterSerial = powerlimiter["inverter_id"].as<uint64_t>();
if (previousInverterSerial < INV_MAX_COUNT) {
// 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;
}
deserializePowerLimiterConfig(doc["powerlimiter"], config.PowerLimiter);

deserializeBatteryConfig(doc["battery"], config.Battery);

Expand Down Expand Up @@ -869,6 +795,113 @@ void ConfigurationClass::migrate()
read();
}

void ConfigurationClass::migrateOnBattery()
{
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
if (!f) {
MessageOutput.println("Failed to open file, cancel OpenDTU-OnBattery migration");
return;
}

Utils::skipBom(f);

JsonDocument doc;

// Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f);
if (error) {
MessageOutput.printf("Failed to read file, cancel OpenDTU-OnBattery "
"migration: %s\r\n", error.c_str());
return;
}

if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}

if (config.Cfg.VersionOnBattery < 1) {
// all migrations in this block need to check whether or not the
// respective legacy setting is even present, as OpenDTU-OnBattery
// config version 0 identifies multiple different legacy versions of
// OpenDTU-OnBattery-specific settings, i.e., all before the
// OpenDTU-OnBattery config version value was introduced.

JsonObject powermeter = doc["powermeter"];

if (!powermeter["mqtt_topic_powermeter_1"].isNull()) {
auto& values = config.PowerMeter.Mqtt.Values;
strlcpy(values[0].Topic, powermeter["mqtt_topic_powermeter_1"], sizeof(values[0].Topic));
strlcpy(values[1].Topic, powermeter["mqtt_topic_powermeter_2"], sizeof(values[1].Topic));
strlcpy(values[2].Topic, powermeter["mqtt_topic_powermeter_3"], sizeof(values[2].Topic));
}

if (!powermeter["sdmaddress"].isNull()) {
config.PowerMeter.SerialSdm.Address = powermeter["sdmaddress"];
}

if (!powermeter["http_phases"].isNull()) {
auto& target = config.PowerMeter.HttpJson;

for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) {
PowerMeterHttpJsonValue& t = target.Values[i];
JsonObject s = powermeter["http_phases"][i];

deserializeHttpRequestConfig(s, t.HttpRequest);

t.Enabled = s["enabled"] | false;
strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath));
t.PowerUnit = s["unit"] | PowerMeterHttpJsonValue::Unit::Watts;
t.SignInverted = s["sign_inverted"] | false;
}

target.IndividualRequests = powermeter["http_individual_requests"] | false;
}

JsonObject powerlimiter = doc["powerlimiter"];

if (powerlimiter["battery_drain_strategy"].as<uint8_t>() == 1) {
config.PowerLimiter.BatteryAlwaysUseAtNight = true;
}

if (!powerlimiter["solar_passtrough_enabled"].isNull()) {
config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"].as<bool>();
}

if (!powerlimiter["solar_passtrough_losses"].isNull()) {
config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passtrough_losses"].as<uint8_t>();
}

if (!powerlimiter["inverter_id"].isNull()) {
config.PowerLimiter.InverterChannelIdForDcVoltage = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID;

auto& inv = config.PowerLimiter.Inverters[0];
uint64_t previousInverterSerial = powerlimiter["inverter_id"].as<uint64_t>();
if (previousInverterSerial < INV_MAX_COUNT) {
// 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;
}
}

f.close();

config.Cfg.VersionOnBattery = CONFIG_VERSION_ONBATTERY;
write();
read();
}

CONFIG_T const& ConfigurationClass::get()
{
return config;
Expand Down
4 changes: 4 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ void setup()
MessageOutput.print("migrated... ");
Configuration.migrate();
}
if (Configuration.get().Cfg.VersionOnBattery != CONFIG_VERSION_ONBATTERY) {
Configuration.migrateOnBattery();
MessageOutput.print("migrated OpenDTU-OnBattery-specific config... ");
}
auto& config = Configuration.get();
MessageOutput.println("done");

Expand Down

0 comments on commit fc965c1

Please sign in to comment.