OpenDTU/src/Configuration.cpp
2024-11-01 22:07:48 +01:00

460 lines
18 KiB
C++

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022-2024 Thomas Basler and others
*/
#include "Configuration.h"
#include "MessageOutput.h"
#include "NetworkSettings.h"
#include "Utils.h"
#include "defaults.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <nvs_flash.h>
CONFIG_T config;
void ConfigurationClass::init()
{
memset(&config, 0x0, sizeof(config));
}
bool ConfigurationClass::write()
{
File f = LittleFS.open(CONFIG_FILENAME, "w");
if (!f) {
return false;
}
config.Cfg.SaveCount++;
JsonDocument doc;
JsonObject cfg = doc["cfg"].to<JsonObject>();
cfg["version"] = config.Cfg.Version;
cfg["save_count"] = config.Cfg.SaveCount;
JsonObject wifi = doc["wifi"].to<JsonObject>();
wifi["ssid"] = config.WiFi.Ssid;
wifi["password"] = config.WiFi.Password;
wifi["ip"] = IPAddress(config.WiFi.Ip).toString();
wifi["netmask"] = IPAddress(config.WiFi.Netmask).toString();
wifi["gateway"] = IPAddress(config.WiFi.Gateway).toString();
wifi["dns1"] = IPAddress(config.WiFi.Dns1).toString();
wifi["dns2"] = IPAddress(config.WiFi.Dns2).toString();
wifi["dhcp"] = config.WiFi.Dhcp;
wifi["hostname"] = config.WiFi.Hostname;
wifi["aptimeout"] = config.WiFi.ApTimeout;
JsonObject mdns = doc["mdns"].to<JsonObject>();
mdns["enabled"] = config.Mdns.Enabled;
JsonObject ntp = doc["ntp"].to<JsonObject>();
ntp["server"] = config.Ntp.Server;
ntp["timezone"] = config.Ntp.Timezone;
ntp["timezone_descr"] = config.Ntp.TimezoneDescr;
ntp["latitude"] = config.Ntp.Latitude;
ntp["longitude"] = config.Ntp.Longitude;
ntp["sunsettype"] = config.Ntp.SunsetType;
JsonObject mqtt = doc["mqtt"].to<JsonObject>();
mqtt["enabled"] = config.Mqtt.Enabled;
mqtt["hostname"] = config.Mqtt.Hostname;
mqtt["port"] = config.Mqtt.Port;
mqtt["clientid"] = config.Mqtt.ClientId;
mqtt["username"] = config.Mqtt.Username;
mqtt["password"] = config.Mqtt.Password;
mqtt["topic"] = config.Mqtt.Topic;
mqtt["retain"] = config.Mqtt.Retain;
mqtt["publish_interval"] = config.Mqtt.PublishInterval;
mqtt["clean_session"] = config.Mqtt.CleanSession;
JsonObject mqtt_lwt = mqtt["lwt"].to<JsonObject>();
mqtt_lwt["topic"] = config.Mqtt.Lwt.Topic;
mqtt_lwt["value_online"] = config.Mqtt.Lwt.Value_Online;
mqtt_lwt["value_offline"] = config.Mqtt.Lwt.Value_Offline;
mqtt_lwt["qos"] = config.Mqtt.Lwt.Qos;
JsonObject mqtt_tls = mqtt["tls"].to<JsonObject>();
mqtt_tls["enabled"] = config.Mqtt.Tls.Enabled;
mqtt_tls["root_ca_cert"] = config.Mqtt.Tls.RootCaCert;
mqtt_tls["certlogin"] = config.Mqtt.Tls.CertLogin;
mqtt_tls["client_cert"] = config.Mqtt.Tls.ClientCert;
mqtt_tls["client_key"] = config.Mqtt.Tls.ClientKey;
JsonObject mqtt_hass = mqtt["hass"].to<JsonObject>();
mqtt_hass["enabled"] = config.Mqtt.Hass.Enabled;
mqtt_hass["retain"] = config.Mqtt.Hass.Retain;
mqtt_hass["topic"] = config.Mqtt.Hass.Topic;
mqtt_hass["individual_panels"] = config.Mqtt.Hass.IndividualPanels;
mqtt_hass["expire"] = config.Mqtt.Hass.Expire;
JsonObject dtu = doc["dtu"].to<JsonObject>();
dtu["serial"] = config.Dtu.Serial;
dtu["poll_interval"] = config.Dtu.PollInterval;
dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel;
dtu["cmt_pa_level"] = config.Dtu.Cmt.PaLevel;
dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency;
dtu["cmt_country_mode"] = config.Dtu.Cmt.CountryMode;
JsonObject security = doc["security"].to<JsonObject>();
security["password"] = config.Security.Password;
security["allow_readonly"] = config.Security.AllowReadonly;
JsonObject device = doc["device"].to<JsonObject>();
device["pinmapping"] = config.Dev_PinMapping;
JsonObject display = device["display"].to<JsonObject>();
display["powersafe"] = config.Display.PowerSafe;
display["screensaver"] = config.Display.ScreenSaver;
display["rotation"] = config.Display.Rotation;
display["contrast"] = config.Display.Contrast;
display["locale"] = config.Display.Locale;
display["diagram_duration"] = config.Display.Diagram.Duration;
display["diagram_mode"] = config.Display.Diagram.Mode;
JsonArray leds = device["led"].to<JsonArray>();
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
JsonObject led = leds.add<JsonObject>();
led["brightness"] = config.Led_Single[i].Brightness;
}
JsonArray inverters = doc["inverters"].to<JsonArray>();
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
JsonObject inv = inverters.add<JsonObject>();
inv["serial"] = config.Inverter[i].Serial;
inv["name"] = config.Inverter[i].Name;
inv["order"] = config.Inverter[i].Order;
inv["poll_enable"] = config.Inverter[i].Poll_Enable;
inv["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night;
inv["command_enable"] = config.Inverter[i].Command_Enable;
inv["command_enable_night"] = config.Inverter[i].Command_Enable_Night;
inv["reachable_threshold"] = config.Inverter[i].ReachableThreshold;
inv["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable;
inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight;
inv["clear_eventlog"] = config.Inverter[i].ClearEventlogOnMidnight;
inv["yieldday_correction"] = config.Inverter[i].YieldDayCorrection;
JsonArray channel = inv["channel"].to<JsonArray>();
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
JsonObject chanData = channel.add<JsonObject>();
chanData["name"] = config.Inverter[i].channel[c].Name;
chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower;
chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset;
}
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return false;
}
// Serialize JSON to file
if (serializeJson(doc, f) == 0) {
MessageOutput.println("Failed to write file");
return false;
}
f.close();
return true;
}
bool ConfigurationClass::read()
{
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
Utils::skipBom(f);
JsonDocument doc;
// Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f);
if (error) {
MessageOutput.println("Failed to read file, using default configuration");
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return false;
}
JsonObject cfg = doc["cfg"];
config.Cfg.Version = cfg["version"] | CONFIG_VERSION;
config.Cfg.SaveCount = cfg["save_count"] | 0;
JsonObject wifi = doc["wifi"];
strlcpy(config.WiFi.Ssid, wifi["ssid"] | WIFI_SSID, sizeof(config.WiFi.Ssid));
strlcpy(config.WiFi.Password, wifi["password"] | WIFI_PASSWORD, sizeof(config.WiFi.Password));
strlcpy(config.WiFi.Hostname, wifi["hostname"] | APP_HOSTNAME, sizeof(config.WiFi.Hostname));
IPAddress wifi_ip;
wifi_ip.fromString(wifi["ip"] | "");
config.WiFi.Ip[0] = wifi_ip[0];
config.WiFi.Ip[1] = wifi_ip[1];
config.WiFi.Ip[2] = wifi_ip[2];
config.WiFi.Ip[3] = wifi_ip[3];
IPAddress wifi_netmask;
wifi_netmask.fromString(wifi["netmask"] | "");
config.WiFi.Netmask[0] = wifi_netmask[0];
config.WiFi.Netmask[1] = wifi_netmask[1];
config.WiFi.Netmask[2] = wifi_netmask[2];
config.WiFi.Netmask[3] = wifi_netmask[3];
IPAddress wifi_gateway;
wifi_gateway.fromString(wifi["gateway"] | "");
config.WiFi.Gateway[0] = wifi_gateway[0];
config.WiFi.Gateway[1] = wifi_gateway[1];
config.WiFi.Gateway[2] = wifi_gateway[2];
config.WiFi.Gateway[3] = wifi_gateway[3];
IPAddress wifi_dns1;
wifi_dns1.fromString(wifi["dns1"] | "");
config.WiFi.Dns1[0] = wifi_dns1[0];
config.WiFi.Dns1[1] = wifi_dns1[1];
config.WiFi.Dns1[2] = wifi_dns1[2];
config.WiFi.Dns1[3] = wifi_dns1[3];
IPAddress wifi_dns2;
wifi_dns2.fromString(wifi["dns2"] | "");
config.WiFi.Dns2[0] = wifi_dns2[0];
config.WiFi.Dns2[1] = wifi_dns2[1];
config.WiFi.Dns2[2] = wifi_dns2[2];
config.WiFi.Dns2[3] = wifi_dns2[3];
config.WiFi.Dhcp = wifi["dhcp"] | WIFI_DHCP;
config.WiFi.ApTimeout = wifi["aptimeout"] | ACCESS_POINT_TIMEOUT;
JsonObject mdns = doc["mdns"];
config.Mdns.Enabled = mdns["enabled"] | MDNS_ENABLED;
JsonObject ntp = doc["ntp"];
strlcpy(config.Ntp.Server, ntp["server"] | NTP_SERVER, sizeof(config.Ntp.Server));
strlcpy(config.Ntp.Timezone, ntp["timezone"] | NTP_TIMEZONE, sizeof(config.Ntp.Timezone));
strlcpy(config.Ntp.TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp.TimezoneDescr));
config.Ntp.Latitude = ntp["latitude"] | NTP_LATITUDE;
config.Ntp.Longitude = ntp["longitude"] | NTP_LONGITUDE;
config.Ntp.SunsetType = ntp["sunsettype"] | NTP_SUNSETTYPE;
JsonObject mqtt = doc["mqtt"];
config.Mqtt.Enabled = mqtt["enabled"] | MQTT_ENABLED;
strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname));
config.Mqtt.Port = mqtt["port"] | MQTT_PORT;
strlcpy(config.Mqtt.ClientId, mqtt["clientid"] | NetworkSettings.getApName().c_str(), sizeof(config.Mqtt.ClientId));
strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username));
strlcpy(config.Mqtt.Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt.Password));
strlcpy(config.Mqtt.Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt.Topic));
config.Mqtt.Retain = mqtt["retain"] | MQTT_RETAIN;
config.Mqtt.PublishInterval = mqtt["publish_interval"] | MQTT_PUBLISH_INTERVAL;
config.Mqtt.CleanSession = mqtt["clean_session"] | MQTT_CLEAN_SESSION;
JsonObject mqtt_lwt = mqtt["lwt"];
strlcpy(config.Mqtt.Lwt.Topic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt.Lwt.Topic));
strlcpy(config.Mqtt.Lwt.Value_Online, mqtt_lwt["value_online"] | MQTT_LWT_ONLINE, sizeof(config.Mqtt.Lwt.Value_Online));
strlcpy(config.Mqtt.Lwt.Value_Offline, mqtt_lwt["value_offline"] | MQTT_LWT_OFFLINE, sizeof(config.Mqtt.Lwt.Value_Offline));
config.Mqtt.Lwt.Qos = mqtt_lwt["qos"] | MQTT_LWT_QOS;
JsonObject mqtt_tls = mqtt["tls"];
config.Mqtt.Tls.Enabled = mqtt_tls["enabled"] | MQTT_TLS;
strlcpy(config.Mqtt.Tls.RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt.Tls.RootCaCert));
config.Mqtt.Tls.CertLogin = mqtt_tls["certlogin"] | MQTT_TLSCERTLOGIN;
strlcpy(config.Mqtt.Tls.ClientCert, mqtt_tls["client_cert"] | MQTT_TLSCLIENTCERT, sizeof(config.Mqtt.Tls.ClientCert));
strlcpy(config.Mqtt.Tls.ClientKey, mqtt_tls["client_key"] | MQTT_TLSCLIENTKEY, sizeof(config.Mqtt.Tls.ClientKey));
JsonObject mqtt_hass = mqtt["hass"];
config.Mqtt.Hass.Enabled = mqtt_hass["enabled"] | MQTT_HASS_ENABLED;
config.Mqtt.Hass.Retain = mqtt_hass["retain"] | MQTT_HASS_RETAIN;
config.Mqtt.Hass.Expire = mqtt_hass["expire"] | MQTT_HASS_EXPIRE;
config.Mqtt.Hass.IndividualPanels = mqtt_hass["individual_panels"] | MQTT_HASS_INDIVIDUALPANELS;
strlcpy(config.Mqtt.Hass.Topic, mqtt_hass["topic"] | MQTT_HASS_TOPIC, sizeof(config.Mqtt.Hass.Topic));
JsonObject dtu = doc["dtu"];
config.Dtu.Serial = dtu["serial"] | DTU_SERIAL;
config.Dtu.PollInterval = dtu["poll_interval"] | DTU_POLL_INTERVAL;
config.Dtu.Nrf.PaLevel = dtu["nrf_pa_level"] | DTU_NRF_PA_LEVEL;
config.Dtu.Cmt.PaLevel = dtu["cmt_pa_level"] | DTU_CMT_PA_LEVEL;
config.Dtu.Cmt.Frequency = dtu["cmt_frequency"] | DTU_CMT_FREQUENCY;
config.Dtu.Cmt.CountryMode = dtu["cmt_country_mode"] | DTU_CMT_COUNTRY_MODE;
JsonObject security = doc["security"];
strlcpy(config.Security.Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security.Password));
config.Security.AllowReadonly = security["allow_readonly"] | SECURITY_ALLOW_READONLY;
JsonObject device = doc["device"];
strlcpy(config.Dev_PinMapping, device["pinmapping"] | DEV_PINMAPPING, sizeof(config.Dev_PinMapping));
JsonObject display = device["display"];
config.Display.PowerSafe = display["powersafe"] | DISPLAY_POWERSAFE;
config.Display.ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER;
config.Display.Rotation = display["rotation"] | DISPLAY_ROTATION;
config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST;
strlcpy(config.Display.Locale, display["locale"] | DISPLAY_LOCALE, sizeof(config.Display.Locale));
config.Display.Diagram.Duration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION;
config.Display.Diagram.Mode = display["diagram_mode"] | DISPLAY_DIAGRAM_MODE;
JsonArray leds = device["led"];
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
JsonObject led = leds[i].as<JsonObject>();
config.Led_Single[i].Brightness = led["brightness"] | LED_BRIGHTNESS;
}
JsonArray inverters = doc["inverters"];
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
JsonObject inv = inverters[i].as<JsonObject>();
config.Inverter[i].Serial = inv["serial"] | 0ULL;
strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name));
config.Inverter[i].Order = inv["order"] | 0;
config.Inverter[i].Poll_Enable = inv["poll_enable"] | true;
config.Inverter[i].Poll_Enable_Night = inv["poll_enable_night"] | true;
config.Inverter[i].Command_Enable = inv["command_enable"] | true;
config.Inverter[i].Command_Enable_Night = inv["command_enable_night"] | true;
config.Inverter[i].ReachableThreshold = inv["reachable_threshold"] | REACHABLE_THRESHOLD;
config.Inverter[i].ZeroRuntimeDataIfUnrechable = inv["zero_runtime"] | false;
config.Inverter[i].ZeroYieldDayOnMidnight = inv["zero_day"] | false;
config.Inverter[i].ClearEventlogOnMidnight = inv["clear_eventlog"] | false;
config.Inverter[i].YieldDayCorrection = inv["yieldday_correction"] | false;
JsonArray channel = inv["channel"];
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
config.Inverter[i].channel[c].MaxChannelPower = channel[c]["max_power"] | 0;
config.Inverter[i].channel[c].YieldTotalOffset = channel[c]["yield_total_offset"] | 0.0f;
strlcpy(config.Inverter[i].channel[c].Name, channel[c]["name"] | "", sizeof(config.Inverter[i].channel[c].Name));
}
}
f.close();
return true;
}
void ConfigurationClass::migrate()
{
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
if (!f) {
MessageOutput.println("Failed to open file, cancel migration");
return;
}
JsonDocument doc;
// Deserialize the JSON document
const DeserializationError error = deserializeJson(doc, f);
if (error) {
MessageOutput.printf("Failed to read file, cancel migration: %s\r\n", error.c_str());
return;
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
if (config.Cfg.Version < 0x00011700) {
JsonArray inverters = doc["inverters"];
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
JsonObject inv = inverters[i].as<JsonObject>();
JsonArray channels = inv["channels"];
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
config.Inverter[i].channel[c].MaxChannelPower = channels[c];
strlcpy(config.Inverter[i].channel[c].Name, "", sizeof(config.Inverter[i].channel[c].Name));
}
}
}
if (config.Cfg.Version < 0x00011800) {
JsonObject mqtt = doc["mqtt"];
config.Mqtt.PublishInterval = mqtt["publish_invterval"];
}
if (config.Cfg.Version < 0x00011900) {
JsonObject dtu = doc["dtu"];
config.Dtu.Nrf.PaLevel = dtu["pa_level"];
}
if (config.Cfg.Version < 0x00011a00) {
// This migration fixes this issue: https://github.com/espressif/arduino-esp32/issues/8828
// It occours when migrating from Core 2.0.9 to 2.0.14
// which was done by updating ESP32 PlatformIO from 6.3.2 to 6.5.0
nvs_flash_erase();
nvs_flash_init();
}
if (config.Cfg.Version < 0x00011b00) {
// Convert from kHz to Hz
config.Dtu.Cmt.Frequency *= 1000;
}
if (config.Cfg.Version < 0x00011c00) {
if (!strcmp(config.Ntp.Server, NTP_SERVER_OLD)) {
strlcpy(config.Ntp.Server, NTP_SERVER, sizeof(config.Ntp.Server));
}
}
if (config.Cfg.Version < 0x00011d00) {
JsonObject device = doc["device"];
JsonObject display = device["display"];
switch (display["language"] | 0U) {
case 0U:
strlcpy(config.Display.Locale, "en", sizeof(config.Display.Locale));
break;
case 1U:
strlcpy(config.Display.Locale, "de", sizeof(config.Display.Locale));
break;
case 2U:
strlcpy(config.Display.Locale, "fr", sizeof(config.Display.Locale));
break;
}
}
f.close();
config.Cfg.Version = CONFIG_VERSION;
write();
read();
}
CONFIG_T& ConfigurationClass::get()
{
return config;
}
INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot()
{
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial == 0) {
return &config.Inverter[i];
}
}
return nullptr;
}
INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(const uint64_t serial)
{
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial == serial) {
return &config.Inverter[i];
}
}
return nullptr;
}
void ConfigurationClass::deleteInverterById(const uint8_t id)
{
config.Inverter[id].Serial = 0ULL;
strlcpy(config.Inverter[id].Name, "", sizeof(config.Inverter[id].Name));
config.Inverter[id].Order = 0;
config.Inverter[id].Poll_Enable = true;
config.Inverter[id].Poll_Enable_Night = true;
config.Inverter[id].Command_Enable = true;
config.Inverter[id].Command_Enable_Night = true;
config.Inverter[id].ReachableThreshold = REACHABLE_THRESHOLD;
config.Inverter[id].ZeroRuntimeDataIfUnrechable = false;
config.Inverter[id].ZeroYieldDayOnMidnight = false;
config.Inverter[id].YieldDayCorrection = false;
for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) {
config.Inverter[id].channel[c].MaxChannelPower = 0;
config.Inverter[id].channel[c].YieldTotalOffset = 0.0f;
strlcpy(config.Inverter[id].channel[c].Name, "", sizeof(config.Inverter[id].channel[c].Name));
}
}
ConfigurationClass Configuration;