From 9dd24c9f8717999ec782f7b20ae6728415d163d5 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 25 Dec 2023 23:38:41 +0100 Subject: [PATCH 01/27] webapp: update dependencies --- webapp/package.json | 2 +- webapp/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index 60e57e94..6ad1bd90 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -30,7 +30,7 @@ "@types/node": "^20.10.5", "@types/sortablejs": "^1.15.7", "@types/spark-md5": "^3.0.4", - "@vitejs/plugin-vue": "^4.5.2", + "@vitejs/plugin-vue": "^5.0.0", "@vue/eslint-config-typescript": "^12.0.0", "@vue/tsconfig": "^0.5.1", "eslint": "^8.56.0", diff --git a/webapp/yarn.lock b/webapp/yarn.lock index b3b54b48..74d7e343 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -547,10 +547,10 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vitejs/plugin-vue@^4.5.2": - version "4.5.2" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.5.2.tgz#1212d81bc83680e14448fefe55abd9fe1ed49ed1" - integrity sha512-UGR3DlzLi/SaVBPX0cnSyE37vqxU3O6chn8l0HJNzQzDia6/Au2A4xKv+iIJW8w2daf80G7TYHhi1pAUjdZ0bQ== +"@vitejs/plugin-vue@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.0.tgz#8e8946548e6dfd9eb0391bbc282ec8e52103e0dd" + integrity sha512-7x5e8X4J1Wi4NxudGjJBd2OFerAi/0nzF80ojCzvfj347WVr0YSn82C8BSsgwSHzlk9Kw5xnZfj0/7RLnNwP5w== "@volar/language-core@1.11.1", "@volar/language-core@~1.11.1": version "1.11.1" From 5b39c1803ef842785f7f3c038e39bf3409012625 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 26 Dec 2023 12:52:21 +0100 Subject: [PATCH 02/27] Feature: Added latest grid profile information from wiki --- lib/Hoymiles/src/parser/GridProfileParser.cpp | 72 +++++++++++++------ lib/Hoymiles/src/parser/GridProfileParser.h | 4 +- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp index da1c8060..b6c1573b 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.cpp +++ b/lib/Hoymiles/src/parser/GridProfileParser.cpp @@ -9,13 +9,16 @@ #include const std::array GridProfileParser::_profileTypes = { { - { 0x02, 0x00, "no data (yet)" }, - { 0x03, 0x00, "Germany - DE_VDE4105_2018" }, - { 0x0a, 0x00, "European - EN 50549-1:2019" }, - { 0x0c, 0x00, "AT Tor - EU_EN50438" }, - { 0x0d, 0x04, "France" }, - { 0x12, 0x00, "Poland - EU_EN50438" }, - { 0x37, 0x00, "Swiss - CH_NA EEA-NE7-CH2020" }, + { 0x02, 0x00, "US - NA_IEEE1547_240V" }, + { 0x03, 0x00, "DE - DE_VDE4105_2018" }, + { 0x03, 0x01, "XX - unknown" }, + { 0x0a, 0x00, "XX - EN 50549-1:2019" }, + { 0x0c, 0x00, "AT - AT_TOR_Erzeuger_default" }, + { 0x0d, 0x04, "FR -" }, + { 0x10, 0x00, "ES - ES_RD1699" }, + { 0x12, 0x00, "PL - EU_EN50438" }, + { 0x29, 0x00, "NL - NL_NEN-EN50549-1_2019" }, + { 0x37, 0x00, "CH - CH_NA EEA-NE7-CH2020" }, } }; constexpr frozen::map profileSection = { @@ -45,19 +48,19 @@ constexpr GridProfileItemDefinition_t make_value(frozen::string Name, frozen::st return v; } -constexpr frozen::map itemDefinitions = { +constexpr frozen::map itemDefinitions = { { 0x01, make_value("Nominale Voltage (NV)", "V", 10) }, { 0x02, make_value("Low Voltage 1 (LV1)", "V", 10) }, { 0x03, make_value("LV1 Maximum Trip Time (MTT)", "s", 10) }, { 0x04, make_value("High Voltage 1 (HV1)", "V", 10) }, { 0x05, make_value("HV1 Maximum Trip Time (MTT)", "s", 10) }, { 0x06, make_value("Low Voltage 2 (LV2)", "V", 10) }, - { 0x07, make_value("LV2 Maximum Trip Time (MTT)", "s", 10) }, + { 0x07, make_value("LV2 Maximum Trip Time (MTT)", "s", 100) }, { 0x08, make_value("High Voltage 2 (HV2)", "V", 10) }, - { 0x09, make_value("HV2 Maximum Trip Time (MTT)", "s", 10) }, + { 0x09, make_value("HV2 Maximum Trip Time (MTT)", "s", 100) }, { 0x0a, make_value("10mins Average High Voltage (AHV)", "V", 10) }, { 0x0b, make_value("High Voltage 3 (HV3)", "V", 10) }, - { 0x0c, make_value("HV3 Maximum Trip Time (MTT)", "s", 10) }, + { 0x0c, make_value("HV3 Maximum Trip Time (MTT)", "s", 100) }, { 0x0d, make_value("Nominal Frequency", "Hz", 100) }, { 0x0e, make_value("Low Frequency 1 (LF1)", "Hz", 100) }, { 0x0f, make_value("LF1 Maximum Trip Time (MTT)", "s", 10) }, @@ -94,7 +97,7 @@ constexpr frozen::map itemDefinition { 0x2e, make_value("Voltage Set Point V3", "V", 10) }, { 0x2f, make_value("Voltage Set Point V4", "V", 10) }, { 0x30, make_value("Reactive Set Point Q4", "%Pn", 10) }, - { 0x31, make_value("Setting Time (Tr)", "s", 10) }, + { 0x31, make_value("VV Setting Time (Tr)", "s", 10) }, { 0x32, make_value("SPF Function Activated", "bool", 1) }, { 0x33, make_value("Power Factor (PF)", "", 100) }, { 0x34, make_value("RPC Function Activated", "bool", 1) }, @@ -102,6 +105,15 @@ constexpr frozen::map itemDefinition { 0x36, make_value("WPF Function Activated", "bool", 1) }, { 0x37, make_value("Start of Power of WPF (Pstart)", "%Pn", 10) }, { 0x38, make_value("Power Factor ar Rated Power (PFRP)", "", 100) }, + { 0x39, make_value("Low Voltage 3 (LV3)", "V", 10) }, + { 0x3a, make_value("LV3 Maximum Trip Time (MTT)", "s", 100) }, + { 0x3b, make_value("Momentary Cessition Low Voltage", "V", 10) }, + { 0x3c, make_value("Momentary Cessition High Voltage", "V", 10) }, + { 0x3d, make_value("FW Settling Time (Tr)", "s", 10) }, + { 0x3e, make_value("LF2 Maximum Trip Time (MTT)", "s", 100) }, + { 0x3f, make_value("HF2 Maximum Trip time (MTT)", "s", 100) }, + { 0x40, make_value("Short Interruption Reconnect Time (SRT)", "s", 10) }, + { 0x41, make_value("Short Interruption Time (SIT)", "s", 10) }, { 0xff, make_value("Unkown Value", "", 1) }, }; @@ -114,6 +126,24 @@ const std::array GridProfileParse { 0x00, 0x00, 0x04 }, { 0x00, 0x00, 0x05 }, + // Version 0x01 + { 0x00, 0x01, 0x01 }, + { 0x00, 0x01, 0x02 }, + { 0x00, 0x01, 0x03 }, + { 0x00, 0x01, 0x04 }, + { 0x00, 0x01, 0x05 }, + { 0x00, 0x01, 0x08 }, + { 0x00, 0x01, 0x09 }, + + // Version 0x02 + { 0x00, 0x02, 0x01 }, + { 0x00, 0x02, 0x02 }, + { 0x00, 0x02, 0x03 }, + { 0x00, 0x02, 0x04 }, + { 0x00, 0x02, 0x05 }, + { 0x00, 0x02, 0x06 }, + { 0x00, 0x02, 0x07 }, + // Version 0x03 { 0x00, 0x03, 0x01 }, { 0x00, 0x03, 0x02 }, @@ -178,10 +208,10 @@ const std::array GridProfileParse { 0x00, 0x35, 0x07 }, { 0x00, 0x35, 0x08 }, { 0x00, 0x35, 0x09 }, - { 0x00, 0x35, 0xff }, - { 0x00, 0x35, 0xff }, - { 0x00, 0x35, 0xff }, - { 0x00, 0x35, 0xff }, + { 0x00, 0x35, 0x39 }, + { 0x00, 0x35, 0x3a }, + { 0x00, 0x35, 0x3b }, + { 0x00, 0x35, 0x3c }, // Frequency (H/LFRT) // Version 0x00 @@ -198,9 +228,9 @@ const std::array GridProfileParse { 0x10, 0x03, 0x10 }, { 0x10, 0x03, 0x11 }, { 0x10, 0x03, 0x12 }, - { 0x10, 0x03, 0x13 }, + { 0x10, 0x03, 0x3e }, { 0x10, 0x03, 0x14 }, - { 0x10, 0x03, 0x15 }, + { 0x10, 0x03, 0x3f }, // Island Detection (ID) // Version 0x00 @@ -220,8 +250,8 @@ const std::array GridProfileParse { 0x30, 0x07, 0x19 }, { 0x30, 0x07, 0x1a }, { 0x30, 0x07, 0x1b }, - { 0x30, 0x07, 0xff }, - { 0x30, 0x07, 0xff }, + { 0x30, 0x07, 0x40 }, + { 0x30, 0x07, 0x41 }, // Ramp Rates (RR) // Version 0x00 @@ -255,7 +285,7 @@ const std::array GridProfileParse { 0x50, 0x11, 0x1f }, { 0x50, 0x11, 0x20 }, { 0x50, 0x11, 0x21 }, - { 0x50, 0x11, 0x22 }, + { 0x50, 0x11, 0x3d }, // Volt Watt (VW) // Version 0x00 diff --git a/lib/Hoymiles/src/parser/GridProfileParser.h b/lib/Hoymiles/src/parser/GridProfileParser.h index 031891f3..1be12e1d 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.h +++ b/lib/Hoymiles/src/parser/GridProfileParser.h @@ -4,8 +4,8 @@ #include #define GRID_PROFILE_SIZE 141 -#define PROFILE_TYPE_COUNT 7 -#define SECTION_VALUE_COUNT 144 +#define PROFILE_TYPE_COUNT 10 +#define SECTION_VALUE_COUNT 158 typedef struct { uint8_t lIdx; From 34983471dee6474c17edcde1346a791e778486ba Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 28 Dec 2023 00:10:40 +0100 Subject: [PATCH 03/27] Simplfy diagram drawing code --- src/Display_Graphic_Diagram.cpp | 43 +++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/Display_Graphic_Diagram.cpp b/src/Display_Graphic_Diagram.cpp index 489f295f..0fd13618 100644 --- a/src/Display_Graphic_Diagram.cpp +++ b/src/Display_Graphic_Diagram.cpp @@ -62,20 +62,28 @@ void DisplayGraphicDiagramClass::updatePeriod() void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX) { - const uint8_t graphPosX = DIAG_POSX + ((screenSaverOffsetX > 3) ? 1 : 0); // screenSaverOffsetX expected to be in range 0..6 + // screenSaverOffsetX expected to be in range 0..6 + const uint8_t graphPosX = DIAG_POSX + ((screenSaverOffsetX > 3) ? 1 : 0); const uint8_t graphPosY = DIAG_POSY + ((screenSaverOffsetX > 3) ? 1 : 0); + const uint8_t horizontal_line_y = graphPosY + CHART_HEIGHT - 1; + const uint8_t arrow_size = 2; + // draw diagram axis _display->drawVLine(graphPosX, graphPosY, CHART_HEIGHT); - _display->drawHLine(graphPosX, graphPosY + CHART_HEIGHT - 1, CHART_WIDTH); + _display->drawHLine(graphPosX, horizontal_line_y, CHART_WIDTH); - _display->drawLine(graphPosX + 1, graphPosY + 1, graphPosX + 2, graphPosY + 2); // UP-arrow - _display->drawLine(graphPosX - 2, graphPosY + 2, graphPosX - 1, graphPosY + 1); // UP-arrow - _display->drawLine(graphPosX + CHART_WIDTH - 3, graphPosY + CHART_HEIGHT - 3, graphPosX + CHART_WIDTH - 2, graphPosY + CHART_HEIGHT - 2); // LEFT-arrow - _display->drawLine(graphPosX + CHART_WIDTH - 3, graphPosY + CHART_HEIGHT + 1, graphPosX + CHART_WIDTH - 2, graphPosY + CHART_HEIGHT); // LEFT-arrow + // UP-arrow + _display->drawLine(graphPosX, graphPosY, graphPosX + arrow_size, graphPosY + arrow_size); + _display->drawLine(graphPosX, graphPosY, graphPosX - arrow_size, graphPosY + arrow_size); + + // LEFT-arrow + _display->drawLine(graphPosX + CHART_WIDTH - 1, horizontal_line_y, graphPosX + CHART_WIDTH - 1 - arrow_size, horizontal_line_y - arrow_size); + _display->drawLine(graphPosX + CHART_WIDTH - 1, horizontal_line_y, graphPosX + CHART_WIDTH - 1 - arrow_size, horizontal_line_y + arrow_size); // draw AC value - _display->setFont(u8g2_font_tom_thumb_4x6_mr); // 4 pixels per char + // 4 pixels per char + _display->setFont(u8g2_font_tom_thumb_4x6_mr); char fmtText[7]; const float maxWatts = *std::max_element(_graphValues.begin(), _graphValues.end()); if (maxWatts > 999) { @@ -84,25 +92,24 @@ void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX) snprintf(fmtText, sizeof(fmtText), "%dW", static_cast(maxWatts)); } const uint8_t textLength = strlen(fmtText); - const uint8_t space_and_arrow_pixels = 2; - _display->drawStr(graphPosX - space_and_arrow_pixels - (textLength * 4), graphPosY + 5, fmtText); + _display->drawStr(graphPosX - arrow_size - textLength * 4, graphPosY + 5, fmtText); // draw chart const float scaleFactor = maxWatts / CHART_HEIGHT; uint8_t axisTick = 1; - for (int i = 0; i < _graphValuesCount; i++) { - if (scaleFactor > 0) { - if (i == 0) { - _display->drawPixel(graphPosX + 1 + i, graphPosY + CHART_HEIGHT - ((_graphValues[i] / scaleFactor) + 0.5)); // + 0.5 to round mathematical - } else { - _display->drawLine(graphPosX + i, graphPosY + CHART_HEIGHT - ((_graphValues[i - 1] / scaleFactor) + 0.5), graphPosX + 1 + i, graphPosY + CHART_HEIGHT - ((_graphValues[i] / scaleFactor) + 0.5)); - } - } - + for (uint8_t i = 1; i < _graphValuesCount; i++) { // draw one tick per hour to the x-axis if (i * getSecondsPerDot() > (3600u * axisTick)) { _display->drawPixel(graphPosX + 1 + i, graphPosY + CHART_HEIGHT); axisTick++; } + + if (scaleFactor == 0) { + continue; + } + + _display->drawLine( + graphPosX + i - 1, horizontal_line_y - std::max(0, _graphValues[i - 1] / scaleFactor - 0.5), + graphPosX + i, horizontal_line_y - std::max(0, _graphValues[i] / scaleFactor - 0.5)); } } From 32fff131c8e61e6ef4eea7e0fab12f532f18b719 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 28 Dec 2023 14:06:34 +0100 Subject: [PATCH 04/27] Update espressif32 from 6.3.2 to 6.5.0 --- platformio.ini | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 945ab8b7..b84fa1de 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,12 +19,16 @@ extra_configs = custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb framework = arduino -platform = espressif32@6.3.2 +platform = espressif32@6.5.0 build_flags = -DPIOENV=\"$PIOENV\" -D_TASK_STD_FUNCTION=1 - -Wall -Wextra -Werror -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference + -Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference +; Have to remove -Werror because of +; https://github.com/espressif/arduino-esp32/issues/9044 and +; https://github.com/espressif/arduino-esp32/issues/9045 +; -Werror -std=c++17 -std=gnu++17 build_unflags = From 92504875bf7b5f38b0363476c3c05eb8dab6fddf Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 28 Dec 2023 14:08:27 +0100 Subject: [PATCH 05/27] webapp: update dependencies --- webapp/package.json | 2 +- webapp/yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index 6ad1bd90..91bfb181 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -42,6 +42,6 @@ "vite": "^5.0.10", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.3.1", - "vue-tsc": "^1.8.26" + "vue-tsc": "^1.8.27" } } diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 74d7e343..904b3667 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -690,10 +690,10 @@ "@typescript-eslint/parser" "^6.7.0" vue-eslint-parser "^9.3.1" -"@vue/language-core@1.8.26": - version "1.8.26" - resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-1.8.26.tgz#7edb6b51a6ed57b618928500c3cbda9757a9f5f0" - integrity sha512-9cmza/Y2YTiOnKZ0Mi9zsNn7Irw+aKirP+5LLWVSNaL3fjKJjW1cD3HGBckasY2RuVh4YycvdA9/Q6EBpVd/7Q== +"@vue/language-core@1.8.27": + version "1.8.27" + resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-1.8.27.tgz#2ca6892cb524e024a44e554e4c55d7a23e72263f" + integrity sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA== dependencies: "@volar/language-core" "~1.11.1" "@volar/source-map" "~1.11.1" @@ -2605,13 +2605,13 @@ vue-template-compiler@^2.7.14: de-indent "^1.0.2" he "^1.2.0" -vue-tsc@^1.8.26: - version "1.8.26" - resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.8.26.tgz#f66abd1dab4e4593590b2b7d4ede0a696882feec" - integrity sha512-jMEJ4aqU/l1hdgmeExH5h1TFoN+hbho0A2ZAhHy53/947DGm7Qj/bpB85VpECOCwV00h7JYNVnvoD2ceOorB4Q== +vue-tsc@^1.8.27: + version "1.8.27" + resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.8.27.tgz#feb2bb1eef9be28017bb9e95e2bbd1ebdd48481c" + integrity sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg== dependencies: "@volar/typescript" "~1.11.1" - "@vue/language-core" "1.8.26" + "@vue/language-core" "1.8.27" semver "^7.5.4" vue@^3.3.13: From 4182e3a071c8ad4abf14dd41df0b0ea5125c2ded Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 28 Dec 2023 14:52:36 +0100 Subject: [PATCH 06/27] Upgrade olikraus/U8g2 from 2.35.8 to 2.35.9 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index b84fa1de..019ee67c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,7 +39,7 @@ lib_deps = bblanchon/ArduinoJson @ ^6.21.4 https://github.com/bertmelis/espMqttClient.git#v1.5.0 nrf24/RF24 @ ^1.4.8 - olikraus/U8g2 @ ^2.35.8 + olikraus/U8g2 @ ^2.35.9 buelowp/sunset @ ^1.1.7 https://github.com/arkhipenko/TaskScheduler#testing From d769cdd30a27a5687c2bac073c1e96859bc6712c Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 29 Dec 2023 10:48:25 +0100 Subject: [PATCH 07/27] Fix: move battery's lock_guard to updateSettings() (#566) the updateSettings method is called from the web server's context and therefore accesses _upProvider in a different context than the TaskScheduler. the lock_guard needs to protect _upProvider. --- src/Battery.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Battery.cpp b/src/Battery.cpp index d68db08e..720d056f 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -26,13 +26,14 @@ void BatteryClass::init(Scheduler& scheduler) _loopTask.setCallback(std::bind(&BatteryClass::loop, this)); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); - std::lock_guard lock(_mutex); this->updateSettings(); } void BatteryClass::updateSettings() { + std::lock_guard lock(_mutex); + if (_upProvider) { _upProvider->deinit(); _upProvider = nullptr; From 03060e453cfc087fb767faa8d6babea4f628e20e Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sat, 30 Dec 2023 10:10:31 +0100 Subject: [PATCH 08/27] remove obsolete save button texts in french locale --- webapp/src/locales/fr.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index dde76f0c..9c31ad6b 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -406,7 +406,6 @@ "ApTimeout": "Délai d'attente du point d'accès", "ApTimeoutHint": "Durée pendant laquelle le point d'accès reste ouvert. Une valeur de 0 signifie infini.", "Minutes": "minutes", - "Save": "@:dtuadmin.Save", "EnableMdns": "Activer mDNS", "MdnsSettings": "mDNS Settings" }, @@ -450,8 +449,7 @@ "HassPrefixTopicHint": "Le préfixe de découverte du sujet", "HassRetain": "Activer du maintien", "HassExpire": "Activer l'expiration", - "HassIndividual": "Panneaux individuels", - "Save": "@:dtuadmin.Save" + "HassIndividual": "Panneaux individuels" }, "inverteradmin": { "InverterSettings": "Paramètres des onduleurs", From aa8068370e30e148954cdaff22a4cd50f400e428 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sat, 30 Dec 2023 12:42:42 +0100 Subject: [PATCH 09/27] Feature: Add support for SSD1309 2.4" Display --- include/Display_Graphic.h | 3 +++ src/Display_Graphic.cpp | 30 ++++++++++++++++-------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/include/Display_Graphic.h b/include/Display_Graphic.h index 1d620e54..70781227 100644 --- a/include/Display_Graphic.h +++ b/include/Display_Graphic.h @@ -11,6 +11,8 @@ enum DisplayType_t { PCD8544, SSD1306, SH1106, + SSD1309, + DisplayType_Max, }; class DisplayGraphicClass { @@ -35,6 +37,7 @@ private: void printText(const char* text, const uint8_t line); void calcLineHeights(); void setFont(const uint8_t line); + bool isValidDisplay(); Task _loopTask; diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 94a0de84..feb4ce80 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -12,6 +12,7 @@ std::map { DisplayType_t::PCD8544, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_PCD8544_84X48_F_4W_HW_SPI(U8G2_R0, cs, data, reset); } }, { DisplayType_t::SSD1306, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); } }, { DisplayType_t::SH1106, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); } }, + { DisplayType_t::SSD1309, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SSD1309_128X64_NONAME0_F_HW_I2C(U8G2_R0, reset, clock, data); } }, }; // Language defintion, respect order in languages[] and translation lists @@ -45,20 +46,20 @@ DisplayGraphicClass::~DisplayGraphicClass() void DisplayGraphicClass::init(Scheduler& scheduler, const DisplayType_t type, const uint8_t data, const uint8_t clk, const uint8_t cs, const uint8_t reset) { _display_type = type; - if (_display_type > DisplayType_t::None) { + if (isValidDisplay()) { auto constructor = display_types[_display_type]; _display = constructor(reset, clk, data, cs); _display->begin(); setContrast(DISPLAY_CONTRAST); setStatus(true); _diagram.init(scheduler, _display); - } - scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&DisplayGraphicClass::loop, this)); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.setInterval(_period); - _loopTask.enable(); + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&DisplayGraphicClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.setInterval(_period); + _loopTask.enable(); + } } void DisplayGraphicClass::calcLineHeights() @@ -86,6 +87,11 @@ void DisplayGraphicClass::setFont(const uint8_t line) } } +bool DisplayGraphicClass::isValidDisplay() +{ + return _display_type > DisplayType_t::None && _display_type < DisplayType_Max; +} + void DisplayGraphicClass::printText(const char* text, const uint8_t line) { uint8_t dispX; @@ -102,7 +108,7 @@ void DisplayGraphicClass::printText(const char* text, const uint8_t line) void DisplayGraphicClass::setOrientation(const uint8_t rotation) { - if (_display_type == DisplayType_t::None) { + if (!isValidDisplay()) { return; } @@ -132,7 +138,7 @@ void DisplayGraphicClass::setLanguage(const uint8_t language) void DisplayGraphicClass::setStartupDisplay() { - if (_display_type == DisplayType_t::None) { + if (!isValidDisplay()) { return; } @@ -148,10 +154,6 @@ DisplayGraphicDiagramClass& DisplayGraphicClass::Diagram() void DisplayGraphicClass::loop() { - if (_display_type == DisplayType_t::None) { - return; - } - _loopTask.setInterval(_period); _display->clearBuffer(); @@ -215,7 +217,7 @@ void DisplayGraphicClass::loop() void DisplayGraphicClass::setContrast(const uint8_t contrast) { - if (_display_type == DisplayType_t::None) { + if (!isValidDisplay()) { return; } _display->setContrast(contrast * 2.55f); From c7098b6c422b8677a2a4dd3533a9c6501c655759 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sat, 30 Dec 2023 16:45:56 +0100 Subject: [PATCH 10/27] Fix: thread-safety and dynamic memory for MessageOutput (#567) this commit re-introduces the changes from #418, which were effectively reverted with d49481097 (merge commit introducing TaskScheduler). these adjustments are important to guarantee unmangled log messages and more importantly, to guarantee that all messages from a particular component are printed to the web console, which most people use to copy messages from when reporting issues. * use dynamic memory to allow handling of arbitrary message lenghts. * keep a message buffer for every task so no task ever mangles the message of another task. * every complete line is written to the serial console and moved to a line buffer for sending them through the websocket. * the websocket is always fed complete lines. * make sure to feed only as many lines as possible to the websocket handler, so that no lines are dropped. * lock all MessageOutput state against concurrent access. * respect HardwareSerial buffer size: the MessageOutput class buffers whole lines of output printed by any task in order to avoid mangling of text. that means we hand over full lines to the HardwareSerial instance, which might be too much in one call to write(buffer, size). we now check the return value of write(buffer, size) and call the function again with the part of the message that could not yet be written by HardwareSerial. --- include/MessageOutput.h | 23 +++++---- src/MessageOutput.cpp | 100 ++++++++++++++++++++++++++++++---------- 2 files changed, 90 insertions(+), 33 deletions(-) diff --git a/include/MessageOutput.h b/include/MessageOutput.h index 94f915a5..fc06f385 100644 --- a/include/MessageOutput.h +++ b/include/MessageOutput.h @@ -2,12 +2,13 @@ #pragma once #include -#include -#include #include +#include +#include #include - -#define BUFFER_SIZE 500 +#include +#include +#include class MessageOutputClass : public Print { public: @@ -21,13 +22,19 @@ private: Task _loopTask; + using message_t = std::vector; + + // we keep a buffer for every task and only write complete lines to the + // serial output and then move them to be pushed through the websocket. + // this way we prevent mangling of messages from different contexts. + std::unordered_map _task_messages; + std::queue _lines; + AsyncWebSocket* _ws = nullptr; - char _buffer[BUFFER_SIZE]; - uint16_t _buff_pos = 0; - uint32_t _lastSend = 0; - bool _forceSend = false; std::mutex _msgLock; + + void serialWrite(message_t const& m); }; extern MessageOutputClass MessageOutput; \ No newline at end of file diff --git a/src/MessageOutput.cpp b/src/MessageOutput.cpp index f602bee1..027ce20c 100644 --- a/src/MessageOutput.cpp +++ b/src/MessageOutput.cpp @@ -2,10 +2,9 @@ /* * Copyright (C) 2022-2023 Thomas Basler and others */ +#include #include "MessageOutput.h" -#include - MessageOutputClass MessageOutput; void MessageOutputClass::init(Scheduler& scheduler) @@ -18,46 +17,97 @@ void MessageOutputClass::init(Scheduler& scheduler) void MessageOutputClass::register_ws_output(AsyncWebSocket* output) { + std::lock_guard lock(_msgLock); + _ws = output; } +void MessageOutputClass::serialWrite(MessageOutputClass::message_t const& m) +{ + // on ESP32-S3, Serial.flush() blocks until a serial console is attached. + // operator bool() of HWCDC returns false if the device is not attached to + // a USB host. in general it makes sense to skip writing entirely if the + // default serial port is not ready. + if (!Serial) { return; } + + size_t written = 0; + while (written < m.size()) { + written += Serial.write(m.data() + written, m.size() - written); + } + Serial.flush(); +} + size_t MessageOutputClass::write(uint8_t c) { - if (_buff_pos < BUFFER_SIZE) { - std::lock_guard lock(_msgLock); - _buffer[_buff_pos] = c; - _buff_pos++; - } else { - _forceSend = true; + std::lock_guard lock(_msgLock); + + auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t()); + auto iter = res.first; + auto& message = iter->second; + + message.push_back(c); + + if (c == '\n') { + serialWrite(message); + _lines.emplace(std::move(message)); + _task_messages.erase(iter); } - return Serial.write(c); + return 1; } -size_t MessageOutputClass::write(const uint8_t* buffer, size_t size) +size_t MessageOutputClass::write(const uint8_t *buffer, size_t size) { std::lock_guard lock(_msgLock); - if (_buff_pos + size < BUFFER_SIZE) { - memcpy(&_buffer[_buff_pos], buffer, size); - _buff_pos += size; - } - _forceSend = true; - return Serial.write(buffer, size); + auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t()); + auto iter = res.first; + auto& message = iter->second; + + message.reserve(message.size() + size); + + for (size_t idx = 0; idx < size; ++idx) { + uint8_t c = buffer[idx]; + + message.push_back(c); + + if (c == '\n') { + serialWrite(message); + _lines.emplace(std::move(message)); + message.clear(); + message.reserve(size - idx - 1); + } + } + + if (message.empty()) { _task_messages.erase(iter); } + + return size; } void MessageOutputClass::loop() { - // Send data via websocket if either time is over or buffer is full - if (_forceSend || (millis() - _lastSend > 1000)) { - std::lock_guard lock(_msgLock); - if (_ws && _buff_pos > 0) { - _ws->textAll(_buffer, _buff_pos); - _buff_pos = 0; + std::lock_guard lock(_msgLock); + + // clean up (possibly filled) buffers of deleted tasks + auto map_iter = _task_messages.begin(); + while (map_iter != _task_messages.end()) { + if (eTaskGetState(map_iter->first) == eDeleted) { + map_iter = _task_messages.erase(map_iter); + continue; } - if (_forceSend) { - _buff_pos = 0; + + ++map_iter; + } + + if (!_ws) { + while (!_lines.empty()) { + _lines.pop(); // do not hog memory } - _forceSend = false; + return; + } + + while (!_lines.empty() && _ws->availableForWriteAll()) { + _ws->textAll(std::make_shared(std::move(_lines.front()))); + _lines.pop(); } } \ No newline at end of file From bb34fa74fdd9a5a8d4780d4b73d5a7c72107b161 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sat, 30 Dec 2023 16:46:16 +0100 Subject: [PATCH 11/27] Fix: use FormFooter in OnBattery-specific forms (#569) the upstream project introduced a new Vue component "FormFooter", which is used to end an input form, namely all settings forms. we should not only use this component as well, but the save button on our forms actually broke since the text dtuadmin.Save is replaced by base.Save. also replace the use of dtuadmin.Seconds with base.Seconds, such that an upstream change to dtuadmin.Seconds will not break the battery admin an AC charger views. --- webapp/src/locales/de.json | 14 +++++--------- webapp/src/locales/en.json | 14 +++++--------- webapp/src/locales/fr.json | 13 +++++-------- webapp/src/views/AcChargerAdminView.vue | 4 +++- webapp/src/views/BatteryAdminView.vue | 4 +++- webapp/src/views/PowerLimiterAdminView.vue | 4 +++- webapp/src/views/PowerMeterAdminView.vue | 4 +++- webapp/src/views/VedirectAdminView.vue | 4 +++- 8 files changed, 30 insertions(+), 31 deletions(-) diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 8514e248..e2a52f78 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -31,6 +31,7 @@ "Yes": "Ja", "No": "Nein", "VerboseLogging": "Ausführliche Protokollierung", + "Seconds": "Sekunden", "Loading": "Lade...", "Reload": "Aktualisieren", "Cancel": "Abbrechen", @@ -519,8 +520,7 @@ "EnableVedirect": "Aktiviere VE.Direct", "VedirectParameter": "VE.Direct Parameter", "VerboseLogging": "@:base.VerboseLogging", - "UpdatesOnly": "Werte nur bei Änderung an MQTT broker senden", - "Save": "@:dtuadmin.Save" + "UpdatesOnly": "Werte nur bei Änderung an MQTT broker senden" }, "powermeteradmin":{ "PowerMeterSettings": "Stromzähler Einstellungen", @@ -541,7 +541,6 @@ "SDM": "SDM-Stromzähler Konfiguration", "sdmbaudrate": "Baudrate", "sdmaddress": "Modbus Adresse", - "Save": "@:dtuadmin.Save", "HTTP": "HTTP(S) + JSON - Allgemeine Konfiguration", "httpIndividualRequests": "Individuelle HTTP requests pro Phase", "httpUrlDescription": "Die URL muss mit http:// oder https:// beginnen. Manche Zeichen wie Leerzeichen und = müssen mit URL-Kodierung kodiert werden (%xx). Achtung: Ein Überprüfung von SSL Server Zertifikaten ist nicht implementiert (MITM-Attacken sind möglich)! Beispiele gibt es unten.", @@ -596,8 +595,7 @@ "VoltageLoadCorrectionInfo": "Hinweis: Wenn Leistung von der Batterie abgegeben wird, bricht normalerweise die Spannung etwas ein. Damit nicht vorzeitig der Wechelrichter ausgeschaltet wird sobald der \"Stop\"-Schwellenwert erreicht wird, wird der hier angegebene Korrekturfaktor mit einberechnet. Korrigierte Spannung = DC Spannung + (Aktuelle Leistung (W) * Korrekturfaktor).", "InverterRestart": "Wechselrichter Neustart", "InverterRestartHour": "Stunde für Neustart", - "InverterRestartHint": "Neustart des Wechselrichter einmal täglich um die \"Tagesertrag\" Werte wieder auf Null zu setzen.", - "Save": "@:dtuadmin.Save" + "InverterRestartHint": "Neustart des Wechselrichter einmal täglich um die \"Tagesertrag\" Werte wieder auf Null zu setzen." }, "batteryadmin": { "BatterySettings": "Batterie Einstellungen", @@ -613,8 +611,7 @@ "JkBmsInterfaceUart": "TTL-UART an der MCU", "JkBmsInterfaceTransceiver": "RS-485 Transceiver an der MCU", "PollingInterval": "Abfrageintervall", - "Seconds": "@:dtuadmin.Seconds", - "Save": "@:dtuadmin.Save" + "Seconds": "@:base.Seconds" }, "inverteradmin": { "InverterSettings": "Wechselrichter Einstellungen", @@ -800,8 +797,7 @@ "enableVoltageLimitHint": "Die automatische Leistungssteuerung wird deaktiviert wenn die Ausgangsspannung über diesem Wert liegt und wenn gleichzeitig die Ausgangsleistung unter die minimale Leistung fällt.\nDie automatische Leistungssteuerung wird re-aktiveiert wenn die Batteriespannung unter diesen Wert fällt.", "lowerPowerLimit": "Minimale Leistung", "upperPowerLimit": "Maximale Leistung", - "Seconds": "@:dtuadmin.Seconds", - "Save": "@:dtuadmin.Save" + "Seconds": "@:base.Seconds" }, "battery": { "battery": "Batterie", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 0347aec8..91df2dbf 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -31,6 +31,7 @@ "Yes": "Yes", "No": "No", "VerboseLogging": "Verbose Logging", + "Seconds": "Seconds", "Loading": "Loading...", "Reload": "Reload", "Cancel": "Cancel", @@ -521,8 +522,7 @@ "EnableVedirect": "Enable VE.Direct", "VedirectParameter": "VE.Direct Parameter", "VerboseLogging": "@:base.VerboseLogging", - "UpdatesOnly": "Publish values to MQTT only when they change", - "Save": "@:dtuadmin.Save" + "UpdatesOnly": "Publish values to MQTT only when they change" }, "powermeteradmin":{ "PowerMeterSettings": "Power Meter Settings", @@ -543,7 +543,6 @@ "SDM": "SDM-Power Meter Parameter", "sdmbaudrate": "Baudrate", "sdmaddress": "Modbus Address", - "Save": "@:dtuadmin.Save", "HTTP": "HTTP(S) + Json - General configuration", "httpIndividualRequests": "Individual HTTP requests per phase", "httpPhase": "HTTP(S) + Json configuration - Phase {phaseNumber}", @@ -605,8 +604,7 @@ "VoltageLoadCorrectionInfo": "Hint: When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor).", "InverterRestart": "Inverter Restart", "InverterRestartHour": "Restart Hour", - "InverterRestartHint": "Restart the Inverter once a day to reset the \"YieldDay\" values.", - "Save": "@:dtuadmin.Save" + "InverterRestartHint": "Restart the Inverter once a day to reset the \"YieldDay\" values." }, "batteryadmin": { "BatterySettings": "Battery Settings", @@ -622,8 +620,7 @@ "JkBmsInterfaceUart": "TTL-UART on MCU", "JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU", "PollingInterval": "Polling Interval", - "Seconds": "@:dtuadmin.Seconds", - "Save": "@:dtuadmin.Save" + "Seconds": "@:base.Seconds" }, "inverteradmin": { "InverterSettings": "Inverter Settings", @@ -810,8 +807,7 @@ "enableVoltageLimitHint": "Automatic power control is disabled if the output voltage is higher then this value and if the output power drops below the minimum output power limit (set below).\nAutomatic power control is re-enabled if the battery voltage drops below the value set in this field.", "lowerPowerLimit": "Minimum output power", "upperPowerLimit": "Maximum output power", - "Seconds": "@:dtuadmin.Seconds", - "Save": "@:dtuadmin.Save" + "Seconds": "@:base.Seconds" }, "battery": { "battery": "Battery", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 3d0c5343..7b735771 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -31,6 +31,7 @@ "Yes": "Oui", "No": "Non", "VerboseLogging": "Journalisation Détaillée", + "Seconds": "Secondes", "Loading": "Chargement...", "Reload": "Reload", "Cancel": "Annuler", @@ -521,8 +522,7 @@ "EnableVedirect": "Enable VE.Direct", "VedirectParameter": "VE.Direct Parameter", "VerboseLogging": "@:base.VerboseLogging", - "UpdatesOnly": "Publish values to MQTT only when they change", - "Save": "@:dtuadmin.Save" + "UpdatesOnly": "Publish values to MQTT only when they change" }, "batteryadmin": { "BatterySettings": "Battery Settings", @@ -538,8 +538,7 @@ "JkBmsInterfaceUart": "TTL-UART on MCU", "JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU", "PollingInterval": "Polling Interval", - "Seconds": "@:dtuadmin.Seconds", - "Save": "@:dtuadmin.Save" + "Seconds": "@:base.Seconds" }, "inverteradmin": { "InverterSettings": "Paramètres des onduleurs", @@ -648,8 +647,7 @@ "BatterySocInfo": "Hint: The battery SoC (State of Charge) values can only be used if the battery communication interface is enabled. If the battery has not reported any SoC updates in the last minute, the voltage thresholds will be used as fallback.", "InverterIsBehindPowerMeter": "Inverter is behind Power meter", "Battery": "DC / Battery", - "VoltageLoadCorrectionInfo": "Hint: When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor).", - "Save": "@:dtuadmin.Save" + "VoltageLoadCorrectionInfo": "Hint: When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor)." }, "login": { "Login": "Connexion", @@ -768,8 +766,7 @@ "enableVoltageLimitHint": "Automatic power control is disabled if the output voltage is higher then this value and if the output power drops below the minimum output power limit (set below).\nAutomatic power control is re-enabled if the battery voltage drops below the value set in this field.", "lowerPowerLimit": "Minimum output power", "upperPowerLimit": "Maximum output power", - "Seconds": "@:dtuadmin.Seconds", - "Save": "@:dtuadmin.Save" + "Seconds": "@:base.Seconds" }, "battery": { "battery": "Battery", diff --git a/webapp/src/views/AcChargerAdminView.vue b/webapp/src/views/AcChargerAdminView.vue index 09527d58..c9cdf787 100644 --- a/webapp/src/views/AcChargerAdminView.vue +++ b/webapp/src/views/AcChargerAdminView.vue @@ -73,7 +73,7 @@ - + @@ -82,6 +82,7 @@ import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; +import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import { BIconInfoCircle } from 'bootstrap-icons-vue'; import type { AcChargerConfig } from "@/types/AcChargerConfig"; @@ -93,6 +94,7 @@ export default defineComponent({ BasePage, BootstrapAlert, CardElement, + FormFooter, InputElement, BIconInfoCircle, }, diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue index 4cf1e673..e599425b 100644 --- a/webapp/src/views/BatteryAdminView.vue +++ b/webapp/src/views/BatteryAdminView.vue @@ -49,7 +49,7 @@ type="number" min="2" max="90" step="1" :postfix="$t('batteryadmin.Seconds')"/> - + @@ -58,6 +58,7 @@ import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; +import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import type { BatteryConfig } from "@/types/BatteryConfig"; import { authHeader, handleResponse } from '@/utils/authentication'; @@ -68,6 +69,7 @@ export default defineComponent({ BasePage, BootstrapAlert, CardElement, + FormFooter, InputElement, }, data() { diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index f70f8221..8f10a4cd 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -265,7 +265,7 @@ - + @@ -276,6 +276,7 @@ import BasePage from '@/components/BasePage.vue'; import BootstrapAlert from "@/components/BootstrapAlert.vue"; import { handleResponse, authHeader } from '@/utils/authentication'; import CardElement from '@/components/CardElement.vue'; +import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import { BIconInfoCircle } from 'bootstrap-icons-vue'; import type { PowerLimiterConfig } from "@/types/PowerLimiterConfig"; @@ -285,6 +286,7 @@ export default defineComponent({ BasePage, BootstrapAlert, CardElement, + FormFooter, InputElement, BIconInfoCircle, }, diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index 287d9445..280171cf 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -185,7 +185,7 @@ - +