From d2feac7a002eb553eba825419e836d3883727d27 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 11 Oct 2022 22:09:37 +0200 Subject: [PATCH] Migrate config from binary blob to json --- include/Configuration.h | 6 + src/Configuration.cpp | 203 +++++++++++++++++++++- src/WebApi_config.cpp | 6 +- webapp/src/components/ConfigAdminView.vue | 4 +- 4 files changed, 206 insertions(+), 13 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 83bdf75..14ed23b 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -4,6 +4,7 @@ #include #define CONFIG_FILENAME "/config.bin" +#define CONFIG_FILENAME_JSON "/config.json" #define CONFIG_VERSION 0x00011500 // 0.1.21 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 31 @@ -26,6 +27,8 @@ #define INV_MAX_COUNT 10 #define INV_MAX_CHAN_COUNT 4 +#define JSON_BUFFER_SIZE 6144 + struct INVERTER_CONFIG_T { uint64_t Serial; char Name[INV_MAX_NAME_STRLEN + 1]; @@ -89,6 +92,9 @@ public: CONFIG_T& get(); INVERTER_CONFIG_T* getFreeInverterSlot(); + +private: + bool readJson(); }; extern ConfigurationClass Configuration; \ No newline at end of file diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 5e30a98..591ec00 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -4,6 +4,7 @@ */ #include "Configuration.h" #include "defaults.h" +#include #include CONFIG_T config; @@ -61,29 +62,215 @@ void ConfigurationClass::init() bool ConfigurationClass::write() { - File f = LittleFS.open(CONFIG_FILENAME, "w"); + File f = LittleFS.open(CONFIG_FILENAME_JSON, "w"); if (!f) { return false; } config.Cfg_SaveCount++; - uint8_t* bytes = reinterpret_cast(&config); - for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) { - f.write(bytes[i]); + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + + JsonObject cfg = doc.createNestedObject("cfg"); + cfg["version"] = config.Cfg_Version; + cfg["save_count"] = config.Cfg_SaveCount; + + JsonObject wifi = doc.createNestedObject("wifi"); + 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; + + JsonObject ntp = doc.createNestedObject("ntp"); + ntp["server"] = config.Ntp_Server; + ntp["timezone"] = config.Ntp_Timezone; + ntp["timezone_descr"] = config.Ntp_TimezoneDescr; + + JsonObject mqtt = doc.createNestedObject("mqtt"); + mqtt["enabled"] = config.Mqtt_Enabled; + mqtt["hostname"] = config.Mqtt_Hostname; + mqtt["port"] = config.Mqtt_Port; + mqtt["username"] = config.Mqtt_Username; + mqtt["password"] = config.Mqtt_Password; + mqtt["topic"] = config.Mqtt_Topic; + mqtt["retain"] = config.Mqtt_Retain; + mqtt["publish_invterval"] = config.Mqtt_PublishInterval; + + JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); + mqtt_lwt["topic"] = config.Mqtt_LwtTopic; + mqtt_lwt["value_online"] = config.Mqtt_LwtValue_Online; + mqtt_lwt["value_offline"] = config.Mqtt_LwtValue_Offline; + + JsonObject mqtt_tls = mqtt.createNestedObject("tls"); + mqtt_tls["enabled"] = config.Mqtt_Tls; + mqtt_tls["root_ca_cert"] = config.Mqtt_RootCaCert; + + JsonObject mqtt_hass = mqtt.createNestedObject("hass"); + 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.createNestedObject("dtu"); + dtu["serial"] = config.Dtu_Serial; + dtu["poll_interval"] = config.Dtu_PollInterval; + dtu["pa_level"] = config.Dtu_PaLevel; + + JsonArray inverters = doc.createNestedArray("inverters"); + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + JsonObject inv = inverters.createNestedObject(); + inv["serial"] = config.Inverter[i].Serial; + inv["name"] = config.Inverter[i].Name; + + JsonArray channels = inv.createNestedArray("channels"); + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + channels.add(config.Inverter[i].MaxChannelPower[c]); + } } + + // Serialize JSON to file + if (serializeJson(doc, f) == 0) { + Serial.println("Failed to write file"); + return false; + } + f.close(); return true; } bool ConfigurationClass::read() { - File f = LittleFS.open(CONFIG_FILENAME, "r"); + if (!LittleFS.exists(CONFIG_FILENAME_JSON)) { + Serial.println("Converting binary config to json... "); + File f = LittleFS.open(CONFIG_FILENAME, "r"); + if (!f) { + return false; + } + uint8_t* bytes = reinterpret_cast(&config); + for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) { + bytes[i] = f.read(); + } + f.close(); + write(); + Serial.println("done"); + LittleFS.remove(CONFIG_FILENAME); + } + return readJson(); +} + +bool ConfigurationClass::readJson() +{ + File f = LittleFS.open(CONFIG_FILENAME_JSON, "r", false); if (!f) { return false; } - uint8_t* bytes = reinterpret_cast(&config); - for (unsigned int i = 0; i < sizeof(CONFIG_T); i++) { - bytes[i] = f.read(); + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, f); + if (error) { + Serial.println(F("Failed to read file, using default configuration")); } + + 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; + + 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)); + + 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_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_invterval"] | MQTT_PUBLISH_INTERVAL; + + JsonObject mqtt_lwt = mqtt["lwt"]; + strlcpy(config.Mqtt_LwtTopic, mqtt_lwt["topic"] | MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); + strlcpy(config.Mqtt_LwtValue_Online, mqtt_lwt["value_online"] | MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online)); + strlcpy(config.Mqtt_LwtValue_Offline, mqtt_lwt["value_offline"] | MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline)); + + JsonObject mqtt_tls = mqtt["tls"]; + config.Mqtt_Tls = mqtt_tls["enabled"] | MQTT_TLS; + strlcpy(config.Mqtt_RootCaCert, mqtt_tls["root_ca_cert"] | MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert)); + + 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_PaLevel = dtu["pa_level"] | DTU_PA_LEVEL; + + JsonArray inverters = doc["inverters"]; + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + JsonObject inv = inverters[i].as(); + config.Inverter[i].Serial = inv["serial"] | 0ULL; + strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name)); + + JsonArray channels = inv["channels"]; + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + config.Inverter[i].MaxChannelPower[c] = channels[c]; + } + } + f.close(); return true; } diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 7bda713..e437643 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -32,7 +32,7 @@ void WebApiConfigClass::loop() void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) { - request->send(LittleFS, CONFIG_FILENAME, String(), true); + request->send(LittleFS, CONFIG_FILENAME_JSON, String(), true); } void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) @@ -87,7 +87,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) response->setLength(); request->send(response); - LittleFS.remove(CONFIG_FILENAME); + LittleFS.remove(CONFIG_FILENAME_JSON); ESP.restart(); } @@ -110,7 +110,7 @@ void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String fi { if (!index) { // open the file on first call and store the file handle in the request object - request->_tempFile = LittleFS.open(CONFIG_FILENAME, "w"); + request->_tempFile = LittleFS.open(CONFIG_FILENAME_JSON, "w"); } if (len) { diff --git a/webapp/src/components/ConfigAdminView.vue b/webapp/src/components/ConfigAdminView.vue index 04d15ca..9055cc1 100644 --- a/webapp/src/components/ConfigAdminView.vue +++ b/webapp/src/components/ConfigAdminView.vue @@ -55,7 +55,7 @@
- +
@@ -175,7 +175,7 @@ export default defineComponent({ downloadConfig() { const link = document.createElement('a') link.href = "/api/config/get" - link.download = 'config.bin' + link.download = 'config.json' link.click() }, uploadConfig(event: Event | null) {