From 5f55414c0a110d2a7c21b689c750b413509d23de Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 14 Nov 2022 22:16:56 +0100 Subject: [PATCH 01/17] Show Irradiation only in Live Data View if configured for specific channel --- src/WebApi_ws_live.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 2c6f9c8b..cc5e0bf5 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -119,7 +119,9 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) addField(invObject, i, inv, c, FLD_PF); addField(invObject, i, inv, c, FLD_PRA); addField(invObject, i, inv, c, FLD_EFF); - addField(invObject, i, inv, c, FLD_IRR); + if (c > 0 && inv->Statistics()->getChannelMaxPower(c - 1) > 0) { + addField(invObject, i, inv, c, FLD_IRR); + } } if (inv->Statistics()->hasChannelFieldValue(CH0, FLD_EVT_LOG)) { From daf847e7b322974f33f0ac510a51928b2201d759 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 14 Nov 2022 22:24:16 +0100 Subject: [PATCH 02/17] BREAKING CHANGE: Removed deprecated config parsing method After this commit its not possible to migrate from the old binary blob config to the new json based config!! If you still running a old version before 12. October please upgrade to a version before this commit. See https://github.com/tbnobody/OpenDTU/discussions/285 --- include/Configuration.h | 8 +-- src/Configuration.cpp | 145 +--------------------------------------- src/WebApi_config.cpp | 6 +- 3 files changed, 6 insertions(+), 153 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index ee3754c6..c2fc6d3c 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -3,8 +3,7 @@ #include -#define CONFIG_FILENAME "/config.bin" -#define CONFIG_FILENAME_JSON "/config.json" +#define CONFIG_FILENAME "/config.json" #define CONFIG_VERSION 0x00011600 // 0.1.22 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 31 @@ -15,7 +14,6 @@ #define NTP_MAX_TIMEZONE_STRLEN 50 #define NTP_MAX_TIMEZONEDESCR_STRLEN 50 -#define MQTT_MAX_HOSTNAME_OLD_STRLEN 31 #define MQTT_MAX_HOSTNAME_STRLEN 128 #define MQTT_MAX_USERNAME_STRLEN 32 #define MQTT_MAX_PASSWORD_STRLEN 32 @@ -54,7 +52,6 @@ struct CONFIG_T { char Ntp_TimezoneDescr[NTP_MAX_TIMEZONEDESCR_STRLEN + 1]; bool Mqtt_Enabled; - char Mqtt_Hostname_Short[MQTT_MAX_HOSTNAME_OLD_STRLEN + 1]; // Deprecated but for config compatibility uint Mqtt_Port; char Mqtt_Username[MQTT_MAX_USERNAME_STRLEN + 1]; char Mqtt_Password[MQTT_MAX_PASSWORD_STRLEN + 1]; @@ -94,9 +91,6 @@ 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 d9dbcd11..bfd14607 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -12,59 +12,11 @@ CONFIG_T config; void ConfigurationClass::init() { memset(&config, 0x0, sizeof(config)); - config.Cfg_SaveCount = 0; - config.Cfg_Version = CONFIG_VERSION; - - // WiFi Settings - strlcpy(config.WiFi_Ssid, WIFI_SSID, sizeof(config.WiFi_Ssid)); - strlcpy(config.WiFi_Password, WIFI_PASSWORD, sizeof(config.WiFi_Password)); - config.WiFi_Dhcp = WIFI_DHCP; - strlcpy(config.WiFi_Hostname, APP_HOSTNAME, sizeof(config.WiFi_Hostname)); - - // NTP Settings - strlcpy(config.Ntp_Server, NTP_SERVER, sizeof(config.Ntp_Server)); - strlcpy(config.Ntp_Timezone, NTP_TIMEZONE, sizeof(config.Ntp_Timezone)); - strlcpy(config.Ntp_TimezoneDescr, NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr)); - - // MqTT Settings - config.Mqtt_Enabled = MQTT_ENABLED; - strlcpy(config.Mqtt_Hostname, MQTT_HOST, sizeof(config.Mqtt_Hostname)); - config.Mqtt_Port = MQTT_PORT; - strlcpy(config.Mqtt_Username, MQTT_USER, sizeof(config.Mqtt_Username)); - strlcpy(config.Mqtt_Password, MQTT_PASSWORD, sizeof(config.Mqtt_Password)); - strlcpy(config.Mqtt_Topic, MQTT_TOPIC, sizeof(config.Mqtt_Topic)); - config.Mqtt_Retain = MQTT_RETAIN; - config.Mqtt_Tls = MQTT_TLS; - strlcpy(config.Mqtt_RootCaCert, MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert)); - strlcpy(config.Mqtt_LwtTopic, MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); - strlcpy(config.Mqtt_LwtValue_Online, MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online)); - strlcpy(config.Mqtt_LwtValue_Offline, MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline)); - config.Mqtt_PublishInterval = MQTT_PUBLISH_INTERVAL; - - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - config.Inverter[i].Serial = 0; - strlcpy(config.Inverter[i].Name, "", 0); - for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - config.Inverter[0].MaxChannelPower[c] = 0; - } - } - - config.Dtu_Serial = DTU_SERIAL; - config.Dtu_PollInterval = DTU_POLL_INTERVAL; - config.Dtu_PaLevel = DTU_PA_LEVEL; - - config.Mqtt_Hass_Enabled = MQTT_HASS_ENABLED; - config.Mqtt_Hass_Expire = MQTT_HASS_EXPIRE; - config.Mqtt_Hass_Retain = MQTT_HASS_RETAIN; - strlcpy(config.Mqtt_Hass_Topic, MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic)); - config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS; - - strlcpy(config.Security_Password, ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); } bool ConfigurationClass::write() { - File f = LittleFS.open(CONFIG_FILENAME_JSON, "w"); + File f = LittleFS.open(CONFIG_FILENAME, "w"); if (!f) { return false; } @@ -150,30 +102,7 @@ bool ConfigurationClass::write() bool ConfigurationClass::read() { - 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; - } + File f = LittleFS.open(CONFIG_FILENAME, "r", false); DynamicJsonDocument doc(JSON_BUFFER_SIZE); // Deserialize the JSON document @@ -285,76 +214,6 @@ bool ConfigurationClass::readJson() void ConfigurationClass::migrate() { - if (config.Cfg_Version < 0x00010400) { - strlcpy(config.Ntp_Server, NTP_SERVER, sizeof(config.Ntp_Server)); - strlcpy(config.Ntp_Timezone, NTP_TIMEZONE, sizeof(config.Ntp_Timezone)); - strlcpy(config.Ntp_TimezoneDescr, NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr)); - } - - if (config.Cfg_Version < 0x00010500) { - config.Mqtt_Enabled = MQTT_ENABLED; - strlcpy(config.Mqtt_Hostname, MQTT_HOST, sizeof(config.Mqtt_Hostname)); - config.Mqtt_Port = MQTT_PORT; - strlcpy(config.Mqtt_Username, MQTT_USER, sizeof(config.Mqtt_Username)); - strlcpy(config.Mqtt_Password, MQTT_PASSWORD, sizeof(config.Mqtt_Password)); - strlcpy(config.Mqtt_Topic, MQTT_TOPIC, sizeof(config.Mqtt_Topic)); - } - - if (config.Cfg_Version < 0x00010600) { - config.Mqtt_Retain = MQTT_RETAIN; - } - - if (config.Cfg_Version < 0x00010700) { - strlcpy(config.Mqtt_LwtTopic, MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); - strlcpy(config.Mqtt_LwtValue_Online, MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online)); - strlcpy(config.Mqtt_LwtValue_Offline, MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline)); - } - - if (config.Cfg_Version < 0x00010800) { - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - config.Inverter[i].Serial = 0; - strlcpy(config.Inverter[i].Name, "", 0); - } - } - - if (config.Cfg_Version < 0x00010900) { - config.Dtu_Serial = DTU_SERIAL; - config.Dtu_PollInterval = DTU_POLL_INTERVAL; - config.Dtu_PaLevel = DTU_PA_LEVEL; - } - - if (config.Cfg_Version < 0x00011000) { - config.Mqtt_PublishInterval = MQTT_PUBLISH_INTERVAL; - } - - if (config.Cfg_Version < 0x00011100) { - init(); // Config will be completly incompatible after this update - } - - if (config.Cfg_Version < 0x00011200) { - config.Mqtt_Hass_Enabled = MQTT_HASS_ENABLED; - config.Mqtt_Hass_Retain = MQTT_HASS_RETAIN; - strlcpy(config.Mqtt_Hass_Topic, MQTT_HASS_TOPIC, sizeof(config.Mqtt_Hass_Topic)); - config.Mqtt_Hass_IndividualPanels = MQTT_HASS_INDIVIDUALPANELS; - } - - if (config.Cfg_Version < 0x00011300) { - config.Mqtt_Tls = MQTT_TLS; - strlcpy(config.Mqtt_RootCaCert, MQTT_ROOT_CA_CERT, sizeof(config.Mqtt_RootCaCert)); - } - - if (config.Cfg_Version < 0x00011400) { - strlcpy(config.Mqtt_Hostname, config.Mqtt_Hostname_Short, sizeof(config.Mqtt_Hostname_Short)); - } - - if (config.Cfg_Version < 0x00011500) { - config.Mqtt_Hass_Expire = MQTT_HASS_EXPIRE; - } - - if (config.Cfg_Version < 0x00011600) { - strlcpy(config.Security_Password, ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); - } - config.Cfg_Version = CONFIG_VERSION; write(); } diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index fe522652..84e6af95 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -37,7 +37,7 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) return; } - request->send(LittleFS, CONFIG_FILENAME_JSON, String(), true); + request->send(LittleFS, CONFIG_FILENAME, String(), true); } void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) @@ -96,7 +96,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) response->setLength(); request->send(response); - LittleFS.remove(CONFIG_FILENAME_JSON); + LittleFS.remove(CONFIG_FILENAME); ESP.restart(); } @@ -127,7 +127,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_JSON, "w"); + request->_tempFile = LittleFS.open(CONFIG_FILENAME, "w"); } if (len) { From 0c46ecf121cfb9d3d55bbeddd1a031c6cb3f0153 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 14 Nov 2022 22:32:24 +0100 Subject: [PATCH 03/17] Fix #302: Allow longer MQTT usernames and passwords Changed max length from 32 to 64 characters --- include/Configuration.h | 4 ++-- webapp/src/views/MqttAdminView.vue | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index c2fc6d3c..7db930a5 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -15,8 +15,8 @@ #define NTP_MAX_TIMEZONEDESCR_STRLEN 50 #define MQTT_MAX_HOSTNAME_STRLEN 128 -#define MQTT_MAX_USERNAME_STRLEN 32 -#define MQTT_MAX_PASSWORD_STRLEN 32 +#define MQTT_MAX_USERNAME_STRLEN 64 +#define MQTT_MAX_PASSWORD_STRLEN 64 #define MQTT_MAX_TOPIC_STRLEN 32 #define MQTT_MAX_LWTVALUE_STRLEN 20 #define MQTT_MAX_ROOT_CA_CERT_STRLEN 2048 diff --git a/webapp/src/views/MqttAdminView.vue b/webapp/src/views/MqttAdminView.vue index b2f62d96..76941294 100644 --- a/webapp/src/views/MqttAdminView.vue +++ b/webapp/src/views/MqttAdminView.vue @@ -55,7 +55,7 @@
-
@@ -64,7 +64,7 @@
-
From d28fadbdac7331fcd9816d617b1853dd44a805a4 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 15 Nov 2022 19:31:31 +0100 Subject: [PATCH 04/17] Extended configuration to allow string names * Current config will be migrated to new format * Already extended web API to get/post new format --- include/Configuration.h | 11 ++++++-- src/Configuration.cpp | 38 +++++++++++++++++++++++--- src/WebApi_inverter.cpp | 20 ++++++++------ src/main.cpp | 2 +- webapp/src/views/InverterAdminView.vue | 11 ++++++-- 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 7db930a5..bda8d4d0 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -4,7 +4,7 @@ #include #define CONFIG_FILENAME "/config.json" -#define CONFIG_VERSION 0x00011600 // 0.1.22 // make sure to clean all after change +#define CONFIG_VERSION 0x00011700 // 0.1.23 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 31 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -25,12 +25,19 @@ #define INV_MAX_COUNT 10 #define INV_MAX_CHAN_COUNT 4 +#define CHAN_MAX_NAME_STRLEN 31 + #define JSON_BUFFER_SIZE 6144 +struct CHANNEL_CONFIG_T { + uint16_t MaxChannelPower; + char Name[CHAN_MAX_NAME_STRLEN]; +}; + struct INVERTER_CONFIG_T { uint64_t Serial; char Name[INV_MAX_NAME_STRLEN + 1]; - uint16_t MaxChannelPower[INV_MAX_CHAN_COUNT]; + CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; struct CONFIG_T { diff --git a/src/Configuration.cpp b/src/Configuration.cpp index bfd14607..b7d09e3d 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -84,9 +84,11 @@ bool ConfigurationClass::write() inv["serial"] = config.Inverter[i].Serial; inv["name"] = config.Inverter[i].Name; - JsonArray channels = inv.createNestedArray("channels"); + JsonArray channel = inv.createNestedArray("channel"); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - channels.add(config.Inverter[i].MaxChannelPower[c]); + JsonObject chanData = channel.createNestedObject(); + chanData["name"] = config.Inverter[i].channel[c].Name; + chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; } } @@ -202,9 +204,10 @@ bool ConfigurationClass::read() config.Inverter[i].Serial = inv["serial"] | 0ULL; strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name)); - JsonArray channels = inv["channels"]; + JsonArray channel = inv["channel"]; for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - config.Inverter[i].MaxChannelPower[c] = channels[c]; + config.Inverter[i].channel[c].MaxChannelPower = channel[c]["max_power"] | 0; + strlcpy(config.Inverter[i].channel[c].Name, channel[c]["name"] | "", sizeof(config.Inverter[i].channel[c].Name)); } } @@ -214,8 +217,35 @@ bool ConfigurationClass::read() void ConfigurationClass::migrate() { + if (config.Cfg_Version < 0x00011700) { + File f = LittleFS.open(CONFIG_FILENAME, "r", false); + if (!f) { + Serial.println(F("Failed to open file, cancel migration")); + return; + } + + DynamicJsonDocument doc(JSON_BUFFER_SIZE); + // Deserialize the JSON document + DeserializationError error = deserializeJson(doc, f); + if (error) { + Serial.println(F("Failed to read file, cancel migration")); + return; + } + + JsonArray inverters = doc["inverters"]; + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + JsonObject inv = inverters[i].as(); + 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)); + } + } + } + config.Cfg_Version = CONFIG_VERSION; write(); + read(); } CONFIG_T& ConfigurationClass::get() diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 2fb254f9..a99fe7c2 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -62,8 +62,11 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) max_channels = inv->Statistics()->getChannelCount(); } + JsonArray channel = obj.createNestedArray("channel"); for (uint8_t c = 0; c < max_channels; c++) { - obj[F("max_power")][c] = config.Inverter[i].MaxChannelPower[c]; + JsonObject chanData = channel.createNestedObject(); + chanData["name"] = config.Inverter[i].channel[c].Name; + chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; } } } @@ -154,7 +157,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (inv != nullptr) { for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - inv->Statistics()->setChannelMaxPower(c, inverter->MaxChannelPower[c]); + inv->Statistics()->setChannelMaxPower(c, inverter->channel[c].MaxChannelPower); } } @@ -197,7 +200,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) return; } - if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("max_power"))) { + if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) { retMsg[F("message")] = F("Values are missing!"); response->setLength(); request->send(response); @@ -225,8 +228,8 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) return; } - JsonArray maxPowerArray = root[F("max_power")].as(); - if (maxPowerArray.size() == 0 || maxPowerArray.size() > INV_MAX_CHAN_COUNT) { + JsonArray channelArray = root[F("channel")].as(); + if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) { retMsg[F("message")] = F("Invalid amount of max channel setting given!"); response->setLength(); request->send(response); @@ -243,8 +246,9 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) strncpy(inverter.Name, root[F("name")].as().c_str(), INV_MAX_NAME_STRLEN); uint8_t arrayCount = 0; - for (JsonVariant maxPower : maxPowerArray) { - inverter.MaxChannelPower[arrayCount] = maxPower.as(); + for (JsonVariant channel : channelArray) { + inverter.channel[arrayCount].MaxChannelPower = channel[F("max_power")].as(); + strncpy(inverter.channel[arrayCount].Name, channel[F("name")] | "", sizeof(inverter.channel[arrayCount].Name)); arrayCount++; } @@ -272,7 +276,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (inv != nullptr) { for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - inv->Statistics()->setChannelMaxPower(c, inverter.MaxChannelPower[c]); + inv->Statistics()->setChannelMaxPower(c, inverter.channel[c].MaxChannelPower); } } diff --git a/src/main.cpp b/src/main.cpp index 6004b442..24102c19 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -116,7 +116,7 @@ void setup() if (inv != nullptr) { for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - inv->Statistics()->setChannelMaxPower(c, config.Inverter[i].MaxChannelPower[c]); + inv->Statistics()->setChannelMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); } } Serial.println(F(" done")); diff --git a/webapp/src/views/InverterAdminView.vue b/webapp/src/views/InverterAdminView.vue index 1efed4c8..2129b526 100644 --- a/webapp/src/views/InverterAdminView.vue +++ b/webapp/src/views/InverterAdminView.vue @@ -81,12 +81,12 @@ class="form-control" maxlength="31" />
-
+
W*
@@ -139,12 +139,17 @@ import * as bootstrap from 'bootstrap'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import { handleResponse, authHeader } from '@/utils/authentication'; +declare interface Channel { + name: string; + max_power: number; +} + declare interface Inverter { id: string; serial: number; name: string; type: string; - max_power: number[]; + channel: Array; } declare interface AlertResponse { From d0d8be9c0540b8d36d64918a0993bf5fb904cbe0 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 15 Nov 2022 19:55:04 +0100 Subject: [PATCH 05/17] webapp: Added input fields to InverterAdminView to enter channel names --- webapp/src/views/InverterAdminView.vue | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/webapp/src/views/InverterAdminView.vue b/webapp/src/views/InverterAdminView.vue index 2129b526..893d54b8 100644 --- a/webapp/src/views/InverterAdminView.vue +++ b/webapp/src/views/InverterAdminView.vue @@ -73,15 +73,22 @@