From 811b64adb5d97c140f76f2e25f95726965a18ce4 Mon Sep 17 00:00:00 2001 From: David von Oheimb Date: Mon, 25 Mar 2024 10:10:03 +0100 Subject: [PATCH 01/70] PowerLimiter.cpp: simplification and minor correction of logic table comments --- src/PowerLimiter.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index b2ce2392a..f3163f841 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -404,12 +404,11 @@ uint8_t PowerLimiterClass::getPowerLimiterState() { } // Logic table -// | Case # | batteryPower | solarPower > 0 | useFullSolarPassthrough | Result | -// | 1 | false | false | doesn't matter | PL = 0 | -// | 2 | false | true | doesn't matter | PL = Victron Power | -// | 3 | true | doesn't matter | false | PL = PowerMeter value (Battery can supply unlimited energy) | -// | 4 | true | false | true | PL = PowerMeter value | -// | 5 | true | true | true | PL = max(PowerMeter value, Victron Power) | +// | Case # | batteryPower | solarPower | useFullSolarPassthrough | Resulting inverter limit | +// | 1 | false | < 20 W | doesn't matter | 0 (inverter off) | +// | 2 | false | >= 20 W | doesn't matter | min(PowerMeter value, solarPower) | +// | 3 | true | doesn't matter | false | PowerMeter value (Battery can supply unlimited energy) | +// | 4 | true | fully passed | true | max(PowerMeter value, solarPower) | bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, int32_t solarPowerDC, bool batteryPower) { @@ -418,6 +417,7 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte (batteryPower?"allowed":"prevented"), solarPowerDC); } + // Case 1: if (solarPowerDC <= 0 && !batteryPower) { return shutdown(Status::NoEnergy); } @@ -460,9 +460,9 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte // We're not trying to hit 0 exactly but take an offset into account // This means we never fully compensate the used power with the inverter - // Case 3 newPowerLimit -= config.PowerLimiter.TargetPowerConsumption; + // Case 2: if (!batteryPower) { newPowerLimit = std::min(newPowerLimit, solarPowerAC); @@ -476,6 +476,7 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte return setNewPowerLimit(inverter, newPowerLimit); } + // Case 4: // convert all solar power if full solar-passthrough is active if (useFullSolarPassthrough()) { newPowerLimit = std::max(newPowerLimit, solarPowerAC); @@ -493,6 +494,7 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte newPowerLimit); } + // Case 3: return setNewPowerLimit(inverter, newPowerLimit); } From eff8d52014540b085eb84257fb4e178fe98b43af Mon Sep 17 00:00:00 2001 From: Rene Date: Mon, 25 Mar 2024 20:30:02 +0100 Subject: [PATCH 02/70] better alignment inverter, issue 360 --- webapp/src/views/HomeView.vue | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index 0bd013335..f05c42974 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -5,14 +5,20 @@
From 91a0992964b53dc6198dc53bdc5a169b7bede2b7 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Tue, 26 Mar 2024 20:48:39 +0100 Subject: [PATCH 03/70] fix: VE.Direct MPPT data not always updated in websocket set the "last published" timestampt after handling *all* MPPTs. --- src/WebApi_ws_vedirect_live.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 967372ccc..9ec9d79ff 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -138,9 +138,10 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool ful const JsonObject &nested = array.createNestedObject(serial); nested["data_age_ms"] = VictronMppt.getDataAgeMillis(idx); populateJson(nested, spMpptData); - _lastPublish = millis(); } + _lastPublish = millis(); + // power limiter state root["dpl"]["PLSTATE"] = -1; if (Configuration.get().PowerLimiter.Enabled) From 6f3b8fb8e13ab3443007bcc9aa5517c54356f47d Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sun, 31 Mar 2024 12:27:27 +0200 Subject: [PATCH 04/70] Fix: Change default NTP server Fixes #1877 --- include/Configuration.h | 2 +- include/defaults.h | 3 ++- src/Configuration.cpp | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 8ae3826a3..bb0e478f2 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -5,7 +5,7 @@ #include #define CONFIG_FILENAME "/config.json" -#define CONFIG_VERSION 0x00011b00 // 0.1.27 // make sure to clean all after change +#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_PASSWORD_STRLEN 64 diff --git a/include/defaults.h b/include/defaults.h index ac871fc99..fd41a3d0b 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -22,7 +22,8 @@ #define MDNS_ENABLED false -#define NTP_SERVER "pool.ntp.org" +#define NTP_SERVER_OLD "pool.ntp.org" +#define NTP_SERVER "opendtu.pool.ntp.org" #define NTP_TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3" #define NTP_TIMEZONEDESCR "Europe/Berlin" #define NTP_LONGITUDE 10.4515f diff --git a/src/Configuration.cpp b/src/Configuration.cpp index de4efa34b..3b189187c 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -372,6 +372,12 @@ void ConfigurationClass::migrate() 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)); + } + } + f.close(); config.Cfg.Version = CONFIG_VERSION; From 188805462743252ad51a265fc9d0a530e24ee1be Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sun, 31 Mar 2024 12:42:00 +0200 Subject: [PATCH 05/70] Fix: Re-Request grid profile parameters if received data are invalid / to short Fixes #1874 --- lib/Hoymiles/src/Hoymiles.cpp | 2 +- lib/Hoymiles/src/parser/GridProfileParser.cpp | 7 ++++++- lib/Hoymiles/src/parser/GridProfileParser.h | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index eda3500b5..b14585905 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -114,7 +114,7 @@ void HoymilesClass::loop() } // Fetch grid profile - if (iv->Statistics()->getLastUpdate() > 0 && iv->GridProfile()->getLastUpdate() == 0) { + if (iv->Statistics()->getLastUpdate() > 0 && (iv->GridProfile()->getLastUpdate() == 0 || !iv->GridProfile()->containsValidData())) { iv->sendGridOnProFileParaRequest(); } diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp index 37cb1d4ac..a7b912a9e 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.cpp +++ b/lib/Hoymiles/src/parser/GridProfileParser.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023 - 2024 Thomas Basler and others */ #include "GridProfileParser.h" #include "../Hoymiles.h" @@ -446,6 +446,11 @@ std::list GridProfileParser::getProfile() const return l; } +bool GridProfileParser::containsValidData() const +{ + return _gridProfileLength > 6; +} + uint8_t GridProfileParser::getSectionSize(const uint8_t section_id, const uint8_t section_version) { uint8_t count = 0; diff --git a/lib/Hoymiles/src/parser/GridProfileParser.h b/lib/Hoymiles/src/parser/GridProfileParser.h index 1be12e1d3..7afdfb825 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.h +++ b/lib/Hoymiles/src/parser/GridProfileParser.h @@ -43,6 +43,8 @@ class GridProfileParser : public Parser { std::list getProfile() const; + bool containsValidData() const; + private: static uint8_t getSectionSize(const uint8_t section_id, const uint8_t section_version); static int16_t getSectionStart(const uint8_t section_id, const uint8_t section_version); @@ -52,4 +54,4 @@ class GridProfileParser : public Parser { static const std::array _profileTypes; static const std::array _profileValues; -}; \ No newline at end of file +}; From f0a8cabc2c1bdd186b2335026257d4201ec6048c Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sun, 31 Mar 2024 14:31:57 +0200 Subject: [PATCH 06/70] webapp: update dependencies --- webapp/package.json | 14 ++--- webapp/yarn.lock | 140 ++++++++++++++++---------------------------- 2 files changed, 58 insertions(+), 96 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index 5088ac7fb..5e92ee74d 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -23,11 +23,11 @@ "vue-router": "^4.3.0" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^3.0.1", - "@rushstack/eslint-patch": "^1.8.0", - "@tsconfig/node18": "^18.2.2", + "@intlify/unplugin-vue-i18n": "^4.0.0", + "@rushstack/eslint-patch": "^1.10.1", + "@tsconfig/node18": "^18.2.4", "@types/bootstrap": "^5.2.10", - "@types/node": "^20.11.30", + "@types/node": "^20.12.2", "@types/pulltorefreshjs": "^0.1.7", "@types/sortablejs": "^1.15.8", "@types/spark-md5": "^3.0.4", @@ -35,13 +35,13 @@ "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.23.0", + "eslint-plugin-vue": "^9.24.0", "npm-run-all": "^4.1.5", "pulltorefreshjs": "^0.1.22", "sass": "^1.72.0", - "terser": "^5.29.2", + "terser": "^5.30.0", "typescript": "^5.4.3", - "vite": "^5.2.3", + "vite": "^5.2.7", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.5.0", "vue-tsc": "^2.0.7" diff --git a/webapp/yarn.lock b/webapp/yarn.lock index af59fb24c..1d7bc2022 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -195,18 +195,17 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== -"@intlify/bundle-utils@^7.4.0": - version "7.4.0" - resolved "https://registry.yarnpkg.com/@intlify/bundle-utils/-/bundle-utils-7.4.0.tgz#b4dc41026d2d98d2e8a2bd83851c1883a48f1254" - integrity sha512-AQfjBe2HUxzyN8ignIk3WhhSuVcSuirgzOzkd17nb337rCbI4Gv/t1R60UUyIqFoFdviLb/wLcDUzTD/xXjv9w== +"@intlify/bundle-utils@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@intlify/bundle-utils/-/bundle-utils-8.0.0.tgz#4e05153ac031bfc7adef70baedc9b0744a93adfd" + integrity sha512-1B++zykRnMwQ+20SpsZI1JCnV/YJt9Oq7AGlEurzkWJOFtFAVqaGc/oV36PBRYeiKnTbY9VYfjBimr2Vt42wLQ== dependencies: "@intlify/message-compiler" "^9.4.0" "@intlify/shared" "^9.4.0" acorn "^8.8.2" - escodegen "^2.0.0" + escodegen "^2.1.0" estree-walker "^2.0.2" jsonc-eslint-parser "^2.3.0" - magic-string "^0.30.0" mlly "^1.2.0" source-map-js "^1.0.1" yaml-eslint-parser "^1.2.2" @@ -245,12 +244,12 @@ resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.4.0.tgz#4a78d462fc82433db900981e12eb5b1aae3d6085" integrity sha512-AFqymip2kToqA0B6KZPg5jSrdcVHoli9t/VhGKE2iiMq9utFuMoGdDC/JOCIZgwxo6aXAk86QyU2XtzEoMuZ6A== -"@intlify/unplugin-vue-i18n@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-3.0.1.tgz#8bed58d5cbaadda056c2ff88acf99300db516639" - integrity sha512-q1zJhA/WpoLBzAAuKA5/AEp0e+bMOM10ll/HxT4g1VAw/9JhC4TTobP9KobKH90JMZ4U2daLFlYQfKNd29lpqw== +"@intlify/unplugin-vue-i18n@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-4.0.0.tgz#b82fb1bb1a3b982d8f35d07729ca5337d6018269" + integrity sha512-q2Mhqa/mLi0tulfLFO4fMXXvEbkSZpI5yGhNNsLTNJJ41icEGUuyDe+j5zRZIKSkOJRgX6YbCyibTDJdRsukmw== dependencies: - "@intlify/bundle-utils" "^7.4.0" + "@intlify/bundle-utils" "^8.0.0" "@intlify/shared" "^9.4.0" "@rollup/pluginutils" "^5.1.0" "@vue/compiler-sfc" "^3.2.47" @@ -290,7 +289,7 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13": +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== @@ -413,15 +412,15 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz#6abd79db7ff8d01a58865ba20a63cfd23d9e2a10" integrity sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw== -"@rushstack/eslint-patch@^1.8.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.8.0.tgz#c5545e6a5d2bd5c26b4021c357177a28698c950e" - integrity sha512-0HejFckBN2W+ucM6cUOlwsByTKt9/+0tWhqUffNIcHqCXkthY/mZ7AuYPK/2IIaGWhdl0h+tICDO0ssLMd6XMQ== +"@rushstack/eslint-patch@^1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf" + integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg== -"@tsconfig/node18@^18.2.2": - version "18.2.2" - resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.2.tgz#81fb16ecff0d400b1cbadbf76713b50f331029ce" - integrity sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw== +"@tsconfig/node18@^18.2.4": + version "18.2.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.4.tgz#094efbdd70f697d37c09f34067bf41bc4a828ae3" + integrity sha512-5xxU8vVs9/FNcvm3gE07fPbn9tl6tqGGWA9tSlwsUEkBxtRnTsNmwrV8gasZ9F/EobaSv9+nu8AxUKccw77JpQ== "@types/bootstrap@^5.2.10": version "5.2.10" @@ -445,10 +444,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== -"@types/node@^20.11.30": - version "20.11.30" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f" - integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw== +"@types/node@^20.12.2": + version "20.12.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e" + integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ== dependencies: undici-types "~5.26.4" @@ -1009,7 +1008,7 @@ debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: dependencies: ms "2.1.2" -deep-is@^0.1.3, deep-is@~0.1.3: +deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== @@ -1126,24 +1125,24 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -escodegen@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" - integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== +escodegen@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== dependencies: esprima "^4.0.1" estraverse "^5.2.0" esutils "^2.0.2" - optionator "^0.8.1" optionalDependencies: source-map "~0.6.1" -eslint-plugin-vue@^9.23.0: - version "9.23.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.23.0.tgz#1354a33b0cd21e0cb373557ff73c5d7a6698fbcd" - integrity sha512-Bqd/b7hGYGrlV+wP/g77tjyFmp81lh5TMw0be9093X02SyelxRRfCI6/IsGq/J7Um0YwB9s0Ry0wlFyjPdmtUw== +eslint-plugin-vue@^9.24.0: + version "9.24.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.24.0.tgz#71209f4652ee767f18c0bf56f25991b7cdc5aa46" + integrity sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw== dependencies: "@eslint-community/eslint-utils" "^4.4.0" + globals "^13.24.0" natural-compare "^1.4.0" nth-check "^2.1.1" postcss-selector-parser "^6.0.15" @@ -1326,7 +1325,7 @@ fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== @@ -1467,6 +1466,13 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" +globals@^13.24.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -1770,14 +1776,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -1819,13 +1817,6 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" -magic-string@^0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529" - integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - magic-string@^0.30.7: version "0.30.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.7.tgz#0cecd0527d473298679da95a2d7aeb8c64048505" @@ -1974,18 +1965,6 @@ once@^1.3.0: dependencies: wrappy "1" -optionator@^0.8.1: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -2129,7 +2108,7 @@ postcss@^8.4.35: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.36: +postcss@^8.4.38: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== @@ -2143,11 +2122,6 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== - pulltorefreshjs@^0.1.22: version "0.1.22" resolved "https://registry.yarnpkg.com/pulltorefreshjs/-/pulltorefreshjs-0.1.22.tgz#ddb5e3feee0b2a49fd46e1b18e84fffef2c47ac0" @@ -2457,10 +2431,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -terser@^5.29.2: - version "5.29.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.2.tgz#c17d573ce1da1b30f21a877bffd5655dd86fdb35" - integrity sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw== +terser@^5.30.0: + version "5.30.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.0.tgz#64cb2af71e16ea3d32153f84d990f9be0cdc22bf" + integrity sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -2491,13 +2465,6 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== - dependencies: - prelude-ls "~1.1.2" - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -2577,13 +2544,13 @@ vite-plugin-css-injected-by-js@^3.5.0: resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.0.tgz#784c0f42c2b42155eb4c726c6addfa24aba9f4fb" integrity sha512-d0QaHH9kS93J25SwRqJNEfE29PSuQS5jn51y9N9i2Yoq0FRO7rjuTeLvjM5zwklZlRrIn6SUdtOEDKyHokgJZg== -vite@^5.2.3: - version "5.2.3" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.3.tgz#198efc2fd4d80eac813b146a68a4b0dbde884fc2" - integrity sha512-+i1oagbvkVIhEy9TnEV+fgXsng13nZM90JQbrcPrf6DvW2mXARlz+DK7DLiDP+qeKoD1FCVx/1SpFL1CLq9Mhw== +vite@^5.2.7: + version "5.2.7" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.7.tgz#e1b8a985eb54fcb9467d7f7f009d87485016df6e" + integrity sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA== dependencies: esbuild "^0.20.1" - postcss "^8.4.36" + postcss "^8.4.38" rollup "^4.13.0" optionalDependencies: fsevents "~2.3.3" @@ -2693,11 +2660,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" From bdff1e1ac3b684b28e7b21998f4f1e1cb375af49 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sun, 31 Mar 2024 22:39:59 +0200 Subject: [PATCH 07/70] Added github workflow to do some repository cleanup --- .github/workflows/repo-maintenance.yml | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/repo-maintenance.yml diff --git a/.github/workflows/repo-maintenance.yml b/.github/workflows/repo-maintenance.yml new file mode 100644 index 000000000..f7290c2e5 --- /dev/null +++ b/.github/workflows/repo-maintenance.yml @@ -0,0 +1,54 @@ +name: 'Repository Maintenance' + +on: + schedule: + - cron: '0 4 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + discussions: write + +concurrency: + group: lock + +jobs: + stale: + name: 'Stale' + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 14 + days-before-close: 60 + any-of-labels: 'cant-reproduce,not a bug' + stale-issue-label: stale + stale-pr-label: stale + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + + lock-threads: + name: 'Lock Old Threads' + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v5 + with: + issue-inactive-days: '30' + pr-inactive-days: '30' + discussion-inactive-days: '30' + log-output: true + issue-comment: > + This issue has been automatically locked since there + has not been any recent activity after it was closed. + Please open a new discussion or issue for related concerns. + pr-comment: > + This pull request has been automatically locked since there + has not been any recent activity after it was closed. + Please open a new discussion or issue for related concerns. + discussion-comment: > + This discussion has been automatically locked since there + has not been any recent activity after it was closed. + Please open a new discussion for related concerns. From 12588655dfa9b06aff4bff04ee1d1a2280dda525 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Sun, 31 Mar 2024 23:07:08 +0200 Subject: [PATCH 08/70] webapp: add app.js.gz --- webapp_dist/js/app.js.gz | Bin 182484 -> 182520 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index 3729c9bf8a436738d4f31b06351edef8971e9ffd..48d1893fac8e3e3f9271144f4a8a8042e71ca580 100644 GIT binary patch delta 81892 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

`&zc#?>PP{k&Bb-R6HwREaty;w?fvpjv|{ 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 z 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 ViN1hin2!I?{{tu9yB7Qn3; Date: Mon, 1 Apr 2024 00:00:36 +0200 Subject: [PATCH 09/70] Move source files for TimeoutHelper to correct directories --- lib/TimeoutHelper/README.md | 0 lib/TimeoutHelper/library.json | 13 +++++++++++++ lib/TimeoutHelper/{ => src}/TimeoutHelper.cpp | 0 lib/TimeoutHelper/{ => src}/TimeoutHelper.h | 0 4 files changed, 13 insertions(+) create mode 100644 lib/TimeoutHelper/README.md create mode 100644 lib/TimeoutHelper/library.json rename lib/TimeoutHelper/{ => src}/TimeoutHelper.cpp (100%) rename lib/TimeoutHelper/{ => src}/TimeoutHelper.h (100%) diff --git a/lib/TimeoutHelper/README.md b/lib/TimeoutHelper/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/lib/TimeoutHelper/library.json b/lib/TimeoutHelper/library.json new file mode 100644 index 000000000..0e0472ba6 --- /dev/null +++ b/lib/TimeoutHelper/library.json @@ -0,0 +1,13 @@ +{ + "name": "TimeoutHelper", + "keywords": "timeout", + "description": "An Arduino for ESP32 timeout helper", + "authors": { + "name": "Thomas Basler" + }, + "version": "0.0.1", + "frameworks": "arduino", + "platforms": [ + "espressif32" + ] +} diff --git a/lib/TimeoutHelper/TimeoutHelper.cpp b/lib/TimeoutHelper/src/TimeoutHelper.cpp similarity index 100% rename from lib/TimeoutHelper/TimeoutHelper.cpp rename to lib/TimeoutHelper/src/TimeoutHelper.cpp diff --git a/lib/TimeoutHelper/TimeoutHelper.h b/lib/TimeoutHelper/src/TimeoutHelper.h similarity index 100% rename from lib/TimeoutHelper/TimeoutHelper.h rename to lib/TimeoutHelper/src/TimeoutHelper.h From 58efd9e9543e475863a3cebdc85fcd9a95fefb07 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 1 Apr 2024 00:03:04 +0200 Subject: [PATCH 10/70] Move source files for ThreadSafeQueue to correct directories --- lib/ThreadSafeQueue/README.md | 0 lib/ThreadSafeQueue/library.json | 13 +++++++++++++ lib/ThreadSafeQueue/{ => src}/ThreadSafeQueue.h | 0 3 files changed, 13 insertions(+) create mode 100644 lib/ThreadSafeQueue/README.md create mode 100644 lib/ThreadSafeQueue/library.json rename lib/ThreadSafeQueue/{ => src}/ThreadSafeQueue.h (100%) diff --git a/lib/ThreadSafeQueue/README.md b/lib/ThreadSafeQueue/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/lib/ThreadSafeQueue/library.json b/lib/ThreadSafeQueue/library.json new file mode 100644 index 000000000..768cb8b23 --- /dev/null +++ b/lib/ThreadSafeQueue/library.json @@ -0,0 +1,13 @@ +{ + "name": "ThreadSafeQueue", + "keywords": "queue, threadsafe", + "description": "An Arduino for ESP32 thread safe queue implementation", + "authors": { + "name": "Thomas Basler" + }, + "version": "0.0.1", + "frameworks": "arduino", + "platforms": [ + "espressif32" + ] +} diff --git a/lib/ThreadSafeQueue/ThreadSafeQueue.h b/lib/ThreadSafeQueue/src/ThreadSafeQueue.h similarity index 100% rename from lib/ThreadSafeQueue/ThreadSafeQueue.h rename to lib/ThreadSafeQueue/src/ThreadSafeQueue.h From 8add226a7c79760c2ef8df0bdca617e676be1866 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 1 Apr 2024 13:52:09 +0200 Subject: [PATCH 11/70] Save flash: Move WebApi json parsing to separate method to prevent a lot of redundant code --- include/WebApi.h | 3 + src/WebApi.cpp | 34 ++++++++++ src/WebApi_config.cpp | 31 +--------- src/WebApi_device.cpp | 31 +--------- src/WebApi_dtu.cpp | 31 +--------- src/WebApi_inverter.cpp | 124 ++++--------------------------------- src/WebApi_limit.cpp | 31 +--------- src/WebApi_maintenance.cpp | 31 +--------- src/WebApi_mqtt.cpp | 31 +--------- src/WebApi_network.cpp | 31 +--------- src/WebApi_ntp.cpp | 62 ++----------------- src/WebApi_power.cpp | 31 +--------- src/WebApi_security.cpp | 31 +--------- 13 files changed, 82 insertions(+), 420 deletions(-) diff --git a/include/WebApi.h b/include/WebApi.h index 28ae7d33b..5e5af5278 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -22,6 +22,7 @@ #include "WebApi_webapp.h" #include "WebApi_ws_console.h" #include "WebApi_ws_live.h" +#include #include #include @@ -37,6 +38,8 @@ class WebApiClass { static void writeConfig(JsonVariant& retMsg, const WebApiError code = WebApiError::GenericSuccess, const String& message = "Settings saved!"); + static bool parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, DynamicJsonDocument& json_document, size_t max_document_size = 1024); + private: AsyncWebServer _server; diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 40809927a..10f3e28d9 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -85,4 +85,38 @@ void WebApiClass::writeConfig(JsonVariant& retMsg, const WebApiError code, const } } +bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, DynamicJsonDocument& json_document, size_t max_document_size) +{ + auto& retMsg = response->getRoot(); + retMsg["type"] = "warning"; + + if (!request->hasParam("data", true)) { + retMsg["message"] = "No values found!"; + retMsg["code"] = WebApiError::GenericNoValueFound; + response->setLength(); + request->send(response); + return false; + } + + const String json = request->getParam("data", true)->value(); + if (json.length() > max_document_size) { + retMsg["message"] = "Data too large!"; + retMsg["code"] = WebApiError::GenericDataTooLarge; + response->setLength(); + request->send(response); + return false; + } + + const DeserializationError error = deserializeJson(json_document, json); + if (error) { + retMsg["message"] = "Failed to parse data!"; + retMsg["code"] = WebApiError::GenericParseError; + response->setLength(); + request->send(response); + return false; + } + + return true; +} + WebApiClass WebApi; diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 29f353192..f76a2e0ad 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -53,38 +53,13 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("delete"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 2042f7daa..1cc142207 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -97,38 +97,13 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > MQTT_JSON_DOC_SIZE) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root, MQTT_JSON_DOC_SIZE)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("curPin") || root.containsKey("display"))) { retMsg["message"] = "Values are missing!"; diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 817e71b27..c6678b0a6 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -84,38 +84,13 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("serial") && root.containsKey("pollinterval") && root.containsKey("nrf_palevel") diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 5f5e41016..eb48c8efa 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -88,38 +88,13 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("serial") && root.containsKey("name"))) { retMsg["message"] = "Values are missing!"; @@ -188,38 +163,13 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; @@ -333,38 +283,13 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("id"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; @@ -403,38 +328,13 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("order"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index b5b9e1726..890589267 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -58,38 +58,13 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("serial") && root.containsKey("limit_value") && root.containsKey("limit_type"))) { diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index ba257efa8..538f087ab 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -23,38 +23,13 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > MQTT_JSON_DOC_SIZE) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root, MQTT_JSON_DOC_SIZE)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("reboot"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 29459a5b3..78fed2046 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -99,38 +99,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > MQTT_JSON_DOC_SIZE) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root, MQTT_JSON_DOC_SIZE)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("mqtt_enabled") && root.containsKey("mqtt_hostname") && root.containsKey("mqtt_port") diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 12f637adc..b7fbbe518 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -83,38 +83,13 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("ssid") && root.containsKey("password") && root.containsKey("hostname") diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index 02bbfb105..343d94b5a 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -95,38 +95,13 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone") && root.containsKey("longitude") @@ -219,38 +194,13 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("year") && root.containsKey("month") && root.containsKey("day") diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index 08fe9c051..2f921d74d 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -51,38 +51,13 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("serial") && (root.containsKey("power") || root.containsKey("restart")))) { diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index b95ebb299..05f829c55 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -42,38 +42,13 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!root.containsKey("password") && root.containsKey("allow_readonly")) { retMsg["message"] = "Values are missing!"; From 718690030e543f99628efdd64b424822ae2c169a Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Mon, 1 Apr 2024 13:52:59 +0200 Subject: [PATCH 12/70] Fix include for TimeoutHelper --- lib/Hoymiles/src/HoymilesRadio.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index 33b8c613b..cb2a947cd 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "TimeoutHelper.h" #include "commands/CommandAbstract.h" #include "types.h" -#include #include +#include +#include class HoymilesRadio { public: @@ -43,4 +43,4 @@ class HoymilesRadio { bool _busyFlag = false; TimeoutHelper _rxTimeout; -}; \ No newline at end of file +}; From da962730853bfdabe166e01c3914100b866ba69d Mon Sep 17 00:00:00 2001 From: PhilJaro <122352327+PhilJaro@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:05:45 +0200 Subject: [PATCH 13/70] Fix typo in German locale (#831) --- webapp/src/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 568476407..ff372786e 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -910,7 +910,7 @@ "chargedEnergy": "Geladene Energie", "dischargedEnergy": "Entladene Energie", "instantaneousPower": "Aktuelle Leistung", - "consumedAmpHours": "Verbrauche Amperestunden", + "consumedAmpHours": "Verbrauchte Amperestunden", "lastFullCharge": "Letztes mal Vollgeladen" } } From d2d775d687b1b5cd4898931b48de9b80c6ef57aa Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 2 Apr 2024 19:58:42 +0200 Subject: [PATCH 14/70] Update espressif32 from 6.5.0 to 6.6.0 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 108c12bec..12938d3ed 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,7 +19,7 @@ extra_configs = custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb framework = arduino -platform = espressif32@6.5.0 +platform = espressif32@6.6.0 build_flags = -DPIOENV=\"$PIOENV\" From 8abf614047a6e134132b70f69345c7819467db54 Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Mon, 18 Mar 2024 09:08:40 +0100 Subject: [PATCH 15/70] Feature: BMS initiated emergency charging This change logically connects the AC-Charger with the BMS to add BMS initiated emergency charging and respecting BMS current limits. --- include/BatteryStats.h | 10 ++++- include/Configuration.h | 1 + include/Huawei_can.h | 1 + src/Configuration.cpp | 2 + src/Huawei_can.cpp | 52 +++++++++++++++++++++---- src/WebApi_Huawei.cpp | 4 +- webapp/src/locales/de.json | 5 ++- webapp/src/locales/en.json | 5 ++- webapp/src/locales/fr.json | 5 ++- webapp/src/types/AcChargerConfig.ts | 1 + webapp/src/views/AcChargerAdminView.vue | 15 +++++-- 11 files changed, 84 insertions(+), 17 deletions(-) diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 86e1750b2..5c5f06b1c 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -7,6 +7,7 @@ #include "Arduino.h" #include "JkBmsDataPoints.h" #include "VeDirectShuntController.h" +#include // mandatory interface for all kinds of batteries class BatteryStats { @@ -37,7 +38,10 @@ class BatteryStats { // returns true if the battery reached a critically low voltage/SoC, // such that it is in need of charging to prevent degredation. - virtual bool needsCharging() const { return false; } + virtual bool getImmediateChargingRequest() const { return false; }; + + virtual float getChargeCurrent() const { return 0; }; + virtual float getChargeCurrentLimitation() const { return FLT_MAX; }; protected: virtual void mqttPublish() const; @@ -71,7 +75,9 @@ class PylontechBatteryStats : public BatteryStats { public: void getLiveViewData(JsonVariant& root) const final; void mqttPublish() const final; - bool needsCharging() const final { return _chargeImmediately; } + bool getImmediateChargingRequest() const { return _chargeImmediately; } ; + float getChargeCurrent() const { return _current; } ; + float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ; private: void setManufacturer(String&& m) { _manufacturer = std::move(m); } diff --git a/include/Configuration.h b/include/Configuration.h index 077ba9414..c14b1842b 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -240,6 +240,7 @@ struct CONFIG_T { bool Enabled; uint32_t CAN_Controller_Frequency; bool Auto_Power_Enabled; + bool Emergency_Charge_Enabled; float Auto_Power_Voltage_Limit; float Auto_Power_Enable_Voltage_Limit; float Auto_Power_Lower_Power_Limit; diff --git a/include/Huawei_can.h b/include/Huawei_can.h index e9a3a3d79..2b8edc98e 100644 --- a/include/Huawei_can.h +++ b/include/Huawei_can.h @@ -150,6 +150,7 @@ class HuaweiCanClass { uint8_t _autoPowerEnabledCounter = 0; bool _autoPowerEnabled = false; + bool _batteryEmergencyCharging = false; }; extern HuaweiCanClass HuaweiCan; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index c00617cf7..b0a77faf1 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -216,6 +216,7 @@ bool ConfigurationClass::write() huawei["enabled"] = config.Huawei.Enabled; huawei["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency; huawei["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled; + huawei["emergency_charge_enabled"] = config.Huawei.Emergency_Charge_Enabled; huawei["voltage_limit"] = config.Huawei.Auto_Power_Voltage_Limit; huawei["enable_voltage_limit"] = config.Huawei.Auto_Power_Enable_Voltage_Limit; huawei["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit; @@ -464,6 +465,7 @@ bool ConfigurationClass::read() config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED; config.Huawei.CAN_Controller_Frequency = huawei["can_controller_frequency"] | HUAWEI_CAN_CONTROLLER_FREQUENCY; config.Huawei.Auto_Power_Enabled = huawei["auto_power_enabled"] | false; + config.Huawei.Emergency_Charge_Enabled = huawei["emergency_charge_enabled"] | false; config.Huawei.Auto_Power_Voltage_Limit = huawei["voltage_limit"] | HUAWEI_AUTO_POWER_VOLTAGE_LIMIT; config.Huawei.Auto_Power_Enable_Voltage_Limit = huawei["enable_voltage_limit"] | HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT; config.Huawei.Auto_Power_Lower_Power_Limit = huawei["lower_power_limit"] | HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT; diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index df1d5eb24..db464a20a 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -2,6 +2,7 @@ /* * Copyright (C) 2023 Malte Schmidt and others */ +#include "Battery.h" #include "Huawei_can.h" #include "MessageOutput.h" #include "PowerMeter.h" @@ -13,6 +14,7 @@ #include #include #include +#include #include HuaweiCanClass HuaweiCan; @@ -298,18 +300,45 @@ void HuaweiCanClass::loop() digitalWrite(_huaweiPower, 1); } - // *********************** - // Automatic power control - // *********************** - if (_mode == HUAWEI_MODE_AUTO_INT ) { + if (_mode == HUAWEI_MODE_AUTO_INT || _batteryEmergencyCharging) { - // Set voltage limit in periodic intervals + // Set voltage limit in periodic intervals if we're in auto mode or if emergency battery charge is requested. if ( _nextAutoModePeriodicIntMillis < millis()) { MessageOutput.printf("[HuaweiCanClass::loop] Periodically setting voltage limit: %f \r\n", config.Huawei.Auto_Power_Voltage_Limit); _setValue(config.Huawei.Auto_Power_Voltage_Limit, HUAWEI_ONLINE_VOLTAGE); _nextAutoModePeriodicIntMillis = millis() + 60000; } + } + // *********************** + // Emergency charge + // *********************** + auto stats = Battery.getStats(); + if (config.Huawei.Emergency_Charge_Enabled && stats->getImmediateChargingRequest()) { + _batteryEmergencyCharging = true; + + // Set output current + float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); + float outputCurrent = efficiency * (config.Huawei.Auto_Power_Upper_Power_Limit / _rp.output_voltage); + MessageOutput.printf("[HuaweiCanClass::loop] Emergency Charge Output current %f \r\n", outputCurrent); + _setValue(outputCurrent, HUAWEI_ONLINE_CURRENT); + return; + } + + if (_batteryEmergencyCharging && !stats->getImmediateChargingRequest()) { + // Battery request has changed. Set current to 0, wait for PSU to respond and then clear state + _setValue(0, HUAWEI_ONLINE_CURRENT); + if (_rp.output_current < 1) { + _batteryEmergencyCharging = false; + } + return; + } + + // *********************** + // Automatic power control + // *********************** + + if (_mode == HUAWEI_MODE_AUTO_INT ) { // Check if we should run automatic power calculation at all. // We may have set a value recently and still wait for output stabilization @@ -377,10 +406,16 @@ void HuaweiCanClass::loop() newPowerLimit = config.Huawei.Auto_Power_Upper_Power_Limit; } - // Set the actual output limit + // Calculate output current float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); - float outputCurrent = efficiency * (newPowerLimit / _rp.output_voltage); - MessageOutput.printf("[HuaweiCanClass::loop] Output current %f \r\n", outputCurrent); + float calculatedCurrent = efficiency * (newPowerLimit / _rp.output_voltage); + + // Limit output current to value requested by BMS + float permissableCurrent = stats->getChargeCurrentLimitation() - (stats->getChargeCurrent() - _rp.output_current); // BMS current limit - current from other sources + float outputCurrent = std::min(calculatedCurrent, permissableCurrent); + outputCurrent= outputCurrent > 0 ? outputCurrent : 0; + + MessageOutput.printf("[HuaweiCanClass::loop] Setting output current to %.2fA. This is the lower value of calculated %.2fA and BMS permissable %.2fA currents\r\n", outputCurrent, calculatedCurrent, permissableCurrent); _autoPowerEnabled = true; _setValue(outputCurrent, HUAWEI_ONLINE_CURRENT); @@ -415,6 +450,7 @@ void HuaweiCanClass::_setValue(float in, uint8_t parameterType) if (in < 0) { MessageOutput.printf("[HuaweiCanClass::_setValue] Error: Tried to set voltage/current to negative value %f \r\n", in); + return; } // Start PSU if needed diff --git a/src/WebApi_Huawei.cpp b/src/WebApi_Huawei.cpp index 778833d11..d0975ddfc 100644 --- a/src/WebApi_Huawei.cpp +++ b/src/WebApi_Huawei.cpp @@ -188,6 +188,7 @@ void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request) root["enabled"] = config.Huawei.Enabled; root["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency; root["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled; + root["emergency_charge_enabled"] = config.Huawei.Emergency_Charge_Enabled; root["voltage_limit"] = static_cast(config.Huawei.Auto_Power_Voltage_Limit * 100) / 100.0; root["enable_voltage_limit"] = static_cast(config.Huawei.Auto_Power_Enable_Voltage_Limit * 100) / 100.0; root["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit; @@ -239,6 +240,7 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) if (!(root.containsKey("enabled")) || !(root.containsKey("can_controller_frequency")) || !(root.containsKey("auto_power_enabled")) || + !(root.containsKey("emergency_charge_enabled")) || !(root.containsKey("voltage_limit")) || !(root.containsKey("lower_power_limit")) || !(root.containsKey("upper_power_limit"))) { @@ -253,11 +255,11 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) config.Huawei.Enabled = root["enabled"].as(); config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as(); config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as(); + config.Huawei.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as(); config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as(); config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as(); config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as(); config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as(); - WebApi.writeConfig(retMsg); response->setLength(); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index ff372786e..3ee53c03e 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -836,10 +836,13 @@ "Limits": "Limits", "VoltageLimit": "Ladespannungslimit", "enableVoltageLimit": "Start Spannungslimit", + "stopVoltageLimitHint": "Maximal Spannung des Ladegeräts. Entspricht der geünschten Ladeschlussspannung der Batterie. Verwendet für die Automatische Leistungssteuerung und beim Notfallladen", "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.", + "maxPowerLimitHint": "Maximale Ausgangsleistung. Verwendet für die Automatische Leistungssteuerung und beim Notfallladen", "lowerPowerLimit": "Minimale Leistung", "upperPowerLimit": "Maximale Leistung", - "Seconds": "@:base.Seconds" + "Seconds": "@:base.Seconds", + "EnableEmergencyCharge": "Notfallladen: Batterie wird mit maximaler Leistung geladen wenn durch das Batterie BMS angefordert" }, "battery": { "battery": "Batterie", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index f7e7da98b..77c087463 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -843,10 +843,13 @@ "Limits": "Limits", "VoltageLimit": "Charge Voltage limit", "enableVoltageLimit": "Re-enable voltage limit", + "stopVoltageLimitHint": "Maximum charger voltage. Equals battery charge voltage limit. Used for automatic power control and when emergency charging", "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.", + "maxPowerLimitHint": "Maximum output power. Used for automatic power control and when emergency charging", "lowerPowerLimit": "Minimum output power", "upperPowerLimit": "Maximum output power", - "Seconds": "@:base.Seconds" + "Seconds": "@:base.Seconds", + "EnableEmergencyCharge": "Emergency charge. Battery charged with maximum power if requested by Battery BMS" }, "battery": { "battery": "Battery", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index df56ac072..d60cb5b65 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -834,10 +834,13 @@ "Limits": "Limits", "VoltageLimit": "Charge Voltage limit", "enableVoltageLimit": "Re-enable voltage limit", + "stopVoltageLimitHint": "Maximum charger voltage. Equals battery charge voltage limit. Used for automatic power control and when emergency charging", "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.", + "maxPowerLimitHint": "Maximum output power. Used for automatic power control and when emergency charging", "lowerPowerLimit": "Minimum output power", "upperPowerLimit": "Maximum output power", - "Seconds": "@:base.Seconds" + "Seconds": "@:base.Seconds", + "EnableEmergencyCharge": "Emergency charge. Battery charged with maximum power if requested by Battery BMS" }, "battery": { "battery": "Battery", diff --git a/webapp/src/types/AcChargerConfig.ts b/webapp/src/types/AcChargerConfig.ts index 2e1b9ca89..e80a65aaa 100644 --- a/webapp/src/types/AcChargerConfig.ts +++ b/webapp/src/types/AcChargerConfig.ts @@ -6,4 +6,5 @@ export interface AcChargerConfig { enable_voltage_limit: number; lower_power_limit: number; upper_power_limit: number; + emergency_charge_enabled: boolean; } diff --git a/webapp/src/views/AcChargerAdminView.vue b/webapp/src/views/AcChargerAdminView.vue index 44f7f8b97..4f811d26f 100644 --- a/webapp/src/views/AcChargerAdminView.vue +++ b/webapp/src/views/AcChargerAdminView.vue @@ -28,10 +28,17 @@ v-model="acChargerConfigList.auto_power_enabled" type="checkbox" wide/> + + + v-show="acChargerConfigList.auto_power_enabled || acChargerConfigList.emergency_charge_enabled">

- +
W
- +
Date: Fri, 29 Mar 2024 16:05:09 +0100 Subject: [PATCH 16/70] Feature: add unique prefix to VE.Direct messages --- .../VeDirectFrameHandler.cpp | 22 ++++++++++--------- .../VeDirectFrameHandler.h | 8 ++++--- .../VeDirectMpptController.cpp | 5 ++--- .../VeDirectShuntController.cpp | 7 ++---- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 64c1e5df0..8fb8800ea 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -72,7 +72,7 @@ VeDirectFrameHandler::VeDirectFrameHandler() : { } -void VeDirectFrameHandler::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) +void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { _vedirectSerial = std::make_unique(hwSerialPort); _vedirectSerial->begin(19200, SERIAL_8N1, rx, tx); @@ -80,13 +80,15 @@ void VeDirectFrameHandler::init(int8_t rx, int8_t tx, Print* msgOut, bool verbos _msgOut = msgOut; _verboseLogging = verboseLogging; _debugIn = 0; + snprintf(_logId, sizeof(_logId), "[VE.Direct %s %d/%d]", who, rx, tx); + if (_verboseLogging) { _msgOut->printf("%s init complete\r\n", _logId); } } void VeDirectFrameHandler::dumpDebugBuffer() { - _msgOut->printf("[VE.Direct] serial input (%d Bytes):", _debugIn); + _msgOut->printf("%s serial input (%d Bytes):", _logId, _debugIn); for (int i = 0; i < _debugIn; ++i) { if (i % 16 == 0) { - _msgOut->printf("\r\n[VE.Direct]"); + _msgOut->printf("\r\n%s", _logId); } _msgOut->printf(" %02x", _debugBuffer[i]); } @@ -105,7 +107,7 @@ void VeDirectFrameHandler::loop() // if such a large gap is observed, reset the state machine so it tries // to decode a new frame once more data arrives. if (IDLE != _state && _lastByteMillis + 500 < millis()) { - _msgOut->printf("[VE.Direct] Resetting state machine (was %d) after timeout\r\n", _state); + _msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", _logId, _state); if (_verboseLogging) { dumpDebugBuffer(); } _checksum = 0; _state = IDLE; @@ -123,7 +125,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) _debugBuffer[_debugIn] = inbyte; _debugIn = (_debugIn + 1) % _debugBuffer.size(); if (0 == _debugIn) { - _msgOut->println("[VE.Direct] ERROR: debug buffer overrun!"); + _msgOut->printf("%s ERROR: debug buffer overrun!\r\n", _logId); } } @@ -201,7 +203,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) { bool valid = _checksum == 0; if (!valid) { - _msgOut->printf("[VE.Direct] checksum 0x%02x != 0, invalid frame\r\n", _checksum); + _msgOut->printf("%s checksum 0x%02x != 0, invalid frame\r\n", _logId, _checksum); } if (_verboseLogging) { dumpDebugBuffer(); } _checksum = 0; @@ -219,10 +221,10 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) * textRxEvent * This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer. */ -bool VeDirectFrameHandler::textRxEvent(std::string const& who, char* name, char* value, veStruct& frame) { +bool VeDirectFrameHandler::textRxEvent(char* name, char* value, veStruct& frame) { if (_verboseLogging) { - _msgOut->printf("[Victron %s] Text Event %s: Value: %s\r\n", - who.c_str(), name, value ); + _msgOut->printf("%s Text Event %s: Value: %s\r\n", + _logId, name, value ); } if (strcmp(name, "PID") == 0) { @@ -271,7 +273,7 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { default: _hexSize++; if (_hexSize>=VE_MAX_HEX_LEN) { // oops -buffer overflow - something went wrong, we abort - _msgOut->println("[VE.Direct] hexRx buffer overflow - aborting read"); + _msgOut->printf("%s hexRx buffer overflow - aborting read\r\n", _logId); _hexSize=0; ret=IDLE; } diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index 1acf56ad6..a3947f3d9 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -22,12 +22,13 @@ class VeDirectFrameHandler { public: - VeDirectFrameHandler(); - virtual void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); void loop(); // main loop to read ve.direct data uint32_t getLastUpdate() const; // timestamp of last successful frame read protected: + VeDirectFrameHandler(); + void init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); + bool _verboseLogging; Print* _msgOut; uint32_t _lastUpdate; @@ -43,7 +44,7 @@ class VeDirectFrameHandler { frozen::string const& getPidAsString() const; // product ID as string } veStruct; - bool textRxEvent(std::string const& who, char* name, char* value, veStruct& frame); + bool textRxEvent(char* name, char* value, veStruct& frame); bool isDataValid(veStruct const& frame) const; // return true if data valid and not outdated template @@ -76,4 +77,5 @@ class VeDirectFrameHandler { std::array _debugBuffer; unsigned _debugIn; uint32_t _lastByteMillis; + char _logId[32]; }; diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 4112510f4..16df560c8 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -3,9 +3,8 @@ void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { - VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, hwSerialPort); + VeDirectFrameHandler::init("MPPT", rx, tx, msgOut, verboseLogging, hwSerialPort); _spData = std::make_shared(); - if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); } } bool VeDirectMpptController::isDataValid() const { @@ -14,7 +13,7 @@ bool VeDirectMpptController::isDataValid() const { void VeDirectMpptController::textRxEvent(char* name, char* value) { - if (VeDirectFrameHandler::textRxEvent("MPPT", name, value, _tmpFrame)) { + if (VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame)) { return; } diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp index 27357a1d1..351712dea 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -9,15 +9,12 @@ VeDirectShuntController::VeDirectShuntController() void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) { - VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 2); - if (_verboseLogging) { - _msgOut->println("Finished init ShuntController"); - } + VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, verboseLogging, 2); } void VeDirectShuntController::textRxEvent(char* name, char* value) { - if (VeDirectFrameHandler::textRxEvent("SmartShunt", name, value, _tmpFrame)) { + if (VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame)) { return; } From ad125ea804efcacb7405615712283cb79e748cc1 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 29 Mar 2024 20:01:14 +0100 Subject: [PATCH 17/70] Fix: properly handle fragmented VE.Direct messages queue every text event until the frame was checked by it checksum. then process the data directly into the buffer struct. do not clear the buffer struct, so it will always include the most recent value of a particular data point. --- include/BatteryStats.h | 2 +- include/MqttHandleVedirect.h | 4 +- lib/VeDirectFrameHandler/VeDirectData.cpp | 224 +++++++++++++++++ lib/VeDirectFrameHandler/VeDirectData.h | 70 ++++++ .../VeDirectFrameHandler.cpp | 229 +++++------------- .../VeDirectFrameHandler.h | 54 ++--- .../VeDirectMpptController.cpp | 176 ++++---------- .../VeDirectMpptController.h | 33 +-- .../VeDirectShuntController.cpp | 139 ++++++----- .../VeDirectShuntController.h | 40 +-- src/BatteryStats.cpp | 2 +- src/MqttHandleVedirect.cpp | 4 +- 12 files changed, 515 insertions(+), 462 deletions(-) create mode 100644 lib/VeDirectFrameHandler/VeDirectData.cpp create mode 100644 lib/VeDirectFrameHandler/VeDirectData.h diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 5c5f06b1c..ca669aa87 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -147,7 +147,7 @@ class VictronSmartShuntStats : public BatteryStats { void getLiveViewData(JsonVariant& root) const final; void mqttPublish() const final; - void updateFrom(VeDirectShuntController::veShuntStruct const& shuntData); + void updateFrom(VeDirectShuntController::data_t const& shuntData); private: float _current; diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h index c420d0884..897dc00ac 100644 --- a/include/MqttHandleVedirect.h +++ b/include/MqttHandleVedirect.h @@ -21,7 +21,7 @@ class MqttHandleVedirectClass { void forceUpdate(); private: void loop(); - std::map _kvFrames; + std::map _kvFrames; Task _loopTask; @@ -34,7 +34,7 @@ class MqttHandleVedirectClass { bool _PublishFull; void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, - VeDirectMpptController::veMpptStruct &frame) const; + const VeDirectMpptController::data_t &frame) const; }; extern MqttHandleVedirectClass MqttHandleVedirect; diff --git a/lib/VeDirectFrameHandler/VeDirectData.cpp b/lib/VeDirectFrameHandler/VeDirectData.cpp new file mode 100644 index 000000000..20c2cdf63 --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectData.cpp @@ -0,0 +1,224 @@ +#include "VeDirectData.h" + +template +static frozen::string const& getAsString(frozen::map const& values, T val) +{ + auto pos = values.find(val); + if (pos == values.end()) { + static constexpr frozen::string dummy("???"); + return dummy; + } + return pos->second; +} + +/* + * This function returns the product id (PID) as readable text. + */ +frozen::string const& veStruct::getPidAsString() const +{ + /** + * this map is rendered from [1], which is more recent than [2]. Phoenix + * inverters are not included in the map. unfortunately, the documents do + * not fully align. PID 0xA07F is only present in [1]. PIDs 0xA048, 0xA110, + * and 0xA111 are only present in [2]. PIDs 0xA06D and 0xA078 are rev3 in + * [1] but rev2 in [2]. + * + * [1] https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf + * [2] https://www.victronenergy.com/upload/documents/BlueSolar-HEX-protocol.pdf + */ + static constexpr frozen::map values = { + { 0x0203, "BMV-700" }, + { 0x0204, "BMV-702" }, + { 0x0205, "BMV-700H" }, + { 0x0300, "BlueSolar MPPT 70|15" }, + { 0xA040, "BlueSolar MPPT 75|50" }, + { 0xA041, "BlueSolar MPPT 150|35" }, + { 0xA042, "BlueSolar MPPT 75|15" }, + { 0xA043, "BlueSolar MPPT 100|15" }, + { 0xA044, "BlueSolar MPPT 100|30" }, + { 0xA045, "BlueSolar MPPT 100|50" }, + { 0xA046, "BlueSolar MPPT 150|70" }, + { 0xA047, "BlueSolar MPPT 150|100" }, + { 0xA048, "BlueSolar MPPT 75|50 rev2" }, + { 0xA049, "BlueSolar MPPT 100|50 rev2" }, + { 0xA04A, "BlueSolar MPPT 100|30 rev2" }, + { 0xA04B, "BlueSolar MPPT 150|35 rev2" }, + { 0xA04C, "BlueSolar MPPT 75|10" }, + { 0xA04D, "BlueSolar MPPT 150|45" }, + { 0xA04E, "BlueSolar MPPT 150|60" }, + { 0xA04F, "BlueSolar MPPT 150|85" }, + { 0xA050, "SmartSolar MPPT 250|100" }, + { 0xA051, "SmartSolar MPPT 150|100" }, + { 0xA052, "SmartSolar MPPT 150|85" }, + { 0xA053, "SmartSolar MPPT 75|15" }, + { 0xA054, "SmartSolar MPPT 75|10" }, + { 0xA055, "SmartSolar MPPT 100|15" }, + { 0xA056, "SmartSolar MPPT 100|30" }, + { 0xA057, "SmartSolar MPPT 100|50" }, + { 0xA058, "SmartSolar MPPT 150|35" }, + { 0xA059, "SmartSolar MPPT 150|100 rev2" }, + { 0xA05A, "SmartSolar MPPT 150|85 rev2" }, + { 0xA05B, "SmartSolar MPPT 250|70" }, + { 0xA05C, "SmartSolar MPPT 250|85" }, + { 0xA05D, "SmartSolar MPPT 250|60" }, + { 0xA05E, "SmartSolar MPPT 250|45" }, + { 0xA05F, "SmartSolar MPPT 100|20" }, + { 0xA060, "SmartSolar MPPT 100|20 48V" }, + { 0xA061, "SmartSolar MPPT 150|45" }, + { 0xA062, "SmartSolar MPPT 150|60" }, + { 0xA063, "SmartSolar MPPT 150|70" }, + { 0xA064, "SmartSolar MPPT 250|85 rev2" }, + { 0xA065, "SmartSolar MPPT 250|100 rev2" }, + { 0xA066, "BlueSolar MPPT 100|20" }, + { 0xA067, "BlueSolar MPPT 100|20 48V" }, + { 0xA068, "SmartSolar MPPT 250|60 rev2" }, + { 0xA069, "SmartSolar MPPT 250|70 rev2" }, + { 0xA06A, "SmartSolar MPPT 150|45 rev2" }, + { 0xA06B, "SmartSolar MPPT 150|60 rev2" }, + { 0xA06C, "SmartSolar MPPT 150|70 rev2" }, + { 0xA06D, "SmartSolar MPPT 150|85 rev3" }, + { 0xA06E, "SmartSolar MPPT 150|100 rev3" }, + { 0xA06F, "BlueSolar MPPT 150|45 rev2" }, + { 0xA070, "BlueSolar MPPT 150|60 rev2" }, + { 0xA071, "BlueSolar MPPT 150|70 rev2" }, + { 0xA072, "BlueSolar MPPT 150|45 rev3" }, + { 0xA073, "SmartSolar MPPT 150|45 rev3" }, + { 0xA074, "SmartSolar MPPT 75|10 rev2" }, + { 0xA075, "SmartSolar MPPT 75|15 rev2" }, + { 0xA076, "BlueSolar MPPT 100|30 rev3" }, + { 0xA077, "BlueSolar MPPT 100|50 rev3" }, + { 0xA078, "BlueSolar MPPT 150|35 rev3" }, + { 0xA079, "BlueSolar MPPT 75|10 rev2" }, + { 0xA07A, "BlueSolar MPPT 75|15 rev2" }, + { 0xA07B, "BlueSolar MPPT 100|15 rev2" }, + { 0xA07C, "BlueSolar MPPT 75|10 rev3" }, + { 0xA07D, "BlueSolar MPPT 75|15 rev3" }, + { 0xA07E, "SmartSolar MPPT 100|30 12V" }, + { 0xA07F, "All-In-1 SmartSolar MPPT 75|15 12V" }, + { 0xA102, "SmartSolar MPPT VE.Can 150|70" }, + { 0xA103, "SmartSolar MPPT VE.Can 150|45" }, + { 0xA104, "SmartSolar MPPT VE.Can 150|60" }, + { 0xA105, "SmartSolar MPPT VE.Can 150|85" }, + { 0xA106, "SmartSolar MPPT VE.Can 150|100" }, + { 0xA107, "SmartSolar MPPT VE.Can 250|45" }, + { 0xA108, "SmartSolar MPPT VE.Can 250|60" }, + { 0xA109, "SmartSolar MPPT VE.Can 250|70" }, + { 0xA10A, "SmartSolar MPPT VE.Can 250|85" }, + { 0xA10B, "SmartSolar MPPT VE.Can 250|100" }, + { 0xA10C, "SmartSolar MPPT VE.Can 150|70 rev2" }, + { 0xA10D, "SmartSolar MPPT VE.Can 150|85 rev2" }, + { 0xA10E, "SmartSolar MPPT VE.Can 150|100 rev2" }, + { 0xA10F, "BlueSolar MPPT VE.Can 150|100" }, + { 0xA110, "SmartSolar MPPT RS 450|100" }, + { 0xA111, "SmartSolar MPPT RS 450|200" }, + { 0xA112, "BlueSolar MPPT VE.Can 250|70" }, + { 0xA113, "BlueSolar MPPT VE.Can 250|100" }, + { 0xA114, "SmartSolar MPPT VE.Can 250|70 rev2" }, + { 0xA115, "SmartSolar MPPT VE.Can 250|100 rev2" }, + { 0xA116, "SmartSolar MPPT VE.Can 250|85 rev2" }, + { 0xA117, "BlueSolar MPPT VE.Can 150|100 rev2" }, + { 0xA340, "Phoenix Smart IP43 Charger 12|50 (1+1)" }, + { 0xA341, "Phoenix Smart IP43 Charger 12|50 (3)" }, + { 0xA342, "Phoenix Smart IP43 Charger 24|25 (1+1)" }, + { 0xA343, "Phoenix Smart IP43 Charger 24|25 (3)" }, + { 0xA344, "Phoenix Smart IP43 Charger 12|30 (1+1)" }, + { 0xA345, "Phoenix Smart IP43 Charger 12|30 (3)" }, + { 0xA346, "Phoenix Smart IP43 Charger 24|16 (1+1)" }, + { 0xA347, "Phoenix Smart IP43 Charger 24|16 (3)" }, + { 0xA381, "BMV-712 Smart" }, + { 0xA382, "BMV-710H Smart" }, + { 0xA383, "BMV-712 Smart Rev2" }, + { 0xA389, "SmartShunt 500A/50mV" }, + { 0xA38A, "SmartShunt 1000A/50mV" }, + { 0xA38B, "SmartShunt 2000A/50mV" }, + { 0xA3F0, "Smart BuckBoost 12V/12V-50A" }, + }; + + return getAsString(values, PID); +} + +/* + * This function returns the state of operations (CS) as readable text. + */ +frozen::string const& veMpptStruct::getCsAsString() const +{ + static constexpr frozen::map values = { + { 0, "OFF" }, + { 2, "Fault" }, + { 3, "Bulk" }, + { 4, "Absorbtion" }, + { 5, "Float" }, + { 7, "Equalize (manual)" }, + { 245, "Starting-up" }, + { 247, "Auto equalize / Recondition" }, + { 252, "External Control" } + }; + + return getAsString(values, CS); +} + +/* + * This function returns the state of MPPT (MPPT) as readable text. + */ +frozen::string const& veMpptStruct::getMpptAsString() const +{ + static constexpr frozen::map values = { + { 0, "OFF" }, + { 1, "Voltage or current limited" }, + { 2, "MPP Tracker active" } + }; + + return getAsString(values, MPPT); +} + +/* + * This function returns error state (ERR) as readable text. + */ +frozen::string const& veMpptStruct::getErrAsString() const +{ + static constexpr frozen::map values = { + { 0, "No error" }, + { 2, "Battery voltage too high" }, + { 17, "Charger temperature too high" }, + { 18, "Charger over current" }, + { 19, "Charger current reversed" }, + { 20, "Bulk time limit exceeded" }, + { 21, "Current sensor issue(sensor bias/sensor broken)" }, + { 26, "Terminals overheated" }, + { 28, "Converter issue (dual converter models only)" }, + { 33, "Input voltage too high (solar panel)" }, + { 34, "Input current too high (solar panel)" }, + { 38, "Input shutdown (due to excessive battery voltage)" }, + { 39, "Input shutdown (due to current flow during off mode)" }, + { 40, "Input" }, + { 65, "Lost communication with one of devices" }, + { 67, "Synchronisedcharging device configuration issue" }, + { 68, "BMS connection lost" }, + { 116, "Factory calibration data lost" }, + { 117, "Invalid/incompatible firmware" }, + { 118, "User settings invalid" } + }; + + return getAsString(values, ERR); +} + +/* + * This function returns the off reason (OR) as readable text. + */ +frozen::string const& veMpptStruct::getOrAsString() const +{ + static constexpr frozen::map values = { + { 0x00000000, "Not off" }, + { 0x00000001, "No input power" }, + { 0x00000002, "Switched off (power switch)" }, + { 0x00000004, "Switched off (device moderegister)" }, + { 0x00000008, "Remote input" }, + { 0x00000010, "Protection active" }, + { 0x00000020, "Paygo" }, + { 0x00000040, "BMS" }, + { 0x00000080, "Engine shutdown detection" }, + { 0x00000100, "Analysing input voltage" } + }; + + return getAsString(values, OR); +} diff --git a/lib/VeDirectFrameHandler/VeDirectData.h b/lib/VeDirectFrameHandler/VeDirectData.h new file mode 100644 index 000000000..0c43b555a --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectData.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0 +#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer + +typedef struct { + uint16_t PID = 0; // product id + char SER[VE_MAX_VALUE_LEN]; // serial number + char FW[VE_MAX_VALUE_LEN]; // firmware release number + double V = 0; // battery voltage in V + double I = 0; // battery current in A + double E = 0; // efficiency in percent (calculated, moving average) + + frozen::string const& getPidAsString() const; // product ID as string +} veStruct; + +struct veMpptStruct : veStruct { + uint8_t MPPT; // state of MPP tracker + int32_t PPV; // panel power in W + int32_t P; // battery output power in W (calculated) + double VPV; // panel voltage in V + double IPV; // panel current in A (calculated) + bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit) + uint8_t CS; // current state of operation e.g. OFF or Bulk + uint8_t ERR; // error code + uint32_t OR; // off reason + uint32_t HSDS; // day sequence number 1...365 + double H19; // yield total kWh + double H20; // yield today kWh + int32_t H21; // maximum power today W + double H22; // yield yesterday kWh + int32_t H23; // maximum power yesterday W + + frozen::string const& getMpptAsString() const; // state of mppt as string + frozen::string const& getCsAsString() const; // current state as string + frozen::string const& getErrAsString() const; // error state as string + frozen::string const& getOrAsString() const; // off reason as string +}; + +struct veShuntStruct : veStruct { + int32_t T; // Battery temperature + bool tempPresent; // Battery temperature sensor is attached to the shunt + int32_t P; // Instantaneous power + int32_t CE; // Consumed Amp Hours + int32_t SOC; // State-of-charge + uint32_t TTG; // Time-to-go + bool ALARM; // Alarm condition active + uint32_t AR; // Alarm Reason + int32_t H1; // Depth of the deepest discharge + int32_t H2; // Depth of the last discharge + int32_t H3; // Depth of the average discharge + int32_t H4; // Number of charge cycles + int32_t H5; // Number of full discharges + int32_t H6; // Cumulative Amp Hours drawn + int32_t H7; // Minimum main (battery) voltage + int32_t H8; // Maximum main (battery) voltage + int32_t H9; // Number of seconds since last full charge + int32_t H10; // Number of automatic synchronizations + int32_t H11; // Number of low main voltage alarms + int32_t H12; // Number of high main voltage alarms + int32_t H13; // Number of low auxiliary voltage alarms + int32_t H14; // Number of high auxiliary voltage alarms + int32_t H15; // Minimum auxiliary (battery) voltage + int32_t H16; // Maximum auxiliary (battery) voltage + int32_t H17; // Amount of discharged energy + int32_t H18; // Amount of charged energy +}; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 8fb8800ea..a308f7f96 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -58,7 +58,8 @@ class Silent : public Print { static Silent MessageOutputDummy; -VeDirectFrameHandler::VeDirectFrameHandler() : +template +VeDirectFrameHandler::VeDirectFrameHandler() : _msgOut(&MessageOutputDummy), _lastUpdate(0), _state(IDLE), @@ -72,7 +73,8 @@ VeDirectFrameHandler::VeDirectFrameHandler() : { } -void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) +template +void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { _vedirectSerial = std::make_unique(hwSerialPort); _vedirectSerial->begin(19200, SERIAL_8N1, rx, tx); @@ -84,7 +86,8 @@ void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* ms if (_verboseLogging) { _msgOut->printf("%s init complete\r\n", _logId); } } -void VeDirectFrameHandler::dumpDebugBuffer() { +template +void VeDirectFrameHandler::dumpDebugBuffer() { _msgOut->printf("%s serial input (%d Bytes):", _logId, _debugIn); for (int i = 0; i < _debugIn; ++i) { if (i % 16 == 0) { @@ -96,7 +99,16 @@ void VeDirectFrameHandler::dumpDebugBuffer() { _debugIn = 0; } -void VeDirectFrameHandler::loop() +template +void VeDirectFrameHandler::reset() +{ + _checksum = 0; + _state = IDLE; + _textData.clear(); +} + +template +void VeDirectFrameHandler::loop() { while ( _vedirectSerial->available()) { rxData(_vedirectSerial->read()); @@ -109,8 +121,7 @@ void VeDirectFrameHandler::loop() if (IDLE != _state && _lastByteMillis + 500 < millis()) { _msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", _logId, _state); if (_verboseLogging) { dumpDebugBuffer(); } - _checksum = 0; - _state = IDLE; + reset(); } } @@ -119,7 +130,8 @@ void VeDirectFrameHandler::loop() * This function is called by loop() which passes a byte of serial data * Based on Victron's example code. But using String and Map instead of pointer and arrays */ -void VeDirectFrameHandler::rxData(uint8_t inbyte) +template +void VeDirectFrameHandler::rxData(uint8_t inbyte) { if (_verboseLogging) { _debugBuffer[_debugIn] = inbyte; @@ -186,7 +198,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) case '\n': if ( _textPointer < (_value + sizeof(_value)) ) { *_textPointer = 0; // make zero ended - textRxEvent(_name, _value); + _textData.push_back({_name, _value}); } _state = RECORD_BEGIN; break; @@ -201,14 +213,18 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) break; case CHECKSUM: { - bool valid = _checksum == 0; - if (!valid) { - _msgOut->printf("%s checksum 0x%02x != 0, invalid frame\r\n", _logId, _checksum); - } if (_verboseLogging) { dumpDebugBuffer(); } - _checksum = 0; - _state = IDLE; - if (valid) { frameValidEvent(); } + if (_checksum == 0) { + for (auto const& event : _textData) { + processTextData(event.first, event.second); + } + _lastUpdate = millis(); + frameValidEvent(); + } + else { + _msgOut->printf("%s checksum 0x%02x != 0x00, invalid frame\r\n", _logId, _checksum); + } + reset(); break; } case RECORD_HEX: @@ -218,41 +234,44 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) } /* - * textRxEvent * This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer. */ -bool VeDirectFrameHandler::textRxEvent(char* name, char* value, veStruct& frame) { +template +void VeDirectFrameHandler::processTextData(std::string const& name, std::string const& value) { if (_verboseLogging) { - _msgOut->printf("%s Text Event %s: Value: %s\r\n", - _logId, name, value ); + _msgOut->printf("%s Text Data '%s' = '%s'\r\n", + _logId, name.c_str(), value.c_str()); } - if (strcmp(name, "PID") == 0) { - frame.PID = strtol(value, nullptr, 0); - return true; + if (processTextDataDerived(name, value)) { return; } + + if (name == "PID") { + _tmpFrame.PID = strtol(value.c_str(), nullptr, 0); + return; } - if (strcmp(name, "SER") == 0) { - strcpy(frame.SER, value); - return true; + if (name == "SER") { + strcpy(_tmpFrame.SER, value.c_str()); + return; } - if (strcmp(name, "FW") == 0) { - strcpy(frame.FW, value); - return true; + if (name == "FW") { + strcpy(_tmpFrame.FW, value.c_str()); + return; } - if (strcmp(name, "V") == 0) { - frame.V = round(atof(value) / 10.0) / 100.0; - return true; + if (name == "V") { + _tmpFrame.V = round(atof(value.c_str()) / 10.0) / 100.0; + return; } - if (strcmp(name, "I") == 0) { - frame.I = round(atof(value) / 10.0) / 100.0; - return true; + if (name == "I") { + _tmpFrame.I = round(atof(value.c_str()) / 10.0) / 100.0; + return; } - return false; + _msgOut->printf("%s Unknown text data '%s' (value '%s')\r\n", + _logId, name.c_str(), value.c_str()); } @@ -261,7 +280,9 @@ bool VeDirectFrameHandler::textRxEvent(char* name, char* value, veStruct& frame) * hexRxEvent * This function records hex answers or async messages */ -int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { +template +int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) +{ int ret=RECORD_HEX; // default - continue recording until end of frame switch (inbyte) { @@ -282,138 +303,14 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { return ret; } -bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const { - return strlen(frame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000); -} - -uint32_t VeDirectFrameHandler::getLastUpdate() const +template +bool VeDirectFrameHandler::isDataValid() const { - return _lastUpdate; + return strlen(_tmpFrame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000); } -/* - * getPidAsString - * This function returns the product id (PID) as readable text. - */ -frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const +template +uint32_t VeDirectFrameHandler::getLastUpdate() const { - /** - * this map is rendered from [1], which is more recent than [2]. Phoenix - * inverters are not included in the map. unfortunately, the documents do - * not fully align. PID 0xA07F is only present in [1]. PIDs 0xA048, 0xA110, - * and 0xA111 are only present in [2]. PIDs 0xA06D and 0xA078 are rev3 in - * [1] but rev2 in [2]. - * - * [1] https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf - * [2] https://www.victronenergy.com/upload/documents/BlueSolar-HEX-protocol.pdf - */ - static constexpr frozen::map values = { - { 0x0203, "BMV-700" }, - { 0x0204, "BMV-702" }, - { 0x0205, "BMV-700H" }, - { 0x0300, "BlueSolar MPPT 70|15" }, - { 0xA040, "BlueSolar MPPT 75|50" }, - { 0xA041, "BlueSolar MPPT 150|35" }, - { 0xA042, "BlueSolar MPPT 75|15" }, - { 0xA043, "BlueSolar MPPT 100|15" }, - { 0xA044, "BlueSolar MPPT 100|30" }, - { 0xA045, "BlueSolar MPPT 100|50" }, - { 0xA046, "BlueSolar MPPT 150|70" }, - { 0xA047, "BlueSolar MPPT 150|100" }, - { 0xA048, "BlueSolar MPPT 75|50 rev2" }, - { 0xA049, "BlueSolar MPPT 100|50 rev2" }, - { 0xA04A, "BlueSolar MPPT 100|30 rev2" }, - { 0xA04B, "BlueSolar MPPT 150|35 rev2" }, - { 0xA04C, "BlueSolar MPPT 75|10" }, - { 0xA04D, "BlueSolar MPPT 150|45" }, - { 0xA04E, "BlueSolar MPPT 150|60" }, - { 0xA04F, "BlueSolar MPPT 150|85" }, - { 0xA050, "SmartSolar MPPT 250|100" }, - { 0xA051, "SmartSolar MPPT 150|100" }, - { 0xA052, "SmartSolar MPPT 150|85" }, - { 0xA053, "SmartSolar MPPT 75|15" }, - { 0xA054, "SmartSolar MPPT 75|10" }, - { 0xA055, "SmartSolar MPPT 100|15" }, - { 0xA056, "SmartSolar MPPT 100|30" }, - { 0xA057, "SmartSolar MPPT 100|50" }, - { 0xA058, "SmartSolar MPPT 150|35" }, - { 0xA059, "SmartSolar MPPT 150|100 rev2" }, - { 0xA05A, "SmartSolar MPPT 150|85 rev2" }, - { 0xA05B, "SmartSolar MPPT 250|70" }, - { 0xA05C, "SmartSolar MPPT 250|85" }, - { 0xA05D, "SmartSolar MPPT 250|60" }, - { 0xA05E, "SmartSolar MPPT 250|45" }, - { 0xA05F, "SmartSolar MPPT 100|20" }, - { 0xA060, "SmartSolar MPPT 100|20 48V" }, - { 0xA061, "SmartSolar MPPT 150|45" }, - { 0xA062, "SmartSolar MPPT 150|60" }, - { 0xA063, "SmartSolar MPPT 150|70" }, - { 0xA064, "SmartSolar MPPT 250|85 rev2" }, - { 0xA065, "SmartSolar MPPT 250|100 rev2" }, - { 0xA066, "BlueSolar MPPT 100|20" }, - { 0xA067, "BlueSolar MPPT 100|20 48V" }, - { 0xA068, "SmartSolar MPPT 250|60 rev2" }, - { 0xA069, "SmartSolar MPPT 250|70 rev2" }, - { 0xA06A, "SmartSolar MPPT 150|45 rev2" }, - { 0xA06B, "SmartSolar MPPT 150|60 rev2" }, - { 0xA06C, "SmartSolar MPPT 150|70 rev2" }, - { 0xA06D, "SmartSolar MPPT 150|85 rev3" }, - { 0xA06E, "SmartSolar MPPT 150|100 rev3" }, - { 0xA06F, "BlueSolar MPPT 150|45 rev2" }, - { 0xA070, "BlueSolar MPPT 150|60 rev2" }, - { 0xA071, "BlueSolar MPPT 150|70 rev2" }, - { 0xA072, "BlueSolar MPPT 150|45 rev3" }, - { 0xA073, "SmartSolar MPPT 150|45 rev3" }, - { 0xA074, "SmartSolar MPPT 75|10 rev2" }, - { 0xA075, "SmartSolar MPPT 75|15 rev2" }, - { 0xA076, "BlueSolar MPPT 100|30 rev3" }, - { 0xA077, "BlueSolar MPPT 100|50 rev3" }, - { 0xA078, "BlueSolar MPPT 150|35 rev3" }, - { 0xA079, "BlueSolar MPPT 75|10 rev2" }, - { 0xA07A, "BlueSolar MPPT 75|15 rev2" }, - { 0xA07B, "BlueSolar MPPT 100|15 rev2" }, - { 0xA07C, "BlueSolar MPPT 75|10 rev3" }, - { 0xA07D, "BlueSolar MPPT 75|15 rev3" }, - { 0xA07E, "SmartSolar MPPT 100|30 12V" }, - { 0xA07F, "All-In-1 SmartSolar MPPT 75|15 12V" }, - { 0xA102, "SmartSolar MPPT VE.Can 150|70" }, - { 0xA103, "SmartSolar MPPT VE.Can 150|45" }, - { 0xA104, "SmartSolar MPPT VE.Can 150|60" }, - { 0xA105, "SmartSolar MPPT VE.Can 150|85" }, - { 0xA106, "SmartSolar MPPT VE.Can 150|100" }, - { 0xA107, "SmartSolar MPPT VE.Can 250|45" }, - { 0xA108, "SmartSolar MPPT VE.Can 250|60" }, - { 0xA109, "SmartSolar MPPT VE.Can 250|70" }, - { 0xA10A, "SmartSolar MPPT VE.Can 250|85" }, - { 0xA10B, "SmartSolar MPPT VE.Can 250|100" }, - { 0xA10C, "SmartSolar MPPT VE.Can 150|70 rev2" }, - { 0xA10D, "SmartSolar MPPT VE.Can 150|85 rev2" }, - { 0xA10E, "SmartSolar MPPT VE.Can 150|100 rev2" }, - { 0xA10F, "BlueSolar MPPT VE.Can 150|100" }, - { 0xA110, "SmartSolar MPPT RS 450|100" }, - { 0xA111, "SmartSolar MPPT RS 450|200" }, - { 0xA112, "BlueSolar MPPT VE.Can 250|70" }, - { 0xA113, "BlueSolar MPPT VE.Can 250|100" }, - { 0xA114, "SmartSolar MPPT VE.Can 250|70 rev2" }, - { 0xA115, "SmartSolar MPPT VE.Can 250|100 rev2" }, - { 0xA116, "SmartSolar MPPT VE.Can 250|85 rev2" }, - { 0xA117, "BlueSolar MPPT VE.Can 150|100 rev2" }, - { 0xA340, "Phoenix Smart IP43 Charger 12|50 (1+1)" }, - { 0xA341, "Phoenix Smart IP43 Charger 12|50 (3)" }, - { 0xA342, "Phoenix Smart IP43 Charger 24|25 (1+1)" }, - { 0xA343, "Phoenix Smart IP43 Charger 24|25 (3)" }, - { 0xA344, "Phoenix Smart IP43 Charger 12|30 (1+1)" }, - { 0xA345, "Phoenix Smart IP43 Charger 12|30 (3)" }, - { 0xA346, "Phoenix Smart IP43 Charger 24|16 (1+1)" }, - { 0xA347, "Phoenix Smart IP43 Charger 24|16 (3)" }, - { 0xA381, "BMV-712 Smart" }, - { 0xA382, "BMV-710H Smart" }, - { 0xA383, "BMV-712 Smart Rev2" }, - { 0xA389, "SmartShunt 500A/50mV" }, - { 0xA38A, "SmartShunt 1000A/50mV" }, - { 0xA38B, "SmartShunt 2000A/50mV" }, - { 0xA3F0, "Smart BuckBoost 12V/12V-50A" }, - }; - - return getAsString(values, PID); + return _lastUpdate; } diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index a3947f3d9..aa8857a42 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -13,17 +13,17 @@ #include #include -#include -#include #include +#include +#include +#include "VeDirectData.h" -#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0 -#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer - +template class VeDirectFrameHandler { public: void loop(); // main loop to read ve.direct data uint32_t getLastUpdate() const; // timestamp of last successful frame read + bool isDataValid() const; // return true if data valid and not outdated protected: VeDirectFrameHandler(); @@ -33,36 +33,14 @@ class VeDirectFrameHandler { Print* _msgOut; uint32_t _lastUpdate; - typedef struct { - uint16_t PID = 0; // product id - char SER[VE_MAX_VALUE_LEN]; // serial number - char FW[VE_MAX_VALUE_LEN]; // firmware release number - double V = 0; // battery voltage in V - double I = 0; // battery current in A - double E = 0; // efficiency in percent (calculated, moving average) - - frozen::string const& getPidAsString() const; // product ID as string - } veStruct; - - bool textRxEvent(char* name, char* value, veStruct& frame); - bool isDataValid(veStruct const& frame) const; // return true if data valid and not outdated - - template - static frozen::string const& getAsString(frozen::map const& values, T val) - { - auto pos = values.find(val); - if (pos == values.end()) { - static constexpr frozen::string dummy("???"); - return dummy; - } - return pos->second; - } + T _tmpFrame; private: - void setLastUpdate(); // set timestampt after successful frame read + void reset(); void dumpDebugBuffer(); void rxData(uint8_t inbyte); // byte of serial data - virtual void textRxEvent(char *, char *) = 0; + void processTextData(std::string const& name, std::string const& value); + virtual bool processTextDataDerived(std::string const& name, std::string const& value) = 0; virtual void frameValidEvent() = 0; int hexRxEvent(uint8_t); @@ -78,4 +56,18 @@ class VeDirectFrameHandler { unsigned _debugIn; uint32_t _lastByteMillis; char _logId[32]; + + /** + * not every frame contains every value the device is communicating, i.e., + * a set of values can be fragmented across multiple frames. frames can be + * invalid. in order to only process data from valid frames, we add data + * to this queue and only process it once the frame was found to be valid. + * this also handles fragmentation nicely, since there is no need to reset + * our data buffer. we simply update the interpreted data from this event + * queue, which is fine as we know the source frame was valid. + */ + std::deque> _textData; }; + +template class VeDirectFrameHandler; +template class VeDirectFrameHandler; diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 16df560c8..06616202b 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -7,58 +7,62 @@ void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verb _spData = std::make_shared(); } -bool VeDirectMpptController::isDataValid() const { - return VeDirectFrameHandler::isDataValid(*_spData); -} - -void VeDirectMpptController::textRxEvent(char* name, char* value) +bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value) { - if (VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame)) { - return; - } - - if (strcmp(name, "LOAD") == 0) { - if (strcmp(value, "ON") == 0) - _tmpFrame.LOAD = true; - else - _tmpFrame.LOAD = false; + if (name == "LOAD") { + _tmpFrame.LOAD = (value == "ON"); + return true; } - else if (strcmp(name, "CS") == 0) { - _tmpFrame.CS = atoi(value); + if (name == "CS") { + _tmpFrame.CS = atoi(value.c_str()); + return true; } - else if (strcmp(name, "ERR") == 0) { - _tmpFrame.ERR = atoi(value); + if (name == "ERR") { + _tmpFrame.ERR = atoi(value.c_str()); + return true; } - else if (strcmp(name, "OR") == 0) { - _tmpFrame.OR = strtol(value, nullptr, 0); + if (name == "OR") { + _tmpFrame.OR = strtol(value.c_str(), nullptr, 0); + return true; } - else if (strcmp(name, "MPPT") == 0) { - _tmpFrame.MPPT = atoi(value); + if (name == "MPPT") { + _tmpFrame.MPPT = atoi(value.c_str()); + return true; } - else if (strcmp(name, "HSDS") == 0) { - _tmpFrame.HSDS = atoi(value); + if (name == "HSDS") { + _tmpFrame.HSDS = atoi(value.c_str()); + return true; } - else if (strcmp(name, "VPV") == 0) { - _tmpFrame.VPV = round(atof(value) / 10.0) / 100.0; + if (name == "VPV") { + _tmpFrame.VPV = round(atof(value.c_str()) / 10.0) / 100.0; + return true; } - else if (strcmp(name, "PPV") == 0) { - _tmpFrame.PPV = atoi(value); + if (name == "PPV") { + _tmpFrame.PPV = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H19") == 0) { - _tmpFrame.H19 = atof(value) / 100.0; + if (name == "H19") { + _tmpFrame.H19 = atof(value.c_str()) / 100.0; + return true; } - else if (strcmp(name, "H20") == 0) { - _tmpFrame.H20 = atof(value) / 100.0; + if (name == "H20") { + _tmpFrame.H20 = atof(value.c_str()) / 100.0; + return true; } - else if (strcmp(name, "H21") == 0) { - _tmpFrame.H21 = atoi(value); + if (name == "H21") { + _tmpFrame.H21 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H22") == 0) { - _tmpFrame.H22 = atof(value) / 100.0; + if (name == "H22") { + _tmpFrame.H22 = atof(value.c_str()) / 100.0; + return true; } - else if (strcmp(name, "H23") == 0) { - _tmpFrame.H23 = atoi(value); + if (name == "H23") { + _tmpFrame.H23 = atoi(value.c_str()); + return true; } + + return false; } /* @@ -68,108 +72,14 @@ void VeDirectMpptController::textRxEvent(char* name, char* value) void VeDirectMpptController::frameValidEvent() { _tmpFrame.P = _tmpFrame.V * _tmpFrame.I; - _tmpFrame.IPV = 0; if (_tmpFrame.VPV > 0) { _tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV; } - _tmpFrame.E = 0; - if ( _tmpFrame.PPV > 0) { + if (_tmpFrame.PPV > 0) { _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); _tmpFrame.E = _efficiency.getAverage(); } _spData = std::make_shared(_tmpFrame); - _tmpFrame = {}; - _lastUpdate = millis(); -} - -/* - * getCsAsString - * This function returns the state of operations (CS) as readable text. - */ -frozen::string const& VeDirectMpptController::veMpptStruct::getCsAsString() const -{ - static constexpr frozen::map values = { - { 0, "OFF" }, - { 2, "Fault" }, - { 3, "Bulk" }, - { 4, "Absorbtion" }, - { 5, "Float" }, - { 7, "Equalize (manual)" }, - { 245, "Starting-up" }, - { 247, "Auto equalize / Recondition" }, - { 252, "External Control" } - }; - - return getAsString(values, CS); -} - -/* - * getMpptAsString - * This function returns the state of MPPT (MPPT) as readable text. - */ -frozen::string const& VeDirectMpptController::veMpptStruct::getMpptAsString() const -{ - static constexpr frozen::map values = { - { 0, "OFF" }, - { 1, "Voltage or current limited" }, - { 2, "MPP Tracker active" } - }; - - return getAsString(values, MPPT); -} - -/* - * getErrAsString - * This function returns error state (ERR) as readable text. - */ -frozen::string const& VeDirectMpptController::veMpptStruct::getErrAsString() const -{ - static constexpr frozen::map values = { - { 0, "No error" }, - { 2, "Battery voltage too high" }, - { 17, "Charger temperature too high" }, - { 18, "Charger over current" }, - { 19, "Charger current reversed" }, - { 20, "Bulk time limit exceeded" }, - { 21, "Current sensor issue(sensor bias/sensor broken)" }, - { 26, "Terminals overheated" }, - { 28, "Converter issue (dual converter models only)" }, - { 33, "Input voltage too high (solar panel)" }, - { 34, "Input current too high (solar panel)" }, - { 38, "Input shutdown (due to excessive battery voltage)" }, - { 39, "Input shutdown (due to current flow during off mode)" }, - { 40, "Input" }, - { 65, "Lost communication with one of devices" }, - { 67, "Synchronisedcharging device configuration issue" }, - { 68, "BMS connection lost" }, - { 116, "Factory calibration data lost" }, - { 117, "Invalid/incompatible firmware" }, - { 118, "User settings invalid" } - }; - - return getAsString(values, ERR); -} - -/* - * getOrAsString - * This function returns the off reason (OR) as readable text. - */ -frozen::string const& VeDirectMpptController::veMpptStruct::getOrAsString() const -{ - static constexpr frozen::map values = { - { 0x00000000, "Not off" }, - { 0x00000001, "No input power" }, - { 0x00000002, "Switched off (power switch)" }, - { 0x00000004, "Switched off (device moderegister)" }, - { 0x00000008, "Remote input" }, - { 0x00000010, "Protection active" }, - { 0x00000020, "Paygo" }, - { 0x00000040, "BMS" }, - { 0x00000080, "Engine shutdown detection" }, - { 0x00000100, "Analysing input voltage" } - }; - - return getAsString(values, OR); } diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index 08574252d..277a73765 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -1,6 +1,7 @@ #pragma once #include +#include "VeDirectData.h" #include "VeDirectFrameHandler.h" template @@ -35,43 +36,19 @@ class MovingAverage { size_t _count; }; -class VeDirectMpptController : public VeDirectFrameHandler { +class VeDirectMpptController : public VeDirectFrameHandler { public: VeDirectMpptController() = default; void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); - bool isDataValid() const; // return true if data valid and not outdated - struct veMpptStruct : veStruct { - uint8_t MPPT; // state of MPP tracker - int32_t PPV; // panel power in W - int32_t P; // battery output power in W (calculated) - double VPV; // panel voltage in V - double IPV; // panel current in A (calculated) - bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit) - uint8_t CS; // current state of operation e.g. OFF or Bulk - uint8_t ERR; // error code - uint32_t OR; // off reason - uint32_t HSDS; // day sequence number 1...365 - double H19; // yield total kWh - double H20; // yield today kWh - int32_t H21; // maximum power today W - double H22; // yield yesterday kWh - int32_t H23; // maximum power yesterday W - - frozen::string const& getMpptAsString() const; // state of mppt as string - frozen::string const& getCsAsString() const; // current state as string - frozen::string const& getErrAsString() const; // error state as string - frozen::string const& getOrAsString() const; // off reason as string - }; - - using spData_t = std::shared_ptr; + using data_t = veMpptStruct; + using spData_t = std::shared_ptr; spData_t getData() const { return _spData; } private: - void textRxEvent(char* name, char* value) final; + bool processTextDataDerived(std::string const& name, std::string const& value) final; void frameValidEvent() final; spData_t _spData = nullptr; - veMpptStruct _tmpFrame{}; // private struct for received name and value pairs MovingAverage _efficiency; }; diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp index 351712dea..be9c8e115 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -3,94 +3,112 @@ VeDirectShuntController VeDirectShunt; -VeDirectShuntController::VeDirectShuntController() -{ -} - void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) { VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, verboseLogging, 2); } -void VeDirectShuntController::textRxEvent(char* name, char* value) +bool VeDirectShuntController::processTextDataDerived(std::string const& name, std::string const& value) { - if (VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame)) { - return; - } - - if (strcmp(name, "T") == 0) { - _tmpFrame.T = atoi(value); + if (name == "T") { + _tmpFrame.T = atoi(value.c_str()); _tmpFrame.tempPresent = true; + return true; } - else if (strcmp(name, "P") == 0) { - _tmpFrame.P = atoi(value); + if (name == "P") { + _tmpFrame.P = atoi(value.c_str()); + return true; } - else if (strcmp(name, "CE") == 0) { - _tmpFrame.CE = atoi(value); + if (name == "CE") { + _tmpFrame.CE = atoi(value.c_str()); + return true; } - else if (strcmp(name, "SOC") == 0) { - _tmpFrame.SOC = atoi(value); + if (name == "SOC") { + _tmpFrame.SOC = atoi(value.c_str()); + return true; } - else if (strcmp(name, "TTG") == 0) { - _tmpFrame.TTG = atoi(value); + if (name == "TTG") { + _tmpFrame.TTG = atoi(value.c_str()); + return true; } - else if (strcmp(name, "ALARM") == 0) { - _tmpFrame.ALARM = (strcmp(value, "ON") == 0); + if (name == "ALARM") { + _tmpFrame.ALARM = (value == "ON"); + return true; } - else if (strcmp(name, "H1") == 0) { - _tmpFrame.H1 = atoi(value); + if (name == "H1") { + _tmpFrame.H1 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H2") == 0) { - _tmpFrame.H2 = atoi(value); + if (name == "H2") { + _tmpFrame.H2 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H3") == 0) { - _tmpFrame.H3 = atoi(value); + if (name == "H3") { + _tmpFrame.H3 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H4") == 0) { - _tmpFrame.H4 = atoi(value); + if (name == "H4") { + _tmpFrame.H4 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H5") == 0) { - _tmpFrame.H5 = atoi(value); + if (name == "H5") { + _tmpFrame.H5 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H6") == 0) { - _tmpFrame.H6 = atoi(value); + if (name == "H6") { + _tmpFrame.H6 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H7") == 0) { - _tmpFrame.H7 = atoi(value); + if (name == "H7") { + _tmpFrame.H7 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H8") == 0) { - _tmpFrame.H8 = atoi(value); + if (name == "H8") { + _tmpFrame.H8 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H9") == 0) { - _tmpFrame.H9 = atoi(value); + if (name == "H9") { + _tmpFrame.H9 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H10") == 0) { - _tmpFrame.H10 = atoi(value); + if (name == "H10") { + _tmpFrame.H10 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H11") == 0) { - _tmpFrame.H11 = atoi(value); + if (name == "H11") { + _tmpFrame.H11 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H12") == 0) { - _tmpFrame.H12 = atoi(value); + if (name == "H12") { + _tmpFrame.H12 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H13") == 0) { - _tmpFrame.H13 = atoi(value); + if (name == "H13") { + _tmpFrame.H13 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H14") == 0) { - _tmpFrame.H14 = atoi(value); + if (name == "H14") { + _tmpFrame.H14 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H15") == 0) { - _tmpFrame.H15 = atoi(value); + if (name == "H15") { + _tmpFrame.H15 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H16") == 0) { - _tmpFrame.H16 = atoi(value); + if (name == "H16") { + _tmpFrame.H16 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H17") == 0) { - _tmpFrame.H17 = atoi(value); + if (name == "H17") { + _tmpFrame.H17 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H18") == 0) { - _tmpFrame.H18 = atoi(value); + if (name == "H18") { + _tmpFrame.H18 = atoi(value.c_str()); + return true; } + + return false; } /* @@ -98,12 +116,5 @@ void VeDirectShuntController::textRxEvent(char* name, char* value) * This function is called at the end of the received frame. */ void VeDirectShuntController::frameValidEvent() { - // other than in the MPPT controller, the SmartShunt seems to split all data - // into two seperate messagesas. Thus we update veFrame only every second message - // after a value for PID has been received - if (_tmpFrame.PID == 0) { return; } - veFrame = _tmpFrame; - _tmpFrame = {}; - _lastUpdate = millis(); } diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.h b/lib/VeDirectFrameHandler/VeDirectShuntController.h index 9e1a5f131..641ec10e3 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.h +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.h @@ -1,49 +1,21 @@ #pragma once #include +#include "VeDirectData.h" #include "VeDirectFrameHandler.h" -class VeDirectShuntController : public VeDirectFrameHandler { +class VeDirectShuntController : public VeDirectFrameHandler { public: - VeDirectShuntController(); + VeDirectShuntController() = default; void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging); - struct veShuntStruct : veStruct { - int32_t T; // Battery temperature - bool tempPresent = false; // Battery temperature sensor is attached to the shunt - int32_t P; // Instantaneous power - int32_t CE; // Consumed Amp Hours - int32_t SOC; // State-of-charge - uint32_t TTG; // Time-to-go - bool ALARM; // Alarm condition active - uint32_t AR; // Alarm Reason - int32_t H1; // Depth of the deepest discharge - int32_t H2; // Depth of the last discharge - int32_t H3; // Depth of the average discharge - int32_t H4; // Number of charge cycles - int32_t H5; // Number of full discharges - int32_t H6; // Cumulative Amp Hours drawn - int32_t H7; // Minimum main (battery) voltage - int32_t H8; // Maximum main (battery) voltage - int32_t H9; // Number of seconds since last full charge - int32_t H10; // Number of automatic synchronizations - int32_t H11; // Number of low main voltage alarms - int32_t H12; // Number of high main voltage alarms - int32_t H13; // Number of low auxiliary voltage alarms - int32_t H14; // Number of high auxiliary voltage alarms - int32_t H15; // Minimum auxiliary (battery) voltage - int32_t H16; // Maximum auxiliary (battery) voltage - int32_t H17; // Amount of discharged energy - int32_t H18; // Amount of charged energy - }; - - veShuntStruct veFrame{}; + using data_t = veShuntStruct; + data_t veFrame{}; private: - void textRxEvent(char * name, char * value) final; + bool processTextDataDerived(std::string const& name, std::string const& value) final; void frameValidEvent() final; - veShuntStruct _tmpFrame{}; // private struct for received name and value pairs }; extern VeDirectShuntController VeDirectShunt; diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 563562c82..12a6b6010 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -373,7 +373,7 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) _lastUpdate = millis(); } -void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct const& shuntData) { +void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) { BatteryStats::setVoltage(shuntData.V, millis()); BatteryStats::setSoC(static_cast(shuntData.SOC) / 10, 1/*precision*/, millis()); diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index 8cfd6efce..92c3d7884 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -70,7 +70,7 @@ void MqttHandleVedirectClass::loop() VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); - VeDirectMpptController::veMpptStruct _kvFrame = _kvFrames[spMpptData->SER]; + VeDirectMpptController::data_t _kvFrame = _kvFrames[spMpptData->SER]; publish_mppt_data(spMpptData, _kvFrame); if (!_PublishFull) { _kvFrames[spMpptData->SER] = *spMpptData; @@ -105,7 +105,7 @@ void MqttHandleVedirectClass::loop() } void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, - VeDirectMpptController::veMpptStruct &frame) const { + const VeDirectMpptController::data_t &frame) const { String value; String topic = "victron/"; topic.concat(spMpptData->SER); From b299b9dc6c8698e8ec1f5262cc6021e1fe36d422 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 29 Mar 2024 20:33:56 +0100 Subject: [PATCH 18/70] VE.Direct: simplify access to data hand out const& to the data structs. this is possible now that this struct is "stable", i.e., not reset regularly. --- include/MqttHandleVedirect.h | 2 +- include/MqttHandleVedirectHass.h | 6 +- include/VictronMppt.h | 2 +- include/WebApi_ws_vedirect_live.h | 2 +- .../VeDirectFrameHandler.h | 3 +- .../VeDirectMpptController.cpp | 3 - .../VeDirectMpptController.h | 3 - .../VeDirectShuntController.cpp | 8 - .../VeDirectShuntController.h | 2 - src/MqttHandlVedirectHass.cpp | 73 ++++---- src/MqttHandleVedirect.cpp | 173 +++++++++--------- src/VictronMppt.cpp | 16 +- src/VictronSmartShunt.cpp | 2 +- src/WebApi_ws_vedirect_live.cpp | 56 +++--- 14 files changed, 166 insertions(+), 185 deletions(-) diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h index 897dc00ac..016ee804a 100644 --- a/include/MqttHandleVedirect.h +++ b/include/MqttHandleVedirect.h @@ -33,7 +33,7 @@ class MqttHandleVedirectClass { bool _PublishFull; - void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, + void publish_mppt_data(const VeDirectMpptController::data_t &mpptData, const VeDirectMpptController::data_t &frame) const; }; diff --git a/include/MqttHandleVedirectHass.h b/include/MqttHandleVedirectHass.h index 86d364cda..6d7a17ac6 100644 --- a/include/MqttHandleVedirectHass.h +++ b/include/MqttHandleVedirectHass.h @@ -16,13 +16,13 @@ class MqttHandleVedirectHassClass { void publish(const String& subtopic, const String& payload); void publishBinarySensor(const char *caption, const char *icon, const char *subTopic, const char *payload_on, const char *payload_off, - const VeDirectMpptController::spData_t &spMpptData); + const VeDirectMpptController::data_t &mpptData); void publishSensor(const char *caption, const char *icon, const char *subTopic, const char *deviceClass, const char *stateClass, const char *unitOfMeasurement, - const VeDirectMpptController::spData_t &spMpptData); + const VeDirectMpptController::data_t &mpptData); void createDeviceInfo(JsonObject &object, - const VeDirectMpptController::spData_t &spMpptData); + const VeDirectMpptController::data_t &mpptData); Task _loopTask; diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 39e85aad7..98aa36dcd 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -25,7 +25,7 @@ class VictronMpptClass { uint32_t getDataAgeMillis(size_t idx) const; size_t controllerAmount() const { return _controllers.size(); } - std::optional getData(size_t idx = 0) const; + std::optional getData(size_t idx = 0) const; // total output of all MPPT charge controllers in Watts int32_t getPowerOutputWatts() const; diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h index d46de4cc2..1d471cf33 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_vedirect_live.h @@ -15,7 +15,7 @@ class WebApiWsVedirectLiveClass { private: void generateJsonResponse(JsonVariant& root, bool fullUpdate); - static void populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData); + static void populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); bool hasUpdate(size_t idx); diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index aa8857a42..dedf59726 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -24,6 +24,7 @@ class VeDirectFrameHandler { void loop(); // main loop to read ve.direct data uint32_t getLastUpdate() const; // timestamp of last successful frame read bool isDataValid() const; // return true if data valid and not outdated + T const& getData() const { return _tmpFrame; } protected: VeDirectFrameHandler(); @@ -41,7 +42,7 @@ class VeDirectFrameHandler { void rxData(uint8_t inbyte); // byte of serial data void processTextData(std::string const& name, std::string const& value); virtual bool processTextDataDerived(std::string const& name, std::string const& value) = 0; - virtual void frameValidEvent() = 0; + virtual void frameValidEvent() { } int hexRxEvent(uint8_t); std::unique_ptr _vedirectSerial; diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 06616202b..24063cf51 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -4,7 +4,6 @@ void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { VeDirectFrameHandler::init("MPPT", rx, tx, msgOut, verboseLogging, hwSerialPort); - _spData = std::make_shared(); } bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value) @@ -80,6 +79,4 @@ void VeDirectMpptController::frameValidEvent() { _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); _tmpFrame.E = _efficiency.getAverage(); } - - _spData = std::make_shared(_tmpFrame); } diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index 277a73765..c5af4521f 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -43,12 +43,9 @@ class VeDirectMpptController : public VeDirectFrameHandler { void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); using data_t = veMpptStruct; - using spData_t = std::shared_ptr; - spData_t getData() const { return _spData; } private: bool processTextDataDerived(std::string const& name, std::string const& value) final; void frameValidEvent() final; - spData_t _spData = nullptr; MovingAverage _efficiency; }; diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp index be9c8e115..522b152b1 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -110,11 +110,3 @@ bool VeDirectShuntController::processTextDataDerived(std::string const& name, st return false; } - -/* - * frameValidEvent - * This function is called at the end of the received frame. - */ -void VeDirectShuntController::frameValidEvent() { - veFrame = _tmpFrame; -} diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.h b/lib/VeDirectFrameHandler/VeDirectShuntController.h index 641ec10e3..03bc96b8b 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.h +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.h @@ -11,11 +11,9 @@ class VeDirectShuntController : public VeDirectFrameHandler { void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging); using data_t = veShuntStruct; - data_t veFrame{}; private: bool processTextDataDerived(std::string const& name, std::string const& value) final; - void frameValidEvent() final; }; extern VeDirectShuntController VeDirectShunt; diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index b839af3c2..4619c1e42 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -58,42 +58,33 @@ void MqttHandleVedirectHassClass::publishConfig() // device info for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { - // ensure data is received from victron - if (!VictronMppt.isDataValid(idx)) { - continue; - } - - std::optional spOptMpptData = VictronMppt.getData(idx); - if (!spOptMpptData.has_value()) { - continue; - } - - VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); - - publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", spMpptData); - publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", spMpptData); + auto optMpptData = VictronMppt.getData(idx); + if (!optMpptData.has_value()) { continue; } + + publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", *optMpptData); + publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", *optMpptData); // battery info - publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", spMpptData); - publishSensor("Battery current", NULL, "I", "current", "measurement", "A", spMpptData); - publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", spMpptData); - publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", spMpptData); + publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", *optMpptData); + publishSensor("Battery current", NULL, "I", "current", "measurement", "A", *optMpptData); + publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", *optMpptData); + publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", *optMpptData); // panel info - publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", spMpptData); - publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", spMpptData); - publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", spMpptData); - publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", spMpptData); - publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", spMpptData); - publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", spMpptData); - publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", spMpptData); - publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", spMpptData); + publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", *optMpptData); + publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", *optMpptData); + publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", *optMpptData); + publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", *optMpptData); + publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", *optMpptData); + publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", *optMpptData); + publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", *optMpptData); + publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", *optMpptData); } yield(); @@ -102,9 +93,9 @@ void MqttHandleVedirectHassClass::publishConfig() void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char *icon, const char *subTopic, const char *deviceClass, const char *stateClass, const char *unitOfMeasurement, - const VeDirectMpptController::spData_t &spMpptData) + const VeDirectMpptController::data_t &mpptData) { - String serial = spMpptData->SER; + String serial = mpptData.SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -139,7 +130,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char } JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj, spMpptData); + createDeviceInfo(deviceObj, mpptData); if (Configuration.get().Mqtt.Hass.Expire) { root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3; @@ -160,9 +151,9 @@ void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char } void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const char *icon, const char *subTopic, const char *payload_on, const char *payload_off, - const VeDirectMpptController::spData_t &spMpptData) + const VeDirectMpptController::data_t &mpptData) { - String serial = spMpptData->SER; + String serial = mpptData.SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -195,7 +186,7 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const } JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj, spMpptData); + createDeviceInfo(deviceObj, mpptData); if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } @@ -205,14 +196,14 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const } void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object, - const VeDirectMpptController::spData_t &spMpptData) + const VeDirectMpptController::data_t &mpptData) { - String serial = spMpptData->SER; + String serial = mpptData.SER; object["name"] = "Victron(" + serial + ")"; object["ids"] = serial; object["cu"] = String("http://") + NetworkSettings.localIP().toString(); object["mf"] = "OpenDTU"; - object["mdl"] = spMpptData->getPidAsString(); + object["mdl"] = mpptData.getPidAsString(); object["sw"] = AUTO_GIT_HASH; } diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index 92c3d7884..6244725ac 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -59,21 +59,13 @@ void MqttHandleVedirectClass::loop() #endif for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { - if (!VictronMppt.isDataValid(idx)) { - continue; - } - - std::optional spOptMpptData = VictronMppt.getData(idx); - if (!spOptMpptData.has_value()) { - continue; - } + std::optional optMpptData = VictronMppt.getData(idx); + if (!optMpptData.has_value()) { continue; } - VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); - - VeDirectMpptController::data_t _kvFrame = _kvFrames[spMpptData->SER]; - publish_mppt_data(spMpptData, _kvFrame); + auto const& kvFrame = _kvFrames[optMpptData->SER]; + publish_mppt_data(*optMpptData, kvFrame); if (!_PublishFull) { - _kvFrames[spMpptData->SER] = *spMpptData; + _kvFrames[optMpptData->SER] = *optMpptData; } } @@ -104,79 +96,94 @@ void MqttHandleVedirectClass::loop() } } -void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, - const VeDirectMpptController::data_t &frame) const { +void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::data_t ¤tData, + const VeDirectMpptController::data_t &previousData) const { String value; String topic = "victron/"; - topic.concat(spMpptData->SER); + topic.concat(currentData.SER); topic.concat("/"); - if (_PublishFull || spMpptData->PID != frame.PID) - MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data()); - if (_PublishFull || strcmp(spMpptData->SER, frame.SER) != 0) - MqttSettings.publish(topic + "SER", spMpptData->SER ); - if (_PublishFull || strcmp(spMpptData->FW, frame.FW) != 0) - MqttSettings.publish(topic + "FW", spMpptData->FW); - if (_PublishFull || spMpptData->LOAD != frame.LOAD) - MqttSettings.publish(topic + "LOAD", spMpptData->LOAD ? "ON" : "OFF"); - if (_PublishFull || spMpptData->CS != frame.CS) - MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data()); - if (_PublishFull || spMpptData->ERR != frame.ERR) - MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data()); - if (_PublishFull || spMpptData->OR != frame.OR) - MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data()); - if (_PublishFull || spMpptData->MPPT != frame.MPPT) - MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data()); - if (_PublishFull || spMpptData->HSDS != frame.HSDS) { - value = spMpptData->HSDS; - MqttSettings.publish(topic + "HSDS", value); - } - if (_PublishFull || spMpptData->V != frame.V) { - value = spMpptData->V; - MqttSettings.publish(topic + "V", value); - } - if (_PublishFull || spMpptData->I != frame.I) { - value = spMpptData->I; - MqttSettings.publish(topic + "I", value); - } - if (_PublishFull || spMpptData->P != frame.P) { - value = spMpptData->P; - MqttSettings.publish(topic + "P", value); - } - if (_PublishFull || spMpptData->VPV != frame.VPV) { - value = spMpptData->VPV; - MqttSettings.publish(topic + "VPV", value); - } - if (_PublishFull || spMpptData->IPV != frame.IPV) { - value = spMpptData->IPV; - MqttSettings.publish(topic + "IPV", value); - } - if (_PublishFull || spMpptData->PPV != frame.PPV) { - value = spMpptData->PPV; - MqttSettings.publish(topic + "PPV", value); - } - if (_PublishFull || spMpptData->E != frame.E) { - value = spMpptData->E; - MqttSettings.publish(topic + "E", value); - } - if (_PublishFull || spMpptData->H19 != frame.H19) { - value = spMpptData->H19; - MqttSettings.publish(topic + "H19", value); - } - if (_PublishFull || spMpptData->H20 != frame.H20) { - value = spMpptData->H20; - MqttSettings.publish(topic + "H20", value); - } - if (_PublishFull || spMpptData->H21 != frame.H21) { - value = spMpptData->H21; - MqttSettings.publish(topic + "H21", value); - } - if (_PublishFull || spMpptData->H22 != frame.H22) { - value = spMpptData->H22; - MqttSettings.publish(topic + "H22", value); - } - if (_PublishFull || spMpptData->H23 != frame.H23) { - value = spMpptData->H23; - MqttSettings.publish(topic + "H23", value); + if (_PublishFull || currentData.PID != previousData.PID) { + MqttSettings.publish(topic + "PID", currentData.getPidAsString().data()); + } + + if (_PublishFull || strcmp(currentData.SER, previousData.SER) != 0) { + MqttSettings.publish(topic + "SER", currentData.SER); + } + + if (_PublishFull || strcmp(currentData.FW, previousData.FW) != 0) { + MqttSettings.publish(topic + "FW", currentData.FW); + } + + if (_PublishFull || currentData.LOAD != previousData.LOAD) { + MqttSettings.publish(topic + "LOAD", currentData.LOAD ? "ON" : "OFF"); + } + + if (_PublishFull || currentData.CS != previousData.CS) { + MqttSettings.publish(topic + "CS", currentData.getCsAsString().data()); + } + + if (_PublishFull || currentData.ERR != previousData.ERR) { + MqttSettings.publish(topic + "ERR", currentData.getErrAsString().data()); + } + + if (_PublishFull || currentData.OR != previousData.OR) { + MqttSettings.publish(topic + "OR", currentData.getOrAsString().data()); + } + + if (_PublishFull || currentData.MPPT != previousData.MPPT) { + MqttSettings.publish(topic + "MPPT", currentData.getMpptAsString().data()); + } + + if (_PublishFull || currentData.HSDS != previousData.HSDS) { + MqttSettings.publish(topic + "HSDS", String(currentData.HSDS)); + } + + if (_PublishFull || currentData.V != previousData.V) { + MqttSettings.publish(topic + "V", String(currentData.V)); + } + + if (_PublishFull || currentData.I != previousData.I) { + MqttSettings.publish(topic + "I", String(currentData.I)); + } + + if (_PublishFull || currentData.P != previousData.P) { + MqttSettings.publish(topic + "P", String(currentData.P)); + } + + if (_PublishFull || currentData.VPV != previousData.VPV) { + MqttSettings.publish(topic + "VPV", String(currentData.VPV)); + } + + if (_PublishFull || currentData.IPV != previousData.IPV) { + MqttSettings.publish(topic + "IPV", String(currentData.IPV)); + } + + if (_PublishFull || currentData.PPV != previousData.PPV) { + MqttSettings.publish(topic + "PPV", String(currentData.PPV)); + } + + if (_PublishFull || currentData.E != previousData.E) { + MqttSettings.publish(topic + "E", String(currentData.E)); + } + + if (_PublishFull || currentData.H19 != previousData.H19) { + MqttSettings.publish(topic + "H19", String(currentData.H19)); + } + + if (_PublishFull || currentData.H20 != previousData.H20) { + MqttSettings.publish(topic + "H20", String(currentData.H20)); + } + + if (_PublishFull || currentData.H21 != previousData.H21) { + MqttSettings.publish(topic + "H21", String(currentData.H21)); + } + + if (_PublishFull || currentData.H22 != previousData.H22) { + MqttSettings.publish(topic + "H22", String(currentData.H22)); + } + + if (_PublishFull || currentData.H23 != previousData.H23) { + MqttSettings.publish(topic + "H23", String(currentData.H23)); } } diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index e39cf3aad..c555e9e81 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -119,7 +119,7 @@ uint32_t VictronMpptClass::getDataAgeMillis(size_t idx) const return millis() - _controllers[idx]->getLastUpdate(); } -std::optional VictronMpptClass::getData(size_t idx) const +std::optional VictronMpptClass::getData(size_t idx) const { std::lock_guard lock(_mutex); @@ -129,7 +129,9 @@ std::optional VictronMpptClass::getData(size_t return std::nullopt; } - return std::optional{_controllers[idx]->getData()}; + if (!_controllers[idx]->isDataValid()) { return std::nullopt; } + + return _controllers[idx]->getData(); } int32_t VictronMpptClass::getPowerOutputWatts() const @@ -138,7 +140,7 @@ int32_t VictronMpptClass::getPowerOutputWatts() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - sum += upController->getData()->P; + sum += upController->getData().P; } return sum; @@ -150,7 +152,7 @@ int32_t VictronMpptClass::getPanelPowerWatts() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - sum += upController->getData()->PPV; + sum += upController->getData().PPV; } return sum; @@ -162,7 +164,7 @@ double VictronMpptClass::getYieldTotal() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - sum += upController->getData()->H19; + sum += upController->getData().H19; } return sum; @@ -174,7 +176,7 @@ double VictronMpptClass::getYieldDay() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - sum += upController->getData()->H20; + sum += upController->getData().H20; } return sum; @@ -186,7 +188,7 @@ double VictronMpptClass::getOutputVoltage() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - double volts = upController->getData()->V; + double volts = upController->getData().V; if (min == -1) { min = volts; } min = std::min(min, volts); } diff --git a/src/VictronSmartShunt.cpp b/src/VictronSmartShunt.cpp index 7b6da145a..e3da07fa4 100644 --- a/src/VictronSmartShunt.cpp +++ b/src/VictronSmartShunt.cpp @@ -31,6 +31,6 @@ void VictronSmartShunt::loop() if (VeDirectShunt.getLastUpdate() <= _lastUpdate) { return; } - _stats->updateFrom(VeDirectShunt.veFrame); + _stats->updateFrom(VeDirectShunt.getData()); _lastUpdate = VeDirectShunt.getLastUpdate(); } diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 9ec9d79ff..d843475c4 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -123,21 +123,17 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool ful root["vedirect"]["full_update"] = fullUpdate; for (size_t idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { - std::optional spOptMpptData = VictronMppt.getData(idx); - if (!spOptMpptData.has_value()) { - continue; - } + auto optMpptData = VictronMppt.getData(idx); + if (!optMpptData.has_value()) { continue; } if (!fullUpdate && !hasUpdate(idx)) { continue; } - VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); - - String serial(spMpptData->SER); + String serial(optMpptData->SER); if (serial.isEmpty()) { continue; } // serial required as index const JsonObject &nested = array.createNestedObject(serial); nested["data_age_ms"] = VictronMppt.getDataAgeMillis(idx); - populateJson(nested, spMpptData); + populateJson(nested, *optMpptData); } _lastPublish = millis(); @@ -149,56 +145,56 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool ful root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); } -void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) { +void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) { // device info - root["device"]["PID"] = spMpptData->getPidAsString(); - root["device"]["SER"] = spMpptData->SER; - root["device"]["FW"] = spMpptData->FW; - root["device"]["LOAD"] = spMpptData->LOAD ? "ON" : "OFF"; - root["device"]["CS"] = spMpptData->getCsAsString(); - root["device"]["ERR"] = spMpptData->getErrAsString(); - root["device"]["OR"] = spMpptData->getOrAsString(); - root["device"]["MPPT"] = spMpptData->getMpptAsString(); - root["device"]["HSDS"]["v"] = spMpptData->HSDS; + root["device"]["PID"] = mpptData.getPidAsString(); + root["device"]["SER"] = String(mpptData.SER); + root["device"]["FW"] = String(mpptData.FW); + root["device"]["LOAD"] = mpptData.LOAD ? "ON" : "OFF"; + root["device"]["CS"] = mpptData.getCsAsString(); + root["device"]["ERR"] = mpptData.getErrAsString(); + root["device"]["OR"] = mpptData.getOrAsString(); + root["device"]["MPPT"] = mpptData.getMpptAsString(); + root["device"]["HSDS"]["v"] = mpptData.HSDS; root["device"]["HSDS"]["u"] = "d"; // battery info - root["output"]["P"]["v"] = spMpptData->P; + root["output"]["P"]["v"] = mpptData.P; root["output"]["P"]["u"] = "W"; root["output"]["P"]["d"] = 0; - root["output"]["V"]["v"] = spMpptData->V; + root["output"]["V"]["v"] = mpptData.V; root["output"]["V"]["u"] = "V"; root["output"]["V"]["d"] = 2; - root["output"]["I"]["v"] = spMpptData->I; + root["output"]["I"]["v"] = mpptData.I; root["output"]["I"]["u"] = "A"; root["output"]["I"]["d"] = 2; - root["output"]["E"]["v"] = spMpptData->E; + root["output"]["E"]["v"] = mpptData.E; root["output"]["E"]["u"] = "%"; root["output"]["E"]["d"] = 1; // panel info - root["input"]["PPV"]["v"] = spMpptData->PPV; + root["input"]["PPV"]["v"] = mpptData.PPV; root["input"]["PPV"]["u"] = "W"; root["input"]["PPV"]["d"] = 0; - root["input"]["VPV"]["v"] = spMpptData->VPV; + root["input"]["VPV"]["v"] = mpptData.VPV; root["input"]["VPV"]["u"] = "V"; root["input"]["VPV"]["d"] = 2; - root["input"]["IPV"]["v"] = spMpptData->IPV; + root["input"]["IPV"]["v"] = mpptData.IPV; root["input"]["IPV"]["u"] = "A"; root["input"]["IPV"]["d"] = 2; - root["input"]["YieldToday"]["v"] = spMpptData->H20; + root["input"]["YieldToday"]["v"] = mpptData.H20; root["input"]["YieldToday"]["u"] = "kWh"; root["input"]["YieldToday"]["d"] = 3; - root["input"]["YieldYesterday"]["v"] = spMpptData->H22; + root["input"]["YieldYesterday"]["v"] = mpptData.H22; root["input"]["YieldYesterday"]["u"] = "kWh"; root["input"]["YieldYesterday"]["d"] = 3; - root["input"]["YieldTotal"]["v"] = spMpptData->H19; + root["input"]["YieldTotal"]["v"] = mpptData.H19; root["input"]["YieldTotal"]["u"] = "kWh"; root["input"]["YieldTotal"]["d"] = 3; - root["input"]["MaximumPowerToday"]["v"] = spMpptData->H21; + root["input"]["MaximumPowerToday"]["v"] = mpptData.H21; root["input"]["MaximumPowerToday"]["u"] = "W"; root["input"]["MaximumPowerToday"]["d"] = 0; - root["input"]["MaximumPowerYesterday"]["v"] = spMpptData->H23; + root["input"]["MaximumPowerYesterday"]["v"] = mpptData.H23; root["input"]["MaximumPowerYesterday"]["u"] = "W"; root["input"]["MaximumPowerYesterday"]["d"] = 0; } From 92a7f27919bd88762518a736c4a8f11f280a3d77 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 29 Mar 2024 20:56:50 +0100 Subject: [PATCH 19/70] VE.Direct: use float rather than double double precision floating point numbers are not needed to handle VE.Direct values. handling double is implemented in software and hence *much* more resource intensive. --- include/VictronMppt.h | 6 +++--- lib/VeDirectFrameHandler/VeDirectData.h | 16 ++++++++-------- .../VeDirectMpptController.cpp | 2 +- .../VeDirectMpptController.h | 6 +++--- src/VictronMppt.cpp | 14 +++++++------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 98aa36dcd..a4f1b1ab8 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -34,13 +34,13 @@ class VictronMpptClass { int32_t getPanelPowerWatts() const; // sum of total yield of all MPPT charge controllers in kWh - double getYieldTotal() const; + float getYieldTotal() const; // sum of today's yield of all MPPT charge controllers in kWh - double getYieldDay() const; + float getYieldDay() const; // minimum of all MPPT charge controllers' output voltages in V - double getOutputVoltage() const; + float getOutputVoltage() const; private: void loop(); diff --git a/lib/VeDirectFrameHandler/VeDirectData.h b/lib/VeDirectFrameHandler/VeDirectData.h index 0c43b555a..f651193b0 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.h +++ b/lib/VeDirectFrameHandler/VeDirectData.h @@ -10,9 +10,9 @@ typedef struct { uint16_t PID = 0; // product id char SER[VE_MAX_VALUE_LEN]; // serial number char FW[VE_MAX_VALUE_LEN]; // firmware release number - double V = 0; // battery voltage in V - double I = 0; // battery current in A - double E = 0; // efficiency in percent (calculated, moving average) + float V = 0; // battery voltage in V + float I = 0; // battery current in A + float E = 0; // efficiency in percent (calculated, moving average) frozen::string const& getPidAsString() const; // product ID as string } veStruct; @@ -21,17 +21,17 @@ struct veMpptStruct : veStruct { uint8_t MPPT; // state of MPP tracker int32_t PPV; // panel power in W int32_t P; // battery output power in W (calculated) - double VPV; // panel voltage in V - double IPV; // panel current in A (calculated) + float VPV; // panel voltage in V + float IPV; // panel current in A (calculated) bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit) uint8_t CS; // current state of operation e.g. OFF or Bulk uint8_t ERR; // error code uint32_t OR; // off reason uint32_t HSDS; // day sequence number 1...365 - double H19; // yield total kWh - double H20; // yield today kWh + float H19; // yield total kWh + float H20; // yield today kWh int32_t H21; // maximum power today W - double H22; // yield yesterday kWh + float H22; // yield yesterday kWh int32_t H23; // maximum power yesterday W frozen::string const& getMpptAsString() const; // state of mppt as string diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 24063cf51..63825d47a 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -76,7 +76,7 @@ void VeDirectMpptController::frameValidEvent() { } if (_tmpFrame.PPV > 0) { - _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); + _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); _tmpFrame.E = _efficiency.getAverage(); } } diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index c5af4521f..eddf8be70 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -24,9 +24,9 @@ class MovingAverage { _index = (_index + 1) % WINDOW_SIZE; } - double getAverage() const { + float getAverage() const { if (_count == 0) { return 0.0; } - return static_cast(_sum) / _count; + return static_cast(_sum) / _count; } private: @@ -47,5 +47,5 @@ class VeDirectMpptController : public VeDirectFrameHandler { private: bool processTextDataDerived(std::string const& name, std::string const& value) final; void frameValidEvent() final; - MovingAverage _efficiency; + MovingAverage _efficiency; }; diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index c555e9e81..d152c61ec 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -158,9 +158,9 @@ int32_t VictronMpptClass::getPanelPowerWatts() const return sum; } -double VictronMpptClass::getYieldTotal() const +float VictronMpptClass::getYieldTotal() const { - double sum = 0; + float sum = 0; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } @@ -170,9 +170,9 @@ double VictronMpptClass::getYieldTotal() const return sum; } -double VictronMpptClass::getYieldDay() const +float VictronMpptClass::getYieldDay() const { - double sum = 0; + float sum = 0; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } @@ -182,13 +182,13 @@ double VictronMpptClass::getYieldDay() const return sum; } -double VictronMpptClass::getOutputVoltage() const +float VictronMpptClass::getOutputVoltage() const { - double min = -1; + float min = -1; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - double volts = upController->getData().V; + float volts = upController->getData().V; if (min == -1) { min = volts; } min = std::min(min, volts); } From 43f553d2d4aec2d56ef82b8f3d3b250aada9384f Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 29 Mar 2024 20:58:07 +0100 Subject: [PATCH 20/70] VE.Direct MQTT: simplify code the use of a #define is warranted here since it saves a lot of code duplication and improves code readability. --- src/MqttHandleVedirect.cpp | 110 +++++++++---------------------------- 1 file changed, 27 insertions(+), 83 deletions(-) diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index 6244725ac..ce96c8275 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -103,87 +103,31 @@ void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::da topic.concat(currentData.SER); topic.concat("/"); - if (_PublishFull || currentData.PID != previousData.PID) { - MqttSettings.publish(topic + "PID", currentData.getPidAsString().data()); - } - - if (_PublishFull || strcmp(currentData.SER, previousData.SER) != 0) { - MqttSettings.publish(topic + "SER", currentData.SER); - } - - if (_PublishFull || strcmp(currentData.FW, previousData.FW) != 0) { - MqttSettings.publish(topic + "FW", currentData.FW); - } - - if (_PublishFull || currentData.LOAD != previousData.LOAD) { - MqttSettings.publish(topic + "LOAD", currentData.LOAD ? "ON" : "OFF"); - } - - if (_PublishFull || currentData.CS != previousData.CS) { - MqttSettings.publish(topic + "CS", currentData.getCsAsString().data()); - } - - if (_PublishFull || currentData.ERR != previousData.ERR) { - MqttSettings.publish(topic + "ERR", currentData.getErrAsString().data()); - } - - if (_PublishFull || currentData.OR != previousData.OR) { - MqttSettings.publish(topic + "OR", currentData.getOrAsString().data()); - } - - if (_PublishFull || currentData.MPPT != previousData.MPPT) { - MqttSettings.publish(topic + "MPPT", currentData.getMpptAsString().data()); - } - - if (_PublishFull || currentData.HSDS != previousData.HSDS) { - MqttSettings.publish(topic + "HSDS", String(currentData.HSDS)); - } - - if (_PublishFull || currentData.V != previousData.V) { - MqttSettings.publish(topic + "V", String(currentData.V)); - } - - if (_PublishFull || currentData.I != previousData.I) { - MqttSettings.publish(topic + "I", String(currentData.I)); - } - - if (_PublishFull || currentData.P != previousData.P) { - MqttSettings.publish(topic + "P", String(currentData.P)); - } - - if (_PublishFull || currentData.VPV != previousData.VPV) { - MqttSettings.publish(topic + "VPV", String(currentData.VPV)); - } - - if (_PublishFull || currentData.IPV != previousData.IPV) { - MqttSettings.publish(topic + "IPV", String(currentData.IPV)); - } - - if (_PublishFull || currentData.PPV != previousData.PPV) { - MqttSettings.publish(topic + "PPV", String(currentData.PPV)); - } - - if (_PublishFull || currentData.E != previousData.E) { - MqttSettings.publish(topic + "E", String(currentData.E)); - } - - if (_PublishFull || currentData.H19 != previousData.H19) { - MqttSettings.publish(topic + "H19", String(currentData.H19)); - } - - if (_PublishFull || currentData.H20 != previousData.H20) { - MqttSettings.publish(topic + "H20", String(currentData.H20)); - } - - if (_PublishFull || currentData.H21 != previousData.H21) { - MqttSettings.publish(topic + "H21", String(currentData.H21)); - } - - if (_PublishFull || currentData.H22 != previousData.H22) { - MqttSettings.publish(topic + "H22", String(currentData.H22)); - } - - if (_PublishFull || currentData.H23 != previousData.H23) { - MqttSettings.publish(topic + "H23", String(currentData.H23)); - } +#define PUBLISH(sm, t, val) \ + if (_PublishFull || currentData.sm != previousData.sm) { \ + MqttSettings.publish(topic + t, String(val)); \ + } + + PUBLISH(PID, "PID", currentData.getPidAsString().data()); + PUBLISH(SER, "SER", currentData.SER); + PUBLISH(FW, "FW", currentData.FW); + PUBLISH(LOAD, "LOAD", (currentData.LOAD ? "ON" : "OFF")); + PUBLISH(CS, "CS", currentData.getCsAsString().data()); + PUBLISH(ERR, "ERR", currentData.getErrAsString().data()); + PUBLISH(OR, "OR", currentData.getOrAsString().data()); + PUBLISH(MPPT, "MPPT", currentData.getMpptAsString().data()); + PUBLISH(HSDS, "HSDS", currentData.HSDS); + PUBLISH(V, "V", currentData.V); + PUBLISH(I, "I", currentData.I); + PUBLISH(P, "P", currentData.P); + PUBLISH(VPV, "VPV", currentData.VPV); + PUBLISH(IPV, "IPV", currentData.IPV); + PUBLISH(PPV, "PPV", currentData.PPV); + PUBLISH(E, "E", currentData.E); + PUBLISH(H19, "H19", currentData.H19); + PUBLISH(H20, "H20", currentData.H20); + PUBLISH(H21, "H21", currentData.H21); + PUBLISH(H22, "H22", currentData.H22); + PUBLISH(H23, "H23", currentData.H23); +#undef PUBLILSH } From 8c6e925ca44f6fdd3901a1995020415c03ef3d5a Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 29 Mar 2024 21:20:03 +0100 Subject: [PATCH 21/70] VE.Direct: make state machine timeout robust against overflow --- lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index a308f7f96..63db05dd5 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -118,7 +118,7 @@ void VeDirectFrameHandler::loop() // there will never be a large gap between two bytes of the same frame. // if such a large gap is observed, reset the state machine so it tries // to decode a new frame once more data arrives. - if (IDLE != _state && _lastByteMillis + 500 < millis()) { + if (IDLE != _state && (millis() - _lastByteMillis) > 500) { _msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", _logId, _state); if (_verboseLogging) { dumpDebugBuffer(); } reset(); From aadd7303acfe9901999415233305d50963965cbb Mon Sep 17 00:00:00 2001 From: SW-Nico Date: Mon, 11 Mar 2024 15:22:08 +0100 Subject: [PATCH 22/70] Feature: add support for VE.Direct hex messages --- .../VeDirectFrameHandler.cpp | 13 +- .../VeDirectFrameHandler.h | 40 ++- .../VeDirectFrameHexHandler.cpp | 227 ++++++++++++++++++ .../VeDirectMpptController.cpp | 106 ++++++++ .../VeDirectMpptController.h | 16 ++ 5 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 63db05dd5..c26c97055 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -30,7 +30,7 @@ * 2020.05.05 - 0.2 - initial release * 2020.06.21 - 0.2 - add MIT license, no code changes * 2020.08.20 - 0.3 - corrected #include reference - * + * 2024.03.08 - 0.4 - adds the ability to send hex commands and disassemble hex messages */ #include @@ -274,8 +274,6 @@ void VeDirectFrameHandler::processTextData(std::string const& name, std::stri _logId, name.c_str(), value.c_str()); } - - /* * hexRxEvent * This function records hex answers or async messages @@ -287,12 +285,19 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) switch (inbyte) { case '\n': + // now we can analyse the hex message + _hexBuffer[_hexSize] = '\0'; + VeDirectHexData data; + if (disassembleHexData(data)) + hexDataHandler(data); + // restore previous state ret=_prevState; break; default: - _hexSize++; + _hexBuffer[_hexSize++]=inbyte; + if (_hexSize>=VE_MAX_HEX_LEN) { // oops -buffer overflow - something went wrong, we abort _msgOut->printf("%s hexRx buffer overflow - aborting read\r\n", _logId); _hexSize=0; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index dedf59726..a2cd28348 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -6,6 +6,7 @@ * 2020.05.05 - 0.2 - initial release * 2021.02.23 - 0.3 - change frameLen to 22 per VE.Direct Protocol version 3.30 * 2022.08.20 - 0.4 - changes for OpenDTU + * 2024.03.08 - 0.4 - adds the ability to send hex commands and disassemble hex messages * */ @@ -18,17 +19,52 @@ #include #include "VeDirectData.h" +// hex send commands +enum VeDirectHexCommand { + ENTER_BOOT = 0x00, + PING = 0x01, + APP_VERSION = 0x02, + PRODUCT_ID = 0x04, + RESTART = 0x06, + GET = 0x07, + SET = 0x08, + ASYNC = 0x0A, + UNKNOWN = 0x0F +}; + +// hex receive responses +enum VeDirectHexResponse { + R_DONE = 0x01, + R_UNKNOWN = 0x03, + R_ERROR = 0x04, + R_PING = 0x05, + R_GET = 0x07, + R_SET = 0x08, + R_ASYNC = 0x0A, +}; + +// hex response data, contains the disassembeled hex message, forwarded to virtual funktion hexDataHandler() +struct VeDirectHexData { + VeDirectHexResponse rsp; // hex response type + uint16_t id; // register address + uint8_t flag; // flag + uint32_t value; // value from register + char text[VE_MAX_HEX_LEN]; // text/string response +}; + template class VeDirectFrameHandler { public: - void loop(); // main loop to read ve.direct data + virtual void loop(); // main loop to read ve.direct data uint32_t getLastUpdate() const; // timestamp of last successful frame read bool isDataValid() const; // return true if data valid and not outdated T const& getData() const { return _tmpFrame; } + bool sendHexCommand(VeDirectHexCommand cmd, uint16_t id = 0, uint32_t value = 0, uint8_t valunibble = 0); // send hex commands via ve.direct protected: VeDirectFrameHandler(); void init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); + virtual void hexDataHandler(VeDirectHexData const &data) { } // handles the disassembeled hex response bool _verboseLogging; Print* _msgOut; @@ -44,6 +80,7 @@ class VeDirectFrameHandler { virtual bool processTextDataDerived(std::string const& name, std::string const& value) = 0; virtual void frameValidEvent() { } int hexRxEvent(uint8_t); + bool disassembleHexData(VeDirectHexData &data); //return true if disassembling was possible std::unique_ptr _vedirectSerial; int _state; // current state @@ -51,6 +88,7 @@ class VeDirectFrameHandler { uint8_t _checksum; // checksum value char * _textPointer; // pointer to the private buffer we're writing to, name or value int _hexSize; // length of hex buffer + char _hexBuffer[VE_MAX_HEX_LEN] = { }; // buffer for received hex frames char _name[VE_MAX_VALUE_LEN]; // buffer for the field name char _value[VE_MAX_VALUE_LEN]; // buffer for the field value std::array _debugBuffer; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp new file mode 100644 index 000000000..b8cd0f277 --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp @@ -0,0 +1,227 @@ +/* VeDirectFrame +HexHandler.cpp + * + * Library to read/write from Victron devices using VE.Direct Hex protocol. + * Add on to Victron framehandler reference implementation. + * + * How to use: + * 1. Use sendHexCommand() to send hex messages. Use the Victron documentation to find the parameter. + * 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function + * void VeDirectFrameHandler::hexDataHandler(VeDirectHexData const &data) + * to handle the received hex messages. All hex messages will be forwarted to function hexDataHandler() + * 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits. + * + * 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages + * + */ +#include +#include "VeDirectFrameHandler.h" + + +// support for debugging, 0=without extended logging, 1=with extended logging +constexpr int MODUL_DEBUG = 0; + + +/* + * calcHexFrameCheckSum() + * help function to calculate the hex checksum + */ +#define ascii2hex(v) (v-48-(v>='A'?7:0)) +#define hex2byte(b) (ascii2hex(*(b)))*16+((ascii2hex(*(b+1)))) +static uint8_t calcHexFrameCheckSum(const char* buffer, int size) { + uint8_t checksum=0x55-ascii2hex(buffer[1]); + for (int i=2; i(strtoul(help, nullptr, 16))); +} + + +/* + * disassembleHexData() + * analysis the hex message and extract: response, id, flag and value/text + * buffer: pointer to message (ascii hex little endian format) + * data: disassembeled message + * return: true = successful disassembeld, false = hex sum fault or message + * do not aligin with VE.Diekt syntax + */ +template +bool VeDirectFrameHandler::disassembleHexData(VeDirectHexData &data) { + bool state = false; + char * buffer = _hexBuffer; + auto len = strlen(buffer); + + // reset hex data first + data = {}; + + if ((len > 3) && (calcHexFrameCheckSum(buffer, len) == 0x00)) { + data.rsp = static_cast(AsciiHexLE2Int(buffer+1, 1)); + + switch (data.rsp) { + case R_DONE: + case R_ERROR: + case R_PING: + case R_UNKNOWN: + strncpy(data.text, buffer+2, len-4); + state = true; + break; + case R_GET: + case R_SET: + case R_ASYNC: + data.id = AsciiHexLE2Int(buffer+2, 4); + + // future option: to analyse the flag here? + data.flag = AsciiHexLE2Int(buffer+6, 2); + + if (len == 12) { // 8bit value + data.value = AsciiHexLE2Int(buffer+8, 2); + state = true; + } + + if (len == 14) { // 16bit value + data.value = AsciiHexLE2Int(buffer+8, 4); + state = true; + } + + if (len == 18) { // 32bit value + data.value = AsciiHexLE2Int(buffer+8, 8); + state = true; + } + break; + default: + break; // something went wrong + } + } + + if constexpr(MODUL_DEBUG == 1) { + _msgOut->printf("[VE.Direct] debug: disassembleHexData(), rsp: %i, id: 0x%04X, value: 0x%X, Flag: 0x%02X\r\n", + data.rsp, data.id, data.value, data.flag); + } + + if (_verboseLogging && !state) + _msgOut->printf("[VE.Direct] failed to disassemble the hex message: %s\r\n", buffer); + + return (state); +} + + +/* + * uint2toHexLEString() + * help function to convert up to 32 bits into little endian hex String + * ascii: pointer to Ascii Hex Little Endian data + * anz: 1,2,4 or 8 nibble + */ +static String Int2HexLEString(uint32_t value, uint8_t anz) { + char hexchar[] = "0123456789ABCDEF"; + char help[9] = {}; + + switch (anz) { + case 1: + help[0] = hexchar[(value & 0x0000000F)]; + break; + case 2: + case 4: + case 8: + for (uint8_t i = 0; i < anz; i += 2) { + help[i] = hexchar[(value>>((1+1*i)*4)) & 0x0000000F]; + help[i+1] = hexchar[(value>>((1*i)*4)) & 0x0000000F]; + } + default: + ; + } + return String(help); +} + + +/* + * sendHexCommand() + * send the hex commend after assembling the command string + * cmd: command + * id: id/register, default 0 + * value: value to write into a id/register, default 0 + * valsize: size of the value/id, 8, 16 or 32 bit, default 0 + * return: true = message assembeld and send, false = it was not possible to put the message together + * SAMPLE: ping command: sendHexCommand(PING), + * read total DC input power sendHexCommand(GET, 0xEDEC) + * set Charge current limit 10A sendHexCommand(SET, 0x2015, 64, 16) + * + * WARNING: some values are stored in non-volatile memory. Continuous writing, for example from a control loop, will + * lead to early failure. + * On MPPT for example 0xEDE0 - 0xEDFF. Check the Vivtron doc "BlueSolar-HEX-protocol.pdf" + */ +template +bool VeDirectFrameHandler::sendHexCommand(VeDirectHexCommand cmd, uint16_t id, uint32_t value, uint8_t valsize) { + bool ret = false; + uint8_t flag = 0x00; // always 0x00 + + String txData = ":" + Int2HexLEString(cmd, 1); // add the command nibble + + switch (cmd) { + case PING: + case APP_VERSION: + case PRODUCT_ID: + ret = true; + break; + case GET: + case ASYNC: + txData += Int2HexLEString(id, 4); // add the id/register (4 nibble) + txData += Int2HexLEString(flag, 2); // add the flag (2 nibble) + ret = true; + break; + case SET: + txData += Int2HexLEString(id, 4); // add the id/register (4 nibble) + txData += Int2HexLEString(flag, 2); // add the flag (2 nibble) + if ((valsize == 8) || (valsize == 16) || (valsize == 32)) { + txData += Int2HexLEString(value, valsize/4); // add value (2-8 nibble) + ret = true; + } + break; + default: + ret = false; + break; + } + + if (ret) { + // add the checksum (2 nibble) + txData += Int2HexLEString(calcHexFrameCheckSum(txData.c_str(), txData.length()), 2); + String send = txData + "\n"; // hex command end byte + _vedirectSerial->write(send.c_str(), send.length()); + + if constexpr(MODUL_DEBUG == 1) { + auto blen = _vedirectSerial->availableForWrite(); + _msgOut->printf("[VE.Direct] debug: sendHexCommand(): %s, Free FIFO-Buffer: %u\r\n", txData.c_str(), blen); + } + } + + if (_verboseLogging && !ret) + _msgOut->println("[VE.Direct] send hex command fault:" + txData); + + return (ret); +} diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 63825d47a..9e94356ae 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -1,6 +1,20 @@ +/* VeDirectMpptController.cpp + * + * + * 2020.08.20 - 0.0 - ??? + * 2024.03.18 - 0.1 - add of: - temperature from "Smart Battery Sense" connected over VE.Smart network + * - temperature from internal MPPT sensor + * - "total DC input power" from MPPT's connected over VE.Smart network + */ + #include #include "VeDirectMpptController.h" + +// support for debugging, 0=without extended logging, 1=with extended logging +constexpr int MODUL_DEBUG = 0; + + void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { VeDirectFrameHandler::init("MPPT", rx, tx, msgOut, verboseLogging, hwSerialPort); @@ -80,3 +94,95 @@ void VeDirectMpptController::frameValidEvent() { _tmpFrame.E = _efficiency.getAverage(); } } + + +/* +// loop() +// send hex commands to MPPT every 5 seconds +*/ +void VeDirectMpptController::loop() +{ + VeDirectFrameHandler::loop(); + + // Copy from the "VE.Direct Protocol" documentation + // For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the + // charger periodically sends human readable (TEXT) data to the serial port. For firmware + // versions v1.53 and above, the charger always periodically sends TEXT data to the serial port. + // --> We just use hex commandes for firmware >= 1.53 to keep text messages alive + if (atoi(_tmpFrame.FW) >= 153 ) { + if ((millis() - _lastPingTime) > 5000) { + + sendHexCommand(GET, 0x2027); // MPPT total DC input power + sendHexCommand(GET, 0xEDDB); // MPPT internal temperature + sendHexCommand(GET, 0xEDEC); // "Smart Battery Sense" temperature + sendHexCommand(GET, 0x200F); // Network info + _lastPingTime = millis(); + } + } +} + + +/* + * hexDataHandler() + * analyse the content of VE.Direct hex messages + * Handels the received hex data from the MPPT + */ +void VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) { + bool state = false; + + switch (data.rsp) { + case R_GET: + case R_ASYNC: + + // check if MPPT internal temperature is available + if(data.id == 0xEDDB) { + _ExData.T = static_cast(data.value) * 10; // conversion from unit [0.01°C] to unit [m°C] + _ExData.Tts = millis(); + state = true; + + if constexpr(MODUL_DEBUG == 1) + _msgOut->printf("[VE.Direct] debug: hexDataHandler(), MTTP Temperature: %.2f°C\r\n", _ExData.T/1000.0); + } + + // check if temperature from "Smart Battery Sense" is available + if(data.id == 0xEDEC) { + _ExData.TSBS = static_cast(data.value) * 10 - 272150; // conversion from unit [0.01K] to unit [m°C] + _ExData.TSBSts = millis(); + state = true; + + if constexpr(MODUL_DEBUG == 1) + _msgOut->printf("[VE.Direct] debug: hexDataHandler(), Battery Temperature: %.2f°C\r\n", _ExData.TSBS/1000.0); + } + + // check if "Total DC power" is available + if(data.id == 0x2027) { + _ExData.TDCP = data.value * 10; // conversion from unit [0.01W] to unit [mW] + _ExData.TDCPts = millis(); + state = true; + + if constexpr(MODUL_DEBUG == 1) + _msgOut->printf("[VE.Direct] debug: hexDataHandler(), Total Power: %.2fW\r\n", _ExData.TDCP/1000.0); + } + + // check if connected MPPT is charge instance master + // Hint: not used right now but maybe necessary for future extensions + if(data.id == 0x200F) { + _veMaster = ((data.value & 0x0F) == 0x02) ? true : false; + state = true; + + if constexpr(MODUL_DEBUG == 1) + _msgOut->printf("[VE.Direct] debug: hexDataHandler(), Networkmode: 0x%X\r\n", data.value); + } + break; + default: + break; + } + + if constexpr(MODUL_DEBUG == 1) + _msgOut->printf("[VE.Direct] debug: hexDataHandler(): rsp: %i, id: 0x%04X, value: %i[0x%08X], text: %s\r\n", + data.rsp, data.id, data.value, data.value, data.text); + + if (_verboseLogging && state) + _msgOut->printf("[VE.Direct] MPPT hex message: rsp: %i, id: 0x%04X, value: %i[0x%08X], text: %s\r\n", + data.rsp, data.id, data.value, data.value, data.text); +} diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index eddf8be70..ebee88513 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -44,8 +44,24 @@ class VeDirectMpptController : public VeDirectFrameHandler { using data_t = veMpptStruct; + virtual void loop() final; // main loop to read ve.direct data + + struct veMPPTExStruct { + int32_t T; // temperature [m°C] from internal MPPT sensor + unsigned long Tts; // time of last recieved value + int32_t TSBS; // temperature [m°C] from the "Smart Battery Sense" + unsigned long TSBSts; // time of last recieved value + uint32_t TDCP; // total DC input power [mW] + unsigned long TDCPts; // time of last recieved value + }; + veMPPTExStruct _ExData{}; + veMPPTExStruct const *getExData() const { return &_ExData; } + private: + void hexDataHandler(VeDirectHexData const &data) final; bool processTextDataDerived(std::string const& name, std::string const& value) final; void frameValidEvent() final; MovingAverage _efficiency; + unsigned long _lastPingTime = 0L; // time of last device PING/GET hex command + bool _veMaster = true; // MPPT is instance master }; From 6b8c93d2e6b4033bae6d3a7cda9504d12fc13d8f Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Sat, 30 Mar 2024 20:14:11 +0100 Subject: [PATCH 23/70] polish VE.Direct HEX support * show charge controller temperature in live view * send hex requests right after decoding a frame. this seems to have the best chance of getting an answer to all requests. * deem 0xFFFFFFFF value of network total DC power as invalid indicator. neither network state, nor network info, nor network mode seem to indicate that the charge controller is part of a VE.Smart network. for that reason, we revert to always querying the network total DC power value, but testing it for max(uin32_t) value, which seems to indicate that the charge controller is not part of a VE.Smart network. * improve (verbose) logging, e.g., use _logId, and print names of response codes and known registers, always print error messages, add additional tests to prevent overly verbose messages. * move hex protocol definitions to VeDirectData.h header and use enum classes * define register addresses in enum class * move values retrieved through hex protocol into main MPPT data struct * do not send HEX requests if the serial interface cannot send data * detect whether smart battery sense temperature is available * web app: make all VE.Direct sub-cards iterable. this makes addind more values much simpler and saves a bunch of code in the web app. * make VeDirectFrameHandler state a type-safe enum class * unindent MPPT controller loop() * whitespace cleanup --- lib/VeDirectFrameHandler/VeDirectData.cpp | 36 +++ lib/VeDirectFrameHandler/VeDirectData.h | 65 ++++++ .../VeDirectFrameHandler.cpp | 66 +++--- .../VeDirectFrameHandler.h | 62 ++--- .../VeDirectFrameHexHandler.cpp | 117 +++++----- .../VeDirectMpptController.cpp | 217 +++++++++++------- .../VeDirectMpptController.h | 17 +- src/WebApi_ws_vedirect_live.cpp | 109 +++++---- webapp/src/components/VedirectView.vue | 108 ++------- webapp/src/locales/de.json | 21 +- webapp/src/locales/en.json | 21 +- webapp/src/locales/fr.json | 21 +- webapp/src/types/VedirectLiveDataStatus.ts | 38 +-- 13 files changed, 463 insertions(+), 435 deletions(-) diff --git a/lib/VeDirectFrameHandler/VeDirectData.cpp b/lib/VeDirectFrameHandler/VeDirectData.cpp index 20c2cdf63..012ec29ea 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.cpp +++ b/lib/VeDirectFrameHandler/VeDirectData.cpp @@ -222,3 +222,39 @@ frozen::string const& veMpptStruct::getOrAsString() const return getAsString(values, OR); } + +frozen::string const& VeDirectHexData::getResponseAsString() const +{ + using Response = VeDirectHexResponse; + static constexpr frozen::map values = { + { Response::DONE, "Done" }, + { Response::UNKNOWN, "Unknown" }, + { Response::ERROR, "Error" }, + { Response::PING, "Ping" }, + { Response::GET, "Get" }, + { Response::SET, "Set" }, + { Response::ASYNC, "Async" } + }; + + return getAsString(values, rsp); +} + +frozen::string const& VeDirectHexData::getRegisterAsString() const +{ + using Register = VeDirectHexRegister; + static constexpr frozen::map values = { + { Register::DeviceMode, "Device Mode" }, + { Register::DeviceState, "Device State" }, + { Register::RemoteControlUsed, "Remote Control Used" }, + { Register::PanelVoltage, "Panel Voltage" }, + { Register::ChargerVoltage, "Charger Voltage" }, + { Register::NetworkTotalDcInputPower, "Network Total DC Input Power" }, + { Register::ChargeControllerTemperature, "Charger Controller Temperature" }, + { Register::SmartBatterySenseTemperature, "Smart Battery Sense Temperature" }, + { Register::NetworkInfo, "Network Info" }, + { Register::NetworkMode, "Network Mode" }, + { Register::NetworkStatus, "Network Status" } + }; + + return getAsString(values, addr); +} diff --git a/lib/VeDirectFrameHandler/VeDirectData.h b/lib/VeDirectFrameHandler/VeDirectData.h index f651193b0..b1158d776 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.h +++ b/lib/VeDirectFrameHandler/VeDirectData.h @@ -34,6 +34,17 @@ struct veMpptStruct : veStruct { float H22; // yield yesterday kWh int32_t H23; // maximum power yesterday W + // these are values communicated through the HEX protocol. the pair's first + // value is the timestamp the respective info was last received. if it is + // zero, the value is deemed invalid. the timestamp is reset if no current + // value could be retrieved. + std::pair MpptTemperatureMilliCelsius; + std::pair SmartBatterySenseTemperatureMilliCelsius; + std::pair NetworkTotalDcInputPowerMilliWatts; + std::pair NetworkInfo; + std::pair NetworkMode; + std::pair NetworkStatus; + frozen::string const& getMpptAsString() const; // state of mppt as string frozen::string const& getCsAsString() const; // current state as string frozen::string const& getErrAsString() const; // error state as string @@ -68,3 +79,57 @@ struct veShuntStruct : veStruct { int32_t H17; // Amount of discharged energy int32_t H18; // Amount of charged energy }; + +enum class VeDirectHexCommand : uint8_t { + ENTER_BOOT = 0x0, + PING = 0x1, + RSV1 = 0x2, + APP_VERSION = 0x3, + PRODUCT_ID = 0x4, + RSV2 = 0x5, + RESTART = 0x6, + GET = 0x7, + SET = 0x8, + RSV3 = 0x9, + ASYNC = 0xA, + RSV4 = 0xB, + RSV5 = 0xC, + RSV6 = 0xD, + RSV7 = 0xE, + RSV8 = 0xF +}; + +enum class VeDirectHexResponse : uint8_t { + DONE = 0x1, + UNKNOWN = 0x3, + ERROR = 0x4, + PING = 0x5, + GET = 0x7, + SET = 0x8, + ASYNC = 0xA +}; + +enum class VeDirectHexRegister : uint16_t { + DeviceMode = 0x0200, + DeviceState = 0x0201, + RemoteControlUsed = 0x0202, + PanelVoltage = 0xEDBB, + ChargerVoltage = 0xEDD5, + NetworkTotalDcInputPower = 0x2027, + ChargeControllerTemperature = 0xEDDB, + SmartBatterySenseTemperature = 0xEDEC, + NetworkInfo = 0x200D, + NetworkMode = 0x200E, + NetworkStatus = 0x200F +}; + +struct VeDirectHexData { + VeDirectHexResponse rsp; // hex response code + VeDirectHexRegister addr; // register address + uint8_t flags; // flags + uint32_t value; // integer value of register + char text[VE_MAX_HEX_LEN]; // text/string response + + frozen::string const& getResponseAsString() const; + frozen::string const& getRegisterAsString() const; +}; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index c26c97055..3e07169a8 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -39,18 +39,6 @@ // The name of the record that contains the checksum. static constexpr char checksumTagName[] = "CHECKSUM"; -// state machine -enum States { - IDLE = 1, - RECORD_BEGIN = 2, - RECORD_NAME = 3, - RECORD_VALUE = 4, - CHECKSUM = 5, - RECORD_HEX = 6 -}; - - - class Silent : public Print { public: size_t write(uint8_t c) final { return 0; } @@ -62,7 +50,7 @@ template VeDirectFrameHandler::VeDirectFrameHandler() : _msgOut(&MessageOutputDummy), _lastUpdate(0), - _state(IDLE), + _state(State::IDLE), _checksum(0), _textPointer(0), _hexSize(0), @@ -79,6 +67,7 @@ void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* _vedirectSerial = std::make_unique(hwSerialPort); _vedirectSerial->begin(19200, SERIAL_8N1, rx, tx); _vedirectSerial->flush(); + _canSend = (tx != -1); _msgOut = msgOut; _verboseLogging = verboseLogging; _debugIn = 0; @@ -103,7 +92,7 @@ template void VeDirectFrameHandler::reset() { _checksum = 0; - _state = IDLE; + _state = State::IDLE; _textData.clear(); } @@ -118,8 +107,9 @@ void VeDirectFrameHandler::loop() // there will never be a large gap between two bytes of the same frame. // if such a large gap is observed, reset the state machine so it tries // to decode a new frame once more data arrives. - if (IDLE != _state && (millis() - _lastByteMillis) > 500) { - _msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", _logId, _state); + if (State::IDLE != _state && (millis() - _lastByteMillis) > 500) { + _msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", + _logId, static_cast(_state)); if (_verboseLogging) { dumpDebugBuffer(); } reset(); } @@ -141,34 +131,34 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) } } - if ( (inbyte == ':') && (_state != CHECKSUM) ) { + if ( (inbyte == ':') && (_state != State::CHECKSUM) ) { _prevState = _state; //hex frame can interrupt TEXT - _state = RECORD_HEX; + _state = State::RECORD_HEX; _hexSize = 0; } - if (_state != RECORD_HEX) { + if (_state != State::RECORD_HEX) { _checksum += inbyte; } inbyte = toupper(inbyte); switch(_state) { - case IDLE: + case State::IDLE: /* wait for \n of the start of an record */ switch(inbyte) { case '\n': - _state = RECORD_BEGIN; + _state = State::RECORD_BEGIN; break; case '\r': /* Skip */ default: break; } break; - case RECORD_BEGIN: + case State::RECORD_BEGIN: _textPointer = _name; *_textPointer++ = inbyte; - _state = RECORD_NAME; + _state = State::RECORD_NAME; break; - case RECORD_NAME: + case State::RECORD_NAME: // The record name is being received, terminated by a \t switch(inbyte) { case '\t': @@ -176,12 +166,12 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) if ( _textPointer < (_name + sizeof(_name)) ) { *_textPointer = 0; /* Zero terminate */ if (strcmp(_name, checksumTagName) == 0) { - _state = CHECKSUM; + _state = State::CHECKSUM; break; } } _textPointer = _value; /* Reset value pointer */ - _state = RECORD_VALUE; + _state = State::RECORD_VALUE; break; case '#': /* Ignore # from serial number*/ break; @@ -192,7 +182,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) break; } break; - case RECORD_VALUE: + case State::RECORD_VALUE: // The record value is being received. The \r indicates a new record. switch(inbyte) { case '\n': @@ -200,7 +190,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) *_textPointer = 0; // make zero ended _textData.push_back({_name, _value}); } - _state = RECORD_BEGIN; + _state = State::RECORD_BEGIN; break; case '\r': /* Skip */ break; @@ -211,7 +201,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) break; } break; - case CHECKSUM: + case State::CHECKSUM: { if (_verboseLogging) { dumpDebugBuffer(); } if (_checksum == 0) { @@ -227,7 +217,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) reset(); break; } - case RECORD_HEX: + case State::RECORD_HEX: _state = hexRxEvent(inbyte); break; } @@ -279,17 +269,23 @@ void VeDirectFrameHandler::processTextData(std::string const& name, std::stri * This function records hex answers or async messages */ template -int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) +typename VeDirectFrameHandler::State VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { - int ret=RECORD_HEX; // default - continue recording until end of frame + State ret = State::RECORD_HEX; // default - continue recording until end of frame switch (inbyte) { case '\n': // now we can analyse the hex message _hexBuffer[_hexSize] = '\0'; VeDirectHexData data; - if (disassembleHexData(data)) - hexDataHandler(data); + if (disassembleHexData(data) && !hexDataHandler(data) && _verboseLogging) { + _msgOut->printf("%s Unhandled Hex %s Response, addr: 0x%04X (%s), " + "value: 0x%08X, flags: 0x%02X\r\n", _logId, + data.getResponseAsString().data(), + static_cast(data.addr), + data.getRegisterAsString().data(), + data.value, data.flags); + } // restore previous state ret=_prevState; @@ -301,7 +297,7 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) if (_hexSize>=VE_MAX_HEX_LEN) { // oops -buffer overflow - something went wrong, we abort _msgOut->printf("%s hexRx buffer overflow - aborting read\r\n", _logId); _hexSize=0; - ret=IDLE; + ret = State::IDLE; } } diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index a2cd28348..c2d660884 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -19,39 +19,6 @@ #include #include "VeDirectData.h" -// hex send commands -enum VeDirectHexCommand { - ENTER_BOOT = 0x00, - PING = 0x01, - APP_VERSION = 0x02, - PRODUCT_ID = 0x04, - RESTART = 0x06, - GET = 0x07, - SET = 0x08, - ASYNC = 0x0A, - UNKNOWN = 0x0F -}; - -// hex receive responses -enum VeDirectHexResponse { - R_DONE = 0x01, - R_UNKNOWN = 0x03, - R_ERROR = 0x04, - R_PING = 0x05, - R_GET = 0x07, - R_SET = 0x08, - R_ASYNC = 0x0A, -}; - -// hex response data, contains the disassembeled hex message, forwarded to virtual funktion hexDataHandler() -struct VeDirectHexData { - VeDirectHexResponse rsp; // hex response type - uint16_t id; // register address - uint8_t flag; // flag - uint32_t value; // value from register - char text[VE_MAX_HEX_LEN]; // text/string response -}; - template class VeDirectFrameHandler { public: @@ -59,12 +26,12 @@ class VeDirectFrameHandler { uint32_t getLastUpdate() const; // timestamp of last successful frame read bool isDataValid() const; // return true if data valid and not outdated T const& getData() const { return _tmpFrame; } - bool sendHexCommand(VeDirectHexCommand cmd, uint16_t id = 0, uint32_t value = 0, uint8_t valunibble = 0); // send hex commands via ve.direct + bool sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value = 0, uint8_t valsize = 0); protected: VeDirectFrameHandler(); void init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); - virtual void hexDataHandler(VeDirectHexData const &data) { } // handles the disassembeled hex response + virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembeled hex response bool _verboseLogging; Print* _msgOut; @@ -72,6 +39,9 @@ class VeDirectFrameHandler { T _tmpFrame; + bool _canSend; + char _logId[32]; + private: void reset(); void dumpDebugBuffer(); @@ -79,22 +49,32 @@ class VeDirectFrameHandler { void processTextData(std::string const& name, std::string const& value); virtual bool processTextDataDerived(std::string const& name, std::string const& value) = 0; virtual void frameValidEvent() { } - int hexRxEvent(uint8_t); bool disassembleHexData(VeDirectHexData &data); //return true if disassembling was possible std::unique_ptr _vedirectSerial; - int _state; // current state - int _prevState; // previous state + + enum class State { + IDLE = 1, + RECORD_BEGIN = 2, + RECORD_NAME = 3, + RECORD_VALUE = 4, + CHECKSUM = 5, + RECORD_HEX = 6 + }; + State _state; + State _prevState; + + State hexRxEvent(uint8_t inbyte); + uint8_t _checksum; // checksum value char * _textPointer; // pointer to the private buffer we're writing to, name or value - int _hexSize; // length of hex buffer - char _hexBuffer[VE_MAX_HEX_LEN] = { }; // buffer for received hex frames + int _hexSize; // length of hex buffer + char _hexBuffer[VE_MAX_HEX_LEN]; // buffer for received hex frames char _name[VE_MAX_VALUE_LEN]; // buffer for the field name char _value[VE_MAX_VALUE_LEN]; // buffer for the field value std::array _debugBuffer; unsigned _debugIn; uint32_t _lastByteMillis; - char _logId[32]; /** * not every frame contains every value the device is communicating, i.e., diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp index b8cd0f277..f7f44b5b0 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp @@ -6,22 +6,17 @@ HexHandler.cpp * * How to use: * 1. Use sendHexCommand() to send hex messages. Use the Victron documentation to find the parameter. - * 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function + * 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function * void VeDirectFrameHandler::hexDataHandler(VeDirectHexData const &data) * to handle the received hex messages. All hex messages will be forwarted to function hexDataHandler() - * 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits. + * 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits. * - * 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages + * 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages * */ #include #include "VeDirectFrameHandler.h" - -// support for debugging, 0=without extended logging, 1=with extended logging -constexpr int MODUL_DEBUG = 0; - - /* * calcHexFrameCheckSum() * help function to calculate the hex checksum @@ -30,7 +25,7 @@ constexpr int MODUL_DEBUG = 0; #define hex2byte(b) (ascii2hex(*(b)))*16+((ascii2hex(*(b+1)))) static uint8_t calcHexFrameCheckSum(const char* buffer, int size) { uint8_t checksum=0x55-ascii2hex(buffer[1]); - for (int i=2; i(strtoul(help, nullptr, 16))); } /* * disassembleHexData() - * analysis the hex message and extract: response, id, flag and value/text + * analysis the hex message and extract: response, address, flags and value/text * buffer: pointer to message (ascii hex little endian format) * data: disassembeled message * return: true = successful disassembeld, false = hex sum fault or message @@ -76,29 +71,30 @@ template bool VeDirectFrameHandler::disassembleHexData(VeDirectHexData &data) { bool state = false; char * buffer = _hexBuffer; - auto len = strlen(buffer); + auto len = strlen(buffer); // reset hex data first - data = {}; + data = {}; if ((len > 3) && (calcHexFrameCheckSum(buffer, len) == 0x00)) { data.rsp = static_cast(AsciiHexLE2Int(buffer+1, 1)); + using Response = VeDirectHexResponse; switch (data.rsp) { - case R_DONE: - case R_ERROR: - case R_PING: - case R_UNKNOWN: + case Response::DONE: + case Response::ERROR: + case Response::PING: + case Response::UNKNOWN: strncpy(data.text, buffer+2, len-4); state = true; break; - case R_GET: - case R_SET: - case R_ASYNC: - data.id = AsciiHexLE2Int(buffer+2, 4); + case Response::GET: + case Response::SET: + case Response::ASYNC: + data.addr = static_cast(AsciiHexLE2Int(buffer+2, 4)); - // future option: to analyse the flag here? - data.flag = AsciiHexLE2Int(buffer+6, 2); + // future option: to analyse the flags here? + data.flags = AsciiHexLE2Int(buffer+6, 2); if (len == 12) { // 8bit value data.value = AsciiHexLE2Int(buffer+8, 2); @@ -120,13 +116,8 @@ bool VeDirectFrameHandler::disassembleHexData(VeDirectHexData &data) { } } - if constexpr(MODUL_DEBUG == 1) { - _msgOut->printf("[VE.Direct] debug: disassembleHexData(), rsp: %i, id: 0x%04X, value: 0x%X, Flag: 0x%02X\r\n", - data.rsp, data.id, data.value, data.flag); - } - - if (_verboseLogging && !state) - _msgOut->printf("[VE.Direct] failed to disassemble the hex message: %s\r\n", buffer); + if (!state) + _msgOut->printf("%s failed to disassemble the hex message: %s\r\n", _logId, buffer); return (state); } @@ -156,7 +147,7 @@ static String Int2HexLEString(uint32_t value, uint8_t anz) { default: ; } - return String(help); + return String(help); } @@ -164,40 +155,41 @@ static String Int2HexLEString(uint32_t value, uint8_t anz) { * sendHexCommand() * send the hex commend after assembling the command string * cmd: command - * id: id/register, default 0 - * value: value to write into a id/register, default 0 - * valsize: size of the value/id, 8, 16 or 32 bit, default 0 + * addr: register address, default 0 + * value: value to write into a register, default 0 + * valsize: size of the value, 8, 16 or 32 bit, default 0 * return: true = message assembeld and send, false = it was not possible to put the message together - * SAMPLE: ping command: sendHexCommand(PING), + * SAMPLE: ping command: sendHexCommand(PING), * read total DC input power sendHexCommand(GET, 0xEDEC) * set Charge current limit 10A sendHexCommand(SET, 0x2015, 64, 16) - * + * * WARNING: some values are stored in non-volatile memory. Continuous writing, for example from a control loop, will - * lead to early failure. + * lead to early failure. * On MPPT for example 0xEDE0 - 0xEDFF. Check the Vivtron doc "BlueSolar-HEX-protocol.pdf" */ template -bool VeDirectFrameHandler::sendHexCommand(VeDirectHexCommand cmd, uint16_t id, uint32_t value, uint8_t valsize) { +bool VeDirectFrameHandler::sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value, uint8_t valsize) { bool ret = false; - uint8_t flag = 0x00; // always 0x00 + uint8_t flags = 0x00; // always 0x00 - String txData = ":" + Int2HexLEString(cmd, 1); // add the command nibble + String txData = ":" + Int2HexLEString(static_cast(cmd), 1); // add the command nibble + using Command = VeDirectHexCommand; switch (cmd) { - case PING: - case APP_VERSION: - case PRODUCT_ID: - ret = true; - break; - case GET: - case ASYNC: - txData += Int2HexLEString(id, 4); // add the id/register (4 nibble) - txData += Int2HexLEString(flag, 2); // add the flag (2 nibble) + case Command::PING: + case Command::APP_VERSION: + case Command::PRODUCT_ID: + ret = true; + break; + case Command::GET: + case Command::ASYNC: + txData += Int2HexLEString(static_cast(addr), 4); + txData += Int2HexLEString(flags, 2); // add the flags (2 nibble) ret = true; break; - case SET: - txData += Int2HexLEString(id, 4); // add the id/register (4 nibble) - txData += Int2HexLEString(flag, 2); // add the flag (2 nibble) + case Command::SET: + txData += Int2HexLEString(static_cast(addr), 4); + txData += Int2HexLEString(flags, 2); // add the flags (2 nibble) if ((valsize == 8) || (valsize == 16) || (valsize == 32)) { txData += Int2HexLEString(value, valsize/4); // add value (2-8 nibble) ret = true; @@ -205,7 +197,7 @@ bool VeDirectFrameHandler::sendHexCommand(VeDirectHexCommand cmd, uint16_t id break; default: ret = false; - break; + break; } if (ret) { @@ -213,15 +205,16 @@ bool VeDirectFrameHandler::sendHexCommand(VeDirectHexCommand cmd, uint16_t id txData += Int2HexLEString(calcHexFrameCheckSum(txData.c_str(), txData.length()), 2); String send = txData + "\n"; // hex command end byte _vedirectSerial->write(send.c_str(), send.length()); - - if constexpr(MODUL_DEBUG == 1) { - auto blen = _vedirectSerial->availableForWrite(); - _msgOut->printf("[VE.Direct] debug: sendHexCommand(): %s, Free FIFO-Buffer: %u\r\n", txData.c_str(), blen); + + if (_verboseLogging) { + auto blen = _vedirectSerial->availableForWrite(); + _msgOut->printf("%s Sending Hex Command: %s, Free FIFO-Buffer: %u\r\n", + _logId, txData.c_str(), blen); } - } + } - if (_verboseLogging && !ret) - _msgOut->println("[VE.Direct] send hex command fault:" + txData); + if (!ret) + _msgOut->printf("%s send hex command fault: %s\r\n", _logId, txData.c_str()); return (ret); } diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 9e94356ae..ef3ef187b 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -1,6 +1,6 @@ /* VeDirectMpptController.cpp * - * + * * 2020.08.20 - 0.0 - ??? * 2024.03.18 - 0.1 - add of: - temperature from "Smart Battery Sense" connected over VE.Smart network * - temperature from internal MPPT sensor @@ -10,10 +10,7 @@ #include #include "VeDirectMpptController.h" - -// support for debugging, 0=without extended logging, 1=with extended logging -constexpr int MODUL_DEBUG = 0; - +//#define PROCESS_NETWORK_STATE void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { @@ -93,96 +90,156 @@ void VeDirectMpptController::frameValidEvent() { _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); _tmpFrame.E = _efficiency.getAverage(); } -} + if (!_canSend) { return; } -/* -// loop() -// send hex commands to MPPT every 5 seconds -*/ -void VeDirectMpptController::loop() -{ - VeDirectFrameHandler::loop(); - // Copy from the "VE.Direct Protocol" documentation // For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the // charger periodically sends human readable (TEXT) data to the serial port. For firmware // versions v1.53 and above, the charger always periodically sends TEXT data to the serial port. // --> We just use hex commandes for firmware >= 1.53 to keep text messages alive - if (atoi(_tmpFrame.FW) >= 153 ) { - if ((millis() - _lastPingTime) > 5000) { - - sendHexCommand(GET, 0x2027); // MPPT total DC input power - sendHexCommand(GET, 0xEDDB); // MPPT internal temperature - sendHexCommand(GET, 0xEDEC); // "Smart Battery Sense" temperature - sendHexCommand(GET, 0x200F); // Network info - _lastPingTime = millis(); - } - } + if (atoi(_tmpFrame.FW) < 153) { return; } + + using Command = VeDirectHexCommand; + using Register = VeDirectHexRegister; + + sendHexCommand(Command::GET, Register::ChargeControllerTemperature); + sendHexCommand(Command::GET, Register::SmartBatterySenseTemperature); + sendHexCommand(Command::GET, Register::NetworkTotalDcInputPower); + +#ifdef PROCESS_NETWORK_STATE + sendHexCommand(Command::GET, Register::NetworkInfo); + sendHexCommand(Command::GET, Register::NetworkMode); + sendHexCommand(Command::GET, Register::NetworkStatus); +#endif // PROCESS_NETWORK_STATE } -/* - * hexDataHandler() - * analyse the content of VE.Direct hex messages - * Handels the received hex data from the MPPT - */ -void VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) { - bool state = false; - - switch (data.rsp) { - case R_GET: - case R_ASYNC: - - // check if MPPT internal temperature is available - if(data.id == 0xEDDB) { - _ExData.T = static_cast(data.value) * 10; // conversion from unit [0.01°C] to unit [m°C] - _ExData.Tts = millis(); - state = true; - - if constexpr(MODUL_DEBUG == 1) - _msgOut->printf("[VE.Direct] debug: hexDataHandler(), MTTP Temperature: %.2f°C\r\n", _ExData.T/1000.0); - } +void VeDirectMpptController::loop() +{ + VeDirectFrameHandler::loop(); - // check if temperature from "Smart Battery Sense" is available - if(data.id == 0xEDEC) { - _ExData.TSBS = static_cast(data.value) * 10 - 272150; // conversion from unit [0.01K] to unit [m°C] - _ExData.TSBSts = millis(); - state = true; - - if constexpr(MODUL_DEBUG == 1) - _msgOut->printf("[VE.Direct] debug: hexDataHandler(), Battery Temperature: %.2f°C\r\n", _ExData.TSBS/1000.0); + auto resetTimestamp = [this](auto& pair) { + if (pair.first > 0 && (millis() - pair.first) > (10 * 1000)) { + pair.first = 0; } + }; - // check if "Total DC power" is available - if(data.id == 0x2027) { - _ExData.TDCP = data.value * 10; // conversion from unit [0.01W] to unit [mW] - _ExData.TDCPts = millis(); - state = true; + resetTimestamp(_tmpFrame.MpptTemperatureMilliCelsius); + resetTimestamp(_tmpFrame.SmartBatterySenseTemperatureMilliCelsius); + resetTimestamp(_tmpFrame.NetworkTotalDcInputPowerMilliWatts); - if constexpr(MODUL_DEBUG == 1) - _msgOut->printf("[VE.Direct] debug: hexDataHandler(), Total Power: %.2fW\r\n", _ExData.TDCP/1000.0); - } +#ifdef PROCESS_NETWORK_STATE + resetTimestamp(_tmpFrame.NetworkInfo); + resetTimestamp(_tmpFrame.NetworkMode); + resetTimestamp(_tmpFrame.NetworkStatus); +#endif // PROCESS_NETWORK_STATE +} - // check if connected MPPT is charge instance master - // Hint: not used right now but maybe necessary for future extensions - if(data.id == 0x200F) { - _veMaster = ((data.value & 0x0F) == 0x02) ? true : false; - state = true; - if constexpr(MODUL_DEBUG == 1) - _msgOut->printf("[VE.Direct] debug: hexDataHandler(), Networkmode: 0x%X\r\n", data.value); - } - break; - default: - break; +/* + * hexDataHandler() + * analyse the content of VE.Direct hex messages + * Handels the received hex data from the MPPT + */ +bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) { + if (data.rsp != VeDirectHexResponse::GET && + data.rsp != VeDirectHexResponse::ASYNC) { return false; } + + auto regLog = static_cast(data.addr); + + switch (data.addr) { + case VeDirectHexRegister::ChargeControllerTemperature: + _tmpFrame.MpptTemperatureMilliCelsius = + { millis(), static_cast(data.value) * 10 }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: MPPT Temperature (0x%04X): %.2f°C\r\n", + _logId, regLog, + _tmpFrame.MpptTemperatureMilliCelsius.second / 1000.0); + } + return true; + break; + + case VeDirectHexRegister::SmartBatterySenseTemperature: + if (data.value == 0xFFFF) { + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Smart Battery Sense Temperature is not available\r\n", _logId); + } + return true; // we know what to do with it, and we decided to ignore the value + } + + _tmpFrame.SmartBatterySenseTemperatureMilliCelsius = + { millis(), static_cast(data.value) * 10 - 272150 }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Smart Battery Sense Temperature (0x%04X): %.2f°C\r\n", + _logId, regLog, + _tmpFrame.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0); + } + return true; + break; + + case VeDirectHexRegister::NetworkTotalDcInputPower: + if (data.value == 0xFFFFFFFF) { + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Network total DC power value " + "indicates non-networked controller\r\n", _logId); + } + _tmpFrame.NetworkTotalDcInputPowerMilliWatts = { 0, 0 }; + return true; // we know what to do with it, and we decided to ignore the value + } + + _tmpFrame.NetworkTotalDcInputPowerMilliWatts = + { millis(), data.value * 10 }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Network Total DC Power (0x%04X): %.2fW\r\n", + _logId, regLog, + _tmpFrame.NetworkTotalDcInputPowerMilliWatts.second / 1000.0); + } + return true; + break; + +#ifdef PROCESS_NETWORK_STATE + case VeDirectHexRegister::NetworkInfo: + _tmpFrame.NetworkInfo = + { millis(), static_cast(data.value) }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Network Info (0x%04X): 0x%X\r\n", + _logId, regLog, data.value); + } + return true; + break; + + case VeDirectHexRegister::NetworkMode: + _tmpFrame.NetworkMode = + { millis(), static_cast(data.value) }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Network Mode (0x%04X): 0x%X\r\n", + _logId, regLog, data.value); + } + return true; + break; + + case VeDirectHexRegister::NetworkStatus: + _tmpFrame.NetworkStatus = + { millis(), static_cast(data.value) }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Network Status (0x%04X): 0x%X\r\n", + _logId, regLog, data.value); + } + return true; + break; +#endif // PROCESS_NETWORK_STATE + + default: + return false; + break; } - if constexpr(MODUL_DEBUG == 1) - _msgOut->printf("[VE.Direct] debug: hexDataHandler(): rsp: %i, id: 0x%04X, value: %i[0x%08X], text: %s\r\n", - data.rsp, data.id, data.value, data.value, data.text); - - if (_verboseLogging && state) - _msgOut->printf("[VE.Direct] MPPT hex message: rsp: %i, id: 0x%04X, value: %i[0x%08X], text: %s\r\n", - data.rsp, data.id, data.value, data.value, data.text); -} + return false; +} diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index ebee88513..595988985 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -44,24 +44,11 @@ class VeDirectMpptController : public VeDirectFrameHandler { using data_t = veMpptStruct; - virtual void loop() final; // main loop to read ve.direct data - - struct veMPPTExStruct { - int32_t T; // temperature [m°C] from internal MPPT sensor - unsigned long Tts; // time of last recieved value - int32_t TSBS; // temperature [m°C] from the "Smart Battery Sense" - unsigned long TSBSts; // time of last recieved value - uint32_t TDCP; // total DC input power [mW] - unsigned long TDCPts; // time of last recieved value - }; - veMPPTExStruct _ExData{}; - veMPPTExStruct const *getExData() const { return &_ExData; } + void loop() final; private: - void hexDataHandler(VeDirectHexData const &data) final; + bool hexDataHandler(VeDirectHexData const &data) final; bool processTextDataDerived(std::string const& name, std::string const& value) final; void frameValidEvent() final; MovingAverage _efficiency; - unsigned long _lastPingTime = 0L; // time of last device PING/GET hex command - bool _veMaster = true; // MPPT is instance master }; diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index d843475c4..c9c556c10 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -146,57 +146,64 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool ful } void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) { - // device info - root["device"]["PID"] = mpptData.getPidAsString(); - root["device"]["SER"] = String(mpptData.SER); - root["device"]["FW"] = String(mpptData.FW); - root["device"]["LOAD"] = mpptData.LOAD ? "ON" : "OFF"; - root["device"]["CS"] = mpptData.getCsAsString(); - root["device"]["ERR"] = mpptData.getErrAsString(); - root["device"]["OR"] = mpptData.getOrAsString(); - root["device"]["MPPT"] = mpptData.getMpptAsString(); - root["device"]["HSDS"]["v"] = mpptData.HSDS; - root["device"]["HSDS"]["u"] = "d"; - - // battery info - root["output"]["P"]["v"] = mpptData.P; - root["output"]["P"]["u"] = "W"; - root["output"]["P"]["d"] = 0; - root["output"]["V"]["v"] = mpptData.V; - root["output"]["V"]["u"] = "V"; - root["output"]["V"]["d"] = 2; - root["output"]["I"]["v"] = mpptData.I; - root["output"]["I"]["u"] = "A"; - root["output"]["I"]["d"] = 2; - root["output"]["E"]["v"] = mpptData.E; - root["output"]["E"]["u"] = "%"; - root["output"]["E"]["d"] = 1; - - // panel info - root["input"]["PPV"]["v"] = mpptData.PPV; - root["input"]["PPV"]["u"] = "W"; - root["input"]["PPV"]["d"] = 0; - root["input"]["VPV"]["v"] = mpptData.VPV; - root["input"]["VPV"]["u"] = "V"; - root["input"]["VPV"]["d"] = 2; - root["input"]["IPV"]["v"] = mpptData.IPV; - root["input"]["IPV"]["u"] = "A"; - root["input"]["IPV"]["d"] = 2; - root["input"]["YieldToday"]["v"] = mpptData.H20; - root["input"]["YieldToday"]["u"] = "kWh"; - root["input"]["YieldToday"]["d"] = 3; - root["input"]["YieldYesterday"]["v"] = mpptData.H22; - root["input"]["YieldYesterday"]["u"] = "kWh"; - root["input"]["YieldYesterday"]["d"] = 3; - root["input"]["YieldTotal"]["v"] = mpptData.H19; - root["input"]["YieldTotal"]["u"] = "kWh"; - root["input"]["YieldTotal"]["d"] = 3; - root["input"]["MaximumPowerToday"]["v"] = mpptData.H21; - root["input"]["MaximumPowerToday"]["u"] = "W"; - root["input"]["MaximumPowerToday"]["d"] = 0; - root["input"]["MaximumPowerYesterday"]["v"] = mpptData.H23; - root["input"]["MaximumPowerYesterday"]["u"] = "W"; - root["input"]["MaximumPowerYesterday"]["d"] = 0; + root["product_id"] = mpptData.getPidAsString(); + root["firmware_version"] = String(mpptData.FW); + + const JsonObject &values = root.createNestedObject("values"); + + const JsonObject &device = values.createNestedObject("device"); + device["LOAD"] = mpptData.LOAD ? "ON" : "OFF"; + device["CS"] = mpptData.getCsAsString(); + device["MPPT"] = mpptData.getMpptAsString(); + device["OR"] = mpptData.getOrAsString(); + device["ERR"] = mpptData.getErrAsString(); + device["HSDS"]["v"] = mpptData.HSDS; + device["HSDS"]["u"] = "d"; + if (mpptData.MpptTemperatureMilliCelsius.first > 0) { + device["MpptTemperature"]["v"] = mpptData.MpptTemperatureMilliCelsius.second / 1000.0; + device["MpptTemperature"]["u"] = "°C"; + device["MpptTemperature"]["d"] = "1"; + } + + const JsonObject &output = values.createNestedObject("output"); + output["P"]["v"] = mpptData.P; + output["P"]["u"] = "W"; + output["P"]["d"] = 0; + output["V"]["v"] = mpptData.V; + output["V"]["u"] = "V"; + output["V"]["d"] = 2; + output["I"]["v"] = mpptData.I; + output["I"]["u"] = "A"; + output["I"]["d"] = 2; + output["E"]["v"] = mpptData.E; + output["E"]["u"] = "%"; + output["E"]["d"] = 1; + + const JsonObject &input = values.createNestedObject("input"); + input["PPV"]["v"] = mpptData.PPV; + input["PPV"]["u"] = "W"; + input["PPV"]["d"] = 0; + input["VPV"]["v"] = mpptData.VPV; + input["VPV"]["u"] = "V"; + input["VPV"]["d"] = 2; + input["IPV"]["v"] = mpptData.IPV; + input["IPV"]["u"] = "A"; + input["IPV"]["d"] = 2; + input["YieldToday"]["v"] = mpptData.H20; + input["YieldToday"]["u"] = "kWh"; + input["YieldToday"]["d"] = 3; + input["YieldYesterday"]["v"] = mpptData.H22; + input["YieldYesterday"]["u"] = "kWh"; + input["YieldYesterday"]["d"] = 3; + input["YieldTotal"]["v"] = mpptData.H19; + input["YieldTotal"]["u"] = "kWh"; + input["YieldTotal"]["d"] = 3; + input["MaximumPowerToday"]["v"] = mpptData.H21; + input["MaximumPowerToday"]["u"] = "W"; + input["MaximumPowerToday"]["d"] = 0; + input["MaximumPowerYesterday"]["v"] = mpptData.H23; + input["MaximumPowerYesterday"]["u"] = "W"; + input["MaximumPowerYesterday"]["d"] = 0; } void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) diff --git a/webapp/src/components/VedirectView.vue b/webapp/src/components/VedirectView.vue index 91efeaebc..e79b265fe 100644 --- a/webapp/src/components/VedirectView.vue +++ b/webapp/src/components/VedirectView.vue @@ -18,13 +18,13 @@
- {{ item.device.PID }} + {{ item.product_id }}
- {{ $t('vedirecthome.SerialNumber') }} {{ item.device.SER }} + {{ $t('vedirecthome.SerialNumber') }} {{ serial }}
- {{ $t('vedirecthome.FirmwareNumber') }} {{ item.device.FW }} + {{ $t('vedirecthome.FirmwareNumber') }} {{ item.firmware_version }}
{{ $t('vedirecthome.DataAge') }} {{ $t('vedirecthome.Seconds', {'val': Math.floor(item.data_age_ms / 1000)}) }} @@ -55,9 +55,9 @@
-
-
-
{{ $t('vedirecthome.DeviceInfo') }}
+
+
+
{{ $t('vedirecthome.section_' + section) }}
@@ -69,95 +69,21 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ $t('vedirecthome.LoadOutputState') }}{{item.device.LOAD}}
{{ $t('vedirecthome.StateOfOperation') }}{{item.device.CS}}
{{ $t('vedirecthome.TrackerOperationMode') }}{{item.device.MPPT}}
{{ $t('vedirecthome.OffReason') }}{{item.device.OR}}
{{ $t('vedirecthome.ErrorCode') }}{{item.device.ERR}}
{{ $t('vedirecthome.DaySequenceNumber') }}{{item.device.HSDS.v}}{{item.device.HSDS.u}}
-
-
-
-
-
-
-
{{ $t('vedirecthome.Battery') }}
-
-
- - - - - - - - - - - - - - - -
{{ $t('vedirecthome.Property') }}{{ $t('vedirecthome.Value') }}{{ $t('vedirecthome.Unit') }}
{{ $t('vedirecthome.output.' + key) }} - {{ $n(prop.v, 'decimal', { - minimumFractionDigits: prop.d, - maximumFractionDigits: prop.d}) - }} - {{prop.u}}
-
-
-
-
-
-
-
{{ $t('vedirecthome.Panel') }}
-
-
- - - - - - - - - - - + + - + +
{{ $t('vedirecthome.Property') }}{{ $t('vedirecthome.Value') }}{{ $t('vedirecthome.Unit') }}
{{ $t('vedirecthome.input.' + key) }}
{{ $t('vedirecthome.' + section + '.' + key) }} - {{ $n(prop.v, 'decimal', { + + {{prop.u}}{{prop.u}}
diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 3ee53c03e..e911c083b 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -153,24 +153,27 @@ "FirmwareNumber": "Firmware Version: ", "DataAge": "letzte Aktualisierung: ", "Seconds": "vor {val} Sekunden", - "DeviceInfo": "Geräteinformation", "Property": "Eigenschaft", "Value": "Wert", "Unit": "Einheit", - "LoadOutputState": "Status Ladeausgang", - "StateOfOperation": "Betriebszustand", - "TrackerOperationMode": "Betriebszustand des Trackers", - "OffReason": "Grund für das Ausschalten", - "ErrorCode": "Fehlerbeschreibung", - "DaySequenceNumber": "Anzahl der Tage (0..364)", - "Battery": "Ausgang (Batterie)", + "section_device": "Geräteinformation", + "device": { + "LOAD": "Status Ladeausgang", + "CS": "Betriebszustand", + "MPPT": "Betriebszustand des Trackers", + "OR": "Grund für das Ausschalten", + "ERR": "Fehlerbeschreibung", + "HSDS": "Anzahl der Tage (0..364)", + "MpptTemperature": "Ladereglertemperatur" + }, + "section_output": "Ausgang (Batterie)", "output": { "P": "Leistung (berechnet)", "V": "Spannung", "I": "Strom", "E": "Effizienz (berechnet)" }, - "Panel": "Eingang (Solarpanele)", + "section_input": "Eingang (Solarpanele)", "input": { "PPV": "Leistung", "VPV": "Spannung", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 77c087463..25973f8db 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -153,24 +153,27 @@ "FirmwareNumber": "Firmware Number: ", "DataAge": "Data Age: ", "Seconds": "{val} seconds", - "DeviceInfo": "Device Info", "Property": "Property", "Value": "Value", "Unit": "Unit", - "LoadOutputState": "Load output state", - "StateOfOperation": "State of operation", - "TrackerOperationMode": "Tracker operation mode", - "OffReason": "Off reason", - "ErrorCode": "Error code", - "DaySequenceNumber": "Day sequence number (0..364)", - "Battery": "Output (Battery)", + "section_device": "Device Info", + "device": { + "LOAD": "Load output state", + "CS": "State of operation", + "MPPT": "Tracker operation mode", + "OR": "Off reason", + "ERR": "Error code", + "HSDS": "Day sequence number (0..364)", + "MpptTemperature": "Charge controller temperature" + }, + "section_output": "Output (Battery)", "output": { "P": "Power (calculated)", "V": "Voltage", "I": "Current", "E": "Efficiency (calculated)" }, - "Panel": "Input (Solar Panels)", + "section_input": "Input (Solar Panels)", "input": { "PPV": "Power", "VPV": "Voltage", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index d60cb5b65..2231bc7da 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -153,24 +153,27 @@ "FirmwareNumber": "Firmware Number: ", "DataAge": "Data Age: ", "Seconds": "{val} seconds", - "DeviceInfo": "Device Info", "Property": "Property", "Value": "Value", "Unit": "Unit", - "LoadOutputState": "Load output state", - "StateOfOperation": "State of operation", - "TrackerOperationMode": "Tracker operation mode", - "OffReason": "Off reason", - "ErrorCode": "Error code", - "DaySequenceNumber": "Day sequence number (0..364)", - "Battery": "Output (Battery)", + "section_device": "Device Info", + "device": { + "LOAD": "Load output state", + "CS": "State of operation", + "MPPT": "Tracker operation mode", + "OR": "Off reason", + "ERR": "Error code", + "HSDS": "Day sequence number (0..364)", + "MpptTemperature": "Charge controller temperature" + }, + "section_output": "Output (Battery)", "output": { "P": "Power (calculated)", "V": "Voltage", "I": "Current", "E": "Efficiency (calculated)" }, - "Panel": "Input (Solar Panels)", + "section_input": "Input (Solar Panels)", "input": { "PPV": "Power", "VPV": "Voltage", diff --git a/webapp/src/types/VedirectLiveDataStatus.ts b/webapp/src/types/VedirectLiveDataStatus.ts index 73b78a454..0cdb996fb 100644 --- a/webapp/src/types/VedirectLiveDataStatus.ts +++ b/webapp/src/types/VedirectLiveDataStatus.ts @@ -10,39 +10,11 @@ export interface Vedirect { instances: { [key: string]: VedirectInstance }; } +type MpptData = (ValueObject | string)[]; + export interface VedirectInstance { data_age_ms: number; - device: VedirectDevice; - output: VedirectOutput; - input: VedirectInput; -} - -export interface VedirectDevice { - SER: string; - PID: string; - FW: string; - LOAD: ValueObject; - CS: ValueObject; - MPPT: ValueObject; - OR: ValueObject; - ERR: ValueObject; - HSDS: ValueObject; -} - -export interface VedirectOutput { - P: ValueObject; - V: ValueObject; - I: ValueObject; - E: ValueObject; -} - -export interface VedirectInput { - PPV: ValueObject; - VPV: ValueObject; - IPV: ValueObject; - H19: ValueObject; - H20: ValueObject; - H21: ValueObject; - H22: ValueObject; - H23: ValueObject; + product_id: string; + firmware_version: string; + values: { [key: string]: MpptData }; } From 21cdc69625307a9af7e0b30128b2a34f104e5292 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Mon, 1 Apr 2024 20:46:00 +0200 Subject: [PATCH 24/70] Feature: use VE.Direct "network total DC power" 1. makes the DPL use the power generated by all connected charge controllers for calculations based on solar passthrough. 2. makes the network total DC power appear as "MPPT Total Power" in the live view at the top. 3. shows the network total DC power in the VE.Direct live data card. --- src/VictronMppt.cpp | 20 ++++++++++++++++++++ src/WebApi_ws_vedirect_live.cpp | 5 +++++ webapp/src/locales/de.json | 1 + webapp/src/locales/en.json | 1 + webapp/src/locales/fr.json | 1 + 5 files changed, 28 insertions(+) diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index d152c61ec..f210c93ec 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -140,6 +140,17 @@ int32_t VictronMpptClass::getPowerOutputWatts() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } + + // if any charge controller is part of a VE.Smart network, and if the + // charge controller is connected in a way that allows to send + // requests, we should have the "network total DC input power" + // available. if so, to estimate the output power, we multiply by + // the calculated efficiency of the connected charge controller. + auto networkPower = upController->getData().NetworkTotalDcInputPowerMilliWatts; + if (networkPower.first > 0) { + return static_cast(networkPower.second / 1000.0 * upController->getData().E / 100); + } + sum += upController->getData().P; } @@ -152,6 +163,15 @@ int32_t VictronMpptClass::getPanelPowerWatts() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } + + // if any charge controller is part of a VE.Smart network, and if the + // charge controller is connected in a way that allows to send + // requests, we should have the "network total DC input power" available. + auto networkPower = upController->getData().NetworkTotalDcInputPowerMilliWatts; + if (networkPower.first > 0) { + return static_cast(networkPower.second / 1000.0); + } + sum += upController->getData().PPV; } diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index c9c556c10..9fffeed65 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -180,6 +180,11 @@ void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDir output["E"]["d"] = 1; const JsonObject &input = values.createNestedObject("input"); + if (mpptData.NetworkTotalDcInputPowerMilliWatts.first > 0) { + input["NetworkPower"]["v"] = mpptData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0; + input["NetworkPower"]["u"] = "W"; + input["NetworkPower"]["d"] = "0"; + } input["PPV"]["v"] = mpptData.PPV; input["PPV"]["u"] = "W"; input["PPV"]["d"] = 0; diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index e911c083b..09613822e 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -175,6 +175,7 @@ }, "section_input": "Eingang (Solarpanele)", "input": { + "NetworkPower": "VE.Smart Netzwerk Gesamtleistung", "PPV": "Leistung", "VPV": "Spannung", "IPV": "Strom (berechnet)", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 25973f8db..5ffac8c5e 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -175,6 +175,7 @@ }, "section_input": "Input (Solar Panels)", "input": { + "NetworkPower": "VE.Smart network total power", "PPV": "Power", "VPV": "Voltage", "IPV": "Current (calculated)", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 2231bc7da..519132bff 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -175,6 +175,7 @@ }, "section_input": "Input (Solar Panels)", "input": { + "NetworkPower": "VE.Smart network total power", "PPV": "Power", "VPV": "Voltage", "IPV": "Current (calculated)", From b55ca53d1d01ab381b7207e766bcdde1e3df22fa Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 3 Apr 2024 18:35:27 +0200 Subject: [PATCH 25/70] Fix: Setting DTU options was only possible once without reboot Fix #1884 --- src/WebApi_dtu.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index c6678b0a6..bbcd909f1 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -176,4 +176,5 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) request->send(response); _applyDataTask.enable(); + _applyDataTask.restart(); } From aa10c2c5e1a686fc5c487d776d230cea1b88d22b Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 3 Apr 2024 19:12:08 +0200 Subject: [PATCH 26/70] Fix: Too small event_queue_size in AsyncTCP lead to wdt reset Fix #1705 --- patches/async_tcp/event_queue_size.patch | 26 ++++++++++++++++++++++++ platformio.ini | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 patches/async_tcp/event_queue_size.patch diff --git a/patches/async_tcp/event_queue_size.patch b/patches/async_tcp/event_queue_size.patch new file mode 100644 index 000000000..1280d46a8 --- /dev/null +++ b/patches/async_tcp/event_queue_size.patch @@ -0,0 +1,26 @@ +diff --color -ruN a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp +--- a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp ++++ b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp +@@ -97,7 +97,7 @@ + + static inline bool _init_async_event_queue(){ + if(!_async_queue){ +- _async_queue = xQueueCreate(32, sizeof(lwip_event_packet_t *)); ++ _async_queue = xQueueCreate(CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE, sizeof(lwip_event_packet_t *)); + if(!_async_queue){ + return false; + } +diff --color -ruN a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h +--- a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h ++++ b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h +@@ -53,6 +53,10 @@ + #define CONFIG_ASYNC_TCP_STACK_SIZE 8192 * 2 + #endif + ++#ifndef CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE ++#define CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE 32 ++#endif ++ + class AsyncClient; + + #define ASYNC_MAX_ACK_TIME 5000 diff --git a/platformio.ini b/platformio.ini index 12938d3ed..1f072bfa2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -25,6 +25,7 @@ build_flags = -DPIOENV=\"$PIOENV\" -D_TASK_STD_FUNCTION=1 -D_TASK_THREAD_SAFE=1 + -DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128 -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 @@ -59,7 +60,7 @@ board_build.embed_files = webapp_dist/js/app.js.gz webapp_dist/site.webmanifest -custom_patches = +custom_patches = async_tcp monitor_filters = esp32_exception_decoder, time, log2file, colorize monitor_speed = 115200 From e7a9c96b724f29f4dad91beb8bb8027620e8c44d Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 3 Apr 2024 23:11:30 +0200 Subject: [PATCH 27/70] Upgrade ESP Async WebServer from 2.8.1 to 2.9.0 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 1f072bfa2..06e663c27 100644 --- a/platformio.ini +++ b/platformio.ini @@ -37,7 +37,7 @@ build_unflags = -std=gnu++11 lib_deps = - mathieucarbou/ESP Async WebServer @ 2.8.1 + mathieucarbou/ESP Async WebServer @ 2.9.0 bblanchon/ArduinoJson @ ^6.21.5 https://github.com/bertmelis/espMqttClient.git#v1.6.0 nrf24/RF24 @ ^1.4.8 From 2e3125fe8d6a3f3ff224aa3aaa1d15040b7135b3 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 2 Apr 2024 23:23:12 +0200 Subject: [PATCH 28/70] Feature: Migrated ArduinoJson 6 to 7 --- include/Configuration.h | 2 -- include/MqttHandleHass.h | 6 ++-- include/Utils.h | 2 +- include/WebApi.h | 2 +- include/WebApi_errors.h | 2 +- include/WebApi_mqtt.h | 2 -- platformio.ini | 2 +- src/Configuration.cpp | 66 +++++++++++++++++++------------------- src/MqttHandleHass.cpp | 62 +++++++++++++++++++---------------- src/PinMapping.cpp | 6 ++-- src/Utils.cpp | 4 +-- src/WebApi.cpp | 10 +----- src/WebApi_config.cpp | 6 ++-- src/WebApi_device.cpp | 26 +++++++-------- src/WebApi_dtu.cpp | 6 ++-- src/WebApi_eventlog.cpp | 6 ++-- src/WebApi_gridprofile.cpp | 14 ++++---- src/WebApi_inverter.cpp | 18 +++++------ src/WebApi_limit.cpp | 2 +- src/WebApi_maintenance.cpp | 6 ++-- src/WebApi_mqtt.cpp | 10 +++--- src/WebApi_network.cpp | 2 +- src/WebApi_ntp.cpp | 4 +-- src/WebApi_power.cpp | 2 +- src/WebApi_security.cpp | 2 +- src/WebApi_ws_live.cpp | 31 ++++++++++-------- 26 files changed, 149 insertions(+), 152 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index bb0e478f2..e13b558aa 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -30,8 +30,6 @@ #define DEV_MAX_MAPPING_NAME_STRLEN 63 -#define JSON_BUFFER_SIZE 12288 - struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; char Name[CHAN_MAX_NAME_STRLEN]; diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index feb867435..a76cb0c7b 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -66,10 +66,10 @@ class MqttHandleHassClass { void publishInverterNumber(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100); void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); - static void createInverterInfo(DynamicJsonDocument& doc, std::shared_ptr inv); - static void createDtuInfo(DynamicJsonDocument& doc); + static void createInverterInfo(JsonDocument& doc, std::shared_ptr inv); + static void createDtuInfo(JsonDocument& doc); - static void createDeviceInfo(DynamicJsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = ""); + static void createDeviceInfo(JsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = ""); static String getDtuUniqueId(); static String getDtuUrl(); diff --git a/include/Utils.h b/include/Utils.h index fddc2ab97..f81e73180 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -10,6 +10,6 @@ class Utils { static uint64_t generateDtuSerial(); static int getTimezoneOffset(); static void restartDtu(); - static bool checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line); + static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static void removeAllFiles(); }; diff --git a/include/WebApi.h b/include/WebApi.h index 5e5af5278..b4c499833 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -38,7 +38,7 @@ class WebApiClass { static void writeConfig(JsonVariant& retMsg, const WebApiError code = WebApiError::GenericSuccess, const String& message = "Settings saved!"); - static bool parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, DynamicJsonDocument& json_document, size_t max_document_size = 1024); + static bool parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document); private: AsyncWebServer _server; diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index efb890c5c..675419d72 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -5,7 +5,7 @@ enum WebApiError { GenericBase = 1000, GenericSuccess, GenericNoValueFound, - GenericDataTooLarge, + GenericDataTooLarge, // not used anymore GenericParseError, GenericValueMissing, GenericWriteFailed, diff --git a/include/WebApi_mqtt.h b/include/WebApi_mqtt.h index b259752b1..6e428249e 100644 --- a/include/WebApi_mqtt.h +++ b/include/WebApi_mqtt.h @@ -4,8 +4,6 @@ #include #include -#define MQTT_JSON_DOC_SIZE 10240 - class WebApiMqttClass { public: void init(AsyncWebServer& server, Scheduler& scheduler); diff --git a/platformio.ini b/platformio.ini index 06e663c27..2eef6f078 100644 --- a/platformio.ini +++ b/platformio.ini @@ -38,7 +38,7 @@ build_unflags = lib_deps = mathieucarbou/ESP Async WebServer @ 2.9.0 - bblanchon/ArduinoJson @ ^6.21.5 + bblanchon/ArduinoJson @ ^7.0.4 https://github.com/bertmelis/espMqttClient.git#v1.6.0 nrf24/RF24 @ ^1.4.8 olikraus/U8g2 @ ^2.35.15 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 3b189187c..8e8030745 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -25,17 +25,13 @@ bool ConfigurationClass::write() } config.Cfg.SaveCount++; - DynamicJsonDocument doc(JSON_BUFFER_SIZE); + JsonDocument doc; - if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { - return false; - } - - JsonObject cfg = doc.createNestedObject("cfg"); + JsonObject cfg = doc["cfg"].to(); cfg["version"] = config.Cfg.Version; cfg["save_count"] = config.Cfg.SaveCount; - JsonObject wifi = doc.createNestedObject("wifi"); + JsonObject wifi = doc["wifi"].to(); wifi["ssid"] = config.WiFi.Ssid; wifi["password"] = config.WiFi.Password; wifi["ip"] = IPAddress(config.WiFi.Ip).toString(); @@ -47,10 +43,10 @@ bool ConfigurationClass::write() wifi["hostname"] = config.WiFi.Hostname; wifi["aptimeout"] = config.WiFi.ApTimeout; - JsonObject mdns = doc.createNestedObject("mdns"); + JsonObject mdns = doc["mdns"].to(); mdns["enabled"] = config.Mdns.Enabled; - JsonObject ntp = doc.createNestedObject("ntp"); + JsonObject ntp = doc["ntp"].to(); ntp["server"] = config.Ntp.Server; ntp["timezone"] = config.Ntp.Timezone; ntp["timezone_descr"] = config.Ntp.TimezoneDescr; @@ -58,7 +54,7 @@ bool ConfigurationClass::write() ntp["longitude"] = config.Ntp.Longitude; ntp["sunsettype"] = config.Ntp.SunsetType; - JsonObject mqtt = doc.createNestedObject("mqtt"); + JsonObject mqtt = doc["mqtt"].to(); mqtt["enabled"] = config.Mqtt.Enabled; mqtt["hostname"] = config.Mqtt.Hostname; mqtt["port"] = config.Mqtt.Port; @@ -69,27 +65,27 @@ bool ConfigurationClass::write() mqtt["publish_interval"] = config.Mqtt.PublishInterval; mqtt["clean_session"] = config.Mqtt.CleanSession; - JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); + JsonObject mqtt_lwt = mqtt["lwt"].to(); 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.createNestedObject("tls"); + JsonObject mqtt_tls = mqtt["tls"].to(); 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.createNestedObject("hass"); + JsonObject mqtt_hass = mqtt["hass"].to(); 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"); + JsonObject dtu = doc["dtu"].to(); dtu["serial"] = config.Dtu.Serial; dtu["poll_interval"] = config.Dtu.PollInterval; dtu["nrf_pa_level"] = config.Dtu.Nrf.PaLevel; @@ -97,14 +93,14 @@ bool ConfigurationClass::write() dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency; dtu["cmt_country_mode"] = config.Dtu.Cmt.CountryMode; - JsonObject security = doc.createNestedObject("security"); + JsonObject security = doc["security"].to(); security["password"] = config.Security.Password; security["allow_readonly"] = config.Security.AllowReadonly; - JsonObject device = doc.createNestedObject("device"); + JsonObject device = doc["device"].to(); device["pinmapping"] = config.Dev_PinMapping; - JsonObject display = device.createNestedObject("display"); + JsonObject display = device["display"].to(); display["powersafe"] = config.Display.PowerSafe; display["screensaver"] = config.Display.ScreenSaver; display["rotation"] = config.Display.Rotation; @@ -113,15 +109,15 @@ bool ConfigurationClass::write() display["diagram_duration"] = config.Display.Diagram.Duration; display["diagram_mode"] = config.Display.Diagram.Mode; - JsonArray leds = device.createNestedArray("led"); + JsonArray leds = device["led"].to(); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { - JsonObject led = leds.createNestedObject(); + JsonObject led = leds.add(); led["brightness"] = config.Led_Single[i].Brightness; } - JsonArray inverters = doc.createNestedArray("inverters"); + JsonArray inverters = doc["inverters"].to(); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - JsonObject inv = inverters.createNestedObject(); + JsonObject inv = inverters.add(); inv["serial"] = config.Inverter[i].Serial; inv["name"] = config.Inverter[i].Name; inv["order"] = config.Inverter[i].Order; @@ -134,15 +130,19 @@ bool ConfigurationClass::write() inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; inv["yieldday_correction"] = config.Inverter[i].YieldDayCorrection; - JsonArray channel = inv.createNestedArray("channel"); + JsonArray channel = inv["channel"].to(); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - JsonObject chanData = channel.createNestedObject(); + JsonObject chanData = channel.add(); 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"); @@ -157,11 +157,7 @@ bool ConfigurationClass::read() { File f = LittleFS.open(CONFIG_FILENAME, "r", false); - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - - if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { - return false; - } + JsonDocument doc; // Deserialize the JSON document const DeserializationError error = deserializeJson(doc, f); @@ -169,6 +165,10 @@ bool ConfigurationClass::read() 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; @@ -324,11 +324,7 @@ void ConfigurationClass::migrate() return; } - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - - if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { - return; - } + JsonDocument doc; // Deserialize the JSON document const DeserializationError error = deserializeJson(doc, f); @@ -337,6 +333,10 @@ void ConfigurationClass::migrate() 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++) { diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 21ff0fa2b..a2d998d10 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -137,10 +137,7 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr name = "CH" + chanNum + " " + fieldName; } - DynamicJsonDocument root(1024); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } + JsonDocument root; root["name"] = name; root["stat_t"] = stateTopic; @@ -163,6 +160,10 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr root["stat_cla"] = stateCls; } + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + String buffer; serializeJson(root, buffer); publish(configTopic, buffer); @@ -185,10 +186,7 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr inv) +void MqttHandleHassClass::createInverterInfo(JsonDocument& root, std::shared_ptr inv) { createDeviceInfo( root, @@ -378,7 +384,7 @@ void MqttHandleHassClass::createInverterInfo(DynamicJsonDocument& root, std::sha getDtuUniqueId()); } -void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root) +void MqttHandleHassClass::createDtuInfo(JsonDocument& root) { createDeviceInfo( root, @@ -391,12 +397,12 @@ void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root) } void MqttHandleHassClass::createDeviceInfo( - DynamicJsonDocument& root, + JsonDocument& root, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device) { - auto object = root.createNestedObject("dev"); + auto object = root["dev"].to(); object["name"] = name; object["ids"] = identifiers; diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 8d7062b01..74f282855 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -8,8 +8,6 @@ #include #include -#define JSON_BUFFER_SIZE 6144 - #ifndef DISPLAY_TYPE #define DISPLAY_TYPE 0U #endif @@ -141,7 +139,7 @@ bool PinMappingClass::init(const String& deviceMapping) return false; } - DynamicJsonDocument doc(JSON_BUFFER_SIZE); + JsonDocument doc; // Deserialize the JSON document DeserializationError error = deserializeJson(doc, f); if (error) { @@ -216,4 +214,4 @@ bool PinMappingClass::isValidCmt2300Config() const bool PinMappingClass::isValidEthConfig() const { return _pinMapping.eth_enabled; -} \ No newline at end of file +} diff --git a/src/Utils.cpp b/src/Utils.cpp index 7ad072938..6bedd2cbd 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -69,9 +69,9 @@ void Utils::restartDtu() ESP.restart(); } -bool Utils::checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line) +bool Utils::checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line) { - if (doc.capacity() == 0) { + if (doc.overflowed()) { MessageOutput.printf("Alloc failed: %s, %d\r\n", function, line); return false; } diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 10f3e28d9..04821d7e1 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -85,7 +85,7 @@ void WebApiClass::writeConfig(JsonVariant& retMsg, const WebApiError code, const } } -bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, DynamicJsonDocument& json_document, size_t max_document_size) +bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document) { auto& retMsg = response->getRoot(); retMsg["type"] = "warning"; @@ -99,14 +99,6 @@ bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResp } const String json = request->getParam("data", true)->value(); - if (json.length() > max_document_size) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return false; - } - const DeserializationError error = deserializeJson(json_document, json); if (error) { retMsg["message"] = "Failed to parse data!"; diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index f76a2e0ad..99a539edc 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -53,7 +53,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } @@ -95,7 +95,7 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - auto data = root.createNestedArray("configs"); + auto data = root["configs"].to(); File rootfs = LittleFS.open("/"); File file = rootfs.openNextFile(); @@ -103,7 +103,7 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) if (file.isDirectory()) { continue; } - JsonObject obj = data.createNestedObject(); + JsonObject obj = data.add(); obj["name"] = String(file.name()); file = rootfs.openNextFile(); diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 1cc142207..421f9456e 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -26,15 +26,15 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); const PinMapping_t& pin = PinMapping.get(); - auto curPin = root.createNestedObject("curPin"); + auto curPin = root["curPin"].to(); curPin["name"] = config.Dev_PinMapping; - auto nrfPinObj = curPin.createNestedObject("nrf24"); + auto nrfPinObj = curPin["nrf24"].to(); nrfPinObj["clk"] = pin.nrf24_clk; nrfPinObj["cs"] = pin.nrf24_cs; nrfPinObj["en"] = pin.nrf24_en; @@ -42,7 +42,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) nrfPinObj["miso"] = pin.nrf24_miso; nrfPinObj["mosi"] = pin.nrf24_mosi; - auto cmtPinObj = curPin.createNestedObject("cmt"); + auto cmtPinObj = curPin["cmt"].to(); cmtPinObj["clk"] = pin.cmt_clk; cmtPinObj["cs"] = pin.cmt_cs; cmtPinObj["fcs"] = pin.cmt_fcs; @@ -50,7 +50,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) cmtPinObj["gpio2"] = pin.cmt_gpio2; cmtPinObj["gpio3"] = pin.cmt_gpio3; - auto ethPinObj = curPin.createNestedObject("eth"); + auto ethPinObj = curPin["eth"].to(); ethPinObj["enabled"] = pin.eth_enabled; ethPinObj["phy_addr"] = pin.eth_phy_addr; ethPinObj["power"] = pin.eth_power; @@ -59,19 +59,19 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) ethPinObj["type"] = pin.eth_type; ethPinObj["clk_mode"] = pin.eth_clk_mode; - auto displayPinObj = curPin.createNestedObject("display"); + auto displayPinObj = curPin["display"].to(); displayPinObj["type"] = pin.display_type; displayPinObj["data"] = pin.display_data; displayPinObj["clk"] = pin.display_clk; displayPinObj["cs"] = pin.display_cs; displayPinObj["reset"] = pin.display_reset; - auto ledPinObj = curPin.createNestedObject("led"); + auto ledPinObj = curPin["led"].to(); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { ledPinObj["led" + String(i)] = pin.led[i]; } - auto display = root.createNestedObject("display"); + auto display = root["display"].to(); display["rotation"] = config.Display.Rotation; display["power_safe"] = config.Display.PowerSafe; display["screensaver"] = config.Display.ScreenSaver; @@ -80,9 +80,9 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) display["diagramduration"] = config.Display.Diagram.Duration; display["diagrammode"] = config.Display.Diagram.Mode; - auto leds = root.createNestedArray("led"); + auto leds = root["led"].to(); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { - auto led = leds.createNestedObject(); + auto led = leds.add(); led["brightness"] = config.Led_Single[i].Brightness; } @@ -96,9 +96,9 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - if (!WebApi.parseRequestData(request, response, root, MQTT_JSON_DOC_SIZE)) { + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index bbcd909f1..a2192c7ce 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -62,10 +62,10 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) root["cmt_country"] = config.Dtu.Cmt.CountryMode; root["cmt_chan_width"] = Hoymiles.getRadioCmt()->getChannelWidth(); - auto data = root.createNestedArray("country_def"); + auto data = root["country_def"].to(); auto countryDefs = Hoymiles.getRadioCmt()->getCountryFrequencyList(); for (const auto& definition : countryDefs) { - auto obj = data.createNestedObject(); + auto obj = data.add(); obj["freq_default"] = definition.definition.Freq_Default; obj["freq_min"] = definition.definition.Freq_Min; obj["freq_max"] = definition.definition.Freq_Max; @@ -84,7 +84,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } diff --git a/src/WebApi_eventlog.cpp b/src/WebApi_eventlog.cpp index 51e85affa..3637a3d9e 100644 --- a/src/WebApi_eventlog.cpp +++ b/src/WebApi_eventlog.cpp @@ -20,7 +20,7 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); uint64_t serial = 0; @@ -47,10 +47,10 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) uint8_t logEntryCount = inv->EventLog()->getEntryCount(); root["count"] = logEntryCount; - JsonArray eventsArray = root.createNestedArray("events"); + JsonArray eventsArray = root["events"].to(); for (uint8_t logEntry = 0; logEntry < logEntryCount; logEntry++) { - JsonObject eventsObject = eventsArray.createNestedObject(); + JsonObject eventsObject = eventsArray.add(); AlarmLogEntry_t entry; inv->EventLog()->getLogEntry(logEntry, entry, locale); diff --git a/src/WebApi_gridprofile.cpp b/src/WebApi_gridprofile.cpp index 60c340fa0..2527396d5 100644 --- a/src/WebApi_gridprofile.cpp +++ b/src/WebApi_gridprofile.cpp @@ -21,7 +21,7 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); uint64_t serial = 0; @@ -36,17 +36,17 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) root["name"] = inv->GridProfile()->getProfileName(); root["version"] = inv->GridProfile()->getProfileVersion(); - auto jsonSections = root.createNestedArray("sections"); + auto jsonSections = root["sections"].to(); auto profSections = inv->GridProfile()->getProfile(); for (auto &profSection : profSections) { - auto jsonSection = jsonSections.createNestedObject(); + auto jsonSection = jsonSections.add(); jsonSection["name"] = profSection.SectionName; - auto jsonItems = jsonSection.createNestedArray("items"); + auto jsonItems = jsonSection["items"].to(); for (auto &profItem : profSection.items) { - auto jsonItem = jsonItems.createNestedObject(); + auto jsonItem = jsonItems.add(); jsonItem["n"] = profItem.Name; jsonItem["u"] = profItem.Unit; @@ -65,7 +65,7 @@ void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); uint64_t serial = 0; @@ -77,7 +77,7 @@ void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { - auto raw = root.createNestedArray("raw"); + auto raw = root["raw"].to(); auto data = inv->GridProfile()->getRawData(); copyArray(&data[0], data.size(), raw); diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index eb48c8efa..0d82a3264 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -29,15 +29,15 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - JsonArray data = root.createNestedArray("inverter"); + JsonArray data = root["inverter"].to(); const CONFIG_T& config = Configuration.get(); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial > 0) { - JsonObject obj = data.createNestedObject(); + JsonObject obj = data.add(); obj["id"] = i; obj["name"] = String(config.Inverter[i].Name); obj["order"] = config.Inverter[i].Order; @@ -67,9 +67,9 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) max_channels = inv->Statistics()->getChannelsByType(TYPE_DC).size(); } - JsonArray channel = obj.createNestedArray("channel"); + JsonArray channel = obj["channel"].to(); for (uint8_t c = 0; c < max_channels; c++) { - JsonObject chanData = channel.createNestedObject(); + JsonObject chanData = channel.add(); 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; @@ -88,7 +88,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } @@ -163,7 +163,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } @@ -283,7 +283,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } @@ -328,7 +328,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 890589267..79d6039fc 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -58,7 +58,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index 538f087ab..a7eeb4240 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -22,9 +22,9 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - if (!WebApi.parseRequestData(request, response, root, MQTT_JSON_DOC_SIZE)) { + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 78fed2046..88c2a4ab2 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -26,7 +26,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -60,7 +60,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -98,9 +98,9 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - if (!WebApi.parseRequestData(request, response, root, MQTT_JSON_DOC_SIZE)) { + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index b7fbbe518..158c8bde9 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -83,7 +83,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index 343d94b5a..07553921d 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -95,7 +95,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } @@ -194,7 +194,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index 2f921d74d..f019ce334 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -51,7 +51,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index 05f829c55..78eaffe0b 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -42,7 +42,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - DynamicJsonDocument root(1024); + JsonDocument root; if (!WebApi.parseRequestData(request, response, root)) { return; } diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 354ed3728..f378e3ab1 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -73,19 +73,20 @@ void WebApiWsLiveClass::sendDataTaskCb() try { std::lock_guard lock(_mutex); - DynamicJsonDocument root(4096); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - continue; - } + JsonDocument root; JsonVariant var = root; - auto invArray = var.createNestedArray("inverters"); - auto invObject = invArray.createNestedObject(); + auto invArray = var["inverters"].to(); + auto invObject = invArray.add(); generateCommonJsonResponse(var); generateInverterCommonJsonResponse(invObject, inv); generateInverterChannelJsonResponse(invObject, inv); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + continue; + } + String buffer; serializeJson(root, buffer); @@ -101,12 +102,12 @@ void WebApiWsLiveClass::sendDataTaskCb() void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root) { - JsonObject totalObj = root.createNestedObject("total"); + auto totalObj = root["total"].to(); addTotalField(totalObj, "Power", Datastore.getTotalAcPowerEnabled(), "W", Datastore.getTotalAcPowerDigits()); addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits()); addTotalField(totalObj, "YieldTotal", Datastore.getTotalAcYieldTotalEnabled(), "kWh", Datastore.getTotalAcYieldTotalDigits()); - JsonObject hintObj = root.createNestedObject("hints"); + JsonObject hintObj = root["hints"].to(); struct tm timeinfo; hintObj["time_sync"] = !getLocalTime(&timeinfo, 5); hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected())); @@ -144,7 +145,7 @@ void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, st // Loop all channels for (auto& t : inv->Statistics()->getChannelTypes()) { - JsonObject chanTypeObj = root.createNestedObject(inv->Statistics()->getChannelTypeName(t)); + auto chanTypeObj = root[inv->Statistics()->getChannelTypeName(t)].to(); for (auto& c : inv->Statistics()->getChannelsByType(t)) { if (t == TYPE_DC) { chanTypeObj[String(static_cast(c))]["name"]["u"] = inv_cfg->channel[c].Name; @@ -221,10 +222,10 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) try { std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - JsonArray invArray = root.createNestedArray("inverters"); + auto invArray = root["inverters"].to(); uint64_t serial = 0; if (request->hasParam("inv")) { @@ -235,7 +236,7 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) if (serial > 0) { auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { - JsonObject invObject = invArray.createNestedObject(); + JsonObject invObject = invArray.add(); generateInverterCommonJsonResponse(invObject, inv); generateInverterChannelJsonResponse(invObject, inv); } @@ -247,13 +248,17 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) continue; } - JsonObject invObject = invArray.createNestedObject(); + JsonObject invObject = invArray.add(); generateInverterCommonJsonResponse(invObject, inv); } } generateCommonJsonResponse(root); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + response->setLength(); request->send(response); From 980e847ccb9b062816887f81da25c79c41457eca Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 4 Apr 2024 20:43:07 +0200 Subject: [PATCH 29/70] Feature: Check for out of memory situations when sending json responses Also shows a nice message in the frontend if an internal error occours --- include/WebApi.h | 1 + include/WebApi_errors.h | 1 + src/WebApi.cpp | 27 +++++++++-- src/WebApi_config.cpp | 12 ++--- src/WebApi_device.cpp | 12 ++--- src/WebApi_devinfo.cpp | 3 +- src/WebApi_dtu.cpp | 27 ++++------- src/WebApi_eventlog.cpp | 3 +- src/WebApi_gridprofile.cpp | 6 +-- src/WebApi_inverter.cpp | 51 +++++++------------- src/WebApi_limit.cpp | 21 +++------ src/WebApi_maintenance.cpp | 9 ++-- src/WebApi_mqtt.cpp | 60 ++++++++---------------- src/WebApi_network.cpp | 39 +++++---------- src/WebApi_ntp.cpp | 48 +++++++------------ src/WebApi_power.cpp | 15 ++---- src/WebApi_security.cpp | 15 ++---- src/WebApi_sysstatus.cpp | 3 +- src/WebApi_ws_live.cpp | 7 +-- webapp/src/locales/de.json | 3 ++ webapp/src/locales/en.json | 3 ++ webapp/src/locales/fr.json | 3 ++ webapp/src/router/index.ts | 8 +++- webapp/src/utils/authentication.ts | 3 +- webapp/src/views/ErrorView.vue | 18 +++++++ webapp/src/views/FirmwareUpgradeView.vue | 2 +- 26 files changed, 171 insertions(+), 229 deletions(-) create mode 100644 webapp/src/views/ErrorView.vue diff --git a/include/WebApi.h b/include/WebApi.h index b4c499833..14eddc313 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -39,6 +39,7 @@ class WebApiClass { static void writeConfig(JsonVariant& retMsg, const WebApiError code = WebApiError::GenericSuccess, const String& message = "Settings saved!"); static bool parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document); + static bool sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line); private: AsyncWebServer _server; diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index 675419d72..97d61b220 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -9,6 +9,7 @@ enum WebApiError { GenericParseError, GenericValueMissing, GenericWriteFailed, + GenericInternalServerError, DtuBase = 2000, DtuSerialZero, diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 04821d7e1..bd59bd3eb 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -4,6 +4,7 @@ */ #include "WebApi.h" #include "Configuration.h" +#include "MessageOutput.h" #include "defaults.h" #include @@ -93,8 +94,7 @@ bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResp if (!request->hasParam("data", true)) { retMsg["message"] = "No values found!"; retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return false; } @@ -103,12 +103,31 @@ bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResp if (error) { retMsg["message"] = "Failed to parse data!"; retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return false; } return true; } +bool WebApiClass::sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line) +{ + bool ret_val = true; + if (response->overflowed()) { + auto& root = response->getRoot(); + + root.clear(); + root["message"] = String("500 Internal Server Error: ") + function + ", " + line; + root["code"] = WebApiError::GenericInternalServerError; + root["type"] = "danger"; + response->setCode(500); + MessageOutput.printf("WebResponse failed: %s, %d\r\n", function, line); + ret_val = false; + } + + response->setLength(); + request->send(response); + return ret_val; +} + WebApiClass WebApi; diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 99a539edc..a67be42fa 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -63,16 +63,14 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) if (!(root.containsKey("delete"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["delete"].as() == false) { retMsg["message"] = "Not deleted anything!"; retMsg["code"] = WebApiError::ConfigNotDeleted; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -80,8 +78,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) retMsg["message"] = "Configuration resettet. Rebooting now..."; retMsg["code"] = WebApiError::ConfigSuccess; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); Utils::removeAllFiles(); Utils::restartDtu(); @@ -110,8 +107,7 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) } file.close(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 421f9456e..078d5b4a1 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -86,8 +86,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) led["brightness"] = config.Led_Single[i].Brightness; } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) @@ -108,8 +107,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) || root.containsKey("display"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -117,8 +115,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Pin mapping must between 1 and " STR(DEV_MAX_MAPPING_NAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::HardwarePinMappingLength; retMsg["param"]["max"] = DEV_MAX_MAPPING_NAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -149,8 +146,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); if (performRestart) { Utils::restartDtu(); diff --git a/src/WebApi_devinfo.cpp b/src/WebApi_devinfo.cpp index 212a7f7d5..68f3396b0 100644 --- a/src/WebApi_devinfo.cpp +++ b/src/WebApi_devinfo.cpp @@ -43,6 +43,5 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) root["fw_build_datetime"] = inv->DevInfo()->getFwBuildDateTimeStr(); } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index a2192c7ce..9b67ec39f 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -73,8 +73,7 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) obj["freq_legal_max"] = definition.definition.Freq_Legal_Max; } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) @@ -99,8 +98,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) && root.containsKey("cmt_country"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -110,40 +108,35 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) if (serial == 0) { retMsg["message"] = "Serial cannot be zero!"; retMsg["code"] = WebApiError::DtuSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["pollinterval"].as() == 0) { retMsg["message"] = "Poll interval must be greater zero!"; retMsg["code"] = WebApiError::DtuPollZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["nrf_palevel"].as() > 3) { retMsg["message"] = "Invalid power level setting!"; retMsg["code"] = WebApiError::DtuInvalidPowerLevel; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["cmt_palevel"].as() < -10 || root["cmt_palevel"].as() > 20) { retMsg["message"] = "Invalid power level setting!"; retMsg["code"] = WebApiError::DtuInvalidPowerLevel; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["cmt_country"].as() >= CountryModeId_t::CountryModeId_Max) { retMsg["message"] = "Invalid country setting!"; retMsg["code"] = WebApiError::DtuInvalidCmtCountry; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -156,8 +149,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::DtuInvalidCmtFrequency; retMsg["param"]["min"] = FrequencyDefinition.Freq_Min; retMsg["param"]["max"] = FrequencyDefinition.Freq_Max; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -172,8 +164,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); _applyDataTask.enable(); _applyDataTask.restart(); diff --git a/src/WebApi_eventlog.cpp b/src/WebApi_eventlog.cpp index 3637a3d9e..e2d34442f 100644 --- a/src/WebApi_eventlog.cpp +++ b/src/WebApi_eventlog.cpp @@ -62,6 +62,5 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) } } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_gridprofile.cpp b/src/WebApi_gridprofile.cpp index 2527396d5..5ed579d04 100644 --- a/src/WebApi_gridprofile.cpp +++ b/src/WebApi_gridprofile.cpp @@ -55,8 +55,7 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) } } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request) @@ -83,6 +82,5 @@ void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request copyArray(&data[0], data.size(), raw); } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 0d82a3264..2d9a56344 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -77,8 +77,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) } } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) @@ -99,8 +98,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) && root.containsKey("name"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -110,8 +108,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -119,8 +116,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::InverterNameLength; retMsg["param"]["max"] = INV_MAX_NAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -130,8 +126,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) retMsg["message"] = "Only " STR(INV_MAX_COUNT) " inverters are supported!"; retMsg["code"] = WebApiError::InverterCount; retMsg["param"]["max"] = INV_MAX_COUNT; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -142,8 +137,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg, WebApiError::InverterAdded, "Inverter created!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); auto inv = Hoymiles.addInverter(inverter->Name, inverter->Serial); @@ -173,16 +167,14 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["id"].as() > INV_MAX_COUNT - 1) { retMsg["message"] = "Invalid ID specified!"; retMsg["code"] = WebApiError::InverterInvalidId; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -192,8 +184,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -201,8 +192,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::InverterNameLength; retMsg["param"]["max"] = INV_MAX_NAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -210,8 +200,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) { retMsg["message"] = "Invalid amount of max channel setting given!"; retMsg["code"] = WebApiError::InverterInvalidMaxChannel; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -243,8 +232,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); std::shared_ptr inv = Hoymiles.getInverterBySerial(old_serial); @@ -293,16 +281,14 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) if (!(root.containsKey("id"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["id"].as() > INV_MAX_COUNT - 1) { retMsg["message"] = "Invalid ID specified!"; retMsg["code"] = WebApiError::InverterInvalidId; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -315,8 +301,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); MqttHandleHass.forceUpdate(); } @@ -338,8 +323,7 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) if (!(root.containsKey("order"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -357,6 +341,5 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 79d6039fc..9a622deae 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -47,8 +47,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) root[serial]["limit_set_status"] = limitStatus; } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) @@ -70,8 +69,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) && root.containsKey("limit_type"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -81,8 +79,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::LimitSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -90,8 +87,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!"; retMsg["code"] = WebApiError::LimitInvalidLimit; retMsg["param"]["max"] = MAX_INVERTER_LIMIT; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -102,8 +98,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) retMsg["message"] = "Invalid type specified!"; retMsg["code"] = WebApiError::LimitInvalidType; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -114,8 +109,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) if (inv == nullptr) { retMsg["message"] = "Invalid inverter specified!"; retMsg["code"] = WebApiError::LimitInvalidInverter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -125,6 +119,5 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) retMsg["message"] = "Settings saved!"; retMsg["code"] = WebApiError::GenericSuccess; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index a7eeb4240..1504f9d75 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -33,8 +33,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) if (!(root.containsKey("reboot"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -43,14 +42,12 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) retMsg["message"] = "Reboot triggered!"; retMsg["code"] = WebApiError::MaintenanceRebootTriggered; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); Utils::restartDtu(); } else { retMsg["message"] = "Reboot cancled!"; retMsg["code"] = WebApiError::MaintenanceRebootCancled; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } } diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 88c2a4ab2..1795b7aae 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -50,8 +50,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic; root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) @@ -88,8 +87,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic; root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) @@ -130,8 +128,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) && root.containsKey("mqtt_hass_individualpanels"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -140,8 +137,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::MqttHostnameLength; retMsg["param"]["max"] = MQTT_MAX_HOSTNAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -149,48 +145,42 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Username must not be longer than " STR(MQTT_MAX_USERNAME_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttUsernameLength; retMsg["param"]["max"] = MQTT_MAX_USERNAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_password"].as().length() > MQTT_MAX_PASSWORD_STRLEN) { retMsg["message"] = "Password must not be longer than " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttPasswordLength; retMsg["param"]["max"] = MQTT_MAX_PASSWORD_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_topic"].as().length() > MQTT_MAX_TOPIC_STRLEN) { retMsg["message"] = "Topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttTopicLength; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_topic"].as().indexOf(' ') != -1) { retMsg["message"] = "Topic must not contain space characters!"; retMsg["code"] = WebApiError::MqttTopicCharacter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (!root["mqtt_topic"].as().endsWith("/")) { retMsg["message"] = "Topic must end with a slash (/)!"; retMsg["code"] = WebApiError::MqttTopicTrailingSlash; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_port"].as() == 0 || root["mqtt_port"].as() > 65535) { retMsg["message"] = "Port must be a number between 1 and 65535!"; retMsg["code"] = WebApiError::MqttPort; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -200,8 +190,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Certificates must not be longer than " STR(MQTT_MAX_CERT_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttCertificateLength; retMsg["param"]["max"] = MQTT_MAX_CERT_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -209,16 +198,14 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttLwtTopicLength; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_lwt_topic"].as().indexOf(' ') != -1) { retMsg["message"] = "LWT topic must not contain space characters!"; retMsg["code"] = WebApiError::MqttLwtTopicCharacter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -226,8 +213,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT online value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttLwtOnlineLength; retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -235,8 +221,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT offline value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttLwtOfflineLength; retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -244,8 +229,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT QoS must not be greater than " STR(2) "!"; retMsg["code"] = WebApiError::MqttLwtQos; retMsg["param"]["max"] = 2; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -254,8 +238,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::MqttPublishInterval; retMsg["param"]["min"] = 5; retMsg["param"]["max"] = 65535; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -264,16 +247,14 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Hass topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttHassTopicLength; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_hass_topic"].as().indexOf(' ') != -1) { retMsg["message"] = "Hass topic must not contain space characters!"; retMsg["code"] = WebApiError::MqttHassTopicCharacter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } } @@ -306,8 +287,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); MqttSettings.performReconnect(); MqttHandleHass.forceUpdate(); diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 158c8bde9..7fec44b2a 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -46,8 +46,7 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request) root["ap_mac"] = WiFi.softAPmacAddress(); root["ap_stationnum"] = WiFi.softAPgetStationNum(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) @@ -72,8 +71,7 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) root["aptimeout"] = config.WiFi.ApTimeout; root["mdnsenabled"] = config.Mdns.Enabled; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) @@ -102,8 +100,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) && root.containsKey("aptimeout"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -111,68 +108,59 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) if (!ipaddress.fromString(root["ipaddress"].as())) { retMsg["message"] = "IP address is invalid!"; retMsg["code"] = WebApiError::NetworkIpInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress netmask; if (!netmask.fromString(root["netmask"].as())) { retMsg["message"] = "Netmask is invalid!"; retMsg["code"] = WebApiError::NetworkNetmaskInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress gateway; if (!gateway.fromString(root["gateway"].as())) { retMsg["message"] = "Gateway is invalid!"; retMsg["code"] = WebApiError::NetworkGatewayInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress dns1; if (!dns1.fromString(root["dns1"].as())) { retMsg["message"] = "DNS Server IP 1 is invalid!"; retMsg["code"] = WebApiError::NetworkDns1Invalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress dns2; if (!dns2.fromString(root["dns2"].as())) { retMsg["message"] = "DNS Server IP 2 is invalid!"; retMsg["code"] = WebApiError::NetworkDns2Invalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["hostname"].as().length() == 0 || root["hostname"].as().length() > WIFI_MAX_HOSTNAME_STRLEN) { retMsg["message"] = "Hostname must between 1 and " STR(WIFI_MAX_HOSTNAME_STRLEN) " characters long!"; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (NetworkSettings.NetworkMode() == network_mode::WiFi) { if (root["ssid"].as().length() == 0 || root["ssid"].as().length() > WIFI_MAX_SSID_STRLEN) { retMsg["message"] = "SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!"; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } } if (root["password"].as().length() > WIFI_MAX_PASSWORD_STRLEN - 1) { retMsg["message"] = "Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["aptimeout"].as() > 99999) { retMsg["message"] = "ApTimeout must be a number between 0 and 99999!"; retMsg["code"] = WebApiError::NetworkApTimeoutInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -210,8 +198,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); NetworkSettings.enableAdminMode(); NetworkSettings.applyConfig(); diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index 07553921d..d50e0f02f 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -63,8 +63,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) root["sun_isSunsetAvailable"] = SunPosition.isSunsetAvailable(); root["sun_isDayPeriod"] = SunPosition.isDayPeriod(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) @@ -84,8 +83,7 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) root["latitude"] = config.Ntp.Latitude; root["sunsettype"] = config.Ntp.SunsetType; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) @@ -109,8 +107,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) && root.containsKey("sunsettype"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -118,8 +115,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!"; retMsg["code"] = WebApiError::NtpServerLength; retMsg["param"]["max"] = NTP_MAX_SERVER_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -127,8 +123,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!"; retMsg["code"] = WebApiError::NtpTimezoneLength; retMsg["param"]["max"] = NTP_MAX_TIMEZONE_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -136,8 +131,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!"; retMsg["code"] = WebApiError::NtpTimezoneDescriptionLength; retMsg["param"]["max"] = NTP_MAX_TIMEZONEDESCR_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -151,8 +145,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); NtpSettings.setServer(); NtpSettings.setTimezone(); @@ -183,8 +176,7 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request) root["minute"] = timeinfo.tm_min; root["second"] = timeinfo.tm_sec; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) @@ -209,8 +201,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) && root.containsKey("second"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -219,8 +210,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpYearInvalid; retMsg["param"]["min"] = 2022; retMsg["param"]["max"] = 2100; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -229,8 +219,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpMonthInvalid; retMsg["param"]["min"] = 1; retMsg["param"]["max"] = 12; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -239,8 +228,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpDayInvalid; retMsg["param"]["min"] = 1; retMsg["param"]["max"] = 31; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -249,8 +237,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpHourInvalid; retMsg["param"]["min"] = 0; retMsg["param"]["max"] = 23; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -259,8 +246,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpMinuteInvalid; retMsg["param"]["min"] = 0; retMsg["param"]["max"] = 59; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -269,8 +255,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpSecondInvalid; retMsg["param"]["min"] = 0; retMsg["param"]["max"] = 59; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -291,6 +276,5 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["message"] = "Time updated!"; retMsg["code"] = WebApiError::NtpTimeUpdated; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index f019ce334..b2b2ce42e 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -40,8 +40,7 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request) root[inv->serialString()]["power_set_status"] = limitStatus; } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) @@ -63,8 +62,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) || root.containsKey("restart")))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -74,8 +72,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::PowerSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -83,8 +80,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) if (inv == nullptr) { retMsg["message"] = "Invalid inverter specified!"; retMsg["code"] = WebApiError::PowerInvalidInverter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -101,6 +97,5 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) retMsg["message"] = "Settings saved!"; retMsg["code"] = WebApiError::GenericSuccess; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index 78eaffe0b..eb0f27d20 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -31,8 +31,7 @@ void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request) root["password"] = config.Security.Password; root["allow_readonly"] = config.Security.AllowReadonly; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) @@ -53,8 +52,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) && root.containsKey("allow_readonly")) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -62,8 +60,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) retMsg["message"] = "Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"; retMsg["code"] = WebApiError::SecurityPasswordLength; retMsg["param"]["max"] = WIFI_MAX_PASSWORD_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -73,8 +70,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) @@ -89,6 +85,5 @@ void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) retMsg["message"] = "Authentication successful!"; retMsg["code"] = WebApiError::SecurityAuthSuccess; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index 11bd29c20..a2893c820 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -76,6 +76,5 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["cmt_configured"] = PinMapping.isValidCmt2300Config(); root["cmt_connected"] = Hoymiles.getRadioCmt()->isConnected(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index f378e3ab1..e79c664ab 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -255,12 +255,7 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) generateCommonJsonResponse(root); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } - - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } catch (const std::bad_alloc& bad_alloc) { MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index c9dfa975b..ad184d1ce 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -32,6 +32,9 @@ "Release": "Loslassen zum Aktualisieren", "Close": "Schließen" }, + "Error": { + "Oops": "Oops!" + }, "localeswitcher": { "Dark": "Dunkel", "Light": "Hell", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 4375137b9..4179227ae 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -32,6 +32,9 @@ "Release": "Release to refresh", "Close": "Close" }, + "Error": { + "Oops": "Oops!" + }, "localeswitcher": { "Dark": "Dark", "Light": "Light", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 24f0a951d..c60a552af 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -32,6 +32,9 @@ "Release": "Release to refresh", "Close": "Fermer" }, + "Error": { + "Oops": "Oops!" + }, "localeswitcher": { "Dark": "Sombre", "Light": "Clair", diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts index 6cfc00ef8..8fd3cfe83 100644 --- a/webapp/src/router/index.ts +++ b/webapp/src/router/index.ts @@ -3,6 +3,7 @@ import ConfigAdminView from '@/views/ConfigAdminView.vue'; import ConsoleInfoView from '@/views/ConsoleInfoView.vue'; import DeviceAdminView from '@/views/DeviceAdminView.vue' import DtuAdminView from '@/views/DtuAdminView.vue'; +import ErrorView from '@/views/ErrorView.vue'; import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue'; import HomeView from '@/views/HomeView.vue'; import InverterAdminView from '@/views/InverterAdminView.vue'; @@ -32,6 +33,11 @@ const router = createRouter({ name: 'Login', component: LoginView }, + { + path: '/error?status=:status&message=:message', + name: 'Error', + component: ErrorView + }, { path: '/about', name: 'About', @@ -115,4 +121,4 @@ const router = createRouter({ ] }); -export default router; \ No newline at end of file +export default router; diff --git a/webapp/src/utils/authentication.ts b/webapp/src/utils/authentication.ts index d1f87e3d8..52d92fd98 100644 --- a/webapp/src/utils/authentication.ts +++ b/webapp/src/utils/authentication.ts @@ -77,6 +77,7 @@ export function handleResponse(response: Response, emitter: Emitter + + + + + + diff --git a/webapp/src/views/FirmwareUpgradeView.vue b/webapp/src/views/FirmwareUpgradeView.vue index 738460791..f7bf0d75f 100644 --- a/webapp/src/views/FirmwareUpgradeView.vue +++ b/webapp/src/views/FirmwareUpgradeView.vue @@ -191,7 +191,7 @@ export default defineComponent({ const remoteHostUrl = "/api/system/status"; // Use a simple fetch request to check if the remote host is reachable - fetch(remoteHostUrl, { method: 'HEAD' }) + fetch(remoteHostUrl, { method: 'GET' }) .then(response => { // Check if the response status is OK (200-299 range) if (response.ok) { From ea289037617f4e2bc144a5bc97b94ece6f5334f6 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Thu, 4 Apr 2024 20:50:38 +0200 Subject: [PATCH 30/70] Move parsing of serial from web request to separate method --- include/WebApi.h | 1 + src/WebApi.cpp | 10 ++++++++++ src/WebApi_devinfo.cpp | 8 +------- src/WebApi_eventlog.cpp | 7 +------ src/WebApi_gridprofile.cpp | 16 ++-------------- src/WebApi_ws_live.cpp | 8 +------- 6 files changed, 16 insertions(+), 34 deletions(-) diff --git a/include/WebApi.h b/include/WebApi.h index 14eddc313..b6fdbd089 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -39,6 +39,7 @@ class WebApiClass { static void writeConfig(JsonVariant& retMsg, const WebApiError code = WebApiError::GenericSuccess, const String& message = "Settings saved!"); static bool parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document); + static uint64_t parseSerialFromRequest(AsyncWebServerRequest* request, String param_name = "inv"); static bool sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line); private: diff --git a/src/WebApi.cpp b/src/WebApi.cpp index bd59bd3eb..1a5b28709 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -110,6 +110,16 @@ bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResp return true; } +uint64_t WebApiClass::parseSerialFromRequest(AsyncWebServerRequest* request, String param_name) +{ + if (request->hasParam(param_name)) { + String s = request->getParam(param_name)->value(); + return strtoll(s.c_str(), NULL, 16); + } + + return 0; +} + bool WebApiClass::sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line) { bool ret_val = true; diff --git a/src/WebApi_devinfo.cpp b/src/WebApi_devinfo.cpp index 68f3396b0..449cd1772 100644 --- a/src/WebApi_devinfo.cpp +++ b/src/WebApi_devinfo.cpp @@ -23,13 +23,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } - + auto serial = WebApi.parseSerialFromRequest(request); auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { diff --git a/src/WebApi_eventlog.cpp b/src/WebApi_eventlog.cpp index e2d34442f..ec8b78c30 100644 --- a/src/WebApi_eventlog.cpp +++ b/src/WebApi_eventlog.cpp @@ -22,12 +22,7 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } + auto serial = WebApi.parseSerialFromRequest(request); AlarmMessageLocale_t locale = AlarmMessageLocale_t::EN; if (request->hasParam("locale")) { diff --git a/src/WebApi_gridprofile.cpp b/src/WebApi_gridprofile.cpp index 5ed579d04..9fc05b032 100644 --- a/src/WebApi_gridprofile.cpp +++ b/src/WebApi_gridprofile.cpp @@ -23,13 +23,7 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } - + auto serial = WebApi.parseSerialFromRequest(request); auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { @@ -66,13 +60,7 @@ void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } - + auto serial = WebApi.parseSerialFromRequest(request); auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index e79c664ab..eb1a83bf5 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -224,14 +224,8 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) std::lock_guard lock(_mutex); AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - auto invArray = root["inverters"].to(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } + auto serial = WebApi.parseSerialFromRequest(request); if (serial > 0) { auto inv = Hoymiles.getInverterBySerial(serial); From b9ad1e3054ce849c68ab3860a91ba9d5f36314da Mon Sep 17 00:00:00 2001 From: SW-Nico Date: Wed, 3 Apr 2024 21:54:52 +0200 Subject: [PATCH 31/70] VE.Direct: process more values and refactor variable names * process "IL", "AR" and "MON" * discard "BMV" and (unsolicited) History Data * simplify isDataValid() * veMpptStruct, veStruct: new, verbose variable names, including units, and replace floats (save values with original integer precision) * comment on rollover situation in isDataValid() --- lib/VeDirectFrameHandler/VeDirectData.cpp | 10 ++-- lib/VeDirectFrameHandler/VeDirectData.h | 50 ++++++++++--------- .../VeDirectFrameHandler.cpp | 21 ++++---- .../VeDirectFrameHandler.h | 2 +- .../VeDirectFrameHexHandler.cpp | 6 +++ .../VeDirectMpptController.cpp | 44 ++++++++-------- .../VeDirectShuntController.cpp | 14 +++++- src/BatteryStats.cpp | 14 +++--- src/MqttHandlVedirectHass.cpp | 6 +-- src/MqttHandleVedirect.cpp | 48 +++++++++--------- src/VictronMppt.cpp | 12 ++--- src/WebApi_ws_vedirect_live.cpp | 32 ++++++------ 12 files changed, 144 insertions(+), 115 deletions(-) diff --git a/lib/VeDirectFrameHandler/VeDirectData.cpp b/lib/VeDirectFrameHandler/VeDirectData.cpp index 012ec29ea..d28f9f783 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.cpp +++ b/lib/VeDirectFrameHandler/VeDirectData.cpp @@ -134,7 +134,7 @@ frozen::string const& veStruct::getPidAsString() const { 0xA3F0, "Smart BuckBoost 12V/12V-50A" }, }; - return getAsString(values, PID); + return getAsString(values, productID_PID); } /* @@ -154,7 +154,7 @@ frozen::string const& veMpptStruct::getCsAsString() const { 252, "External Control" } }; - return getAsString(values, CS); + return getAsString(values, currentState_CS); } /* @@ -168,7 +168,7 @@ frozen::string const& veMpptStruct::getMpptAsString() const { 2, "MPP Tracker active" } }; - return getAsString(values, MPPT); + return getAsString(values, stateOfTracker_MPPT); } /* @@ -199,7 +199,7 @@ frozen::string const& veMpptStruct::getErrAsString() const { 118, "User settings invalid" } }; - return getAsString(values, ERR); + return getAsString(values, errorCode_ERR); } /* @@ -220,7 +220,7 @@ frozen::string const& veMpptStruct::getOrAsString() const { 0x00000100, "Analysing input voltage" } }; - return getAsString(values, OR); + return getAsString(values, offReason_OR); } frozen::string const& VeDirectHexData::getResponseAsString() const diff --git a/lib/VeDirectFrameHandler/VeDirectData.h b/lib/VeDirectFrameHandler/VeDirectData.h index b1158d776..86be497ff 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.h +++ b/lib/VeDirectFrameHandler/VeDirectData.h @@ -7,32 +7,33 @@ #define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer typedef struct { - uint16_t PID = 0; // product id - char SER[VE_MAX_VALUE_LEN]; // serial number - char FW[VE_MAX_VALUE_LEN]; // firmware release number - float V = 0; // battery voltage in V - float I = 0; // battery current in A - float E = 0; // efficiency in percent (calculated, moving average) + uint16_t productID_PID = 0; // product id + char serialNr_SER[VE_MAX_VALUE_LEN]; // serial number + char firmwareNr_FW[VE_MAX_VALUE_LEN]; // firmware release number + uint32_t batteryVoltage_V_mV = 0; // battery voltage in mV + int32_t batteryCurrent_I_mA = 0; // battery current in mA (can be negative) + float mpptEfficiency_Percent = 0; // efficiency in percent (calculated, moving average) frozen::string const& getPidAsString() const; // product ID as string } veStruct; struct veMpptStruct : veStruct { - uint8_t MPPT; // state of MPP tracker - int32_t PPV; // panel power in W - int32_t P; // battery output power in W (calculated) - float VPV; // panel voltage in V - float IPV; // panel current in A (calculated) - bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit) - uint8_t CS; // current state of operation e.g. OFF or Bulk - uint8_t ERR; // error code - uint32_t OR; // off reason - uint32_t HSDS; // day sequence number 1...365 - float H19; // yield total kWh - float H20; // yield today kWh - int32_t H21; // maximum power today W - float H22; // yield yesterday kWh - int32_t H23; // maximum power yesterday W + uint8_t stateOfTracker_MPPT; // state of MPP tracker + uint16_t panelPower_PPV_W; // panel power in W + uint32_t panelVoltage_VPV_mV; // panel voltage in mV + uint32_t panelCurrent_mA; // panel current in mA (calculated) + int16_t batteryOutputPower_W; // battery output power in W (calculated, can be negative if load output is used) + uint32_t loadCurrent_IL_mA; // Load current in mA (Available only for models with a load output) + bool loadOutputState_LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit) + uint8_t currentState_CS; // current state of operation e.g. OFF or Bulk + uint8_t errorCode_ERR; // error code + uint32_t offReason_OR; // off reason + uint16_t daySequenceNr_HSDS; // day sequence number 1...365 + uint32_t yieldTotal_H19_Wh; // yield total resetable Wh + uint32_t yieldToday_H20_Wh; // yield today Wh + uint16_t maxPowerToday_H21_W; // maximum power today W + uint32_t yieldYesterday_H22_Wh; // yield yesterday Wh + uint16_t maxPowerYesterday_H23_W; // maximum power yesterday W // these are values communicated through the HEX protocol. the pair's first // value is the timestamp the respective info was last received. if it is @@ -59,7 +60,7 @@ struct veShuntStruct : veStruct { int32_t SOC; // State-of-charge uint32_t TTG; // Time-to-go bool ALARM; // Alarm condition active - uint32_t AR; // Alarm Reason + uint16_t alarmReason_AR; // Alarm Reason int32_t H1; // Depth of the deepest discharge int32_t H2; // Depth of the last discharge int32_t H3; // Depth of the average discharge @@ -78,6 +79,7 @@ struct veShuntStruct : veStruct { int32_t H16; // Maximum auxiliary (battery) voltage int32_t H17; // Amount of discharged energy int32_t H18; // Amount of charged energy + int8_t dcMonitorMode_MON; // DC monitor mode }; enum class VeDirectHexCommand : uint8_t { @@ -120,7 +122,9 @@ enum class VeDirectHexRegister : uint16_t { SmartBatterySenseTemperature = 0xEDEC, NetworkInfo = 0x200D, NetworkMode = 0x200E, - NetworkStatus = 0x200F + NetworkStatus = 0x200F, + HistoryTotal = 0x104F, + HistoryMPPTD30 = 0x10BE }; struct VeDirectHexData { diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 3e07169a8..074285dbd 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -104,10 +104,10 @@ void VeDirectFrameHandler::loop() _lastByteMillis = millis(); } - // there will never be a large gap between two bytes of the same frame. + // there will never be a large gap between two bytes. // if such a large gap is observed, reset the state machine so it tries - // to decode a new frame once more data arrives. - if (State::IDLE != _state && (millis() - _lastByteMillis) > 500) { + // to decode a new frame / hex messages once more data arrives. + if ((State::IDLE != _state) && ((millis() - _lastByteMillis) > 500)) { _msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", _logId, static_cast(_state)); if (_verboseLogging) { dumpDebugBuffer(); } @@ -236,27 +236,27 @@ void VeDirectFrameHandler::processTextData(std::string const& name, std::stri if (processTextDataDerived(name, value)) { return; } if (name == "PID") { - _tmpFrame.PID = strtol(value.c_str(), nullptr, 0); + _tmpFrame.productID_PID = strtol(value.c_str(), nullptr, 0); return; } if (name == "SER") { - strcpy(_tmpFrame.SER, value.c_str()); + strcpy(_tmpFrame.serialNr_SER, value.c_str()); return; } if (name == "FW") { - strcpy(_tmpFrame.FW, value.c_str()); + strcpy(_tmpFrame.firmwareNr_FW, value.c_str()); return; } if (name == "V") { - _tmpFrame.V = round(atof(value.c_str()) / 10.0) / 100.0; + _tmpFrame.batteryVoltage_V_mV = atol(value.c_str()); return; } if (name == "I") { - _tmpFrame.I = round(atof(value.c_str()) / 10.0) / 100.0; + _tmpFrame.batteryCurrent_I_mA = atol(value.c_str()); return; } @@ -307,7 +307,10 @@ typename VeDirectFrameHandler::State VeDirectFrameHandler::hexRxEvent(uint template bool VeDirectFrameHandler::isDataValid() const { - return strlen(_tmpFrame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000); + // VE.Direct text frame data is valid if we receive a device serialnumber and + // the data is not older as 10 seconds + // we accept a glitch where the data is valid for ten seconds when serialNr_SER != "" and (millis() - _lastUpdate) overflows + return strlen(_tmpFrame.serialNr_SER) > 0 && (millis() - _lastUpdate) < (10 * 1000); } template diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index c2d660884..1c482920c 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -74,7 +74,7 @@ class VeDirectFrameHandler { char _value[VE_MAX_VALUE_LEN]; // buffer for the field value std::array _debugBuffer; unsigned _debugIn; - uint32_t _lastByteMillis; + uint32_t _lastByteMillis; // time of last parsed byte /** * not every frame contains every value the device is communicating, i.e., diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp index f7f44b5b0..392d2f8a9 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp @@ -93,6 +93,12 @@ bool VeDirectFrameHandler::disassembleHexData(VeDirectHexData &data) { case Response::ASYNC: data.addr = static_cast(AsciiHexLE2Int(buffer+2, 4)); + // future option: Up to now we do not use historical data + if ((data.addr >= VeDirectHexRegister::HistoryTotal) && (data.addr <= VeDirectHexRegister::HistoryMPPTD30)) { + state = true; + break; + } + // future option: to analyse the flags here? data.flags = AsciiHexLE2Int(buffer+6, 2); diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index ef3ef187b..de3a7a59b 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -19,56 +19,60 @@ void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verb bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value) { + if (name == "IL") { + _tmpFrame.loadCurrent_IL_mA = atol(value.c_str()); + return true; + } if (name == "LOAD") { - _tmpFrame.LOAD = (value == "ON"); + _tmpFrame.loadOutputState_LOAD = (value == "ON"); return true; } if (name == "CS") { - _tmpFrame.CS = atoi(value.c_str()); + _tmpFrame.currentState_CS = atoi(value.c_str()); return true; } if (name == "ERR") { - _tmpFrame.ERR = atoi(value.c_str()); + _tmpFrame.errorCode_ERR = atoi(value.c_str()); return true; } if (name == "OR") { - _tmpFrame.OR = strtol(value.c_str(), nullptr, 0); + _tmpFrame.offReason_OR = strtol(value.c_str(), nullptr, 0); return true; } if (name == "MPPT") { - _tmpFrame.MPPT = atoi(value.c_str()); + _tmpFrame.stateOfTracker_MPPT = atoi(value.c_str()); return true; } if (name == "HSDS") { - _tmpFrame.HSDS = atoi(value.c_str()); + _tmpFrame.daySequenceNr_HSDS = atoi(value.c_str()); return true; } if (name == "VPV") { - _tmpFrame.VPV = round(atof(value.c_str()) / 10.0) / 100.0; + _tmpFrame.panelVoltage_VPV_mV = atol(value.c_str()); return true; } if (name == "PPV") { - _tmpFrame.PPV = atoi(value.c_str()); + _tmpFrame.panelPower_PPV_W = atoi(value.c_str()); return true; } if (name == "H19") { - _tmpFrame.H19 = atof(value.c_str()) / 100.0; + _tmpFrame.yieldTotal_H19_Wh = atol(value.c_str()) * 10; return true; } if (name == "H20") { - _tmpFrame.H20 = atof(value.c_str()) / 100.0; + _tmpFrame.yieldToday_H20_Wh = atol(value.c_str()) * 10; return true; } if (name == "H21") { - _tmpFrame.H21 = atoi(value.c_str()); + _tmpFrame.maxPowerToday_H21_W = atoi(value.c_str()); return true; } if (name == "H22") { - _tmpFrame.H22 = atof(value.c_str()) / 100.0; + _tmpFrame.yieldYesterday_H22_Wh = atol(value.c_str()) * 10; return true; } if (name == "H23") { - _tmpFrame.H23 = atoi(value.c_str()); + _tmpFrame.maxPowerYesterday_H23_W = atoi(value.c_str()); return true; } @@ -80,15 +84,15 @@ bool VeDirectMpptController::processTextDataDerived(std::string const& name, std * This function is called at the end of the received frame. */ void VeDirectMpptController::frameValidEvent() { - _tmpFrame.P = _tmpFrame.V * _tmpFrame.I; + _tmpFrame.batteryOutputPower_W = static_cast(_tmpFrame.batteryVoltage_V_mV * _tmpFrame.batteryCurrent_I_mA / 1000000); - if (_tmpFrame.VPV > 0) { - _tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV; + if ((_tmpFrame.panelVoltage_VPV_mV > 0) && (_tmpFrame.panelPower_PPV_W >= 1)) { + _tmpFrame.panelCurrent_mA = static_cast(_tmpFrame.panelPower_PPV_W * 1000000) / _tmpFrame.panelVoltage_VPV_mV; } - if (_tmpFrame.PPV > 0) { - _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); - _tmpFrame.E = _efficiency.getAverage(); + if (_tmpFrame.panelPower_PPV_W > 0) { + _efficiency.addNumber(static_cast(_tmpFrame.batteryOutputPower_W * 100) / _tmpFrame.panelPower_PPV_W); + _tmpFrame.mpptEfficiency_Percent = _efficiency.getAverage(); } if (!_canSend) { return; } @@ -98,7 +102,7 @@ void VeDirectMpptController::frameValidEvent() { // charger periodically sends human readable (TEXT) data to the serial port. For firmware // versions v1.53 and above, the charger always periodically sends TEXT data to the serial port. // --> We just use hex commandes for firmware >= 1.53 to keep text messages alive - if (atoi(_tmpFrame.FW) < 153) { return; } + if (atoi(_tmpFrame.firmwareNr_FW) < 153) { return; } using Command = VeDirectHexCommand; using Register = VeDirectHexRegister; diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp index 522b152b1..54229b93d 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -35,6 +35,10 @@ bool VeDirectShuntController::processTextDataDerived(std::string const& name, st _tmpFrame.ALARM = (value == "ON"); return true; } + if (name == "AR") { + _tmpFrame.alarmReason_AR = atoi(value.c_str()); + return true; + } if (name == "H1") { _tmpFrame.H1 = atoi(value.c_str()); return true; @@ -107,6 +111,14 @@ bool VeDirectShuntController::processTextDataDerived(std::string const& name, st _tmpFrame.H18 = atoi(value.c_str()); return true; } - + if (name == "BMV") { + // This field contains a textual description of the BMV model, + // for example 602S or 702. It is deprecated, refer to the field PID instead. + return true; + } + if (name == "MON") { + _tmpFrame.dcMonitorMode_MON = static_cast(atoi(value.c_str())); + return true; + } return false; } diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 12a6b6010..720695f34 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -374,10 +374,10 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) } void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) { - BatteryStats::setVoltage(shuntData.V, millis()); + BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis()); BatteryStats::setSoC(static_cast(shuntData.SOC) / 10, 1/*precision*/, millis()); - _current = shuntData.I; + _current = static_cast(shuntData.batteryCurrent_I_mA) / 1000; _modelName = shuntData.getPidAsString().data(); _chargeCycles = shuntData.H4; _timeToGo = shuntData.TTG / 60; @@ -390,11 +390,11 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& s _consumedAmpHours = static_cast(shuntData.CE) / 1000; _lastFullCharge = shuntData.H9 / 60; // shuntData.AR is a bitfield, so we need to check each bit individually - _alarmLowVoltage = shuntData.AR & 1; - _alarmHighVoltage = shuntData.AR & 2; - _alarmLowSOC = shuntData.AR & 4; - _alarmLowTemperature = shuntData.AR & 32; - _alarmHighTemperature = shuntData.AR & 64; + _alarmLowVoltage = shuntData.alarmReason_AR & 1; + _alarmHighVoltage = shuntData.alarmReason_AR & 2; + _alarmLowSOC = shuntData.alarmReason_AR & 4; + _alarmLowTemperature = shuntData.alarmReason_AR & 32; + _alarmHighTemperature = shuntData.alarmReason_AR & 64; _lastUpdate = VeDirectShunt.getLastUpdate(); } diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index 4619c1e42..a8e4c44b1 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -95,7 +95,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char const char *unitOfMeasurement, const VeDirectMpptController::data_t &mpptData) { - String serial = mpptData.SER; + String serial = mpptData.serialNr_SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -153,7 +153,7 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const const char *payload_on, const char *payload_off, const VeDirectMpptController::data_t &mpptData) { - String serial = mpptData.SER; + String serial = mpptData.serialNr_SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -198,7 +198,7 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object, const VeDirectMpptController::data_t &mpptData) { - String serial = mpptData.SER; + String serial = mpptData.serialNr_SER; object["name"] = "Victron(" + serial + ")"; object["ids"] = serial; object["cu"] = String("http://") + NetworkSettings.localIP().toString(); diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index ce96c8275..9bfd09063 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -62,10 +62,10 @@ void MqttHandleVedirectClass::loop() std::optional optMpptData = VictronMppt.getData(idx); if (!optMpptData.has_value()) { continue; } - auto const& kvFrame = _kvFrames[optMpptData->SER]; + auto const& kvFrame = _kvFrames[optMpptData->serialNr_SER]; publish_mppt_data(*optMpptData, kvFrame); if (!_PublishFull) { - _kvFrames[optMpptData->SER] = *optMpptData; + _kvFrames[optMpptData->serialNr_SER] = *optMpptData; } } @@ -100,7 +100,7 @@ void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::da const VeDirectMpptController::data_t &previousData) const { String value; String topic = "victron/"; - topic.concat(currentData.SER); + topic.concat(currentData.serialNr_SER); topic.concat("/"); #define PUBLISH(sm, t, val) \ @@ -108,26 +108,26 @@ void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::da MqttSettings.publish(topic + t, String(val)); \ } - PUBLISH(PID, "PID", currentData.getPidAsString().data()); - PUBLISH(SER, "SER", currentData.SER); - PUBLISH(FW, "FW", currentData.FW); - PUBLISH(LOAD, "LOAD", (currentData.LOAD ? "ON" : "OFF")); - PUBLISH(CS, "CS", currentData.getCsAsString().data()); - PUBLISH(ERR, "ERR", currentData.getErrAsString().data()); - PUBLISH(OR, "OR", currentData.getOrAsString().data()); - PUBLISH(MPPT, "MPPT", currentData.getMpptAsString().data()); - PUBLISH(HSDS, "HSDS", currentData.HSDS); - PUBLISH(V, "V", currentData.V); - PUBLISH(I, "I", currentData.I); - PUBLISH(P, "P", currentData.P); - PUBLISH(VPV, "VPV", currentData.VPV); - PUBLISH(IPV, "IPV", currentData.IPV); - PUBLISH(PPV, "PPV", currentData.PPV); - PUBLISH(E, "E", currentData.E); - PUBLISH(H19, "H19", currentData.H19); - PUBLISH(H20, "H20", currentData.H20); - PUBLISH(H21, "H21", currentData.H21); - PUBLISH(H22, "H22", currentData.H22); - PUBLISH(H23, "H23", currentData.H23); + PUBLISH(productID_PID, "PID", currentData.getPidAsString().data()); + PUBLISH(serialNr_SER, "SER", currentData.serialNr_SER); + PUBLISH(firmwareNr_FW, "FW", currentData.firmwareNr_FW); + PUBLISH(loadOutputState_LOAD, "LOAD", (currentData.loadOutputState_LOAD ? "ON" : "OFF")); + PUBLISH(currentState_CS, "CS", currentData.getCsAsString().data()); + PUBLISH(errorCode_ERR, "ERR", currentData.getErrAsString().data()); + PUBLISH(offReason_OR, "OR", currentData.getOrAsString().data()); + PUBLISH(stateOfTracker_MPPT, "MPPT", currentData.getMpptAsString().data()); + PUBLISH(daySequenceNr_HSDS, "HSDS", currentData.daySequenceNr_HSDS); + PUBLISH(batteryVoltage_V_mV, "V", currentData.batteryVoltage_V_mV / 1000.0); + PUBLISH(batteryCurrent_I_mA, "I", currentData.batteryCurrent_I_mA / 1000.0); + PUBLISH(batteryOutputPower_W, "P", currentData.batteryOutputPower_W); + PUBLISH(panelVoltage_VPV_mV, "VPV", currentData.panelVoltage_VPV_mV / 1000.0); + PUBLISH(panelCurrent_mA, "IPV", currentData.panelCurrent_mA / 1000.0); + PUBLISH(panelPower_PPV_W, "PPV", currentData.panelPower_PPV_W); + PUBLISH(mpptEfficiency_Percent, "E", currentData.mpptEfficiency_Percent); + PUBLISH(yieldTotal_H19_Wh, "H19", currentData.yieldTotal_H19_Wh / 1000.0); + PUBLISH(yieldToday_H20_Wh, "H20", currentData.yieldToday_H20_Wh / 1000.0); + PUBLISH(maxPowerToday_H21_W, "H21", currentData.maxPowerToday_H21_W); + PUBLISH(yieldYesterday_H22_Wh, "H22", currentData.yieldYesterday_H22_Wh / 1000.0); + PUBLISH(maxPowerYesterday_H23_W, "H23", currentData.maxPowerYesterday_H23_W); #undef PUBLILSH } diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index f210c93ec..770be014b 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -148,10 +148,10 @@ int32_t VictronMpptClass::getPowerOutputWatts() const // the calculated efficiency of the connected charge controller. auto networkPower = upController->getData().NetworkTotalDcInputPowerMilliWatts; if (networkPower.first > 0) { - return static_cast(networkPower.second / 1000.0 * upController->getData().E / 100); + return static_cast(networkPower.second / 1000.0 * upController->getData().mpptEfficiency_Percent / 100); } - sum += upController->getData().P; + sum += upController->getData().batteryOutputPower_W; } return sum; @@ -172,7 +172,7 @@ int32_t VictronMpptClass::getPanelPowerWatts() const return static_cast(networkPower.second / 1000.0); } - sum += upController->getData().PPV; + sum += upController->getData().panelPower_PPV_W; } return sum; @@ -184,7 +184,7 @@ float VictronMpptClass::getYieldTotal() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - sum += upController->getData().H19; + sum += upController->getData().yieldTotal_H19_Wh / 1000.0; } return sum; @@ -196,7 +196,7 @@ float VictronMpptClass::getYieldDay() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - sum += upController->getData().H20; + sum += upController->getData().yieldToday_H20_Wh / 1000.0; } return sum; @@ -208,7 +208,7 @@ float VictronMpptClass::getOutputVoltage() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - float volts = upController->getData().V; + float volts = upController->getData().batteryVoltage_V_mV / 1000.0; if (min == -1) { min = volts; } min = std::min(min, volts); } diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 9fffeed65..f583ebd05 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -128,7 +128,7 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool ful if (!fullUpdate && !hasUpdate(idx)) { continue; } - String serial(optMpptData->SER); + String serial(optMpptData->serialNr_SER); if (serial.isEmpty()) { continue; } // serial required as index const JsonObject &nested = array.createNestedObject(serial); @@ -147,17 +147,17 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool ful void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) { root["product_id"] = mpptData.getPidAsString(); - root["firmware_version"] = String(mpptData.FW); + root["firmware_version"] = String(mpptData.firmwareNr_FW); const JsonObject &values = root.createNestedObject("values"); const JsonObject &device = values.createNestedObject("device"); - device["LOAD"] = mpptData.LOAD ? "ON" : "OFF"; + device["LOAD"] = mpptData.loadOutputState_LOAD ? "ON" : "OFF"; device["CS"] = mpptData.getCsAsString(); device["MPPT"] = mpptData.getMpptAsString(); device["OR"] = mpptData.getOrAsString(); device["ERR"] = mpptData.getErrAsString(); - device["HSDS"]["v"] = mpptData.HSDS; + device["HSDS"]["v"] = mpptData.daySequenceNr_HSDS; device["HSDS"]["u"] = "d"; if (mpptData.MpptTemperatureMilliCelsius.first > 0) { device["MpptTemperature"]["v"] = mpptData.MpptTemperatureMilliCelsius.second / 1000.0; @@ -166,16 +166,16 @@ void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDir } const JsonObject &output = values.createNestedObject("output"); - output["P"]["v"] = mpptData.P; + output["P"]["v"] = mpptData.batteryOutputPower_W; output["P"]["u"] = "W"; output["P"]["d"] = 0; - output["V"]["v"] = mpptData.V; + output["V"]["v"] = mpptData.batteryVoltage_V_mV / 1000.0; output["V"]["u"] = "V"; output["V"]["d"] = 2; - output["I"]["v"] = mpptData.I; + output["I"]["v"] = mpptData.batteryCurrent_I_mA / 1000.0; output["I"]["u"] = "A"; output["I"]["d"] = 2; - output["E"]["v"] = mpptData.E; + output["E"]["v"] = mpptData.mpptEfficiency_Percent; output["E"]["u"] = "%"; output["E"]["d"] = 1; @@ -185,28 +185,28 @@ void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDir input["NetworkPower"]["u"] = "W"; input["NetworkPower"]["d"] = "0"; } - input["PPV"]["v"] = mpptData.PPV; + input["PPV"]["v"] = mpptData.panelPower_PPV_W; input["PPV"]["u"] = "W"; input["PPV"]["d"] = 0; - input["VPV"]["v"] = mpptData.VPV; + input["VPV"]["v"] = mpptData.panelVoltage_VPV_mV / 1000.0; input["VPV"]["u"] = "V"; input["VPV"]["d"] = 2; - input["IPV"]["v"] = mpptData.IPV; + input["IPV"]["v"] = mpptData.panelCurrent_mA / 1000.0; input["IPV"]["u"] = "A"; input["IPV"]["d"] = 2; - input["YieldToday"]["v"] = mpptData.H20; + input["YieldToday"]["v"] = mpptData.yieldToday_H20_Wh / 1000.0; input["YieldToday"]["u"] = "kWh"; input["YieldToday"]["d"] = 3; - input["YieldYesterday"]["v"] = mpptData.H22; + input["YieldYesterday"]["v"] = mpptData.yieldYesterday_H22_Wh / 1000.0; input["YieldYesterday"]["u"] = "kWh"; input["YieldYesterday"]["d"] = 3; - input["YieldTotal"]["v"] = mpptData.H19; + input["YieldTotal"]["v"] = mpptData.yieldTotal_H19_Wh / 1000.0; input["YieldTotal"]["u"] = "kWh"; input["YieldTotal"]["d"] = 3; - input["MaximumPowerToday"]["v"] = mpptData.H21; + input["MaximumPowerToday"]["v"] = mpptData.maxPowerToday_H21_W; input["MaximumPowerToday"]["u"] = "W"; input["MaximumPowerToday"]["d"] = 0; - input["MaximumPowerYesterday"]["v"] = mpptData.H23; + input["MaximumPowerYesterday"]["v"] = mpptData.maxPowerYesterday_H23_W; input["MaximumPowerYesterday"]["u"] = "W"; input["MaximumPowerYesterday"]["d"] = 0; } From 0ed09aeb4c7e6660032c307c736d4e882436f59c Mon Sep 17 00:00:00 2001 From: eu-gh <141823622+eu-gh@users.noreply.github.com> Date: Mon, 18 Sep 2023 21:33:28 +0200 Subject: [PATCH 32/70] Feature: Huawei: add SoC stop threshold and verbose logging switch --- include/Configuration.h | 3 +++ include/defaults.h | 1 + src/Configuration.cpp | 6 ++++++ src/Huawei_can.cpp | 25 +++++++++++++++++++++--- src/WebApi_Huawei.cpp | 6 ++++++ webapp/src/locales/de.json | 5 +++++ webapp/src/locales/en.json | 5 +++++ webapp/src/locales/fr.json | 5 +++++ webapp/src/types/AcChargerConfig.ts | 3 +++ webapp/src/views/AcChargerAdminView.vue | 26 +++++++++++++++++++++++++ 10 files changed, 82 insertions(+), 3 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index ddf29ce0f..a925d74de 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -238,13 +238,16 @@ struct CONFIG_T { struct { bool Enabled; + bool VerboseLogging; uint32_t CAN_Controller_Frequency; bool Auto_Power_Enabled; + bool Auto_Power_BatterySoC_Limits_Enabled; bool Emergency_Charge_Enabled; float Auto_Power_Voltage_Limit; float Auto_Power_Enable_Voltage_Limit; float Auto_Power_Lower_Power_Limit; float Auto_Power_Upper_Power_Limit; + uint8_t Auto_Power_Stop_BatterySoC_Threshold; } Huawei; diff --git a/include/defaults.h b/include/defaults.h index 1a4f686c7..7ea99fbde 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -155,5 +155,6 @@ #define HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT 42.0 #define HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT 150 #define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000 +#define HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD 95 #define VERBOSE_LOGGING true diff --git a/src/Configuration.cpp b/src/Configuration.cpp index b216fb2d7..e99f69140 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -214,13 +214,16 @@ bool ConfigurationClass::write() JsonObject huawei = doc.createNestedObject("huawei"); huawei["enabled"] = config.Huawei.Enabled; + huawei["verbose_logging"] = config.Huawei.VerboseLogging; huawei["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency; huawei["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled; + huawei["auto_power_batterysoc_limits_enabled"] = config.Huawei.Auto_Power_BatterySoC_Limits_Enabled; huawei["emergency_charge_enabled"] = config.Huawei.Emergency_Charge_Enabled; huawei["voltage_limit"] = config.Huawei.Auto_Power_Voltage_Limit; huawei["enable_voltage_limit"] = config.Huawei.Auto_Power_Enable_Voltage_Limit; huawei["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit; huawei["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit; + huawei["stop_batterysoc_threshold"] = config.Huawei.Auto_Power_Stop_BatterySoC_Threshold; // Serialize JSON to file if (serializeJson(doc, f) == 0) { @@ -463,13 +466,16 @@ bool ConfigurationClass::read() JsonObject huawei = doc["huawei"]; config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED; + config.Huawei.VerboseLogging = huawei["verbose_logging"] | VERBOSE_LOGGING; config.Huawei.CAN_Controller_Frequency = huawei["can_controller_frequency"] | HUAWEI_CAN_CONTROLLER_FREQUENCY; config.Huawei.Auto_Power_Enabled = huawei["auto_power_enabled"] | false; + config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = huawei["auto_power_batterysoc_limits_enabled"] | false; config.Huawei.Emergency_Charge_Enabled = huawei["emergency_charge_enabled"] | false; config.Huawei.Auto_Power_Voltage_Limit = huawei["voltage_limit"] | HUAWEI_AUTO_POWER_VOLTAGE_LIMIT; config.Huawei.Auto_Power_Enable_Voltage_Limit = huawei["enable_voltage_limit"] | HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT; config.Huawei.Auto_Power_Lower_Power_Limit = huawei["lower_power_limit"] | HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT; config.Huawei.Auto_Power_Upper_Power_Limit = huawei["upper_power_limit"] | HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT; + config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = huawei["stop_batterysoc_threshold"] | HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD; f.close(); return true; diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index db464a20a..4c5e42d39 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -8,6 +8,7 @@ #include "PowerMeter.h" #include "PowerLimiter.h" #include "Configuration.h" +#include "Battery.h" #include #include @@ -274,6 +275,8 @@ void HuaweiCanClass::loop() return; } + bool verboseLogging = config.Huawei.VerboseLogging; + processReceivedParameters(); uint8_t com_error = HuaweiCanComm.getErrorCode(true); @@ -285,7 +288,7 @@ void HuaweiCanClass::loop() } // Print updated data - if (HuaweiCanComm.gotNewRxDataFrame(false)) { + if (HuaweiCanComm.gotNewRxDataFrame(false) && verboseLogging) { MessageOutput.printf("[HuaweiCanClass::loop] In: %.02fV, %.02fA, %.02fW\n", _rp.input_voltage, _rp.input_current, _rp.input_power); MessageOutput.printf("[HuaweiCanClass::loop] Out: %.02fV, %.02fA of %.02fA, %.02fW\n", _rp.output_voltage, _rp.output_current, _rp.max_output_current, _rp.output_power); MessageOutput.printf("[HuaweiCanClass::loop] Eff : %.01f%%, Temp in: %.01fC, Temp out: %.01fC\n", _rp.efficiency * 100, _rp.input_temp, _rp.output_temp); @@ -382,7 +385,21 @@ void HuaweiCanClass::loop() // Calculate new power limit float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); newPowerLimit += _rp.output_power; - MessageOutput.printf("[HuaweiCanClass::loop] PL: %f, OP: %f \r\n", newPowerLimit, _rp.output_power); + if (verboseLogging){ + MessageOutput.printf("[HuaweiCanClass::loop] newPowerLimit: %f, output_power: %f \r\n", newPowerLimit, _rp.output_power); + } + + if (config.Battery.Enabled && config.Huawei.Auto_Power_BatterySoC_Limits_Enabled) { + uint8_t _batterySoC = Battery.getStats()->getSoC(); + if (_batterySoC >= config.Huawei.Auto_Power_Stop_BatterySoC_Threshold) { + newPowerLimit = 0; + if (verboseLogging) { + MessageOutput.printf("[HuaweiCanClass::loop] Current battery SoC %i reached " + "stop threshold %i, set newPowerLimit to %f \r\n", _batterySoC, + config.Huawei.Auto_Power_Stop_BatterySoC_Threshold, newPowerLimit); + } + } + } if (newPowerLimit > config.Huawei.Auto_Power_Lower_Power_Limit) { @@ -415,7 +432,9 @@ void HuaweiCanClass::loop() float outputCurrent = std::min(calculatedCurrent, permissableCurrent); outputCurrent= outputCurrent > 0 ? outputCurrent : 0; - MessageOutput.printf("[HuaweiCanClass::loop] Setting output current to %.2fA. This is the lower value of calculated %.2fA and BMS permissable %.2fA currents\r\n", outputCurrent, calculatedCurrent, permissableCurrent); + if (verboseLogging) { + MessageOutput.printf("[HuaweiCanClass::loop] Setting output current to %.2fA. This is the lower value of calculated %.2fA and BMS permissable %.2fA currents\r\n", outputCurrent, calculatedCurrent, permissableCurrent); + } _autoPowerEnabled = true; _setValue(outputCurrent, HUAWEI_ONLINE_CURRENT); diff --git a/src/WebApi_Huawei.cpp b/src/WebApi_Huawei.cpp index d0975ddfc..61d88bfd9 100644 --- a/src/WebApi_Huawei.cpp +++ b/src/WebApi_Huawei.cpp @@ -186,13 +186,16 @@ void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request) const CONFIG_T& config = Configuration.get(); root["enabled"] = config.Huawei.Enabled; + root["verbose_logging"] = config.Huawei.VerboseLogging; root["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency; root["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled; + root["auto_power_batterysoc_limits_enabled"] = config.Huawei.Auto_Power_BatterySoC_Limits_Enabled; root["emergency_charge_enabled"] = config.Huawei.Emergency_Charge_Enabled; root["voltage_limit"] = static_cast(config.Huawei.Auto_Power_Voltage_Limit * 100) / 100.0; root["enable_voltage_limit"] = static_cast(config.Huawei.Auto_Power_Enable_Voltage_Limit * 100) / 100.0; root["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit; root["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit; + root["stop_batterysoc_threshold"] = config.Huawei.Auto_Power_Stop_BatterySoC_Threshold; response->setLength(); request->send(response); @@ -253,13 +256,16 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); config.Huawei.Enabled = root["enabled"].as(); + config.Huawei.VerboseLogging = root["verbose_logging"]; config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as(); config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as(); + config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = root["auto_power_batterysoc_limits_enabled"].as(); config.Huawei.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as(); config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as(); config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as(); config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as(); config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as(); + config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = root["stop_batterysoc_threshold"]; WebApi.writeConfig(retMsg); response->setLength(); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 09613822e..009666478 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -835,9 +835,12 @@ "ChargerSettings": "AC Ladegerät Einstellungen", "Configuration": "AC Ladegerät Konfiguration", "EnableHuawei": "Huawei R4850G2 an CAN Bus Interface aktiv", + "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "Frequenz des Quarzes am CAN Controller", "EnableAutoPower": "Automatische Leistungssteuerung", + "EnableBatterySoCLimits": "Ladezustand einer angeschlossenen Batterie berücksichtigen", "Limits": "Limits", + "BatterySoCLimits": "Batterie SoC-Limits", "VoltageLimit": "Ladespannungslimit", "enableVoltageLimit": "Start Spannungslimit", "stopVoltageLimitHint": "Maximal Spannung des Ladegeräts. Entspricht der geünschten Ladeschlussspannung der Batterie. Verwendet für die Automatische Leistungssteuerung und beim Notfallladen", @@ -845,6 +848,8 @@ "maxPowerLimitHint": "Maximale Ausgangsleistung. Verwendet für die Automatische Leistungssteuerung und beim Notfallladen", "lowerPowerLimit": "Minimale Leistung", "upperPowerLimit": "Maximale Leistung", + "StopBatterySoCThreshold": "Laden bei SoC beenden", + "StopBatterySoCThresholdHint": "Zur Verlängerung der Akku-Lebensdauer kann der Ladevorgang bei einem bestimmten SoC gestoppt werden.\nHinweis: Manche LiFePO-Akkus müssen gelegentlich voll geladen werden, um die SoC-Anzeige akkurat zu halten.", "Seconds": "@:base.Seconds", "EnableEmergencyCharge": "Notfallladen: Batterie wird mit maximaler Leistung geladen wenn durch das Batterie BMS angefordert" }, diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 5ffac8c5e..47727645e 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -842,9 +842,12 @@ "ChargerSettings": "AC Charger Settings", "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", + "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "CAN controller quarz frequency", "EnableAutoPower": "Automatic power control", + "EnableBatterySoCLimits": "Use SoC data of a connected battery", "Limits": "Limits", + "BatterySoCLimits": "Battery SoC Limits", "VoltageLimit": "Charge Voltage limit", "enableVoltageLimit": "Re-enable voltage limit", "stopVoltageLimitHint": "Maximum charger voltage. Equals battery charge voltage limit. Used for automatic power control and when emergency charging", @@ -852,6 +855,8 @@ "maxPowerLimitHint": "Maximum output power. Used for automatic power control and when emergency charging", "lowerPowerLimit": "Minimum output power", "upperPowerLimit": "Maximum output power", + "StopBatterySoCThreshold": "Stop charging at SoC", + "StopBatterySoCThresholdHint": "To prolong the battery's lifespan, charging can be stopped at a certain SoC level.\nHint: In order to keep the SoC reading accurate, some LiFePO cells must be charged to full capacity regularly.", "Seconds": "@:base.Seconds", "EnableEmergencyCharge": "Emergency charge. Battery charged with maximum power if requested by Battery BMS" }, diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 519132bff..7bf30bbee 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -833,9 +833,12 @@ "ChargerSettings": "AC Charger Settings", "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", + "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "CAN controller quarz frequency", "EnableAutoPower": "Automatic power control", + "EnableBatterySoCLimits": "Use SoC data of a connected battery", "Limits": "Limits", + "BatterySoCLimits": "Battery SoC Limits", "VoltageLimit": "Charge Voltage limit", "enableVoltageLimit": "Re-enable voltage limit", "stopVoltageLimitHint": "Maximum charger voltage. Equals battery charge voltage limit. Used for automatic power control and when emergency charging", @@ -843,6 +846,8 @@ "maxPowerLimitHint": "Maximum output power. Used for automatic power control and when emergency charging", "lowerPowerLimit": "Minimum output power", "upperPowerLimit": "Maximum output power", + "StopBatterySoCThreshold": "Stop charging at SoC", + "StopBatterySoCThresholdHint": "To prolong the battery's lifespan, charging can be stopped at a certain SoC level.\nHint: In order to keep the SoC reading accurate, some LiFePO cells must be charged to full capacity regularly.", "Seconds": "@:base.Seconds", "EnableEmergencyCharge": "Emergency charge. Battery charged with maximum power if requested by Battery BMS" }, diff --git a/webapp/src/types/AcChargerConfig.ts b/webapp/src/types/AcChargerConfig.ts index e80a65aaa..9dce2f874 100644 --- a/webapp/src/types/AcChargerConfig.ts +++ b/webapp/src/types/AcChargerConfig.ts @@ -1,10 +1,13 @@ export interface AcChargerConfig { enabled: boolean; + verbose_logging: boolean; can_controller_frequency: number; auto_power_enabled: boolean; + auto_power_batterysoc_limits_enabled: boolean; voltage_limit: number; enable_voltage_limit: number; lower_power_limit: number; upper_power_limit: number; emergency_charge_enabled: boolean; + stop_batterysoc_threshold: number; } diff --git a/webapp/src/views/AcChargerAdminView.vue b/webapp/src/views/AcChargerAdminView.vue index 4f811d26f..1feac505c 100644 --- a/webapp/src/views/AcChargerAdminView.vue +++ b/webapp/src/views/AcChargerAdminView.vue @@ -23,11 +23,21 @@
+ + + +
+ +
+ +
+
+ + % +
+
+
+
From 165a9bc16875acd4ee2adb8b77801a82a36a7975 Mon Sep 17 00:00:00 2001 From: PhilJaro <122352327+PhilJaro@users.noreply.github.com> Date: Tue, 9 Apr 2024 20:31:05 +0200 Subject: [PATCH 33/70] adjust VE.Direct MPPT yield resulotion (#859) --- src/WebApi_ws_vedirect_live.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index f583ebd05..b113cecf0 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -196,13 +196,13 @@ void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDir input["IPV"]["d"] = 2; input["YieldToday"]["v"] = mpptData.yieldToday_H20_Wh / 1000.0; input["YieldToday"]["u"] = "kWh"; - input["YieldToday"]["d"] = 3; + input["YieldToday"]["d"] = 2; input["YieldYesterday"]["v"] = mpptData.yieldYesterday_H22_Wh / 1000.0; input["YieldYesterday"]["u"] = "kWh"; - input["YieldYesterday"]["d"] = 3; + input["YieldYesterday"]["d"] = 2; input["YieldTotal"]["v"] = mpptData.yieldTotal_H19_Wh / 1000.0; input["YieldTotal"]["u"] = "kWh"; - input["YieldTotal"]["d"] = 3; + input["YieldTotal"]["d"] = 2; input["MaximumPowerToday"]["v"] = mpptData.maxPowerToday_H21_W; input["MaximumPowerToday"]["u"] = "W"; input["MaximumPowerToday"]["d"] = 0; From a9c3e05f051c09aa2a38fba7f6cddd737912351b Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 10 Apr 2024 16:47:25 +0200 Subject: [PATCH 34/70] PowerMeter admin: URL examples to the top and hidden if disabled --- webapp/src/views/PowerMeterAdminView.vue | 35 ++++++++++++------------ 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index b490b7c58..e19a6b896 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -105,6 +105,24 @@ wide /> + + -
From f634f58788ecb2b57a90291cda1d46a97c463a07 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 11 Apr 2024 08:13:31 +0200 Subject: [PATCH 35/70] Fix: DPL: use correct channel type to get inverter efficiency --- src/PowerLimiter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index f3163f841..138076a8b 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -352,7 +352,7 @@ int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr CONFIG_T& config = Configuration.get(); float inverterEfficiencyPercent = inverter->Statistics()->getChannelFieldValue( - TYPE_AC, CH0, FLD_EFF); + TYPE_INV, CH0, FLD_EFF); // fall back to hoymiles peak efficiency as per datasheet if inverter // is currently not producing (efficiency is zero in that case) From 8b3a1bef47560b83247c28bad583ebaf14f13ed6 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 11 Apr 2024 08:36:59 +0200 Subject: [PATCH 36/70] Fix: show AC input power of Huawei AC charger in live view makes the value match its description. since most values in the top part of the live view are related to the AC side of the system, it makes sense to use the correct value rather than to change the description. --- src/WebApi_ws_live.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 7f42bf443..505f167ae 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -80,7 +80,7 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al if (config.Huawei.Enabled) { const RectifierParameters_t * rp = HuaweiCan.get(); - addTotalField(huaweiObj, "Power", rp->output_power, "W", 2); + addTotalField(huaweiObj, "Power", rp->input_power, "W", 2); } if (!all) { _lastPublishHuawei = millis(); } From 153293e1c7dd5daca4bd4f161b0034dda3181892 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 12 Apr 2024 15:27:24 +0200 Subject: [PATCH 37/70] remove remaining usage of F() macro --- lib/Hoymiles/src/inverters/HMT_4CH.cpp | 2 +- lib/Hoymiles/src/inverters/HMT_6CH.cpp | 2 +- src/InverterSettings.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Hoymiles/src/inverters/HMT_4CH.cpp b/lib/Hoymiles/src/inverters/HMT_4CH.cpp index 609e3350f..c84eff478 100644 --- a/lib/Hoymiles/src/inverters/HMT_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_4CH.cpp @@ -70,7 +70,7 @@ bool HMT_4CH::isValidSerial(const uint64_t serial) String HMT_4CH::typeName() const { - return F("HMT-1600/1800/2000-4T"); + return "HMT-1600/1800/2000-4T"; } const byteAssign_t* HMT_4CH::getByteAssignment() const diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.cpp b/lib/Hoymiles/src/inverters/HMT_6CH.cpp index f8b9f4075..2c3dd5f3a 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_6CH.cpp @@ -84,7 +84,7 @@ bool HMT_6CH::isValidSerial(const uint64_t serial) String HMT_6CH::typeName() const { - return F("HMT-1800/2250-6T"); + return "HMT-1800/2250-6T"; } const byteAssign_t* HMT_6CH::getByteAssignment() const diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 7daad4df5..c08585e2b 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -51,9 +51,9 @@ void InverterSettingsClass::init(Scheduler& scheduler) if (PinMapping.isValidCmt2300Config()) { Hoymiles.initCMT(pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio2, pin.cmt_gpio3); - MessageOutput.println(F(" Setting country mode... ")); + MessageOutput.println(" Setting country mode... "); Hoymiles.getRadioCmt()->setCountryMode(static_cast(config.Dtu.Cmt.CountryMode)); - MessageOutput.println(F(" Setting CMT target frequency... ")); + MessageOutput.println(" Setting CMT target frequency... "); Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency); } From 4c2822cdbc510b1bf99cd8e5390ca040e139369e Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Fri, 12 Apr 2024 15:25:54 +0200 Subject: [PATCH 38/70] remove usage of F() macro frees 888 Bytes of flash. --- src/BatteryStats.cpp | 72 +++++++++++++++++++-------------------- src/WebApi_powermeter.cpp | 10 +++--- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 720695f34..5acffa370 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -61,8 +61,8 @@ bool BatteryStats::updateAvailable(uint32_t since) const void BatteryStats::getLiveViewData(JsonVariant& root) const { - root[F("manufacturer")] = _manufacturer; - root[F("data_age")] = getAgeSeconds(); + root["manufacturer"] = _manufacturer; + root["data_age"] = getAgeSeconds(); addLiveViewValue(root, "SoC", _soc, "%", _socPrecision); addLiveViewValue(root, "voltage", _voltage, "V", 2); @@ -218,39 +218,39 @@ uint32_t BatteryStats::getMqttFullPublishIntervalMs() const void BatteryStats::mqttPublish() const { - MqttSettings.publish(F("battery/manufacturer"), _manufacturer); - MqttSettings.publish(F("battery/dataAge"), String(getAgeSeconds())); - MqttSettings.publish(F("battery/stateOfCharge"), String(_soc)); - MqttSettings.publish(F("battery/voltage"), String(_voltage)); + MqttSettings.publish("battery/manufacturer", _manufacturer); + MqttSettings.publish("battery/dataAge", String(getAgeSeconds())); + MqttSettings.publish("battery/stateOfCharge", String(_soc)); + MqttSettings.publish("battery/voltage", String(_voltage)); } void PylontechBatteryStats::mqttPublish() const { BatteryStats::mqttPublish(); - MqttSettings.publish(F("battery/settings/chargeVoltage"), String(_chargeVoltage)); - MqttSettings.publish(F("battery/settings/chargeCurrentLimitation"), String(_chargeCurrentLimitation)); - MqttSettings.publish(F("battery/settings/dischargeCurrentLimitation"), String(_dischargeCurrentLimitation)); - MqttSettings.publish(F("battery/stateOfHealth"), String(_stateOfHealth)); - MqttSettings.publish(F("battery/current"), String(_current)); - MqttSettings.publish(F("battery/temperature"), String(_temperature)); - MqttSettings.publish(F("battery/alarm/overCurrentDischarge"), String(_alarmOverCurrentDischarge)); - MqttSettings.publish(F("battery/alarm/overCurrentCharge"), String(_alarmOverCurrentCharge)); - MqttSettings.publish(F("battery/alarm/underTemperature"), String(_alarmUnderTemperature)); - MqttSettings.publish(F("battery/alarm/overTemperature"), String(_alarmOverTemperature)); - MqttSettings.publish(F("battery/alarm/underVoltage"), String(_alarmUnderVoltage)); - MqttSettings.publish(F("battery/alarm/overVoltage"), String(_alarmOverVoltage)); - MqttSettings.publish(F("battery/alarm/bmsInternal"), String(_alarmBmsInternal)); - MqttSettings.publish(F("battery/warning/highCurrentDischarge"), String(_warningHighCurrentDischarge)); - MqttSettings.publish(F("battery/warning/highCurrentCharge"), String(_warningHighCurrentCharge)); - MqttSettings.publish(F("battery/warning/lowTemperature"), String(_warningLowTemperature)); - MqttSettings.publish(F("battery/warning/highTemperature"), String(_warningHighTemperature)); - MqttSettings.publish(F("battery/warning/lowVoltage"), String(_warningLowVoltage)); - MqttSettings.publish(F("battery/warning/highVoltage"), String(_warningHighVoltage)); - MqttSettings.publish(F("battery/warning/bmsInternal"), String(_warningBmsInternal)); - MqttSettings.publish(F("battery/charging/chargeEnabled"), String(_chargeEnabled)); - MqttSettings.publish(F("battery/charging/dischargeEnabled"), String(_dischargeEnabled)); - MqttSettings.publish(F("battery/charging/chargeImmediately"), String(_chargeImmediately)); + MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage)); + MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation)); + MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimitation)); + MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); + MqttSettings.publish("battery/current", String(_current)); + MqttSettings.publish("battery/temperature", String(_temperature)); + MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge)); + MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge)); + MqttSettings.publish("battery/alarm/underTemperature", String(_alarmUnderTemperature)); + MqttSettings.publish("battery/alarm/overTemperature", String(_alarmOverTemperature)); + MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage)); + MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage)); + MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmBmsInternal)); + MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighCurrentDischarge)); + MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighCurrentCharge)); + MqttSettings.publish("battery/warning/lowTemperature", String(_warningLowTemperature)); + MqttSettings.publish("battery/warning/highTemperature", String(_warningHighTemperature)); + MqttSettings.publish("battery/warning/lowVoltage", String(_warningLowVoltage)); + MqttSettings.publish("battery/warning/highVoltage", String(_warningHighVoltage)); + MqttSettings.publish("battery/warning/bmsInternal", String(_warningBmsInternal)); + MqttSettings.publish("battery/charging/chargeEnabled", String(_chargeEnabled)); + MqttSettings.publish("battery/charging/dischargeEnabled", String(_dischargeEnabled)); + MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately)); } void JkBmsBatteryStats::mqttPublish() const @@ -424,11 +424,11 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const { void VictronSmartShuntStats::mqttPublish() const { BatteryStats::mqttPublish(); - MqttSettings.publish(F("battery/current"), String(_current)); - MqttSettings.publish(F("battery/chargeCycles"), String(_chargeCycles)); - MqttSettings.publish(F("battery/chargedEnergy"), String(_chargedEnergy)); - MqttSettings.publish(F("battery/dischargedEnergy"), String(_dischargedEnergy)); - MqttSettings.publish(F("battery/instantaneousPower"), String(_instantaneousPower)); - MqttSettings.publish(F("battery/consumedAmpHours"), String(_consumedAmpHours)); - MqttSettings.publish(F("battery/lastFullCharge"), String(_lastFullCharge)); + MqttSettings.publish("battery/current", String(_current)); + MqttSettings.publish("battery/chargeCycles", String(_chargeCycles)); + MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy)); + MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy)); + MqttSettings.publish("battery/instantaneousPower", String(_instantaneousPower)); + MqttSettings.publish("battery/consumedAmpHours", String(_consumedAmpHours)); + MqttSettings.publish("battery/lastFullCharge", String(_lastFullCharge)); } diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 137168baf..5a7e9bf22 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -252,11 +252,11 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) char response[256]; int phase = 0;//"absuing" index 0 of the float power[3] in HttpPowerMeter to store the result - if (HttpPowerMeter.queryPhase(phase, root[F("url")].as().c_str(), - root[F("auth_type")].as(), root[F("username")].as().c_str(), root[F("password")].as().c_str(), - root[F("header_key")].as().c_str(), root[F("header_value")].as().c_str(), root[F("timeout")].as(), - root[F("json_path")].as().c_str())) { - retMsg[F("type")] = F("success"); + if (HttpPowerMeter.queryPhase(phase, root["url"].as().c_str(), + root["auth_type"].as(), root["username"].as().c_str(), root["password"].as().c_str(), + root["header_key"].as().c_str(), root["header_value"].as().c_str(), root["timeout"].as(), + root["json_path"].as().c_str())) { + retMsg["type"] = "success"; snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", HttpPowerMeter.getPower(phase + 1)); } else { snprintf_P(response, sizeof(response), "%s", HttpPowerMeter.httpPowerMeterError); From b58d08683e3b4e9d7ed6a9963d943887375db061 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 12 Apr 2024 20:02:18 +0200 Subject: [PATCH 39/70] webapp: update dependencies --- webapp/.eslintrc.cjs | 14 -- webapp/eslint.config.js | 36 ++++ webapp/package.json | 21 ++- webapp/yarn.lock | 371 +++++++++++++++++----------------------- 4 files changed, 204 insertions(+), 238 deletions(-) delete mode 100644 webapp/.eslintrc.cjs create mode 100644 webapp/eslint.config.js diff --git a/webapp/.eslintrc.cjs b/webapp/.eslintrc.cjs deleted file mode 100644 index ade85716e..000000000 --- a/webapp/.eslintrc.cjs +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-env node */ -require('@rushstack/eslint-patch/modern-module-resolution') - -module.exports = { - root: true, - 'extends': [ - 'plugin:vue/vue3-essential', - 'eslint:recommended', - '@vue/eslint-config-typescript' - ], - parserOptions: { - ecmaVersion: 'latest' - } -} diff --git a/webapp/eslint.config.js b/webapp/eslint.config.js new file mode 100644 index 000000000..91657b987 --- /dev/null +++ b/webapp/eslint.config.js @@ -0,0 +1,36 @@ +/* eslint-env node */ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { FlatCompat } from "@eslint/eslintrc"; +import js from "@eslint/js"; +import pluginVue from 'eslint-plugin-vue' + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, +}); + +export default [ + js.configs.recommended, + ...pluginVue.configs['flat/essential'], + ...compat.extends("@vue/eslint-config-typescript/recommended"), + { + files: [ + "**/*.vue", + "**/*.js", + "**/*.jsx", + "**/*.cjs", + "**/*.mjs", + "**/*.ts", + "**/*.tsx", + "**/*.cts", + "**/*.mts", + ], + languageOptions: { + ecmaVersion: 'latest' + }, + } + ] diff --git a/webapp/package.json b/webapp/package.json index 5e92ee74d..86b30904f 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -9,7 +9,7 @@ "preview": "vite preview --port 4173", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + "lint": "eslint ." }, "dependencies": { "@popperjs/core": "^2.11.8", @@ -19,31 +19,30 @@ "sortablejs": "^1.15.2", "spark-md5": "^3.0.2", "vue": "^3.4.21", - "vue-i18n": "^9.10.2", + "vue-i18n": "^9.12.0", "vue-router": "^4.3.0" }, "devDependencies": { "@intlify/unplugin-vue-i18n": "^4.0.0", - "@rushstack/eslint-patch": "^1.10.1", "@tsconfig/node18": "^18.2.4", "@types/bootstrap": "^5.2.10", - "@types/node": "^20.12.2", + "@types/node": "^20.12.7", "@types/pulltorefreshjs": "^0.1.7", "@types/sortablejs": "^1.15.8", "@types/spark-md5": "^3.0.4", "@vitejs/plugin-vue": "^5.0.4", "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.24.0", + "eslint": "^9.0.0", + "eslint-plugin-vue": "^9.24.1", "npm-run-all": "^4.1.5", "pulltorefreshjs": "^0.1.22", - "sass": "^1.72.0", - "terser": "^5.30.0", - "typescript": "^5.4.3", - "vite": "^5.2.7", + "sass": "^1.75.0", + "terser": "^5.30.3", + "typescript": "^5.4.5", + "vite": "^5.2.8", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.5.0", - "vue-tsc": "^2.0.7" + "vue-tsc": "^2.0.13" } } diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 1d7bc2022..e27e24591 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -156,32 +156,32 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== +"@eslint/eslintrc@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.0.2.tgz#36180f8e85bf34d2fe3ccc2261e8e204a411ab4e" + integrity sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" + espree "^10.0.1" + globals "^14.0.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.57.0": - version "8.57.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" - integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@eslint/js@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.0.0.tgz#1a9e4b4c96d8c7886e0110ed310a0135144a1691" + integrity sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ== -"@humanwhocodes/config-array@^0.11.14": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== +"@humanwhocodes/config-array@^0.12.3": + version "0.12.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.12.3.tgz#a6216d90f81a30bedd1d4b5d799b47241f318072" + integrity sha512-jsNnTBlMWuTpDkeE3on7+dWJi0D6fdDfeANj/w7MpS8ztROCoLvIO2nG0CcFj+E4k8j4QrSTh4Oryi3i2G669g== dependencies: - "@humanwhocodes/object-schema" "^2.0.2" + "@humanwhocodes/object-schema" "^2.0.3" debug "^4.3.1" minimatch "^3.0.5" @@ -190,10 +190,10 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" - integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== "@intlify/bundle-utils@^8.0.0": version "8.0.0" @@ -210,20 +210,20 @@ source-map-js "^1.0.1" yaml-eslint-parser "^1.2.2" -"@intlify/core-base@9.10.2": - version "9.10.2" - resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.10.2.tgz#e7f8857f8011184e4afbdcfae7dbd85c50ba5271" - integrity sha512-HGStVnKobsJL0DoYIyRCGXBH63DMQqEZxDUGrkNI05FuTcruYUtOAxyL3zoAZu/uDGO6mcUvm3VXBaHG2GdZCg== +"@intlify/core-base@9.12.0": + version "9.12.0" + resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.12.0.tgz#79f43faa8eb1f3b2bfe569a9fbae9bc50908d311" + integrity sha512-6EnWQXHnCh2bMiXT5N/IWwkcYQXjmF8nnEZ3YhTm23h1ZfOylz83D7pJYhcU8CsTiEdgbGiNdqyZPKwrHw03Ng== dependencies: - "@intlify/message-compiler" "9.10.2" - "@intlify/shared" "9.10.2" + "@intlify/message-compiler" "9.12.0" + "@intlify/shared" "9.12.0" -"@intlify/message-compiler@9.10.2": - version "9.10.2" - resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.10.2.tgz#c44cbb915bdd0d62780a38595a84006c781f717a" - integrity sha512-ntY/kfBwQRtX5Zh6wL8cSATujPzWW2ZQd1QwKyWwAy5fMqJyyixHMeovN4fmEyCqSu+hFfYOE63nU94evsy4YA== +"@intlify/message-compiler@9.12.0": + version "9.12.0" + resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.12.0.tgz#5e152344853c29369911bd5e541e061b09218333" + integrity sha512-2c6VwhvVJ1nur+2cN2NjdrmrV6vXjvyxYVvtUYMXKsWSUwoNURHGds0xJVJmWxbF8qV9oGepcVV6xl9bvadEIg== dependencies: - "@intlify/shared" "9.10.2" + "@intlify/shared" "9.12.0" source-map-js "^1.0.2" "@intlify/message-compiler@^9.4.0": @@ -234,10 +234,10 @@ "@intlify/shared" "9.4.0" source-map-js "^1.0.2" -"@intlify/shared@9.10.2": - version "9.10.2" - resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.10.2.tgz#693300ea033868cbe4086b832170612f002e24a9" - integrity sha512-ttHCAJkRy7R5W2S9RVnN9KYQYPIpV2+GiS79T4EE37nrPyH6/1SrOh3bmdCRC1T3ocL8qCDx7x2lBJ0xaITU7Q== +"@intlify/shared@9.12.0": + version "9.12.0" + resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.12.0.tgz#993383b6a98c8e37a1fa184a677eb39635a14a1c" + integrity sha512-uBcH55x5CfZynnerWHQxrXbT6yD6j6T7Nt+R2+dHAOAneoMd6BoGvfEzfYscE94rgmjoDqdr+PdGDBLk5I5EjA== "@intlify/shared@9.4.0", "@intlify/shared@^9.4.0": version "9.4.0" @@ -412,11 +412,6 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz#6abd79db7ff8d01a58865ba20a63cfd23d9e2a10" integrity sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw== -"@rushstack/eslint-patch@^1.10.1": - version "1.10.1" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf" - integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg== - "@tsconfig/node18@^18.2.4": version "18.2.4" resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.4.tgz#094efbdd70f697d37c09f34067bf41bc4a828ae3" @@ -444,10 +439,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== -"@types/node@^20.12.2": - version "20.12.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e" - integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ== +"@types/node@^20.12.7": + version "20.12.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" + integrity sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg== dependencies: undici-types "~5.26.4" @@ -557,36 +552,31 @@ "@typescript-eslint/types" "7.2.0" eslint-visitor-keys "^3.4.1" -"@ungap/structured-clone@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== - "@vitejs/plugin-vue@^5.0.4": version "5.0.4" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37" integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ== -"@volar/language-core@2.1.3", "@volar/language-core@~2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.1.3.tgz#ac6057ec73c5fcda1fc07677bf0d7be41e6c59b1" - integrity sha512-F93KYZYqcYltG7NihfnLt/omMZOtrQtsh2+wj+cgx3xolopU+TZvmwlZWOjw3ObZGFj3SKBb4jJn6VSfSch6RA== +"@volar/language-core@2.2.0-alpha.8": + version "2.2.0-alpha.8" + resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.2.0-alpha.8.tgz#74120a27ff2498ad297e86d17be95a9c7e1b46f5" + integrity sha512-Ew1Iw7/RIRNuDLn60fWJdOLApAlfTVPxbPiSLzc434PReC9kleYtaa//Wo2WlN1oiRqneW0pWQQV0CwYqaimLQ== dependencies: - "@volar/source-map" "2.1.3" + "@volar/source-map" "2.2.0-alpha.8" -"@volar/source-map@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.1.3.tgz#8f3cb110019c45fa4cd47ad2f5fe5469bd54b9e3" - integrity sha512-j+R+NG/OlDgdNMttADxNuSM9Z26StT/Bjw0NgSydI05Vihngn9zvaP/xXwfWs5qQrRzbKVFxJebS2ks5m/URuA== +"@volar/source-map@2.2.0-alpha.8": + version "2.2.0-alpha.8" + resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.2.0-alpha.8.tgz#ca090f828fbef7e09ea06a636c41a06aa2afe153" + integrity sha512-E1ZVmXFJ5DU4fWDcWHzi8OLqqReqIDwhXvIMhVdk6+VipfMVv4SkryXu7/rs4GA/GsebcRyJdaSkKBB3OAkIcA== dependencies: muggle-string "^0.4.0" -"@volar/typescript@~2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.1.3.tgz#bfdc901afd44c2d05697967211aa55d53fb8bf69" - integrity sha512-ZZqLMih4mvu2eJAW3UCFm84OM/ojYMoA/BU/W1TctT5F2nVzNJmW4jxMWmP3wQzxCbATfTa5gLb1+BSI9NBMBg== +"@volar/typescript@2.2.0-alpha.8": + version "2.2.0-alpha.8" + resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.2.0-alpha.8.tgz#83a056c52995b4142364be3dda41d955a96f7356" + integrity sha512-RLbRDI+17CiayHZs9HhSzlH0FhLl/+XK6o2qoiw2o2GGKcyD1aDoY6AcMd44acYncTOrqoTNoY6LuCiRyiJiGg== dependencies: - "@volar/language-core" "2.1.3" + "@volar/language-core" "2.2.0-alpha.8" path-browserify "^1.0.1" "@vue/compiler-core@3.2.47": @@ -692,12 +682,12 @@ "@typescript-eslint/parser" "^7.1.1" vue-eslint-parser "^9.3.1" -"@vue/language-core@2.0.7": - version "2.0.7" - resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.7.tgz#af12f752a93c4d2498626fca33f5d1ddc8c5ceb9" - integrity sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ== +"@vue/language-core@2.0.13": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.13.tgz#2d1638b882011187b4b57115425d52b0901acab5" + integrity sha512-oQgM+BM66SU5GKtUMLQSQN0bxHFkFpLSSAiY87wVziPaiNQZuKVDt/3yA7GB9PiQw0y/bTNL0bOc0jM/siYjKg== dependencies: - "@volar/language-core" "~2.1.3" + "@volar/language-core" "2.2.0-alpha.8" "@vue/compiler-dom" "^3.4.0" "@vue/shared" "^3.4.0" computeds "^0.0.1" @@ -768,6 +758,11 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn@^8.11.3: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + acorn@^8.5.0, acorn@^8.9.0: version "8.10.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" @@ -1028,13 +1023,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -1136,10 +1124,10 @@ escodegen@^2.1.0: optionalDependencies: source-map "~0.6.1" -eslint-plugin-vue@^9.24.0: - version "9.24.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.24.0.tgz#71209f4652ee767f18c0bf56f25991b7cdc5aa46" - integrity sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw== +eslint-plugin-vue@^9.24.1: + version "9.24.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.24.1.tgz#0d90330c939f9dd2f4c759da5a2ad91dc1c8bac4" + integrity sha512-wk3SuwmS1pZdcuJlokGYEi/buDOwD6KltvhIZyOnpJ/378dcQ4zchu9PAMbbLAaydCz1iYc5AozszcOOgZIIOg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" globals "^13.24.0" @@ -1158,15 +1146,15 @@ eslint-scope@^7.1.1: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== +eslint-scope@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.1.tgz#a9601e4b81a0b9171657c343fb13111688963cfc" + integrity sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.0.0: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== @@ -1181,41 +1169,42 @@ eslint-visitor-keys@^3.4.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^8.57.0: - version "8.57.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" - integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== +eslint-visitor-keys@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" + integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== + +eslint@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.0.0.tgz#6270548758e390343f78c8afd030566d86927d40" + integrity sha512-IMryZ5SudxzQvuod6rUdIUz29qFItWx281VhtFVc2Psy/ZhlCeD/5DT6lBIJ4H3G+iamGJoTln1v+QSuPw0p7Q== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.0" - "@humanwhocodes/config-array" "^0.11.14" + "@eslint/eslintrc" "^3.0.2" + "@eslint/js" "9.0.0" + "@humanwhocodes/config-array" "^0.12.3" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" - doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" + eslint-scope "^8.0.1" + eslint-visitor-keys "^4.0.0" + espree "^10.0.1" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" + file-entry-cache "^8.0.0" find-up "^5.0.0" glob-parent "^6.0.2" - globals "^13.19.0" graphemer "^1.4.0" ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" - js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" @@ -1225,7 +1214,16 @@ eslint@^8.57.0: strip-ansi "^6.0.1" text-table "^0.2.0" -espree@^9.0.0, espree@^9.6.1: +espree@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f" + integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww== + dependencies: + acorn "^8.11.3" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.0.0" + +espree@^9.0.0: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -1243,15 +1241,6 @@ espree@^9.3.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.3.0" -espree@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f" - integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A== - dependencies: - acorn "^8.9.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" - esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -1337,12 +1326,12 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: - flat-cache "^3.0.4" + flat-cache "^4.0.0" fill-range@^7.0.1: version "7.0.1" @@ -1359,18 +1348,18 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" + flatted "^3.2.9" + keyv "^4.5.4" -flatted@^3.1.0: - version "3.2.5" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" - integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== fs-extra@^10.0.0: version "10.1.0" @@ -1381,11 +1370,6 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" @@ -1447,25 +1431,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^13.19.0: - version "13.19.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8" - integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ== - dependencies: - type-fest "^0.20.2" - globals@^13.24.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" @@ -1473,6 +1438,11 @@ globals@^13.24.0: dependencies: type-fest "^0.20.2" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -1574,19 +1544,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" @@ -1724,6 +1681,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -1768,6 +1730,13 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1849,7 +1818,7 @@ minimatch@9.0.3, minimatch@^9.0.3: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -1958,13 +1927,6 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -2016,11 +1978,6 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -2181,13 +2138,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - rollup@^4.13.0: version "4.13.0" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.13.0.tgz#dd2ae144b4cdc2ea25420477f68d4937a721237a" @@ -2226,10 +2176,10 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -sass@^1.72.0: - version "1.72.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.72.0.tgz#5b9978943fcfb32b25a6a5acb102fc9dabbbf41c" - integrity sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA== +sass@^1.75.0: + version "1.75.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.75.0.tgz#91bbe87fb02dfcc34e052ddd6ab80f60d392be6c" + integrity sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -2431,10 +2381,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -terser@^5.30.0: - version "5.30.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.0.tgz#64cb2af71e16ea3d32153f84d990f9be0cdc22bf" - integrity sha512-Y/SblUl5kEyEFzhMAQdsxVHh+utAxd4IuRNJzKywY/4uzSogh3G219jqbDDxYu4MXO9CzY3tSEqmZvW6AoEDJw== +terser@^5.30.3: + version "5.30.3" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.3.tgz#f1bb68ded42408c316b548e3ec2526d7dd03f4d2" + integrity sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -2470,10 +2420,10 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^5.4.3: - version "5.4.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" - integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== +typescript@^5.4.5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== ufo@^1.1.2: version "1.1.2" @@ -2544,10 +2494,10 @@ vite-plugin-css-injected-by-js@^3.5.0: resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.0.tgz#784c0f42c2b42155eb4c726c6addfa24aba9f4fb" integrity sha512-d0QaHH9kS93J25SwRqJNEfE29PSuQS5jn51y9N9i2Yoq0FRO7rjuTeLvjM5zwklZlRrIn6SUdtOEDKyHokgJZg== -vite@^5.2.7: - version "5.2.7" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.7.tgz#e1b8a985eb54fcb9467d7f7f009d87485016df6e" - integrity sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA== +vite@^5.2.8: + version "5.2.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.8.tgz#a99e09939f1a502992381395ce93efa40a2844aa" + integrity sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA== dependencies: esbuild "^0.20.1" postcss "^8.4.38" @@ -2581,13 +2531,13 @@ vue-eslint-parser@^9.4.2: lodash "^4.17.21" semver "^7.3.6" -vue-i18n@^9.10.2: - version "9.10.2" - resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.10.2.tgz#6f4b5d76bce649f1e18bb9b7767b72962b3e30a3" - integrity sha512-ECJ8RIFd+3c1d3m1pctQ6ywG5Yj8Efy1oYoAKQ9neRdkLbuKLVeW4gaY5HPkD/9ssf1pOnUrmIFjx2/gkGxmEw== +vue-i18n@^9.12.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.12.0.tgz#8d073b3d7b92e822dcc3268946af4ecf14b778b3" + integrity sha512-rUxCKTws8NH3XP98W71GA7btAQdAuO7j6BC5y5s1bTNQYo/CIgZQf+p7d1Zo5bo/3v8TIq9aSUMDjpfgKsC3Uw== dependencies: - "@intlify/core-base" "9.10.2" - "@intlify/shared" "9.10.2" + "@intlify/core-base" "9.12.0" + "@intlify/shared" "9.12.0" "@vue/devtools-api" "^6.5.0" vue-router@^4.3.0: @@ -2605,13 +2555,13 @@ vue-template-compiler@^2.7.14: de-indent "^1.0.2" he "^1.2.0" -vue-tsc@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.7.tgz#3177a2fe720bfa7355d3717929ee8c8d132bc5d0" - integrity sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg== +vue-tsc@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.13.tgz#6ee557705456442e0f43ec0d1774ebf5ffec54f1" + integrity sha512-a3nL3FvguCWVJUQW/jFrUxdeUtiEkbZoQjidqvMeBK//tuE2w6NWQAbdrEpY2+6nSa4kZoKZp8TZUMtHpjt4mQ== dependencies: - "@volar/typescript" "~2.1.3" - "@vue/language-core" "2.0.7" + "@volar/typescript" "2.2.0-alpha.8" + "@vue/language-core" "2.0.13" semver "^7.5.4" vue@^3.4.21: @@ -2660,11 +2610,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - xml-name-validator@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" From de156ef10a4606fa7391ea380d0f93446f685ded Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Fri, 12 Apr 2024 20:34:30 +0200 Subject: [PATCH 40/70] webapp: Fix lint errors --- webapp/src/components/BasePage.vue | 5 +- webapp/src/components/BootstrapAlert.vue | 4 +- webapp/src/components/FirmwareInfo.vue | 4 +- webapp/src/components/InputElement.vue | 4 +- webapp/src/components/InputSerial.vue | 2 + webapp/src/components/NavBar.vue | 24 +- webapp/src/components/PinInfo.vue | 2 + webapp/src/utils/authentication.ts | 2 +- webapp/src/views/ConfigAdminView.vue | 4 +- webapp/src/views/ConsoleInfoView.vue | 308 +++++++++++------------ webapp/src/views/HomeView.vue | 12 +- webapp/src/views/SystemInfoView.vue | 2 +- webapp/vite.config.ts | 1 + 13 files changed, 189 insertions(+), 185 deletions(-) diff --git a/webapp/src/components/BasePage.vue b/webapp/src/components/BasePage.vue index 0ec43d36f..3ca9c12b5 100644 --- a/webapp/src/components/BasePage.vue +++ b/webapp/src/components/BasePage.vue @@ -48,15 +48,14 @@ export default defineComponent({ showReload: { type: Boolean, required: false, default: false }, }, mounted() { - var self = this; console.log("init"); PullToRefresh.init({ mainElement: 'body', // above which element? instructionsPullToRefresh: this.$t('base.Pull'), instructionsReleaseToRefresh: this.$t('base.Release'), instructionsRefreshing: this.$t('base.Refreshing'), - onRefresh: function() { - self.$emit('reload'); + onRefresh: () => { + this.$emit('reload'); } }); }, diff --git a/webapp/src/components/BootstrapAlert.vue b/webapp/src/components/BootstrapAlert.vue index df96fb620..a629863db 100644 --- a/webapp/src/components/BootstrapAlert.vue +++ b/webapp/src/components/BootstrapAlert.vue @@ -52,7 +52,7 @@ export default defineComponent({ _countDownTimeout = undefined; }; - var countDown = ref(); + const countDown = ref(); watch(() => props.modelValue, () => { countDown.value = parseCountDown(props.modelValue); }); @@ -116,4 +116,4 @@ export default defineComponent({ }; }, }); - \ No newline at end of file + diff --git a/webapp/src/components/FirmwareInfo.vue b/webapp/src/components/FirmwareInfo.vue index 11aa25a96..7271004d6 100644 --- a/webapp/src/components/FirmwareInfo.vue +++ b/webapp/src/components/FirmwareInfo.vue @@ -83,10 +83,10 @@ export default defineComponent({ }, computed: { modelAllowVersionInfo: { - get(): any { + get(): boolean { return !!this.allowVersionInfo; }, - set(value: any) { + set(value: boolean) { this.$emit('update:allowVersionInfo', value); }, }, diff --git a/webapp/src/components/InputElement.vue b/webapp/src/components/InputElement.vue index eff8e9f66..f12a11720 100644 --- a/webapp/src/components/InputElement.vue +++ b/webapp/src/components/InputElement.vue @@ -83,10 +83,12 @@ export default defineComponent({ }, computed: { model: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any get(): any { if (this.type === 'checkbox') return !!this.modelValue; return this.modelValue; }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any set(value: any) { this.$emit('update:modelValue', value); }, @@ -112,4 +114,4 @@ export default defineComponent({ } }, }); - \ No newline at end of file + diff --git a/webapp/src/components/InputSerial.vue b/webapp/src/components/InputSerial.vue index 9f5ee343b..3669da622 100644 --- a/webapp/src/components/InputSerial.vue +++ b/webapp/src/components/InputSerial.vue @@ -28,9 +28,11 @@ export default defineComponent({ }, computed: { model: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any get(): any { return this.modelValue; }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any set(value: any) { this.$emit('update:modelValue', value); }, diff --git a/webapp/src/components/NavBar.vue b/webapp/src/components/NavBar.vue index 53995df73..e6eb58f27 100644 --- a/webapp/src/components/NavBar.vue +++ b/webapp/src/components/NavBar.vue @@ -146,8 +146,8 @@ export default defineComponent({ }, isEaster() { const easter = this.getEasterSunday(this.now.getFullYear()); - var easterStart = new Date(easter); - var easterEnd = new Date(easter); + const easterStart = new Date(easter); + const easterEnd = new Date(easter); easterStart.setDate(easterStart.getDate() - 2); easterEnd.setDate(easterEnd.getDate() + 1); return this.now >= easterStart && this.now < easterEnd; @@ -170,18 +170,18 @@ export default defineComponent({ this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove("show"); }, getEasterSunday(year: number): Date { - var f = Math.floor; - var G = year % 19; - var C = f(year / 100); - var H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30; - var I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11)); - var J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7; - var L = I - J; - var month = 3 + f((L + 40) / 44); - var day = L + 28 - 31 * f(month / 4); + const f = Math.floor; + const G = year % 19; + const C = f(year / 100); + const H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30; + const I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11)); + const J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7; + const L = I - J; + const month = 3 + f((L + 40) / 44); + const day = L + 28 - 31 * f(month / 4); return new Date(year, month - 1, day); } }, }); - \ No newline at end of file + diff --git a/webapp/src/components/PinInfo.vue b/webapp/src/components/PinInfo.vue index 3d4616adb..c1e84b810 100644 --- a/webapp/src/components/PinInfo.vue +++ b/webapp/src/components/PinInfo.vue @@ -84,9 +84,11 @@ export default defineComponent({ let comCur = 999999; if (this.selectedPinAssignment && category in this.selectedPinAssignment) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any comSel = (this.selectedPinAssignment as any)[category][prop]; } if (this.currentPinAssignment && category in this.currentPinAssignment) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any comCur = (this.currentPinAssignment as any)[category][prop]; } diff --git a/webapp/src/utils/authentication.ts b/webapp/src/utils/authentication.ts index 52d92fd98..f666c36be 100644 --- a/webapp/src/utils/authentication.ts +++ b/webapp/src/utils/authentication.ts @@ -41,7 +41,7 @@ export function isLoggedIn(): boolean { return (localStorage.getItem('user') != null); } -export function login(username: String, password: String) { +export function login(username: string, password: string) { const requestOptions = { method: 'GET', headers: { diff --git a/webapp/src/views/ConfigAdminView.vue b/webapp/src/views/ConfigAdminView.vue index c0954396e..e0bb66dc8 100644 --- a/webapp/src/views/ConfigAdminView.vue +++ b/webapp/src/views/ConfigAdminView.vue @@ -188,8 +188,8 @@ export default defineComponent({ fetch("/api/config/get?file=" + this.backupFileSelect, { headers: authHeader() }) .then(res => res.blob()) .then(blob => { - var file = window.URL.createObjectURL(blob); - var a = document.createElement('a'); + const file = window.URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = file; a.download = this.backupFileSelect; document.body.appendChild(a); diff --git a/webapp/src/views/ConsoleInfoView.vue b/webapp/src/views/ConsoleInfoView.vue index fb17f62de..eba1d533f 100644 --- a/webapp/src/views/ConsoleInfoView.vue +++ b/webapp/src/views/ConsoleInfoView.vue @@ -1,154 +1,154 @@ - - - - - \ No newline at end of file + + + + + diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index f05c42974..d137b05a5 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -5,7 +5,7 @@
+ + Date: Wed, 10 Apr 2024 20:31:29 +0200 Subject: [PATCH 46/70] Feature: DPL: support setups without power meter without a power meter configured, the DPL now sets the base load as the inverter limit if the battery charge allows it. it also takes solar-passthrough into account, i.e., if the battery is in a charge cycle but the solar output (Victron MPPT) is significant, the solar power will be used up until the base load. if the battery reaches the full solar passthrough threshold, the DPL will match the inverter limit to the MPPT solar output. --- include/PowerLimiter.h | 2 -- src/PowerLimiter.cpp | 10 +--------- webapp/src/locales/de.json | 2 +- webapp/src/locales/en.json | 2 +- webapp/src/locales/fr.json | 2 +- webapp/src/views/PowerLimiterAdminView.vue | 11 +++++++---- 6 files changed, 11 insertions(+), 18 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 28463654e..8f1b04f10 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -23,7 +23,6 @@ class PowerLimiterClass { DisabledByConfig, DisabledByMqtt, WaitingForValidTimestamp, - PowerMeterDisabled, PowerMeterPending, InverterInvalid, InverterChanged, @@ -38,7 +37,6 @@ class PowerLimiterClass { NoVeDirect, NoEnergy, HuaweiPsu, - Settling, Stable, }; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 8dd214042..729fcc90d 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -31,12 +31,11 @@ frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status { static const frozen::string missing = "programmer error: missing status text"; - static const frozen::map texts = { + static const frozen::map texts = { { Status::Initializing, "initializing (should not see me)" }, { Status::DisabledByConfig, "disabled by configuration" }, { Status::DisabledByMqtt, "disabled by MQTT" }, { Status::WaitingForValidTimestamp, "waiting for valid date and time to be available" }, - { Status::PowerMeterDisabled, "no power meter is configured/enabled" }, { Status::PowerMeterPending, "waiting for sufficiently recent power meter reading" }, { Status::InverterInvalid, "invalid inverter selection/configuration" }, { Status::InverterChanged, "target inverter changed" }, @@ -174,13 +173,6 @@ void PowerLimiterClass::loop() return unconditionalSolarPassthrough(_inverter); } - // the normal mode of operation requires a valid - // power meter reading to calculate a power limit - if (!config.PowerMeter.Enabled) { - shutdown(Status::PowerMeterDisabled); - return; - } - // concerns both power limits and start/stop/restart commands and is // only updated if a respective response was received from the inverter auto lastUpdateCmd = std::max( diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 3d7c68f71..a56d5050e 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -585,7 +585,7 @@ "ConfigHintRequirement": "Erforderlich", "ConfigHintOptional": "Optional", "ConfigHintsIntro": "Folgende Hinweise zur Konfiguration des Dynamic Power Limiter (DPL) sollen beachtet werden:", - "ConfigHintPowerMeterDisabled": "Zum Betrieb des DPL muss der Power Meter konfiguriert sein und Daten liefern.", + "ConfigHintPowerMeterDisabled": "Der DPL stellt ohne Stromzählerschnittstelle lediglich die konfigurierte Grundlast als Limit am Wechselrichter ein (Ausnahme: (Full) Solar-Passthrough).", "ConfigHintNoInverter": "Vor dem Festlegen von Einstellungen des DPL muss mindestens ein Inverter konfiguriert sein.", "ConfigHintInverterCommunication": "Das Abrufen von Daten und Senden von Kommandos muss für den zu regelnden Wechselrichter aktiviert sein.", "ConfigHintNoChargeController": "Die Solar-Passthrough Funktion kann nur mit aktivierter VE.Direct Schnittstelle genutzt werden.", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index afc036b6e..6599ce797 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -591,7 +591,7 @@ "ConfigHintRequirement": "Required", "ConfigHintOptional": "Optional", "ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", - "ConfigHintPowerMeterDisabled": "Operating the DPL requires the Power Meter being configured and delivering data.", + "ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).", "ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.", "ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.", "ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 67bb9d277..0f5946f87 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -673,7 +673,7 @@ "ConfigHintRequirement": "Required", "ConfigHintOptional": "Optional", "ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", - "ConfigHintPowerMeterDisabled": "Operating the DPL requires the Power Meter being configured and delivering data.", + "ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).", "ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.", "ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.", "ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.", diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index 46da5def7..fec3f4ff8 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -34,7 +34,7 @@ v-model="powerLimiterConfigList.verbose_logging" type="checkbox" wide/> - - @@ -275,8 +276,7 @@ export default defineComponent({ var hints = []; if (meta.power_meter_enabled !== true) { - hints.push({severity: "requirement", subject: "PowerMeterDisabled"}); - this.configAlert = true; + hints.push({severity: "optional", subject: "PowerMeterDisabled"}); } if (typeof meta.inverters === "undefined" || Object.keys(meta.inverters).length == 0) { @@ -305,6 +305,9 @@ export default defineComponent({ isEnabled() { return this.powerLimiterConfigList.enabled; }, + hasPowerMeter() { + return this.powerLimiterMetaData.power_meter_enabled; + }, canUseSolarPassthrough() { var cfg = this.powerLimiterConfigList; var meta = this.powerLimiterMetaData; From 5fcf09d0a030a5b7bb9603ce2e3e753b61665c26 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 10 Apr 2024 21:15:54 +0200 Subject: [PATCH 47/70] fix hysteresis hint texts --- webapp/src/locales/de.json | 2 +- webapp/src/locales/en.json | 2 +- webapp/src/locales/fr.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index a56d5050e..13808ec58 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -606,7 +606,7 @@ "TargetPowerConsumption": "Angestrebter Netzbezug", "TargetPowerConsumptionHint": "Angestrebter erlaubter Stromverbrauch aus dem Netz. Wert darf negativ sein.", "TargetPowerConsumptionHysteresis": "Hysterese", - "TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den Inverter senden, wenn es vom zuletzt gesendeten Limit um mindestens diesen Betrag abweicht.", + "TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den Inverter senden, wenn es vom zurückgemeldeten Limit um mindestens diesen Betrag abweicht.", "LowerPowerLimit": "Minmales Leistungslimit", "LowerPowerLimitHint": "Dieser Wert muss so gewählt werden, dass ein stabiler Betrieb mit diesem Limit möglich ist. Falls der Wechselrichter nur mit einem kleineren Limit betrieben werden könnte, wird er stattdessen in Standby versetzt.", "BaseLoadLimit": "Grundlast", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 6599ce797..727eea19a 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -612,7 +612,7 @@ "TargetPowerConsumption": "Target Grid Consumption", "TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", "TargetPowerConsumptionHysteresis": "Hysteresis", - "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.", + "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last reported power limit exceeds this amount.", "LowerPowerLimit": "Minimum Power Limit", "LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.", "BaseLoadLimit": "Base Load", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 0f5946f87..ae888850f 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -694,7 +694,7 @@ "TargetPowerConsumption": "Target Grid Consumption", "TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", "TargetPowerConsumptionHysteresis": "Hysteresis", - "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.", + "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last reported power limit exceeds this amount.", "LowerPowerLimit": "Minimum Power Limit", "LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.", "BaseLoadLimit": "Base Load", From 4bc4defe665946d1c096206fb70682e31a004dd0 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Thu, 11 Apr 2024 22:06:47 +0200 Subject: [PATCH 48/70] DPL: insist on power meter value more recent than inverter stats avoid performing a calculation based on a (slightly) outdated power meter reading, which was aquired just before the limit was actually applied by the inverter, but which was received by OpenDTU-OnBattery after the inverter stats. --- include/PowerLimiter.h | 1 + src/PowerLimiter.cpp | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 8f1b04f10..043a7fc10 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -61,6 +61,7 @@ class PowerLimiterClass { int32_t _lastRequestedPowerLimit = 0; bool _shutdownPending = false; + std::optional _oInverterStatsMillis = std::nullopt; std::optional _oUpdateStartMillis = std::nullopt; std::optional _oTargetPowerLimitWatts = std::nullopt; std::optional _oTargetPowerState = std::nullopt; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 729fcc90d..6c2c44337 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -179,14 +179,28 @@ void PowerLimiterClass::loop() _inverter->SystemConfigPara()->getLastUpdateCommand(), _inverter->PowerCommand()->getLastUpdateCommand()); - if (_inverter->Statistics()->getLastUpdate() <= lastUpdateCmd) { - return announceStatus(Status::InverterStatsPending); + // we need inverter stats younger than the last update command + if (_oInverterStatsMillis.has_value() && lastUpdateCmd > *_oInverterStatsMillis) { + _oInverterStatsMillis = std::nullopt; + } + + if (!_oInverterStatsMillis.has_value()) { + auto lastStats = _inverter->Statistics()->getLastUpdate(); + if (lastStats <= lastUpdateCmd) { + return announceStatus(Status::InverterStatsPending); + } + + _oInverterStatsMillis = lastStats; } // if the power meter is being used, i.e., if its data is valid, we want to // wait for a new reading after adjusting the inverter limit. otherwise, we // proceed as we will use a fallback limit independent of the power meter. - if (PowerMeter.isDataValid() && PowerMeter.getLastPowerMeterUpdate() <= lastUpdateCmd) { + // the power meter reading is expected to be at most 2 seconds old when it + // arrives. this can be the case for readings provided by networked meter + // readers, where a packet needs to travel through the network for some + // time after the actual measurement was done by the reader. + if (PowerMeter.isDataValid() && PowerMeter.getLastPowerMeterUpdate() <= (*_oInverterStatsMillis + 2000)) { return announceStatus(Status::PowerMeterPending); } From e92701ccdfe87a66745caa445b670d55dc376d3c Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Tue, 16 Apr 2024 21:59:50 +0200 Subject: [PATCH 49/70] reuse power meter's HTTP config struct --- include/Configuration.h | 5 +++-- include/HttpPowerMeter.h | 16 ++++++++-------- include/WebApi_powermeter.h | 4 +++- src/Configuration.cpp | 2 +- src/HttpPowerMeter.cpp | 33 +++++++++++++++------------------ src/WebApi_powermeter.cpp | 35 +++++++++++++++++++---------------- 6 files changed, 49 insertions(+), 46 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index d514f3433..8d22248fd 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -62,8 +62,8 @@ struct INVERTER_CONFIG_T { CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; -enum Auth { none, basic, digest }; struct POWERMETER_HTTP_PHASE_CONFIG_T { + enum Auth { None, Basic, Digest }; bool Enabled; char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1]; Auth AuthType; @@ -74,6 +74,7 @@ struct POWERMETER_HTTP_PHASE_CONFIG_T { uint16_t Timeout; char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1]; }; +using PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T; struct CONFIG_T { struct { @@ -196,7 +197,7 @@ struct CONFIG_T { uint32_t SdmAddress; uint32_t HttpInterval; bool HttpIndividualRequests; - POWERMETER_HTTP_PHASE_CONFIG_T Http_Phase[POWERMETER_MAX_PHASES]; + PowerMeterHttpConfig Http_Phase[POWERMETER_MAX_PHASES]; } PowerMeter; struct { diff --git a/include/HttpPowerMeter.h b/include/HttpPowerMeter.h index 7ac225a47..97e43a2cf 100644 --- a/include/HttpPowerMeter.h +++ b/include/HttpPowerMeter.h @@ -4,6 +4,9 @@ #include #include #include +#include "Configuration.h" + +using Auth_t = PowerMeterHttpConfig::Auth; class HttpPowerMeterClass { public: @@ -11,23 +14,20 @@ class HttpPowerMeterClass { bool updateValues(); float getPower(int8_t phase); char httpPowerMeterError[256]; - bool queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password, - const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath); - + bool queryPhase(int phase, PowerMeterHttpConfig const& config); -private: +private: float power[POWERMETER_MAX_PHASES]; HTTPClient httpClient; String httpResponse; - bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, Auth authType, const char* username, - const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath); + bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); String extractParam(String& authReq, const String& param, const char delimit); String getcNonce(const int len); String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter); bool tryGetFloatValueForPhase(int phase, const char* jsonPath); - void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue); - String sha256(const String& data); + void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue); + String sha256(const String& data); }; extern HttpPowerMeterClass HttpPowerMeter; diff --git a/include/WebApi_powermeter.h b/include/WebApi_powermeter.h index 64a5ab726..7e873b1c1 100644 --- a/include/WebApi_powermeter.h +++ b/include/WebApi_powermeter.h @@ -3,7 +3,8 @@ #include #include - +#include +#include "Configuration.h" class WebApiPowerMeterClass { public: @@ -13,6 +14,7 @@ class WebApiPowerMeterClass { void onStatus(AsyncWebServerRequest* request); void onAdminGet(AsyncWebServerRequest* request); void onAdminPost(AsyncWebServerRequest* request); + void decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const; void onTestHttpRequest(AsyncWebServerRequest* request); AsyncWebServer* _server; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index f4a5a2393..c74bc67b3 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -420,7 +420,7 @@ bool ConfigurationClass::read() config.PowerMeter.Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0); strlcpy(config.PowerMeter.Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.PowerMeter.Http_Phase[i].Url)); - config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | Auth::none; + config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | PowerMeterHttpConfig::Auth::None; strlcpy(config.PowerMeter.Http_Phase[i].Username, powermeter_phase["username"] | "", sizeof(config.PowerMeter.Http_Phase[i].Username)); strlcpy(config.PowerMeter.Http_Phase[i].Password, powermeter_phase["password"] | "", sizeof(config.PowerMeter.Http_Phase[i].Password)); strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderKey)); diff --git a/src/HttpPowerMeter.cpp b/src/HttpPowerMeter.cpp index cb829e1d7..94402fad9 100644 --- a/src/HttpPowerMeter.cpp +++ b/src/HttpPowerMeter.cpp @@ -21,10 +21,10 @@ float HttpPowerMeterClass::getPower(int8_t phase) bool HttpPowerMeterClass::updateValues() { - const CONFIG_T& config = Configuration.get(); + auto const& config = Configuration.get(); for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.PowerMeter.Http_Phase[i]; + auto const& phaseConfig = config.PowerMeter.Http_Phase[i]; if (!phaseConfig.Enabled) { power[i] = 0.0; @@ -32,8 +32,7 @@ bool HttpPowerMeterClass::updateValues() } if (i == 0 || config.PowerMeter.HttpIndividualRequests) { - if (!queryPhase(i, phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout, - phaseConfig.JsonPath)) { + if (!queryPhase(i, phaseConfig)) { MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); return false; @@ -50,8 +49,7 @@ bool HttpPowerMeterClass::updateValues() return true; } -bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password, - const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath) +bool HttpPowerMeterClass::queryPhase(int phase, PowerMeterHttpConfig const& config) { //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 @@ -63,7 +61,7 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType String uri; String base64Authorization; uint16_t port; - extractUrlComponents(url, protocol, host, uri, port, base64Authorization); + extractUrlComponents(config.Url, protocol, host, uri, port, base64Authorization); IPAddress ipaddr((uint32_t)0); //first check if "host" is already an IP adress @@ -105,43 +103,42 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType wifiClient = std::make_unique(); } - return httpRequest(phase, *wifiClient, ipaddr.toString(), port, uri, https, authType, username, password, httpHeader, httpValue, timeout, jsonPath); + return httpRequest(phase, *wifiClient, ipaddr.toString(), port, uri, https, config); } -bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, Auth authType, const char* username, - const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath) +bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config) { if(!httpClient.begin(wifiClient, host, port, uri, https)){ snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); return false; } - prepareRequest(timeout, httpHeader, httpValue); - if (authType == Auth::digest) { + prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue); + if (config.AuthType == Auth_t::Digest) { const char *headers[1] = {"WWW-Authenticate"}; httpClient.collectHeaders(headers, 1); - } else if (authType == Auth::basic) { - String authString = username; + } else if (config.AuthType == Auth_t::Basic) { + String authString = config.Username; authString += ":"; - authString += password; + authString += config.Password; String auth = "Basic "; auth.concat(base64::encode(authString)); httpClient.addHeader("Authorization", auth); } int httpCode = httpClient.GET(); - if (httpCode == HTTP_CODE_UNAUTHORIZED && authType == Auth::digest) { + if (httpCode == HTTP_CODE_UNAUTHORIZED && config.AuthType == Auth_t::Digest) { // Handle authentication challenge if (httpClient.hasHeader("WWW-Authenticate")) { String authReq = httpClient.header("WWW-Authenticate"); - String authorization = getDigestAuth(authReq, String(username), String(password), "GET", String(uri), 1); + String authorization = getDigestAuth(authReq, String(config.Username), String(config.Password), "GET", String(uri), 1); httpClient.end(); if(!httpClient.begin(wifiClient, host, port, uri, https)){ snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), host.c_str()); return false; } - prepareRequest(timeout, httpHeader, httpValue); + prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue); httpClient.addHeader("Authorization", authorization); httpCode = httpClient.GET(); } diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 5a7e9bf22..88eeb069e 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -28,6 +28,19 @@ void WebApiPowerMeterClass::init(AsyncWebServer& server, Scheduler& scheduler) _server->on("/api/powermeter/testhttprequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpRequest, this, _1)); } +void WebApiPowerMeterClass::decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const +{ + config.Enabled = json["enabled"].as(); + strlcpy(config.Url, json["url"].as().c_str(), sizeof(config.Url)); + config.AuthType = json["auth_type"].as(); + strlcpy(config.Username, json["username"].as().c_str(), sizeof(config.Username)); + strlcpy(config.Password, json["password"].as().c_str(), sizeof(config.Password)); + strlcpy(config.HeaderKey, json["header_key"].as().c_str(), sizeof(config.HeaderKey)); + strlcpy(config.HeaderValue, json["header_value"].as().c_str(), sizeof(config.HeaderValue)); + config.Timeout = json["timeout"].as(); + strlcpy(config.JsonPath, json["json_path"].as().c_str(), sizeof(config.JsonPath)); +} + void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) { AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048); @@ -137,7 +150,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) return; } - if ((phase["auth_type"].as() != Auth::none) + if ((phase["auth_type"].as() != PowerMeterHttpConfig::Auth::None) && ( phase["username"].as().length() == 0 || phase["password"].as().length() == 0)) { retMsg["message"] = "Username or password must not be empty!"; response->setLength(); @@ -178,18 +191,9 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) JsonArray http_phases = root["http_phases"]; for (uint8_t i = 0; i < http_phases.size(); i++) { - JsonObject phase = http_phases[i].as(); - - config.PowerMeter.Http_Phase[i].Enabled = (i == 0 ? true : phase["enabled"].as()); - strlcpy(config.PowerMeter.Http_Phase[i].Url, phase["url"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Url)); - config.PowerMeter.Http_Phase[i].AuthType = phase["auth_type"].as(); - strlcpy(config.PowerMeter.Http_Phase[i].Username, phase["username"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Username)); - strlcpy(config.PowerMeter.Http_Phase[i].Password, phase["password"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Password)); - strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, phase["header_key"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].HeaderKey)); - strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, phase["header_value"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].HeaderValue)); - config.PowerMeter.Http_Phase[i].Timeout = phase["timeout"].as(); - strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, phase["json_path"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].JsonPath)); + decodeJsonPhaseConfig(http_phases[i].as(), config.PowerMeter.Http_Phase[i]); } + config.PowerMeter.Http_Phase[0].Enabled = true; WebApi.writeConfig(retMsg); @@ -252,10 +256,9 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) char response[256]; int phase = 0;//"absuing" index 0 of the float power[3] in HttpPowerMeter to store the result - if (HttpPowerMeter.queryPhase(phase, root["url"].as().c_str(), - root["auth_type"].as(), root["username"].as().c_str(), root["password"].as().c_str(), - root["header_key"].as().c_str(), root["header_value"].as().c_str(), root["timeout"].as(), - root["json_path"].as().c_str())) { + PowerMeterHttpConfig phaseConfig; + decodeJsonPhaseConfig(root.as(), phaseConfig); + if (HttpPowerMeter.queryPhase(phase, phaseConfig)) { retMsg["type"] = "success"; snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", HttpPowerMeter.getPower(phase + 1)); } else { From 247cfe712e8279dd8f4714a590cca983a853f68c Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Tue, 16 Apr 2024 22:51:00 +0200 Subject: [PATCH 50/70] HTTP power meter: prevent out-of-bound array access --- src/HttpPowerMeter.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/HttpPowerMeter.cpp b/src/HttpPowerMeter.cpp index 94402fad9..798a9a2f8 100644 --- a/src/HttpPowerMeter.cpp +++ b/src/HttpPowerMeter.cpp @@ -16,6 +16,8 @@ void HttpPowerMeterClass::init() float HttpPowerMeterClass::getPower(int8_t phase) { + if (phase < 1 || phase > POWERMETER_MAX_PHASES) { return 0.0; } + return power[phase - 1]; } From ede1abb5e61d61a97f59fa87635d4dcfad55fadc Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Tue, 16 Apr 2024 22:37:18 +0200 Subject: [PATCH 51/70] Feature: HTTP power meter: support mW/kW as units --- include/Configuration.h | 2 ++ include/HttpPowerMeter.h | 3 ++- src/Configuration.cpp | 2 ++ src/HttpPowerMeter.cpp | 20 ++++++++++++++++---- src/WebApi_powermeter.cpp | 4 +++- webapp/src/locales/de.json | 3 ++- webapp/src/locales/en.json | 5 +++-- webapp/src/types/PowerMeterConfig.ts | 1 + webapp/src/views/PowerMeterAdminView.vue | 13 +++++++++++++ 9 files changed, 44 insertions(+), 9 deletions(-) diff --git a/include/Configuration.h b/include/Configuration.h index 8d22248fd..570b7c3a1 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -64,6 +64,7 @@ struct INVERTER_CONFIG_T { struct POWERMETER_HTTP_PHASE_CONFIG_T { enum Auth { None, Basic, Digest }; + enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; bool Enabled; char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1]; Auth AuthType; @@ -73,6 +74,7 @@ struct POWERMETER_HTTP_PHASE_CONFIG_T { char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1]; uint16_t Timeout; char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1]; + Unit PowerUnit; }; using PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T; diff --git a/include/HttpPowerMeter.h b/include/HttpPowerMeter.h index 97e43a2cf..db79fe09b 100644 --- a/include/HttpPowerMeter.h +++ b/include/HttpPowerMeter.h @@ -7,6 +7,7 @@ #include "Configuration.h" using Auth_t = PowerMeterHttpConfig::Auth; +using Unit_t = PowerMeterHttpConfig::Unit; class HttpPowerMeterClass { public: @@ -25,7 +26,7 @@ class HttpPowerMeterClass { String extractParam(String& authReq, const String& param, const char delimit); String getcNonce(const int len); String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter); - bool tryGetFloatValueForPhase(int phase, const char* jsonPath); + bool tryGetFloatValueForPhase(int phase, const char* jsonPath, Unit_t unit); void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue); String sha256(const String& data); }; diff --git a/src/Configuration.cpp b/src/Configuration.cpp index c74bc67b3..f21571cbb 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -175,6 +175,7 @@ bool ConfigurationClass::write() powermeter_phase["header_value"] = config.PowerMeter.Http_Phase[i].HeaderValue; powermeter_phase["timeout"] = config.PowerMeter.Http_Phase[i].Timeout; powermeter_phase["json_path"] = config.PowerMeter.Http_Phase[i].JsonPath; + powermeter_phase["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit; } JsonObject powerlimiter = doc.createNestedObject("powerlimiter"); @@ -427,6 +428,7 @@ bool ConfigurationClass::read() strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderValue)); config.PowerMeter.Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT; strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.PowerMeter.Http_Phase[i].JsonPath)); + config.PowerMeter.Http_Phase[i].PowerUnit = powermeter_phase["unit"] | PowerMeterHttpConfig::Unit::Watts; } JsonObject powerlimiter = doc["powerlimiter"]; diff --git a/src/HttpPowerMeter.cpp b/src/HttpPowerMeter.cpp index 798a9a2f8..ada189829 100644 --- a/src/HttpPowerMeter.cpp +++ b/src/HttpPowerMeter.cpp @@ -42,7 +42,7 @@ bool HttpPowerMeterClass::updateValues() continue; } - if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath)) { + if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath, phaseConfig.PowerUnit)) { MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); return false; @@ -159,7 +159,9 @@ bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const S httpResponse = httpClient.getString(); // very unfortunate that we cannot parse WifiClient stream directly httpClient.end(); - return tryGetFloatValueForPhase(phase, jsonPath); + // TODO(schlimmchen): postpone calling tryGetFloatValueForPhase, as it + // will be called twice for each phase when doing separate requests. + return tryGetFloatValueForPhase(phase, config.JsonPath, config.PowerUnit); } String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) { @@ -217,7 +219,7 @@ String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& usernam return authorization; } -bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, const char* jsonPath) +bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, const char* jsonPath, Unit_t unit) { FirebaseJson json; json.setJsonData(httpResponse); @@ -227,7 +229,17 @@ bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, const char* jsonPa return false; } - power[phase] = value.to(); + power[phase] = value.to(); // this is supposed to be in Watts + switch (unit) { + case Unit_t::MilliWatts: + power[phase] /= 1000; + break; + case Unit_t::KiloWatts: + power[phase] *= 1000; + break; + default: + break; + } return true; } diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 88eeb069e..d66bfe433 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -39,6 +39,7 @@ void WebApiPowerMeterClass::decodeJsonPhaseConfig(JsonObject const& json, PowerM strlcpy(config.HeaderValue, json["header_value"].as().c_str(), sizeof(config.HeaderValue)); config.Timeout = json["timeout"].as(); strlcpy(config.JsonPath, json["json_path"].as().c_str(), sizeof(config.JsonPath)); + config.PowerUnit = json["unit"].as(); } void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) @@ -71,8 +72,9 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) phaseObject["password"] = String(config.PowerMeter.Http_Phase[i].Password); phaseObject["header_key"] = String(config.PowerMeter.Http_Phase[i].HeaderKey); phaseObject["header_value"] = String(config.PowerMeter.Http_Phase[i].HeaderValue); - phaseObject["json_path"] = String(config.PowerMeter.Http_Phase[i].JsonPath); phaseObject["timeout"] = config.PowerMeter.Http_Phase[i].Timeout; + phaseObject["json_path"] = String(config.PowerMeter.Http_Phase[i].JsonPath); + phaseObject["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit; } response->setLength(); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 13808ec58..14bc10483 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -574,7 +574,8 @@ "httpHeaderKeyDescription": "Ein individueller HTTP request header kann hier definiert werden. Das kann z.B. verwendet werden um einen eigenen Authorization header mitzugeben.", "httpHeaderValue": "Optional: HTTP request header - Wert", "httpJsonPath": "JSON Pfad", - "httpJsonPathDescription": "JSON Pfad um den Leistungswert zu finden. Es verwendet die Selektions-Syntax von mobizt/FirebaseJson. Beispiele gibt es unten.", + "httpJsonPathDescription": "JSON Pfad um den Leistungswert zu finden. Es verwendet die Selektions-Syntax von mobizt/FirebaseJson. Beispiele gibt es oben.", + "httpUnit": "Einheit", "httpTimeout": "Timeout", "testHttpRequest": "Testen" }, diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 727eea19a..b841448cd 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -578,8 +578,9 @@ "httpHeaderKey": "Optional: HTTP request header - Key", "httpHeaderKeyDescription": "A custom HTTP request header can be defined. Might be useful if you have to send something like a custom Authorization header.", "httpHeaderValue": "Optional: HTTP request header - Value", - "httpJsonPath": "Json path", - "httpJsonPathDescription": "JSON path to find the power value in the response. This uses the JSON path query syntax from mobizt/FirebaseJson. See below for some examples.", + "httpJsonPath": "JSON path", + "httpJsonPathDescription": "JSON path to find the power value in the response. This uses the JSON path query syntax from mobizt/FirebaseJson. See above for examples.", + "httpUnit": "Unit", "httpTimeout": "Timeout", "testHttpRequest": "Run test", "milliSeconds": "ms" diff --git a/webapp/src/types/PowerMeterConfig.ts b/webapp/src/types/PowerMeterConfig.ts index d6eb9ee7d..5b38faebb 100644 --- a/webapp/src/types/PowerMeterConfig.ts +++ b/webapp/src/types/PowerMeterConfig.ts @@ -9,6 +9,7 @@ export interface PowerMeterHttpPhaseConfig { header_value: string; json_path: string; timeout: number; + unit: number; } export interface PowerMeterConfig { diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index e19a6b896..51cde9de8 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -189,6 +189,19 @@ placeholder="total_power" :tooltip="$t('powermeteradmin.httpJsonPathDescription')" /> +
+ +
+ +
+
+
+ +
+ aria-describedby="upperPowerLimitDescription" min="100" max="3000" required/> W
+ +
+
+ + W +
+
+
Date: Wed, 24 Apr 2024 22:15:25 +0200 Subject: [PATCH 60/70] Fix: Return 404 (and nothing else) if file not found --- src/WebApi_config.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index a67be42fa..759b6b243 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -40,6 +40,7 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) requestFile = name; } else { request->send(404); + return; } } From 29403013f5c3abe4bfa318558cbddeab87e7a73b Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 24 Apr 2024 22:28:59 +0200 Subject: [PATCH 61/70] Fix: Device Manager shows 404 if no pin_mapping.json was available --- webapp/src/utils/authentication.ts | 6 ++++-- webapp/src/views/DeviceAdminView.vue | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/webapp/src/utils/authentication.ts b/webapp/src/utils/authentication.ts index e0e96b705..0f1debd03 100644 --- a/webapp/src/utils/authentication.ts +++ b/webapp/src/utils/authentication.ts @@ -65,7 +65,7 @@ export function login(username: string, password: string) { }); } -export function handleResponse(response: Response, emitter: Emitter>, router: Router) { +export function handleResponse(response: Response, emitter: Emitter>, router: Router, ignore_error: boolean = false) { return response.text().then(text => { const data = text && JSON.parse(text); if (!response.ok) { @@ -78,7 +78,9 @@ export function handleResponse(response: Response, emitter: Emitter handleResponse(response, this.$emitter, this.$router)) + .then((response) => handleResponse(response, this.$emitter, this.$router, true)) .then( (data) => { this.pinMappingList = data; @@ -246,6 +246,9 @@ export default defineComponent({ .then( (data) => { this.deviceConfigList = data; + if (this.deviceConfigList.curPin.name === "") { + this.deviceConfigList.curPin.name = "Default"; + } this.dataLoading = false; } ) From 5ab4b6d38e3a5f189471fef87334f26efea38a85 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 24 Apr 2024 22:31:13 +0200 Subject: [PATCH 62/70] webapp: update dependencies --- webapp/package.json | 16 +-- webapp/yarn.lock | 307 ++++++++++++++++++++++++-------------------- 2 files changed, 174 insertions(+), 149 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index 86b30904f..fbba65f1e 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -18,9 +18,9 @@ "mitt": "^3.0.1", "sortablejs": "^1.15.2", "spark-md5": "^3.0.2", - "vue": "^3.4.21", - "vue-i18n": "^9.12.0", - "vue-router": "^4.3.0" + "vue": "^3.4.25", + "vue-i18n": "^9.13.1", + "vue-router": "^4.3.2" }, "devDependencies": { "@intlify/unplugin-vue-i18n": "^4.0.0", @@ -33,16 +33,16 @@ "@vitejs/plugin-vue": "^5.0.4", "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", - "eslint": "^9.0.0", - "eslint-plugin-vue": "^9.24.1", + "eslint": "^9.1.1", + "eslint-plugin-vue": "^9.25.0", "npm-run-all": "^4.1.5", "pulltorefreshjs": "^0.1.22", "sass": "^1.75.0", - "terser": "^5.30.3", + "terser": "^5.30.4", "typescript": "^5.4.5", - "vite": "^5.2.8", + "vite": "^5.2.10", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.5.0", - "vue-tsc": "^2.0.13" + "vue-tsc": "^2.0.14" } } diff --git a/webapp/yarn.lock b/webapp/yarn.lock index e27e24591..b1a1203ca 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -17,6 +17,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== +"@babel/parser@^7.24.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88" + integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== + "@esbuild/aix-ppc64@0.20.2": version "0.20.2" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" @@ -171,15 +176,15 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.0.0.tgz#1a9e4b4c96d8c7886e0110ed310a0135144a1691" - integrity sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ== +"@eslint/js@9.1.1": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.1.1.tgz#eb0f82461d12779bbafc1b5045cde3143d350a8a" + integrity sha512-5WoDz3Y19Bg2BnErkZTp0en+c/i9PvgFS7MBe1+m60HjFr0hrphlAGp4yzI7pxpt4xShln4ZyYp4neJm8hmOkQ== -"@humanwhocodes/config-array@^0.12.3": - version "0.12.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.12.3.tgz#a6216d90f81a30bedd1d4b5d799b47241f318072" - integrity sha512-jsNnTBlMWuTpDkeE3on7+dWJi0D6fdDfeANj/w7MpS8ztROCoLvIO2nG0CcFj+E4k8j4QrSTh4Oryi3i2G669g== +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== dependencies: "@humanwhocodes/object-schema" "^2.0.3" debug "^4.3.1" @@ -195,6 +200,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@humanwhocodes/retry@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.2.3.tgz#c9aa036d1afa643f1250e83150f39efb3a15a631" + integrity sha512-X38nUbachlb01YMlvPFojKoiXq+LzZvuSce70KPMPdeM1Rj03k4dR7lDslhbqXn3Ang4EU3+EAmwEAsbrjHW3g== + "@intlify/bundle-utils@^8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@intlify/bundle-utils/-/bundle-utils-8.0.0.tgz#4e05153ac031bfc7adef70baedc9b0744a93adfd" @@ -210,20 +220,20 @@ source-map-js "^1.0.1" yaml-eslint-parser "^1.2.2" -"@intlify/core-base@9.12.0": - version "9.12.0" - resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.12.0.tgz#79f43faa8eb1f3b2bfe569a9fbae9bc50908d311" - integrity sha512-6EnWQXHnCh2bMiXT5N/IWwkcYQXjmF8nnEZ3YhTm23h1ZfOylz83D7pJYhcU8CsTiEdgbGiNdqyZPKwrHw03Ng== +"@intlify/core-base@9.13.1": + version "9.13.1" + resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.13.1.tgz#bd1f38e665095993ef9b67aeeb794f3cabcb515d" + integrity sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w== dependencies: - "@intlify/message-compiler" "9.12.0" - "@intlify/shared" "9.12.0" + "@intlify/message-compiler" "9.13.1" + "@intlify/shared" "9.13.1" -"@intlify/message-compiler@9.12.0": - version "9.12.0" - resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.12.0.tgz#5e152344853c29369911bd5e541e061b09218333" - integrity sha512-2c6VwhvVJ1nur+2cN2NjdrmrV6vXjvyxYVvtUYMXKsWSUwoNURHGds0xJVJmWxbF8qV9oGepcVV6xl9bvadEIg== +"@intlify/message-compiler@9.13.1": + version "9.13.1" + resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.13.1.tgz#ff8129badf77db3fb648b8d3cceee87c8033ed0a" + integrity sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w== dependencies: - "@intlify/shared" "9.12.0" + "@intlify/shared" "9.13.1" source-map-js "^1.0.2" "@intlify/message-compiler@^9.4.0": @@ -234,10 +244,10 @@ "@intlify/shared" "9.4.0" source-map-js "^1.0.2" -"@intlify/shared@9.12.0": - version "9.12.0" - resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.12.0.tgz#993383b6a98c8e37a1fa184a677eb39635a14a1c" - integrity sha512-uBcH55x5CfZynnerWHQxrXbT6yD6j6T7Nt+R2+dHAOAneoMd6BoGvfEzfYscE94rgmjoDqdr+PdGDBLk5I5EjA== +"@intlify/shared@9.13.1": + version "9.13.1" + resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.13.1.tgz#202741d11ece1a9c7480bfd3f27afcf9cb8f72e4" + integrity sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ== "@intlify/shared@9.4.0", "@intlify/shared@^9.4.0": version "9.4.0" @@ -557,26 +567,26 @@ resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37" integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ== -"@volar/language-core@2.2.0-alpha.8": - version "2.2.0-alpha.8" - resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.2.0-alpha.8.tgz#74120a27ff2498ad297e86d17be95a9c7e1b46f5" - integrity sha512-Ew1Iw7/RIRNuDLn60fWJdOLApAlfTVPxbPiSLzc434PReC9kleYtaa//Wo2WlN1oiRqneW0pWQQV0CwYqaimLQ== +"@volar/language-core@2.2.0-alpha.10": + version "2.2.0-alpha.10" + resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.2.0-alpha.10.tgz#e77db9b2ef4826cc55cf929289933d018c48e56c" + integrity sha512-njVJLtpu0zMvDaEk7K5q4BRpOgbyEUljU++un9TfJoJNhxG0z/hWwpwgTRImO42EKvwIxF3XUzeMk+qatAFy7Q== dependencies: - "@volar/source-map" "2.2.0-alpha.8" + "@volar/source-map" "2.2.0-alpha.10" -"@volar/source-map@2.2.0-alpha.8": - version "2.2.0-alpha.8" - resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.2.0-alpha.8.tgz#ca090f828fbef7e09ea06a636c41a06aa2afe153" - integrity sha512-E1ZVmXFJ5DU4fWDcWHzi8OLqqReqIDwhXvIMhVdk6+VipfMVv4SkryXu7/rs4GA/GsebcRyJdaSkKBB3OAkIcA== +"@volar/source-map@2.2.0-alpha.10": + version "2.2.0-alpha.10" + resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.2.0-alpha.10.tgz#d055232eb2a24fb4678db578b55ec095c9925dc3" + integrity sha512-nrdWApVkP5cksAnDEyy1JD9rKdwOJsEq1B+seWO4vNXmZNcxQQCx4DULLBvKt7AzRUAQiAuw5aQkb9RBaSqdVA== dependencies: muggle-string "^0.4.0" -"@volar/typescript@2.2.0-alpha.8": - version "2.2.0-alpha.8" - resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.2.0-alpha.8.tgz#83a056c52995b4142364be3dda41d955a96f7356" - integrity sha512-RLbRDI+17CiayHZs9HhSzlH0FhLl/+XK6o2qoiw2o2GGKcyD1aDoY6AcMd44acYncTOrqoTNoY6LuCiRyiJiGg== +"@volar/typescript@2.2.0-alpha.10": + version "2.2.0-alpha.10" + resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.2.0-alpha.10.tgz#14c002a3549ff3adcf9306933f4bf81e80422eff" + integrity sha512-GCa0vTVVdA9ULUsu2Rx7jwsIuyZQPvPVT9o3NrANTbYv+523Ao1gv3glC5vzNSDPM6bUl37r94HbCj7KINQr+g== dependencies: - "@volar/language-core" "2.2.0-alpha.8" + "@volar/language-core" "2.2.0-alpha.10" path-browserify "^1.0.1" "@vue/compiler-core@3.2.47": @@ -600,6 +610,17 @@ estree-walker "^2.0.2" source-map-js "^1.0.2" +"@vue/compiler-core@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.25.tgz#691f59ee5014f6f2a2488fd4465f892e1e82f729" + integrity sha512-Y2pLLopaElgWnMNolgG8w3C5nNUVev80L7hdQ5iIKPtMJvhVpG0zhnBG/g3UajJmZdvW0fktyZTotEHD1Srhbg== + dependencies: + "@babel/parser" "^7.24.4" + "@vue/shared" "3.4.25" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + "@vue/compiler-dom@3.2.47": version "3.2.47" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz#a0b06caf7ef7056939e563dcaa9cbde30794f305" @@ -608,7 +629,15 @@ "@vue/compiler-core" "3.2.47" "@vue/shared" "3.2.47" -"@vue/compiler-dom@3.4.21", "@vue/compiler-dom@^3.4.0": +"@vue/compiler-dom@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.25.tgz#b367e0c84e11d9e9f70beabdd6f6b2277fde375f" + integrity sha512-Ugz5DusW57+HjllAugLci19NsDK+VyjGvmbB2TXaTcSlQxwL++2PETHx/+Qv6qFwNLzSt7HKepPe4DcTE3pBWg== + dependencies: + "@vue/compiler-core" "3.4.25" + "@vue/shared" "3.4.25" + +"@vue/compiler-dom@^3.4.0": version "3.4.21" resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz#0077c355e2008207283a5a87d510330d22546803" integrity sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA== @@ -616,20 +645,20 @@ "@vue/compiler-core" "3.4.21" "@vue/shared" "3.4.21" -"@vue/compiler-sfc@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz#4af920dc31ab99e1ff5d152b5fe0ad12181145b2" - integrity sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ== +"@vue/compiler-sfc@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.25.tgz#ceab148f81571c8b251e8a8b75a9972addf1db8b" + integrity sha512-m7rryuqzIoQpOBZ18wKyq05IwL6qEpZxFZfRxlNYuIPDqywrXQxgUwLXIvoU72gs6cRdY6wHD0WVZIFE4OEaAQ== dependencies: - "@babel/parser" "^7.23.9" - "@vue/compiler-core" "3.4.21" - "@vue/compiler-dom" "3.4.21" - "@vue/compiler-ssr" "3.4.21" - "@vue/shared" "3.4.21" + "@babel/parser" "^7.24.4" + "@vue/compiler-core" "3.4.25" + "@vue/compiler-dom" "3.4.25" + "@vue/compiler-ssr" "3.4.25" + "@vue/shared" "3.4.25" estree-walker "^2.0.2" - magic-string "^0.30.7" - postcss "^8.4.35" - source-map-js "^1.0.2" + magic-string "^0.30.10" + postcss "^8.4.38" + source-map-js "^1.2.0" "@vue/compiler-sfc@^3.2.47": version "3.2.47" @@ -655,13 +684,13 @@ "@vue/compiler-dom" "3.2.47" "@vue/shared" "3.2.47" -"@vue/compiler-ssr@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz#b84ae64fb9c265df21fc67f7624587673d324fef" - integrity sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q== +"@vue/compiler-ssr@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.25.tgz#7fdd540bfdf2d4a3d6cb107b7ba4c77228d36331" + integrity sha512-H2ohvM/Pf6LelGxDBnfbbXFPyM4NE3hrw0e/EpwuSiYu8c819wx+SVGdJ65p/sFrYDd6OnSDxN1MB2mN07hRSQ== dependencies: - "@vue/compiler-dom" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/compiler-dom" "3.4.25" + "@vue/shared" "3.4.25" "@vue/devtools-api@^6.5.0": version "6.5.0" @@ -682,12 +711,12 @@ "@typescript-eslint/parser" "^7.1.1" vue-eslint-parser "^9.3.1" -"@vue/language-core@2.0.13": - version "2.0.13" - resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.13.tgz#2d1638b882011187b4b57115425d52b0901acab5" - integrity sha512-oQgM+BM66SU5GKtUMLQSQN0bxHFkFpLSSAiY87wVziPaiNQZuKVDt/3yA7GB9PiQw0y/bTNL0bOc0jM/siYjKg== +"@vue/language-core@2.0.14": + version "2.0.14" + resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.14.tgz#99d1dcd7df8a859e12606e80863b3cb4cf045f9e" + integrity sha512-3q8mHSNcGTR7sfp2X6jZdcb4yt8AjBXAfKk0qkZIh7GAJxOnoZ10h5HToZglw4ToFvAnq+xu/Z2FFbglh9Icag== dependencies: - "@volar/language-core" "2.2.0-alpha.8" + "@volar/language-core" "2.2.0-alpha.10" "@vue/compiler-dom" "^3.4.0" "@vue/shared" "^3.4.0" computeds "^0.0.1" @@ -706,37 +735,37 @@ estree-walker "^2.0.2" magic-string "^0.25.7" -"@vue/reactivity@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.21.tgz#affd3415115b8ebf4927c8d2a0d6a24bccfa9f02" - integrity sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw== +"@vue/reactivity@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.25.tgz#74983b146e06ce3341d15382669350125375d36f" + integrity sha512-mKbEtKr1iTxZkAG3vm3BtKHAOhuI4zzsVcN0epDldU/THsrvfXRKzq+lZnjczZGnTdh3ojd86/WrP+u9M51pWQ== dependencies: - "@vue/shared" "3.4.21" + "@vue/shared" "3.4.25" -"@vue/runtime-core@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.21.tgz#3749c3f024a64c4c27ecd75aea4ca35634db0062" - integrity sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA== +"@vue/runtime-core@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.25.tgz#c5545d469ae0827dc471a1376f97c6ace41081ec" + integrity sha512-3qhsTqbEh8BMH3pXf009epCI5E7bKu28fJLi9O6W+ZGt/6xgSfMuGPqa5HRbUxLoehTNp5uWvzCr60KuiRIL0Q== dependencies: - "@vue/reactivity" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/reactivity" "3.4.25" + "@vue/shared" "3.4.25" -"@vue/runtime-dom@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz#91f867ef64eff232cac45095ab28ebc93ac74588" - integrity sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw== +"@vue/runtime-dom@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.25.tgz#9bc195e4860edcd0db4303cbba5a160922b963fd" + integrity sha512-ode0sj77kuwXwSc+2Yhk8JMHZh1sZp9F/51wdBiz3KGaWltbKtdihlJFhQG4H6AY+A06zzeMLkq6qu8uDSsaoA== dependencies: - "@vue/runtime-core" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/runtime-core" "3.4.25" + "@vue/shared" "3.4.25" csstype "^3.1.3" -"@vue/server-renderer@3.4.21": - version "3.4.21" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.21.tgz#150751579d26661ee3ed26a28604667fa4222a97" - integrity sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg== +"@vue/server-renderer@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.25.tgz#6cfc96ee631104951d5d6c09a8f1e7cef3ef3972" + integrity sha512-8VTwq0Zcu3K4dWV0jOwIVINESE/gha3ifYCOKEhxOj6MEl5K5y8J8clQncTcDhKF+9U765nRw4UdUEXvrGhyVQ== dependencies: - "@vue/compiler-ssr" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/compiler-ssr" "3.4.25" + "@vue/shared" "3.4.25" "@vue/shared@3.2.47": version "3.2.47" @@ -748,6 +777,11 @@ resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.21.tgz#de526a9059d0a599f0b429af7037cd0c3ed7d5a1" integrity sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g== +"@vue/shared@3.4.25": + version "3.4.25" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.25.tgz#243ba8543e7401751e0ca319f75a80f153edd273" + integrity sha512-k0yappJ77g2+KNrIaF0FFnzwLvUBLUYr8VOwz+/6vLsmItFp51AcxLL7Ey3iPd7BIRyWPOcqUjMnm7OkahXllA== + "@vue/tsconfig@^0.5.1": version "0.5.1" resolved "https://registry.yarnpkg.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz#3124ec16cc0c7e04165b88dc091e6b97782fffa9" @@ -1124,10 +1158,10 @@ escodegen@^2.1.0: optionalDependencies: source-map "~0.6.1" -eslint-plugin-vue@^9.24.1: - version "9.24.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.24.1.tgz#0d90330c939f9dd2f4c759da5a2ad91dc1c8bac4" - integrity sha512-wk3SuwmS1pZdcuJlokGYEi/buDOwD6KltvhIZyOnpJ/378dcQ4zchu9PAMbbLAaydCz1iYc5AozszcOOgZIIOg== +eslint-plugin-vue@^9.25.0: + version "9.25.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.25.0.tgz#615cb7bb6d0e2140d21840b9aa51dce69e803e7a" + integrity sha512-tDWlx14bVe6Bs+Nnh3IGrD+hb11kf2nukfm6jLsmJIhmiRQ1SUaksvwY9U5MvPB0pcrg0QK0xapQkfITs3RKOA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" globals "^13.24.0" @@ -1174,17 +1208,18 @@ eslint-visitor-keys@^4.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== -eslint@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.0.0.tgz#6270548758e390343f78c8afd030566d86927d40" - integrity sha512-IMryZ5SudxzQvuod6rUdIUz29qFItWx281VhtFVc2Psy/ZhlCeD/5DT6lBIJ4H3G+iamGJoTln1v+QSuPw0p7Q== +eslint@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.1.1.tgz#39ec657ccd12813cb4a1dab2f9229dcc6e468271" + integrity sha512-b4cRQ0BeZcSEzPpY2PjFY70VbO32K7BStTGtBsnIGdTSEEQzBi8hPBcGQmTG2zUvFr9uLe0TK42bw8YszuHEqg== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^3.0.2" - "@eslint/js" "9.0.0" - "@humanwhocodes/config-array" "^0.12.3" + "@eslint/js" "9.1.1" + "@humanwhocodes/config-array" "^0.13.0" "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.2.3" "@nodelib/fs.walk" "^1.2.8" ajv "^6.12.4" chalk "^4.0.0" @@ -1200,7 +1235,6 @@ eslint@^9.0.0: file-entry-cache "^8.0.0" find-up "^5.0.0" glob-parent "^6.0.2" - graphemer "^1.4.0" ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" @@ -1786,10 +1820,10 @@ magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" -magic-string@^0.30.7: - version "0.30.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.7.tgz#0cecd0527d473298679da95a2d7aeb8c64048505" - integrity sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA== +magic-string@^0.30.10: + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" @@ -2056,15 +2090,6 @@ postcss@^8.1.10: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.35: - version "8.4.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" - integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.0.2" - postcss@^8.4.38: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" @@ -2381,10 +2406,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -terser@^5.30.3: - version "5.30.3" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.3.tgz#f1bb68ded42408c316b548e3ec2526d7dd03f4d2" - integrity sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA== +terser@^5.30.4: + version "5.30.4" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.30.4.tgz#62b4d16a819424e6317fd5ceffb4ee8dc769803a" + integrity sha512-xRdd0v64a8mFK9bnsKVdoNP9GQIKUAaJPTaqEQDL4w/J8WaW4sWXXoMZ+6SimPkfT5bElreXf8m9HnmPc3E1BQ== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -2494,10 +2519,10 @@ vite-plugin-css-injected-by-js@^3.5.0: resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.0.tgz#784c0f42c2b42155eb4c726c6addfa24aba9f4fb" integrity sha512-d0QaHH9kS93J25SwRqJNEfE29PSuQS5jn51y9N9i2Yoq0FRO7rjuTeLvjM5zwklZlRrIn6SUdtOEDKyHokgJZg== -vite@^5.2.8: - version "5.2.8" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.8.tgz#a99e09939f1a502992381395ce93efa40a2844aa" - integrity sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA== +vite@^5.2.10: + version "5.2.10" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.10.tgz#2ac927c91e99d51b376a5c73c0e4b059705f5bd7" + integrity sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw== dependencies: esbuild "^0.20.1" postcss "^8.4.38" @@ -2531,19 +2556,19 @@ vue-eslint-parser@^9.4.2: lodash "^4.17.21" semver "^7.3.6" -vue-i18n@^9.12.0: - version "9.12.0" - resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.12.0.tgz#8d073b3d7b92e822dcc3268946af4ecf14b778b3" - integrity sha512-rUxCKTws8NH3XP98W71GA7btAQdAuO7j6BC5y5s1bTNQYo/CIgZQf+p7d1Zo5bo/3v8TIq9aSUMDjpfgKsC3Uw== +vue-i18n@^9.13.1: + version "9.13.1" + resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.13.1.tgz#a292c8021b7be604ebfca5609ae1f8fafe5c36d7" + integrity sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg== dependencies: - "@intlify/core-base" "9.12.0" - "@intlify/shared" "9.12.0" + "@intlify/core-base" "9.13.1" + "@intlify/shared" "9.13.1" "@vue/devtools-api" "^6.5.0" -vue-router@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.3.0.tgz#d5913f27bf68a0a178ee798c3c88be471811a235" - integrity sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ== +vue-router@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.3.2.tgz#08096c7765dacc6832f58e35f7a081a8b34116a7" + integrity sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q== dependencies: "@vue/devtools-api" "^6.5.1" @@ -2555,25 +2580,25 @@ vue-template-compiler@^2.7.14: de-indent "^1.0.2" he "^1.2.0" -vue-tsc@^2.0.13: - version "2.0.13" - resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.13.tgz#6ee557705456442e0f43ec0d1774ebf5ffec54f1" - integrity sha512-a3nL3FvguCWVJUQW/jFrUxdeUtiEkbZoQjidqvMeBK//tuE2w6NWQAbdrEpY2+6nSa4kZoKZp8TZUMtHpjt4mQ== +vue-tsc@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.14.tgz#5a8a652bcba30fa6fd8f7ac6af5df8e387f25cd8" + integrity sha512-DgAO3U1cnCHOUO7yB35LENbkapeRsBZ7Ugq5hGz/QOHny0+1VQN8eSwSBjYbjLVPfvfw6EY7sNPjbuHHUhckcg== dependencies: - "@volar/typescript" "2.2.0-alpha.8" - "@vue/language-core" "2.0.13" + "@volar/typescript" "2.2.0-alpha.10" + "@vue/language-core" "2.0.14" semver "^7.5.4" -vue@^3.4.21: - version "3.4.21" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.21.tgz#69ec30e267d358ee3a0ce16612ba89e00aaeb731" - integrity sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA== +vue@^3.4.25: + version "3.4.25" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.25.tgz#e59d4ed36389647b52ff2fd7aa84bb6691f4205b" + integrity sha512-HWyDqoBHMgav/OKiYA2ZQg+kjfMgLt/T0vg4cbIF7JbXAjDexRf5JRg+PWAfrAkSmTd2I8aPSXtooBFWHB98cg== dependencies: - "@vue/compiler-dom" "3.4.21" - "@vue/compiler-sfc" "3.4.21" - "@vue/runtime-dom" "3.4.21" - "@vue/server-renderer" "3.4.21" - "@vue/shared" "3.4.21" + "@vue/compiler-dom" "3.4.25" + "@vue/compiler-sfc" "3.4.25" + "@vue/runtime-dom" "3.4.25" + "@vue/server-renderer" "3.4.25" + "@vue/shared" "3.4.25" webpack-sources@^3.2.3: version "3.2.3" From 5a93a7e4b93cb77df1f92ee12ed2bf6ee1cc3f2b Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 24 Apr 2024 22:33:37 +0200 Subject: [PATCH 63/70] Updated timezone config --- webapp/public/zones.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/public/zones.json b/webapp/public/zones.json index 2d449fe26..ad90ea014 100644 --- a/webapp/public/zones.json +++ b/webapp/public/zones.json @@ -180,7 +180,7 @@ "America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24", "America/Santo_Domingo":"AST4", "America/Sao_Paulo":"<-03>3", -"America/Scoresbysund":"<-01>1<+00>,M3.5.0/0,M10.5.0/1", +"America/Scoresbysund":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0", "America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0", "America/St_Barthelemy":"AST4", "America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0", @@ -200,7 +200,7 @@ "America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0", "America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0", "America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0", -"Antarctica/Casey":"<+11>-11", +"Antarctica/Casey":"<+08>-8", "Antarctica/Davis":"<+07>-7", "Antarctica/DumontDUrville":"<+10>-10", "Antarctica/Macquarie":"AEST-10AEDT,M10.1.0,M4.1.0/3", @@ -210,10 +210,10 @@ "Antarctica/Rothera":"<-03>3", "Antarctica/Syowa":"<+03>-3", "Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3", -"Antarctica/Vostok":"<+06>-6", +"Antarctica/Vostok":"<+05>-5", "Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3", "Asia/Aden":"<+03>-3", -"Asia/Almaty":"<+06>-6", +"Asia/Almaty":"<+05>-5", "Asia/Amman":"<+03>-3", "Asia/Anadyr":"<+12>-12", "Asia/Aqtau":"<+05>-5", From 4623839425db04ab13a8eea5f44ee2dca2c5d75d Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 24 Apr 2024 22:34:39 +0200 Subject: [PATCH 64/70] webapp: add app.js.gz --- webapp_dist/js/app.js.gz | Bin 182898 -> 183051 bytes webapp_dist/zones.json.gz | Bin 4030 -> 4023 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index 54b00bf1a014b1c75cba753f6b160f004d68731a..80f4a384b880bb52a2d63e86ce2a46d461cd55f8 100644 GIT binary patch delta 128032 zcmV(zK<2;llnaZN3$V@5fBPv&UJneEQ?%r+uC4|Rb7aYGtID#~maD3(SiCBTNRlXn z305K@Stj`cvz-^X=iAKqTg~F`zTN1|d(DZ6%tV5uT##Hncg~zy*dl=Z|0gms;ujIG z5c4&?{!eq0Oy1p*+@2nO>ll;WsgU2XB>T)Gu=NlC22=3iMSMq^e?Jo!&5r$}*R5xC zo45OatyPW15(fyX`p`;8X=@IvBQ{dHWSb1|A_v*ES*k|zux4SGG(F2dX<%0`@9rH! zo%H(tAsKwVH^BM2(kgdTKX==*51}g>Uv$A58YJEsdf-;_`acP6tS0KO3wM)O``eT9p1E7B8#rX}7i(E9*hrNl@!frMf z`%lF7;N-j3?t004e^kkP4v7HswgWJUXiBI+TbSIpwo7_pf89hizMD6WQgT>IRLNHZ zEvk?LR1TWt-$viT=2;1G&T^-&+#gU}P~3i66$PcE#W|y>pErKf?ng>NzDF=&v5}Hg zSekRDlg9ATf5b^s=g<$P>HOBWF9N=exGmugzepPs_BYobDYc&XT1f}FZ0ajgFDKp_ z{a^p9vE)9Of0xgV0Z~IgAJ-o-IbwniPRJ1B)c)a*PjKPf0a(P9Qb&S71f2!iv z?E2#@Pc?17eU!rQ8cT`3(}?kkbn}L>fm35S`CZY}TRPTdl^KwvQLp0EoSH9qJZ}gb zh$-mCN%1CbX6=1)`ey%Ft17@p)$@j~Uvy&X(>tz4f752y6HKbL);yjk5S}(G!UZ(_ zP(L*7YD;IJ}s8IB0C<6Fr?w^nt3c@eJ-WxF-HXmQ|A@_tW&Uoo z*2b!KfAZZ)=LV??EzSJH%h#%!P3n9z4Z2f3Xn9mSOH5e4Mx(Bsof=Z|Dp$A558AS| zG7hZE)TS-K5rcQ542@i`w8Q?E`_(`v#x;z?RQ7YmGgB?S!+Xe ze>C4ulE(2y_N~o>@gp>gi+2qfp*5=!*MINdW5q8UdapgHEJZ*?2`HLaQ_Ms0Io<`q zbfXs1mYBWK?FLAX(DDAk+Hx{9sM$*T{WY_V1pUrz{_cv|w)VTW(6y!WdqZ=#+Nx65 zV{r*(Mo`WF2zJ1Dsj7LQI4x(hxM?h;f9sD0s19!w{hXrf^`?sK3yTO%XZ%Ah8oC{- znUvJ>A6CXE4c(1@!dS(H|GsT1&?3xxO;~(4UI<*&H{@t4kNplmdyD)a{@g6%CAej} zWxpNAfTyJZTd2lYJQRw9cQfrH?z9w5qyHV`B}wsX1-#1Dz6p!cP%x!hHiv~be>9b^ zX=p=3k`6#wYGq@`Rjy|I9$I;e1iBj?+zBS)}524YI8_<`%sFn*4a&m_< z5O{n2@jRbsbg(|^z(?ije+c3h-okG=ZW1)g(2mobg=>kTyiF^=PZQF;kDM+a&-1upY@~yH4z>nKEQ%IJkAXYp&OC;MC5^p$^)Ld8V3bwcc`J0W$hd*z|59 z4YBV~6rsa|&MgN7HFH{7!2W=l`2!ZoUA6VJGWcxHB?d5TIQTQdB$m>#jiR9FjpL_l zgyX!n1m19D;Q3vae<^^nZTJWF46tNr;}OB+@yLq#k0j_FZz0~P((6m1@fJGbk{q=! z{%E)DClsRV76m>Vk2#1!z}T%NoznH--@aws_z?J%XYXeAu2jO>O^e#3VZXO-nj1=* zO8Q4h&ib33o_U$^4?Acpc_CvotgD6%4puuMt-e!9Q^4T;e|5vmSC|s_HfyC>OZ;s8 z=ZNc%Bed2btj=8Kqw9}wxL^Or`ecHO=8|VNCNV@V#%Xu?2~9srRnO{HQ#ZiTrkns4 zoZWz(47#nC2;Zyh9?`+;^~XtUIGQA%qo>ybI0?7vZe^CrFZhSrICHq^5Tx$s`_KZ8 z2x->3Fjhuje_;v$2dSAnf=z5fFJto1sw_7e&;8a@7v3={p6q)x}wB zqZLb3X(BYL^6n<4Wy=P`P-RWra|d6@PHDHWhT3r3;1tjpM*FfdS|f`vSi?CjS6Ch8 zdvuzzUKK39EndQ@BM4EQTz?eTA0^!zXR!vO#u-@ie*iwLm4RP8@M$Ia4B6dkV>qrS zMYT&EhiS$wuMQ=hXW3^+TC^APJH~u7!D!_8Bu2PSv$=co{Lz=KS6jT3C-<*E&eAH3 z@WD*orr?9)w~zk3ySv}0?5fd?*9$=A(7fZt@s6=;ExK^CFQp;haP=q1wQC@atD-2! zk6o!hnr2!lsk^afuKoxNl8(n5_<6^LeVlG;Km z*3PaJ(cl?G`nTR2QziMJBD2-Rkn?S zn#Vo`Lvz>@U}~O=?5b53Rg8nwM4XOslVtfNPfYksiP28c^M)R~c57dUv#u5x*=Jx7 z>vg;Kzsyq60&!f{iVe&S_8&Ov!Y!8;Q=W7;`u0OCMN_8)Z!xN_4li7_DlJ-hsS5C$ ze+XSMJ(C#hITaNA+UAqAA$2diEoaOq8aG51wE-8BoKrmRixgK&D;8IzFNA{1%-wCr zFFt%OT5~1;S*_u1L;p>DuMWO?EfU z_FP=tT=a`{GX0FQU&N)lx$xuo{pSpee?|}ARQ!q+I_R6`VDA(IZp{x+Q4a~X4<+9! zlKZjijn45%YE)@=E=*tY<~+V#X!$qSALn2lPnIL=1)8Gn?9gZ30bjk#$SRcDTtHje zjjG!t8CF%%>>;+j@A|&KkHy^&7=%D6S}{1;qhbnfD5Ab;Rrg^oFdB4g|D@YGf7;Gtd^`+}~u4}{^nn5WVhl7&Z>Y^}Gq~x{8u5Oc^Zu8{B7Myj%g&Eu+Ll6_{ zT787@{<=Ps2ocg7;gy=-c@G$1&?`0n1MYEinX7!1JAfmAk=z~{v;py9&qGIJ6ukQs7ohvuTi9@Jnn-%S5`3=kqKzeA?^h&yNm{fA(KC1%Zhq zmPsp#FGb7$+iv&k|MUN#v-Q>fcW;jl8kX}aE>#Nr6_vBM`^Xe?aP<9Avoa3Si_{eG zx35h8{nNLvU%ftV+>-r5$z0^KT5D{lj$=_0((KfL&^|O@zc!DybtPG3rsdb@HrQ3# z;9ghd9r1irs4L|)?m_Pif0kRr3fd9ijZ3RW0MC@F%WH+*`LzEYqylpQH+;~?I$iRM zwV^jozU>s?nXR4v*`_&zX-2YDq^%hIRn^?AlP`DIWcwJ|0raMp=7PbwIf@HwU|v8w z6)EbNtquj+TqY&&wqE6me{jXa#;6=D$1B@bof91lj)DES{>T|He-16HQLcsI+D4yi!WvC+3CrjAEB5JI$3aShz$eKA$^;-F0-*LUR^_ zGWq{4GB!Bht(F;hFWsMi{;u3^O0hqOQ{ooUp?@bdN4?O-f2WUj2{Gp)L02N+0R#pb z&y!l{56KI~TLil6w!W)E<0ibdiyFCfoT3ZFRHR)avjKOo&HOA=S_foxoHV^6>Mb-o zd_}NE$ayOo4LMsTM+N)VR3C8PcP9BZ^f;9A2he-td#Wl}kJfKW-In026#+QS+ZAzF zASN{Ssm$MTe~ClIo4P8k1{Nxy5~^POKY9YW1+?!$JhF8G za%p5Kfk7HoXkw9zs~P`1L0fn?!I)cwY3YYXE&W_{z0Kdat+g^(>$fr`jwAjxF9Gb$ z1@gi1>|BeWQ2h-Y2*wC22#ya}iaJok;PM^@-Tw;QfB(8M)MtVEiR`p$*#5D>_S45( zApDUkSvrXbt(3bC9-D&x9ST{)|Hq93t7{ni!Tt_pq!~04v6oyGT#V$^Tm>7@ero7# ziEy>%p`uUX{BPR@g7HH^p;OYgyrGs50?Doy@$ynxs(<_Q^yLq4O)0nC(Cb-mxtCx5 z@Rrs0e=vC~E;gve%S-iIWU087^XV#xWWwF1Qc2Beq%O@898E{8?@Aju6W}nvmG{$( z(wXXQz2ot%yWm8gT>v4Q%h!%0zyS_i4$=`1Xwm-T^=UJ= zbGt2nza+28;Fd>_KfS;d(#*r$9u<O3dQSF*sTQ*dYKkY!*FesbX#kDG(yu;E|P})<3)a@ zXW$JV@#fKbMjVFCY2w|L^A4P~PzJ=Oe+7zwX$FU|AYw7`$+US%w{;4Kmbib_qDT*j z3k7?6=Eik^G67nU0%jZ)^l(K04AJ#}hd_vPUS4RdH~GmV5xH^ls^|0eUJ>_4jYSlc zl*Tf?;IC!URPh{N@K!EO1b{=HaWFx>1?>Qbe*@ilr0yfiZ7%$7SzhiY#fM!1IWn{fp%v(2)SvMK{A{(uC& zNMT`CAHZ2Gh#(!{V4X@f?)Dc!%gjA*{qVRb&!5a6KZHN%10sg-l-OiK`QZ7QR>qWF zYZf|10o_#cVwz_SQI{rLf2+!OLwU{BVSJnl2!Nem0yf1%>i#5aAFKZhV$+G3}9 z+(b34yO47K2q=vh004!deEGvksb`|-<|y5mGyvqe<0B2 z4gal7n+7oe{{`!%;T;U_7-{hbn68=Jw{2FiGKY{+-X(H(WJ$1_=6bBawWq;#+$?wk?Isw>ZuL zXN|}Be`pH6QH--I8+#A~qJarvxAn3z7mPuNas9C(*ViA{z14JEFHw`vYi5Q88ik== zFfPGb3{XPnfP-=^_E6rk=KMx-Y@v-GSFJnXV47Vv>-@^b%A=xAWIkyOr#6W-NNEd4 zv|{l&pn@0s@CTTWM3qgWaDtb%yr@h)NUxHQf6Chh0jlNLEaPdOG+L+k!SKCW86s(! zcx|o3Ee}n;&Hu z%C(`TK?u~!f#GNb>$rcwbs=^`RNJ8_Q^hNT-6SnWC9RXKLorL=Y3r6e%ed?ct{y5t ze?vU{%cH;i^{-!Vj*fr4nI))#qHlDm5zOR1Vk(RQ>WlnpmICMEbPs`U*B@IGdHvr; z02X-<4y$shiq=J*!gN9RF$=+UzZvRLT|Bica)f34HJlq25qw+{A1l--0sG>f))IGJZVkGi|v zgmmrz|%MaO&uV;|fw_4r7)s6 zx+HUtwS2y45ok@nzeq>q5~B6$`eQ5*dc?>~jJ#~*qKMB^jFe+dvPd=!RC$h(e?cn? zvE-_#JEsMRG_V+iw&ATw49f6zm1B4N?)tLXRyF?6YIba`33xJ0l|E#Q7qS=>H}m+) zQO}s6WNr$XpMxY3yFddROA&)yzpD*mE(?m?+8A4^;|<{i00Uka)@HrH#&q^2I{Tn# z&qcjV3lN~WQZg?uP(uiNyiBedB7uRiz0gj+tCf&*8;SV3C@1Onn)uY+d&HfxxGK|8Wf0wsjR}ZU` z?vsN>zVxQ4P=yXZcmlJ>n_YE}X7ijlN0c?MIkqU#n+4$TF&v>2YOdRD*`;#<5^BPT z@Yd-1znz1wukup>_P-4s;2CpMai5?+T#RzbHGQeswsu>mvBrR!Oe2yyYcBWt<0M0S zSxu@IRO;z0v~h)^5_Sh=vXs=g#s2Ki7uJYj$?Y{ZdYVF>z*Tf1aZiHqdt*UWkXN z-RM}|?OOXA57aT2qZHXC^pdsV!$uoIC+ObRII_eCQZLBc=oWxtsZYqlX!QSppOMDzcq#z2JQ0m` zCTqCpy7zpe3OJEDe>i<_dP5uon0-SPVCMH0W%+M_Jj4@T6Bh=C$%1kki+jH(ABE;- zj{F~NQ4+Ar>yPtXBw)s`87!v*;K%^1ov>r606#&oA|C^XFgf6JJ1&^(9+uZ+;wVSe zx8{p9uI(f39n>6#^Q@t2dLLXuPmLE&gu9%^#;m3M-__VbR;GcL2Nz!7ff z+Q=k#`*?U8Lt@j<#AMo5(k*ZpyoETDIhSdkv@WOVXbM{EVwztHhGceCv}`yuJXNIeyuUp}@%o zk=X29qeLzDq<4O-_#|)Yxj0tWA1C>pt$q+=b1Uq|=?062Wze~L+gNyDnKdsJ!Jfhe zd6zkxYJqAqKUYTjN`(Np>YPjMsD>6fl?viMe~`m9gs5bIoV#k-T=nwZf~Q=L&o1KZ z#@Ne^82Fu270cF(XK$VxCp*4@J^?$DR4oZC6K~;S>#a$}C))G7HHWPu%HJAI`RH9y z&fM;nw}?Qmqt!dJv0ANy-k%x+kf8kbtoC4J6RKpkm`2wd!)a^>@GG0#%v zcUUI~Y>)apdLp{hxbS&b@O|a)2A;RKx4Zs&FYr9S_U2LWWbe_V<#O%i9|>Q<3oZugAU$dg95>f1c;j zc#OC&*j}(=GgAg?K|sk2eSdu^PISN|Ha4Zsx}!MD@PW;veJSIsZd%}9HiedU20511 zI*#YVP83B4J`dQW_UV+X>eEH-(_yT5_3a$Lnc3|`5wtBIw>Z1PF9qV<-S=no{%Urf zXYTt;crVH0B|Jh;x&;pR*KB7mVDD-@royT|zlE2*N~qWjIx#Lo*0%K~$# z_AcY1oN@iM#}F4DiWMC0wVAzq*==Of~Gu)Q4;Muqj|84zxnTjO8?9-p{=9+XZ;bIl^>rz6^Tl*v|vVPRY=(eIBR*A4d6r z4_D?qS*2tD3h4(>f001H=K28nEqd7V`|ZKM`@=7O4q#z^7d;I9!M_h51P|G-(Zin~ z{`|$@--loHfBxdvRHr#Qhd@$GL=%6}U8ED0twU1(fQ$ zpeOVQJlXR+*nWfEVOPm?<_E(_4ED@Juv!HHQ!Cq=AE7)Le@GQ|Iz8RxKt;UAvx2wG z&^Hh?0);tWfNccZiH@_x{sALE)wbJwx$FS#=G6%Olb8?~ z`6!B%UI^{oe;$p;1yFezUBGdij(xFQN|VHIi{-K{12~~497D+aK;2dKgju39D&x#o z0dopyVTF`rPD$o#Nt`-NgRAE1S!zR#^wZ?p(~WQM+KnV(W<5_uyw7>-lyZ==P+6Nw+gBi@e>WP-MHgaBuBuM;U1;+ z5}2j?tziKAJcQFSig>@%3FnrwAoplk4ybUsY@a)?f}sD+>FZZr;*QcW91`EKM?q*? z$-XhCe~|J#)jd1AI7^E+eBA#AUU%`p2*N3x0*k>f$MM|1w@}?8{lHhjFT-%q?RHfc zpe!jb{9xFXJSj)qhm(1rm>4n{L{EG^;M`YC1`j%|=t*Z73C4qfEmpxQoanNFJKs_^ znKD7qvv^KtEC+HBtim%qo4BununkLSxt!zwfA8SE3hn4K&asnGrvs3bJU)8QSk^(@2hn;`HH3umxXM*?8{cB5d zTE4ObOt{aZ_Fk}nKq=jXt9Uf!NyHgGpHXPZsJ%;iDGp>6-s5#4@)hqQ@hpHnGryz3FbKNyvY2ApU}rB_O#lmqb^CUWyDeJDQs`6J zeO;YtiuaVfvRQcq$r$&8)i@P#mR&92-DR_f`Of8Z?o zQom9%9=$tCBGWKg3J7>q*cZh#FSF!S*Aun(Oj&$hyu{gvcA0xlMv4auB%0I~_+{1* z?g}2Pa5R~uje)K5aX~(cXuRE~E|-7a@>UOw+oe60aJ>RyDf4Kz#~-gZw#O^>N=0^z z27HJM5U?xrQo(|89*Ed(4C0gMf6E{^mpp#A+S}4ZvuNBl?gk84lvRxe)Jj|xc1ZY^ zPV)Yj;VzO3X9v3#u(q=f0=J&A@Pi(WOa}8LYBVLoMI$t2sV{~H4_L~HnlW3Hr95%% zlg+fVgQc;IM|?$8C%`j?_na1bo^Ok`1<{%A>%9pv?(=zovLBEdIF;FLe-x}LRw9LS zaKY``e(-?t`g*O>#$1y^j?2I>{I&Ffy@#r(_MxX9*Rml=D!49dpoWYGy;_QTey7cl z%N5HZa|`S*QH^#K31mZbm*^QQ0%KV?%Sna?OaidRnzX{@Bt!TKOzsG_1l3UTXfUk! zb=WwIlGFaoc))O{SIvVpe`^UQq(cIkaZS{q>gCem@5h*)KX_2Rn!_t(Jo(N}uRY?P zH?eGSFyY-|nvRtp_zqJz<@#=qOC5)V2O#9J0}%jY1h1L%u2+pB?QM6^hdlvAz3&Up zD?q%*1GX*Qg9pP%1>xGemF7Sv_3^u~4ILX2q&aGEG02G#59o*$e?95$*vHEAd}6Oe zUxa4rCGg3z>XYaBa-A42mz(dRUCQTBN>+P}6uzkEYTg8W{`gpk+3KFxmR-C+zJb7D z1z#>7tv6$9W7~U7R%aNl2wZ~BOW?dX9&=Nr;n5Mdw3b9<38i$CH_SP~v7CRXf< zW9lDx{O&lOvojuPe-5|P3D~9kRL$)NcM-x+346Q0;(AT?)N*;n^-5HxMMP~S-A&=n zB$rECu7b(v9Bu<}zc8CvplEK&eZk>qthT`^H|zk*S!y<7>{Ji@POOPNRtkqcyzRhP z$PQkziwJHGadmUB;)o7yLr5k(&y@@D=oPAeK*$qUViR8rf81(0m&^8<50YTbo>eCyMnD1xN(J!D^f&bffFP#X@TdO|?M~`n3;daDAHkd_BV@ z+>Cm7R`;-Y8gp}Jykj8+SDkQ6LMULhMhP;Y1VxZ}-UatF_GQp>R`me~Va(YX%FEBr zn7wYYv$M4Lf4=0MfW72aS);G_6Rw;~IjBMlpH^_t9jBS%67PkbSWd{G!AQ#C808Y5 zg23J%DeHj4^+jht3j+3zM{lt!W;wof4+AS~8+1}J%1ZDV#X4G@(qWw}F4U2J8deKP zMNbxdpoWp<9J}4FBZZqQI_7)T(Yp(R2srfZ9U#X8fA<3r@m;1Kz^*lN3;}lvEK)ab z2RJmjT({iJ!#a2c-a4Lb z-kq7Me=g9t2uZwW@6c&+Iz?I^snvuC9uP+}^6UA`tX27tE}0ch99+kh0?K*<5-vIk zT5bYeX6<`!zstD(MMP2L%YNQZRhX+V?@JX56=*WLTvi+F#AvQwk4a{={gVI@6wxz1 z4=IWjks_1WF7>>z{|@C>x)pEuIMZd}2S(zwe@=apY7KvHWaM$==GK`hk&kkpmngym z+0Oy+g+V_KeYsrXDYgshmAoH^XUdO*e(HH|Axj+ULm|9`*Rr3%=P;)K19L;&=Pd}m zBa*GlCRh~ZoJ+sG$9S+ROyl4R3;Q>ZV2e~2^J?m<+gPIf97kdXN5OKL&>ugCrJV;1 ze>V`E5$!qB+%tIVdF?ZNt5*-OQ#o}f60E&zNG#2Z?y)}9RA42u;h2<9)%dzy=sCjU zIdDdBZRy|h1%n3Q`HY7FPDe=3aDd@+z~U7hH?B&mqIFbr568#sBGZU1`Al1+=Lu7{ zKFyfYrp*D9E7Z-q^_Vivhf36^0?)JSf5u%aVwGQtZ%MkmsK^R@#iQAr!{HeZm?^)? zVph&!vz4>!tQ@O8*xfpI_M@KNBRU-SIXj_Whn&4O??5EPjya+P*ggE~Y@+&3ua3D_ zn(IO)6`N^b4s+#mxPuUZ30R7xH|UAw(!b{`mIj{pf%`d)q5hQ`O1+rIMHsK5e|sF( zbP>t^F%Lz5qQa>%QxelnH)ks71NMS0gF$HDr`7v32#^B#Ags!@UIbECXU7}tiqi)z zRMR~G9f%@H|J>DcD(nx(HU5G7k`5F7D~IDLilV%4U?8FSv0T!x+-bu1RTJ!9vO~yV z*M8tW*3NdByPAm1b>L##f%Rcae|$mLeBI=m4#9gs1fexrcEIrbRNXj9psrImBWbI0 z6uqO|)YkD_U(1!A;q`-@;bclS;VR5=N(C|fX(XvaxL@{CJhG{GwJ)Z`ILe*m_9A2U z0s~s3SWL7=Xj%hbLVDI6Q0wh~%3Z~1z10F+EwR;#TAfFt8m3B~fYB&fi>?(l%2?yKN)XU~ntN%Zy*GPecCbl4IODnDRwc{&&2l zUb2iD9#3XlBQKUlxm}epG$n;Ss$h@u+8zakzz6Qfdby^~<%sU5dcY)uEx~}Y!k%&O zb-&u1<$Ali-R@e$Y@MHF&caA4_4DF1Y)kpUqf9F=r`?Oj@TyajB zKh`ce#2=a}UU9>>uV4?gV0~82`(8^iR}6O98_wb+314sqo{FAxMlA`Sa)ylvf8$KA zobWpi+!F{hnYyt~21$1RH8n%A;uAH0vRe8~C&0pTx|PV*6abC-f%{0a0&E*R0YwW; z6XAuPu|PK#UeT-tf3;Tc(s;>Fwfg_94c`r_%QJ3H)*;zUhuKZd*7re0XL7uE=ELY$ z;4od#2HTXMc|KRMOoc+Xy(T}y;|F>(BYSric zpLqCBppXCH;ol7$7S!4-527bqu6nZH=HZqbp6u(_`ran{e=2nOfH4wm&y#3*0?KEs zL-T;&&_)fvt+o+YwO25U%6X+1k5DzKpsYXU^~bdKSdB~UuD(POIAH|mcYBW=arO;o z!Ua4OZhDjMHD_Eg>88sz)9tbo&Xi)gn{K2kAtAvNxb+dY<1_%VnE)>vQ8I zH%EHzh!|7ze>t^mi(f=Mz*35L>3q4Yuzb(nW6c?vlj>DQuQW>I zLVZHNJppGp(u$Z0dQ?`2U00qP;i%Pe*GI|fs7dRL^OG}fBvKJ&w;)kn9r4J!D>wP7p<10szv}n0HA5qmRp-EPz#PN+QJ1_SjMU(05F)k zu#L-Q`yQ+|(o>rskxhj9;$XSFf|dJaz|N7W=UY1ZBzoj|nCwwt3qqYVHPGs$X^!BM z53N5lN_(bq`&5cO67Q0NH#Fs9a zI3=R*eEzWcdDr=TT9b@W&47U8m#qW(q~$FK8+*;nnoA&VvA<+|iAGOw+J`g^&OXAo ze^X`Ff?3JBndhno57@GxYG{mF6PL>w31*Qv_T2hGFi~$@U;!7lxqN#wnc4AQ*6w33 z-Qm}xs|BNm;=R+{gT4{Ng-D&M+xJ$wRBd3 zIl11cU{%B9kw^F)bV2gRqw*>}g(LWNf8Zsu&*`!mqSg!SMEpEh>?;s*G2u?bGpB@f z?4LX8$UJ(#DU{7U?*ooxPWC5+>fSe#0sL*gTEn~NsJ_>_r8L^@r4>dueefX2Nu38x z8Kgs9VO68k+I`!gsJ*L!PZwbBfp{VkeuD~_P+&cKrw7}Z_g5^9elgy2E=vAuf6>k5 z!CPNFct9awX*P(e`EXdo{F%{SJlTiDmszqjG-%ek!Jv^#I)a2+G>-$ zsBtvktL5^I6|r)#jhS+RgrLv6=c!1DP=!1Q9W?)QEab$DrRSZfu2FURoE<7)lw9yb z|FwFs7rq8by06j6-8AW+C!JT4B^a(M)zUznv` z_PvVU=#l!dCRF}SFZ{?qRMh3&fDk@C@%Th-F=tTFTtPrP*d}wSu~+brCU|O=X%gtr zX}njB6IkIfv9rSztRzRUf2TBl>)68aEe8c?2ct1RR>81a*x=U0@JntY`m-?N>41q$hVYVZmXt?t@2ii%&AF|*UlV(;N@ z2tU{lY6IEgw9(vGghs2)C2)Z>hDO6p2uAwCoiDAul{GS`Hph=Re{{GN*&$9f3^xqX z4=O-pAaoQr zLGNGY+waR|HLsrcBTl~Uz~5;B!Yu1SNegRZXGmLR*nmVCsP4R2E^{rV7rlz$x%Q)_ zwH_^^7?vm5wEY$5f1&Wa0wfT!8irr)Ejr*@BtyOfWEVxBUUb4@Wund%zpw?FQ-_Ly zF%IZ~i4x-&{k`2dXmiTrMFEE-m`0^ve86Z|85bF|-fL6n2X9%@FN|sA3$~l%ffz5w1f5^PHT90~;_%G}3<){983$3ETe# z`YTrqiBl*TGKHaJm#LZ}Cm-evN!DvLxG6&5MvE+0MHsVKDOrRCgQXv441;gqFf%4MqdvAD@9;_M>AIl{3xXIC^DWB*G} zbc<;`=g+fvQW3v=xs(jd8_%OO_e-l7WC@$GIUBQS5U{JLKuS^BUG#PtX5iM$vd{e) zTbOQ{Zc_JFaC4Z^;)c=`k@9{K3gLt5hIs$We_-)cR;SCp@-G<&=HWb2x&K1a(madK z(JoV;T25bF%ekw7Eo^r|!FAT-mKOZgLu!nk0`eL6N3fl)A}RbU;H=qMBy#_ZT?KZH z2#|DuDG7ZF*crZv=ue;i0IxlYl~0L5r;dM_=qm7lmBK23BAmy_{RSO9W8WzMB4Fp5 zf9DVa=^pZzo3?+To`{P=LpVDSI+*K+3~)7`=lj48f?;hCpbk9K?cW0S9! zR~$_mfqn!zsX;nKhXU!)^VdI!TIgqb_*S zsXCO2nh+-uek@2!1s4V*XBY{i#DykzE_OsRazV*tE>@LMh#EE`5$0&!UBj;Yq?XHB zP6pd{wsNGlJ#CH~i)uT(%zXhesvfcFWUfm}=0n5^!LQfEZ&>Ytu#l^n@N;Buf5dPxeKaI!wZ>K3v#_ff_ECd~iq{4O(EEgjl7AzNU0W=?G z_zMy9;b;|+Vn%sh;h3NGR7ARJe^BSqFZK=?UBi~mayEkv+}_nHvNM5#LwwpZl1!x4 zV>V?IV5KiK`$JMJe;koal;p#c2&HO4Sh`)$)9@O)DPG*di!TKe>fWg0t%R5 zhw&cws1-0K#@gkIe4H$%(SbG|B7Zcxf-eK+e4)myRybNTL1$6SR9_&Itf=m>AFzx5 zV5r;9&W1qgYd7hnZeHx~CQ5k44PU7;xW5)LP>g(qMxy7?09MoCC=8C~=dxdy5* zdpsg2JM*U^2&d-S@_{;*e~$eFd+|CTfqFpVC)1V#(gE}U3p!aY{foYtS53M>o_aY@ zLzou)9U$qiW<;POU>IJ)*nl$`jrrFVlb?*!-iMl~Bx<*n z9>XY#(*9}mA?(caLYYVC-jVb_u+sC&Zc!F<5IP2-{{il|$gz8#fB&JICQ*s914enC zEB}KJd!@X zCHc`ou-;IegJVE2)Fd1PKO4BCth}ky?F9WR;Y-G8o51Zc)Y2;n zLQ!I0&jIC{Mk@4Ef3)65nH|{DR*17nuI2JYbRhszs0#@yL~$$j1LnZVuEd(|-#Crx zS}zkgsM=O;%@k8q_b9Z&R3M5R@ZqpxB^1BM@&~i!XH!_-qoFZ=;WU zls>|5+rc>6?ZuCC)4{l62#Y8m#KT?+5`wO2Oq34dp>_g+J1&a~C~zQ$J+S*lf%r4! zXH2lK*?@O>#sEj8hGgCmtX7}$)>di!>f8?=mZ;t=fBgl;HtWKl6>ueZv4ar~{7OTA zfUG~L`}KV~24V|=u()I|1vyUBpVH!6&gc7CdciBc=$&rCdx4ntPEGguw)Qbu`}*E& z;aToF7&$w;Oh)DeOh%DIF@zR&_q4%hS{xEiB6cKt$|_UrwEMvdIn;eZ9SI;|3nDzi zC|% z2dkjQMebL_qIq-Js#R$M8D<~;z#fF<3M0j*h+D>A5vkJXG$ya4gD_Woq&B$zonXV z0`|f@)UHdjmrHc!w8*0^9GdGv3n{A6 ze}O!fs0Yd6_N>TFjxccL)bmofP+PA~9qn0`<=0`+Tm&uy*P7#tbxvN#gfCDP*<-6C1DRvhjeqyjKh z@sPz#Qq2-LWb6v+Z_RZRtd}8q4vOANe?MjR*WEV`SRql5{P}PC810vwS7yyvHljo(~vk^#&P!6~kuo!^_rNx?AJ?zH{Xb&W*7BPpR25SgY+Q zdI3(Me0q$c=nXtV=I&^kW{Koz3vL;dp{j<0`-fj`+j5<21=06##$zx>JWk@%53i^5NK zPx7&T0l(NiY8Je4TgmRpJg?ruL%{B#5EJvlFLp22@?3Z&5zjKtyY2*f8GcUFnElJ{^cf(60C;5G|wU}*J3S)hTY839dOdjsVT=EX+AMgnYo$m@^aFl_0h}@GDC) zt>>%=mdhW3(S#p<7R6?%WPkVuI!J$i5c)@Yd zT3>>q24qI^DkARObVura@1JPdsk-OrtM>p;63_cq`1Mzev(EX*xPKO)>CTzK{)Q== zaW%~oP*@RR&0uyf(u9X2mI^qg!jjQJm4pd9ulx&#GiI*HWBp#!|Eaw(Pnf*`g%>QT zu0!FK&T_~LCG)HB%oIU)f$#%M=nGH6Q&yTALU>>lg)j9F6jk#id}p6gV9k^8EknrH zK|EI_Jilic#Y>(5G=B~H`i$}S^St2UeqD)p$SPe(c*H72NO;1?wL}rVUXdTpvhHS+ zi6{;hb0b6m_-YVrDw8TtNqFZ*mOYOy0ZD0@wN$i3e=L20JS`|GMOS}A3gCXCCzSr= zL4Ug=VYpi^T*tw?Y~8IeVnc)Ii3W2r9iHA`JNYUI6|$i4-n3i=~Ggh4p!ZJM*!U4G0Y-1^~+Te+~f zV-ktX(br}z2lwrefRG(xnS?%?+d}+!Qbx%dkcT>3HAa0z7p&J{(rs^>{-C({yts?_t&6v0IH{b zogf#fe}n$Fw_lfC-n3CTTbOA=)gR1}qTi8PRUUzEuIMF*YKS0#P7)Wheta2d`2Qik zbtzAD7_cLX9bgGibQG`?+6^Mi zP5C{1*MFl!vV|)LrX(WJi}udZ@6iNg^P;-JBc2IgR)nCpe!ZV>ikY2;)oDMwBT5QdhMVnvi1M|m#$)GGQ}RDXoIMG=Az<-VVRC?L05h`csFO8OZC zc{Z?j0mDZyxevlDQjF+nicF4Lv;9)CLVz1hd(}bH?-Y+a&%@LyLpGA-wN@x>1(k)Z z=&g}36_D5E4sDbU+OO5;QotFpT%yz}9`-DYqhmiAex^#R8Jbi&91Y|Y zPO%kVgvn};btWd2>s(AKU3MjwF252>&DL$W0&iE|HG6CLGqa7i%PIH%VPm0W|=DD8Y&y6I;|=oy{xqCy#dN07kViC{hB6 zB?cG*O<8-7t>5oLAah#@=wiiOBMzfeyNsn+dj%UJ&A2p|)WvGWG))@}-R-hix!Yx7 zS8EY&8;2UXcg?Di=xn~Oz`*Wnf=P_Py?%v=k~g~eARz7HoV-cFwp^^xN{bYv5`4x> z_@q->#R*IaORc^4DOVa`Y^_&{2!GK`(c-L3lW-&-JYZ*ba6-Y(h?q)vD^#2cE@@)% z8H52jJFB#w@a2L_j1vAnN0$Y(0;*k_>svj_Nz7LsELN;6IM|$NERmmDZ#;n9I!8vS zP;r)lfW#=f?MuI)j0Dy}DX>XmKfy(X76I_ver|Bhl{HMTT-vwV*ah-bBY$E8)A+f= zp&?hu3eYvxUREosma$rIPR80xI&l1yWo$&mIyTg(1RSWVJ1-rEA_AufQ522(qo`9{ zOgbULtVW2TH^!uy=czs*+Eaj}mMmo>aL3Z#Wnk@Vra4Mk3Rs1bpT+a}*;!Pu0|dvK z4hGAm^F-X_YGqC}KUg5oi+}s3Ra~ZG+ z8bEx)$>Cf&K8@wtO=gfX6hFAaOL8{LG7UGZ3=nhTYhe1{`*~2=bZLBR1b#`3JfEcG zWpoHB`>6A>G^GTAGJkkzCHQeUj7FiLKVaEFb|1PmEC&LFra7KB0(|5k2VFY4JQ+bN zck!Mb*L-3L2(^MXL3LD5yagUyL@~4*q_fev1aUJfm>d*CfbfzGHn1U2oIAp~-0s4c zYkN3i3t|V?=*P?ovJCvzdfRpYkg*YI;p#DJxaYcJ&uhnB(0|H2kHHlD3J(IqnJsW8 zb-;1Fp&&3=kVHsEpz3vR7l;|lgC2c}G^0p{83BuZqnwoyU&R=Q4e;@8-iM1(*p3H# zLkI=bjv`pR?I=2-6B#XMEr`c4l(>>xErcpwfyFfop|}EJn&~nke!TmQj8$oJvvm-t zJ}LJ=Fgc8L8-D=WQ39R^ou7@YJ)r#8+X_KD!Ns-#ceLt8KLE@ibV5<#(g?6s)Bsy% z#T@#@ARPw1Gf5|XhGDuY2}7}tIxF0~8Iffbc}Yc2 zK1g!;2Y-Aah(Ppne>dDQrVj_WaXM!R{z2_o=U$;eT8nSkOZ@K0)zUl&2VnRU4B0#B z#P37US7o=QD(g)ERcSQQOX(XxkqcENd#gPrSM04@rUN_!to)qtk^TTB}6Hv zjHZ%8<^1`PX8;acEV5MhtjGI)i3(Zv26o69c+U*Nw-SA~(`Hwm_ny|kitTR<+JcPI zq&CD#jRKxnT{`H2uBX9vpF?*ytxAp##5Kq z{5t~Zjk0;3Py1$&4@2l2l%g%l4+IKJ+iBexIgY+`2MrJ}vUV+y>(_!%B7ihXkwFb( zwn)L=4>1v}Bsi;NprZ$13OZ%gKNqWzet)rPlyxg5b;WZ4UcF6MoGWCuX24wGp+5Gf ztVAOK^C}3ybu_*!jk^*u$OH9B@E(kcGq(4L4GK0J)kQL>^hf&HjIM)bTAr*Gx%>r_4d;K9OdJng^Iys1)`BvY-5aGXvrsk5Gv@_Xnz=i z5R^*9UX6wkyas@SE*4Yp?mK`t40!k5Fw!54!orkPtd&!M2YI+A>T-&DDZm!?3b?$c zNwl-ajwDL;gE052W9mpob-^A*WFub*TqLQOJe9E+O;HlnUkj^*Br)8GwG{eu2-_C) zk1HnzA_(s(&%5V1vVbGF=EUL^cz<+w1b_UAtMh5P=yXJ==Y zqgte*;NWtdNj}RjfVRv*xC)}Hu+Z}3XBmeJP?+iGw}}4H4+r@SKb9c(s}U3 zjL{|R6%emE16b5)dY+|X5>6ql#=wP>C}z|i*b(UnZ*xM07qF?$BP-L#t$&cypOpT@ z#$QrF&l%XUbdxI)&(hI>0lMk+?7*SWO~Ow^#6X=JeG3ATAITdFDg^X?Vms)Itc#-q zL;~0}f-9`m;!3QD-5NneV;CT7xtf6LK|`TN8VdEj46iCrI#o=I)(aV)ZT;|EnplgT zcj*(ojB6?0$ne;`=8$oKNq=G~wwLr@Zb=q?^(jSW1%JE?6d{*Gh?;8i z>w&4I2v%SmwE%P^qc@>inYNJGkOj#D5ULlX5lZl2M?*5kqqJund#>2QMq#EPv0jj% zf>1lbBhf=Xl)a3$%y#r-fs!{||FEF@u@=R3W-&#Yq4xmDitybm>lj%s@4xdm*3Ujx zQ@vP$@t2+ld^uvJX@7dc#y#h99L}A6G-Z{YbvOaxWP#KI4zmQOD9SYSmBnoCdGl^s zuzA3~_NM(A==H0`5bk~DO+D|KJ7)>`OMy1MmgM6BTE;Fs56xNh*N)b+bprMl!2;Z1 zdD#eNX|CgJ(Xk86&^*vsEwbKyk2kc!Phjw&Ou*l<*l8ZN;eV%~r?Wv3o(CE93!O~^ zFyesu<7nR9jG!KPUY7e~mV>Hzy&H~>&_-@KkG83{&}W7AYU7@R06ilwI3P1G6i-%u z$|gbY-18z%Qo z-B$s4Ibm;|wSR30jOR@dRL|tvJzcu@bVwudf==)r;FN%+>lWqtTgkZ#_#sDQZ?P(6 zSAnWwy_pfJ0;9p6ZP|0tt{ZH96lJ|prJ);DqJu2r$m;at+=pL5nCc&ZRltL=UNn%r z{i;HEmsOXVkwZf>t`_#N3rn_k%7qE-wlSP!C0VpHe-cgWOO}+6tH_u0235yt=H?*T@O21&6 zj+l1oKz|kJFHruMkOvcxn+G=U;tw{}e^Ve?u^axnA!i~lR@t54VhgL?I12M^> zIdIZtGzAnw8AVa%d75~%qbLUQGlJ#7K!4&89_Zvx!D_V!E#>aXs+^N`27}_9tT-l_ zx$Gs&<;JM_?sU@x!j2!i+kNthnb$A{+~07L9)GRvBX4YL1}XpMrH!iu{DI9|Z6aWdH;!qjm($S+y9zQU$9k z*?(wTMXTx*l9be*Lizxb`fmmUa9lszfh#O4qLXN%(+9E>jxg}w47?p84}cW z2-A3jFwL1uYr`oJs3~&mSwAz7glP~?D-OknlL+}K;m_#Dvl{C+0EptuX?+lmpX_?x zS6_N#&zr-(SZhY#-$s1qdJO>2!wm9(tbd6Bhy?cQ_E7P>dsfgLgum$_bQ`f^_io_V z>gP>V*q;7n?-968vyzS+fY)7v%`tZfVA0;xxV9_^zsIV8S9?=m&_y+uaCTTpNmK_B zyB4R&kfo8Y`mR^Y(AD7u%jGC%akKz6ewjabpgl5ivP6;kP6^n47srp5K19Xg1%K&k zX+-IPF5~oD`;?7u;&@`~;*SGkZ zXQRc6r6eBLY@k_K(KaBG)^39`b{%E8X_1AmAsiC80x&p`p6Kh8k?iMT0fqyiHl`=n zWae`onqR=`;OdLgRe*-I5P#x}J%h%`Dvna#uit_yB>?(q;;uc;*4}Ab7dMmvObPqD zFyl?$0^}+Z2!wF!a5qdOE9Y#kuUhM}n@kfmZd)yv3yTcVe%#z9kYU*=ggqnUN^Mz5 z#*t(hsSQfhYK0-iNcc9v`FD)kQ*Z<=83dvm7!|LeD-)WtgI)Uv2!D-+5g5sR?M2Qm zatJVMEdJ=VFQO-l%N%C|W7Qfj(u%xD0|U5Ao;kW^&qpb-oR|%i8ua)fCuIu$Z^A;- zLI*$ZKH!FJ874Sn+Le3)msfen=YGas1Pr|UXO%J~GpDq6Vb?pOJ!y=nuKN`C!!CE|yEU+KIBGBI6mYmCvwJd z{DM5u=p{mNwcV4YrjvR~zAA#A#K2pTV%xSTx~s+jycnqZ$!mo!thHlh#b9AdV@S;8 zhMlqb<{J%&xwJD4g^JXQy{NN>H72dn?AS9A@n9HCEL3>Bv9jW^V$FM@#jhRGS5kvK zD)lhQs=ux1P(>rS z^uGdKh+i5J1NpMf=f%z9gp$Q@;}00suxsepEJA0XYz%alHhItfie~KGAA=7~e+&eH zed~__rVNJ`!{*JPc^ZO{ICgXk!@U}|b%n9%jdPWX8h@BNaoHJW2eA?Qw{|N=@;34; z?n|=*0wytrGzs{9*ec{m1riA6cxr(KKBF9X;l^9fCq~fR(5u_IGl&pKkBqQU9j#9^ zo&n)OT7*PraXt%2Y!;Sg5E7tPv*i*P{7A*L>~it$&12q$K@>Scb+c0!_wf}Yv?IC_ zbg<}YzM96;4_54{b70mE%I#jnd)&}J#)$xv)_jdgt)Hak&XQ^(=Li?jx``D!z9YEK z-h#Cp8RQy(SK}wU!E(9xl^5$c6FrBmMa(i#nu@(m3C#tBbBqE<1mB$ip9C%p0fq?@ za(^^1@3+NrX#s}Lfi?0$ z!WRHe0vvwM2ho+y}6-~)oMi1Of{2CAznKjVkR|* zi>|Hqaf%&@DFDL#fc=J%Z7|dZ`)!+)zrE+m-nlDYgl z*a)m2eeI2FEXuF`2-I42eJ}{G|MJyZ%I~(MTz&1$`zQwYyr29LQL%*3Dl=cOB!56X zTG`+G%A5DM)fMV&Xz%TR4Fy3)korL$P{6Hc30CYoclb#DHA12QH=NY25r_i4T&Bj& zFbAY&RKDhj=^Z<@vJwl_(d71!FU*rK(7M7P_KeN!0>x8CW~dQ zX)?`Ol~0oXPbfEyDb^#K1>rM)t$*2U7OdE>H}nS?9~)PtYCgFMvHq)e_SiT|cJ@fC zTRx6vo>xAO#ttXJ2PK3dQ3%YYew3ykB@qCSYa=#_xP^0g*z6W7A3Runs-79ubHjKw zuV=Qi!^S&1coaUF;qryL90c}*J(foNanc8CT#yz^I{@QepbyxadW`q5wSR6|a#lVW z2eyyB;71>ak0uh6x+G;Dm24W7Y=TE0#+=NhUq(}ENa;d*ZybLT{&Eohf*KQ%ukZkJ zU6||(c4gPWSv206KG^G#E86xR`(QtvI*z^00^N|^UitV8+i?oj@9Nic>RsusbLb~p zJ3ooeg2j)%G@b4l0=yz1f`5glNoZw|MiYd2(-Wz{pq7hh+Pi>JxS#+>a5kL|E_S9v z&%+T|IbU zTuvWEms{l{TzioL2PA`C0-sxA)2qbTPOKj~!7|q;T1H+jpo8ev2!9qUI0_VYgAN&h zRUc=;g4|0mjeV2O$U@<1&G#CFjCmaQf+9*E#1K%355Tfv_&DpQQSo3ejH9BLJ%+dG z5Kk;nL-<&Q7^FLUu+D)8fY@ZIEf#9Pue4v%ot@T`-ClqxV;0ldYRy~y(;y_|$whSV zf3x?d+ie@$y6FG)6n_wow*o{37!6w}Fpn(Dwi0!bWW^4{#Xw|9L<|Bn07{lfKF@uS z`(k&@Syey*q--ZEYwdH#J|nRRREO$o)@;66?l^OQXxO{+7cjjHqnzTCojGm*VC3b~ z9hQ9Xe~ChYOjZYpO7_P{Iw9v=Jp?4V@@e;rj8IVA447ukEf zQ}z?s;=n$D#%C=y4O?KU#Fi|*s@Vz``OC|Wl*Ji6QXv7>hvf4~u zC{(Z{s!m`o_c#-s>4@^4Pe=|dat{Ftx0s}S%!Q(3z4t51`(y6o8c?qp_MqEA)j?n! zqBaH4k$@Xg zz~^kC%ANVM7W1W^uP?U!ac4kC)G(JP~Jmk+06X&ZrS967LdYcCk`~A!4;cOm0{>_Su2nqkadl0 z)IzK9X{gM_rByC2I*lnLi%ip3NEVT)Ii|DBFo=aw@PZot!r7s|BEPQfNF=PPaRq+3 z8-JASX+X68z)?WBR@#dE1oN{ck9+T@O$U?i!3vj?N$VRpADZq>ilYI*J88srnjcG3 zc}k^m6dD4#ImFBS_Vx!&JoDWS{=izkE+V)SC@Rzzr9V3PaktmFpdj@ZGw&Ma#|JX?ej%y%hD>wsoPSJ5-T|47yia6yJ@O7o?EOR%k21FfF(n}d zL5|kPM?j{{Y2goa!G-sflF7q3MIk@s3hR5bnFr7Wl*C|=Rxu(wO*OVHP=0(cHZ3>j z;!3db_O=RpWOy5&3Og~lb_&f`o+OCl1k~7**!avsDJn7_l#cdo#1}RouH;QA2&C*aDHCZgY(Z2{I(uE znnLjFdT<7?+2my^>|{UHt@_`k8pY9k$$|SnfzA|KC4u`A$n!_)+hvAdumk=`cYF!S z?~ZGq0R<(J;TufQm!O#EvNVMfd+q2P{J zbbS9vANaukl=>PXL{m_==l6OJs7!u@NFYHe5TZj!mgCb+l6o;@*nlpMIE$*fLj|lF z?L8or$B?=EK~$hc=>3`#@PEmZ7*!Kaf@h2jNDBpf@PBCd?XXgroCNC6E`dVk)NVl!@j1Q=JA zbUO&8+d)cXZZ{}G)vguno3pB-6X;C+nwL-=JuOR^4Lh9Y=({ck9HA(Dq#79a#i+bf z%5wY%Fsti$c1!1LY%5^vOpD4b^M$oswzfgy;KFq;lFgRHdPK-Hz7Hl9UA?Dg3(O2| zLoz}J)mYQ;hA7O1=p2*aSD?Zv=yhi&D7saoYsx!eB$Jwoo$B!5!7$?*5; z_Fp38qyR}kFGIG+@D365Tlg=+2&W@OUDAlok~GJtR>K2h`d)*%_;Ng}@JJ(Xkr~@r zCBs2JWD;diXqv!UPNbSg9K7S-B`yG@SqBZPC@O%q^Fk8uT83V&)xnyH0S-66_ zww9xx*j|Z#7UPYJUAPcxgCfI@4zNI7as!@%vKHtmrGkw8>gLNdX*oV1?!X!Dz*(^a z!!6|o?7ov8ka4|4)C;#}-Rma=p9UWOfO;n1txF@O%73O!tF|oVR1ljgHQ7UTV-=z7 zwX(%hQ+kuOTj_cu&AwdtqD7uV&#;koY_~kSwG6$mx-C~_l52bPx`y@>+Q{s9;qHp0 z1BObvQqu7y^JR)tth}K~XBjn%=k0A+gXOy}pK%Z_UW$YwK!-DqUGGI_bDkiaH%!rR z1Qi%jiGLDK6QD8_mE^uv0ud(K6$3%8`C~~iwaRrx3)n&poM(`qQ|+zD1xh)I^eVvx zCel{R@0-h|dd(0=B>JbvwCb4G!#n{O9IP<`M_`wo%&$Olb67;(h%@PEy@NDaYez(K9BG0w@5(>?@h zpnrQ>?(^}EamXJwOx{!RW=_L0x&NX2=9^57}MJ+?7!hyi} z;0`7&O!X;xxh|A<(<;m?I`H(;zBNFcoPXU};^*MP1MKw=PcAO^PWBIm7YCP@2j>^V zp-SNVpC&%g7!xV!#l?9&)euwhF1Sr3kL%!nNzA5M!q5kC&o=>ulnB)+7IFjLHD@q1 zs};H9@DS4kG2&`TxGq(RwIwgQ#yU(2^AVbmA+XKLZ1;)Ke8uThdLYlz!#E0KdVh{w z9$wM@avPf6y_ zwYj$86LQuSsCNNdSoGCHf_^MktAC>RMAUK_tcvzYTHk<0h;AJU7(!I@&L#AWi^OM` z;~eu4T$ykuCJd#$9BTA2#8gtb>UxRHBpKOAnB-?Qb^6O3)=`n2a3%aOI{-|tEWC~= z&IYh+Dm~+%_y*^)`%XWr!~gAYMM1M34vY?y5H1b(@;Wa?OnBA0`n zk=b(GLUf+@c;q142G45rOn<|;IQ*BR*m+v|!tKS}71~VTBQi>W;oTKq1%WX?#onjl zrUl;7h~N(u`{3uqS6#DVI!%=vB)&S28Djoe-4ciJI2mEFJ@LS<@Oz1WkZf-sBxVI2 zMN`F6S9S6#@ddJqtiZ^Em{1Q|$arP?N`SA(JG1a!1+@Pzr{f_JQh%zj>}udFG7g0> zz$suGdiC0#TmYXrpLx%p-{0Sx_pX_wx6d8RvYta8xb#Zw>~GC)ce|bEpk{@Cj*qaM zwT_}B?k&R1i&urGY{6iskzl89Z>QVaLg8dOkMeL9QNU);D4L&z*Ylies)$iZnF@T) z$w)QSe^Jx{0-#6XtbeQG*T|`O7-HpeP#dxEfIl;)%;jjlsw3T$sbJUUbUdV!0C1R( zv*Nu1hr=9&l>y|_oTd*K*eyWTV;w7DypH@j#^_R7?W7!)7B?a5Y(~IDv$~Ytg~>b< z70b;i$a0)}94u8!;NAu3!cu*ilwh|u(-U(PB{9W*hy6CzNq@q$TeZZm=HKNa?7hSc zD_3(0Xx3~RC20ml!loN%R5?Dp{Kxm-KWBY)Cf6VV6qHjr zB%6Tel*C_1@FE%|3+7KaUy+5(v0dp4+CNJMz8WVn7=wSO*m_207ZdH&zB@4e@IEnOx4^WU=Xx56p#9fLT3_PqT5+kz^< zyKb!Oae32;{TD^NUd3z_sM7F#;LHVSo&g@VrW%F{zEd)UM;88U;+mU{&Gwlu6XBz; z0BvR(fBq|ObD}cuR{{fx_gzFIn97Q+^aV63?09lDV;^Zh-y~}63%MT=c?{eRQnU;_Upv&tH03ek0I!( zA19A0QKy+YATPlbmk-^i#NT{ycb|Wb!uX>qNfwjRV1fen{J|ZK^CM6`iD{~F|LELf zsDHfRX()U8{YBzC^V_)~O=U2w)c|WN950c)kFR-hl-z>_envHY0Qx3k zpjD5-t2%?8D9hEoGfliWg*HMzCi!5+=YLyT9<=Bb@d>+PVW2m>%QL-v{K1dG90AOX z#SKZgZGve3RgeRPaWLbDe zm)w1g{_HvKC~uNNC;R8bKTDHon9)*0h`eVUlQ&?r{&OOklR-vZCvOsI@Pz)@ugI;j zZRSvTGzrrEWK8#R&C<8GZQH5`+kZN4XX2eQ zGh!`<88RPn3^lm0vL`i+XLA6X78_C?mKzcb*li5xP@%uzP;GFGPLpwX6Mq6jWDy0A zAehh7aO#0tISAyvbOn5p1?}hI!2bz>*2T`10^!RIB}@6e&%+qwi0z*}bu4tRmJ0!OkULo zBxVDWiUCPhE4Zkjw{$)xA);W^JFvLQNS{UTzMyt0#tYf~FjC9q^M6{uji2+wgKtGW z{s6!?0&y11+VT5>CBPLI`5lm5T-6l2r6`_ZJc+8Yqpdm^=rv62=6}4$0?>L-L*SrO z6!?`B7m2H)2jcp0uwgG(*05Z{fCwL=LMQGU6ugWA{|Cz2t(_0l{}stB68cOu!mGX& z>Ip7PCf$^%xUF=1J1mcvK0099jfG)5B{sl76!-|?jdol4o^aokgT5+K9N_GZn#>9j z8j5ll=5Agag$U7s+JEAOJF_ILPp-AjO2Q4kZZU8pnU4pD)P;Ud*w!cd54$Pl3!lxC z7l!TKRukpu`=^@B%eNTOTPAz2LtB$hI+Y&zRrR0Vdo1w;K=pJgk@{}a zmbtkj^hFl(tL#^1_r_c66J^R3TrR~Iq(za1>mm`+86-is5-1&k!*ID|Ygy9v09!rL z@rBmM52392A%CUl#{&|;?i6w|S$3sM1y45E4U5t9)eHrQLjATphCz!tOQsY@wvYmY z9TIyEK-31yHh`tdccZdgwpN33V|%Y*y(I7MD0?j@5_)GSRzfAz2cC1fTpEmqtHhx= zqng^VWo1>IibDN(Sb*+hc`i#@wbA;R#>4R z@DL1vd1ceVSup1FfwAUG&|7&TZ%SAiVQiSbu*i$_X&um0p3fT+ml%-Jw3QMh(2(1v|v(?Lk_m_L;2YW?C92TJ2yo8*WfVCgHIQyHL{Ds4GX~0>IZdeo}RT-T2JO$?UB~$W& zk3*6zP%}xRpXdX@itsRehQJ^aK|zE8P(dC7kADypQI=1=p67WI<5b_?w(zSgh)l!r zIHK!`Y&lUH-cB|YI2mWSq>DG?X@Hu=A4wRatKnkFF}w!!GOEP{{T4~KBFqLbmu&z`PFL6m^)bOE zNq^C$h#(&e_i((-mA)dA&|lDO^8UJQZ}2uxxcjZlCNX@ZDW@6TE#}H^uDQw56#Z=>xiw*Q@kC z0B6L^9=TB8FOC7qc>NQ=vOq?^-UE`7i+`Y?o9;r;$}HU`UF<`;7x!u+9)T{L1G$XPu$TBb zg904uUa0~3g*tL|$OLtacbTmR!!`l0WgPfgN7vZ7UI7jPl_ezeb4}e<^>u4&3xEAu zGNoSCQ+1u+e0DkKx1SHTx8EiRp9RvY!k%1G2+ehon2N*{64w<+V_oGuYc5IDLJ++Z zo+C!~k~rFVtKjUG%~(#70qFE<@s6+Vq9T53j7$?E3XMaq3o%5wq-HYHlF+!Ml&QP_`+k*$o1%|b64M5>4 z=N)eA#MV9apHIYDy0^|CL$Zq#8-?_ZBh;&sK$8b;ZD~1g)~^YQ_R%d96Ryo@6ycdD z@{iNt_Exx{!U92jg7R|79|xOmU|u!d5FCXW`}p>@dU$g7?vjaz_|aKS3xCI^NL!ra zPba?*$J~{{a;Z)z?sQmz3X2LeV*115HzM`b;j4pF@v^^na#1E$)+@YpoeP~$5K7$gFS5?%Dk zO)VW2Y;UV)=cjMZ4=yf%y?+5N@FZWes-emz08+_9K~6nAyF5HSfwBOL^R}{uvCrt> zXG2i6I@9HlDPKG|xKKdsmGP#vmy2d{O4*N@F_dtOsiK`8)4-4xe_H61f*eB9O$A@1 z{?Bk?6d#rDAE>>ks?4FKCtXESlxbPq(5`5_B7SWeOTJzK8D&N;x=8NBBPVX z5IFQdUYwqoY`%pzU>#Bj2A}^ncPyv#{FbP{u}8HY*)1cx)qhCKa@sr+9#x%O;O92k zq=nh*Fb)yRfb6qWeWtjqfaGETQ|>Kt7mup3=k)gA^Gu5Wi1DvOG?)P)23OYmNFTsu zwE!)-3`GRvQ{c#c7HCfgAqrGJhs&k*dGjzZH|&>_Fzc0%-lmjr(dprB=jq|>u^;d0 zFY&K{Bl+?=x_=i|Oe>4d{{i}aJIZ(E5qW`ux79!u13%4^YVda$9L$nzR~7fcS4EeV zoUlodo!-Ynu3T2^Fs#@*z6v?Bj1(rVXCY^+g&F>mQ#P<09BGnc7FBGCMZ_}}v99G$ zPB4Zkc_HI`O{brnW0Dg#Q%*y;Cr@ZBo^cP=$<>BN=YI+JS^5@BxC#qNbKkK9??Rx)^GTZAD<}|4Q!pJqjPHUd94q`-EnH~!7g;(&tQN3t=YT*3 zI-k%`mVeXuF*Ekvt*z2RsU!7YETA&|binHaPdx-Bf3|j7PX{aHJFkkZ zdP=h_xTS{BZ2p@!ZPo|Bg#Pp27Fypjx`FYyXZ$+Y(SKV#7fx$Fe0^5F34NRV9QqD< zANnr&HS`gU>VM3@Ul@k;b0#-NIUZr;`595GlqN3c}}4)l$tVGOJ?1;k_#RbV%vwQMTs|H{&TjFbCVVeA4Wqo3w99#K#E?YD)s zQh!PRC%FRiI2c4lD(NQ>c^1vnAX29DELSGM9Tk%>Q%OH$ zm7`)HDAs}(BiQOH=?7m*2iywM&uU2``hPhb1(6cOLG+M?8QyTK;~YVjc{~Z?aRmL6 zjOi@}OU*G=(EP7RIjW|dhEuHBdQxd?ng(2He6bR%ew<8$F!oX+x&UCD(p&U28!h!=jgKfnfH4H#?#6Oy3*a`hYnQ%U6eltcEal}1TQjcf3y`0v<`ZUVq|xB>U<{^H*2TLTh$eV14RqV`p<-x!d@>$e9has&R_ zF^xSSs;PQ?XHy!7rE$t~EcT1B?tj2v)WQjrNQyG?2}^}1D3{nQZ)4yu)bCWU--KM> zso#11R#{I1wHc^TmJ9hhB!W=>-KNM3AtJUZVp{`$f#u1uP`1!pn+5*!fBu&Bw4Mi7 zo+VytmPW7t+t?iJEWhjj^Sic?uR`Tl|-oY`1totLe0PnvUZ- z#gi)vx-Z;|hmwEs+n}aS!`N%$;%%v0LRARcw_I+8*-3Ds&4M(ehcPoh!@FuuWvHUW_rT=CTJ)t#K#&cvJ)x(iB( z&BB;0sF1BlCiUP_&5ZQJ+OW+$1ab2dY{j*vIn` zMJLnDyu!l!KHZ5ux|8q3I>!Xo$weS22M|H3s&~dHG=9-3iBbTl34eR_D;<-TIl5O! zEG7G>YNIIwvkk6+5F-v_2K49)yJvfrXM40)fZb9kQPR6Q3N%`7`8b@g14f`5vT3w0 zfY6@d8Ho}~xSva}o?(~Df?%rza9NlKU(69A*DaS)P^{~{wG_R|t5->0#KjgJ|K)710z3lxGiKk{gfmFKj!id zzDWeWs_p_=BW^~$lTb_bo)&;P5-0qY%YcNSn1?!Kq3#`r2$uzkmdil*9I1fqZLJnf zjzcZcQ9O}Ac7MGz|gXfUfdq+nv_x67pUY-sw&-YF)j`l7Oyn{f~$^PC6 zOMQO4cR4(Hcl`3;oW;nQSU69$DdVf6Ksg(PRx%yDKRY--JU%$N+&dcXzq`0RJsut( zTwLtEIe!@LpB|qb9*M6HhX?O34^CbU&rT0dE{8)5U6EITKM%+uHuhDZle55I9G+b5 z9UZSYUk*=BPlgwl=Z7b6L@n3T z9G&j(9UTnM&QFgIFAj!Rdq?k{PPKRO;bedK`hVTYKF9=~Om_L@Zo(1LiP6SbTEU}Rkl$o4j2vghV zD&S*K0DC}$zaAGRjn9O-Clm<6h}R`Y4ZT1WOeH*6q{CW6`P#X}^EnN)v%vh?Fcf6k zv%t?+U&()29UA>>$zh13{yh(bWSN^kz;??O=lZCYa2BHRuXlpKGFvIfO*Gega76dx z!9X9B1z!bqk@ay1L~88)0f#x=P0Zntjn@7o2xIid22L?H1Pi)S$hkMAVF2e$#|_ku z1qL7W1zgcC5*?)HOr;)s{Ra4B-wrmaJ-Yj?lGvlp_=(?EDXKQ zmL?l%`LWm4kL{Ljin%&~q3XNiWRdzWA!Wif9SoJ3fePJV!v?Qc_meOJb5T(}MCwb6 zsVjed_GP_|Hpgiwq>(wRKOiOP`Xcq!Ew}>Z{k()Klqy>=)wh7DuA;|6U|E7sq%)Xg zNX5r+MFN+(STsCHUBd0F0$*iJ0+qZAe8VPx1ingRkKDnlzx7Ot$7#R4y>(O%Lv*O}o!szz zVwzPlRstcX8~{M@TV)WNAY_7p^O#W&AjrC96`v(pe#wbrzV2#4tRZ*xE0;{Zb9c48 z&i6qYzs;x7F`NEa!&(@5!1^#A|5=a*Q<~G13@g{seJ-_;mXZka-UQ^nl9+#^=*o#j zkoR-2TwVv{(U-Ojfpx}m)Wf{@!E$*XkPCLD7o915N%z)*#6^DJr{oAS?VH14bxi|Q zj^5iG?s4*gd=z=*C&$`5zH!1$c8JX8V7bNYmGw5_w8`H>5fpy%~|FqBkM+DR)cCxphZslgGOz zwTa_Bkop7}lKL#TCu^hpNb19QLHLQ36W)WD#Xa+LeUtVx^0~SX`w{uPae)0JjHr*F z|B(!bhbNauhp#^Xf&FUu>fq|~^z`UrIIQw{fHslur)p?-Vhh`fr(}bD%LsalTyg2jGNkZ)@r`S4UH? zlQC)|Q=LSNuu@er!ZJ>)vio^f2+r7m)fQR2wye_1vP;$$O<3Oqf^ri8cMGH+k&MBBzqK_gF3jTS znE@?15_cwjxqK$S_Ns4TFY?e?jW;T9SyHH~k_l*=ArkA1-1^8380ouHdHL!KBt(JJ zh^I}M<(%PL7>qFj1(ka|uo^olIM%@2lr<%IXa(+Xaa<$^&N3?Mi8p_*5{D`h+6XVI z{X23^9tQqRuOEMrTaxu}2SC&eyfI8I2m(K#bDa$RJ96(w=n4=i<)OY@W*PpB*bi_d zhyIM*`$L^rmU)(ied(t8(p5#AggOIH#XKBn#sy(z27H5rn7Zld>yZaQ#% zKZ;_8yTVP^Q{FktBP^*LVd2_A5!6>fRz1z4%2793J41iKB@`L^DaUh6{D8T}DddI8 z_V%1tt4|0W_~T5QgC{5M!fF??Q0{&Sxw_biiGZOf`ziFXe^T@an)3dMdH)E8R4)RF zJAWHxh}!E#*@{g41wZ{|qrNA)C-cUuU5Vz~-qxn&n=g}wyD83XOo4Nqy`vjzF4CE@ zC!dGtAm)EaIdQGCqDzs7U&M$uO+iibnmmA$wj=*33j`23mY03aA{XQ+(DrqmeDuMC z%@O#IJc4m~@(`Awr#Bw50FitQeDq%A@hJ<-6!rz)wb#p~CSrIH0Ix8m5f7J0Z&>8b zl6eZYG-|<8|G3)C(W0ri*fGL{{1j+9tX7>|2B3f6%NzQ!JRQf`21KrfMAq?hJzHA> z(K@XJ&brCmAK`T4_8eoFLMw8^eMs=bb>>^UvmfTApknOL`!n$77WBus!3dAm+KnG+ zQ{boornx_b{fTVBl)qt9{)X)vH()kZG_dm4`m3UB-X~Df9VbCL-2u{1P4O zhKzs7RN?_nztly=xfraGpcyBHMj7BHl$IW)@RD_4Ar4(GL3{+>9`NsSnG?Dyc7&iT z0+#T4F(NbM5@H?Dzu%8yW3>lJcP&=T@$oTZbzZQa zQHDQRhA=NO@3=0VDDgBfYp(zV_B1R6*r~j>7s%;N_`c%D-Z2m0hrAzq$e`Qa-ulR6 z*oGB*XDl4}im0AoB9Hj-Qlpf13LziA$$LtJm@-^_8xdAT_$H>o}rC&@1g8BzPo=+GQY2~ zxpWL#(V&*7-c$N<96YE*rD>oN^??QuJWb+!qC&4g)Fhc>U>MJH3TDJ~lmO0)3LW+< z;5G(R2xng`3x|BRBc)SWKVP8D8w(cCP_1~3A0Gz~XEY5H_yW6Khw<&jLzdGiFLeK5 zgqd#A09RWOU0{`7g|}gzse^y=LUp@gX98h9B_Zrh=yZ)_e4D~eN&*z%aA421-^Po^ z%J^-(aQI(cyIlU3={?Q-UVqM}3f;Jmt%*%bHEY^;2R-=1HL)tTHJ7U11O9*C7SX`# zFB7kR_iE9X^>`_AmnliXi~{-zY>WakPYy3m`F^T%VK0EqVl>{UtnREi&WozcMk{xQo>`;rz z_WQF%jvGd@`h7Ru$Z&r`%n4vEJQ2-7aHtFqaa})#sxpSZ;SgHmU)GMG;s4K$pcAP* z|H%W0_2d6J$4{jn{^i5xVbkH0iM=1;B1R9=<122SU4~o$KpRo#8GVx`^BI^B;gJ+h z=hMSDr?)ia+kX-Knja5kc<_01I7<(o2yens5a;zQo1(x3B^ZC@WmSbYM`&7j8bRT*Q@~QImEDe)1%;gk^@hC;~Fcoh@Az=LeO4CG?C7pM}Z#eG+QT{EP zt+4macmL{^`%v9-gFm_D2J)60)V5skQ58sA?)|^E<<_^d%7(iZDQ&lf=~`j!_XQDv zb`Wa1`D6kx1ucIqQ}5=~0IvVP$2RpbD^w8#moD@70<*41MfNjLA-l-*-F?3fwzB@;nBgHz5NfvQz=I7?C9N_!;=j)82)m2`4)y5U;tRW)Y^md^V9P( z7wGlvT^>R$c@@#Ui{0TRzdS#D_2!_^uYyQtczSa5LF$%Q2d@uLSf(Mfr-v5r9UYzi zg5{rH9HM_h-G-i#qRU=l3D=bCAXHMW`<&H{e%3YYxb|h{4?}QAQ?PcJkX|QyB1Vum zc%*>41QEfOp$I+{!2pX~vN%YUTt*))qa@xPD&6&Qp;$4TiMK#vPeqFu%|rsyGYw~Bw^lQQrg;2iboAdD3NfKB@F!XS^K zcKjVEZBW;`PHFIQmH0e(74j_UV1SN=IN%Oq6zGKKPX{1JIL(R|M#MwgnTP@U1D*9wU#z?ZoCAv$9!FIkX>66lHyox(lC#09Ee~`} zMiLHh45C)FWDrA10{E-kb^6oT@Iyz`XgaHp=p>G zDp<7;eo%}+tg^iw^1HYq?|e_2oKp%x8mr+8yYhJQ|B>K-d!=ed8ohScLG4XHvrii1^%?3m`E zc-3Wl4UZG@`{byuy%r-VU>UNz?=yb^VzRvr@O0?;xe$K|J(9;lWk^ih+U%b5;8JcV z0*X3aR78wNL>>y?_aB6_`iF|7bygL_PU=cUcNPA`mPIy=i|_kGQT%XCByzu|S@gJe zRITZkt*KG0sjCTHqQwmqd!M8qJ@sx*)fLG0>PkgZ-d%Z3wdke@->6{pI@EvmpHwg7 z`+UAh1*y!q#+_l=3MOQlX{7{(PZGe**FoDT)Aos&5qt`W+4Ir=m}tx+n(n_4i87iq z4^;&s|Gv1<@gUgKE-QKGwY+mFZ*>~_B59>!aR1WDXDAb=`dB~;lY=5`T7>NtVK*X- zx9F8GqbUi`jnQtc14uFT#ZrIa80^4QeezOAfTY{6YZ&@*FV8dqFsPHrr`mq$E$`2k zCWvgrHWR0^Lg84>xbu<=W*-Ji=3O8otchd{k7`SJ;OJ8j{UR?I{;?lslT!fM_>0msZIj>cs#*^-S7n+;RPQ~@w9=IL3sd{5wg1aD$0B?}x$ja)-{4BAbm|2p7>&o&F{EVd(M_`%#f|V~Fh-KUn zjx6ho>M7RD;&s>_|=!@DdD&P<4#bL%5() zcC$oC(%qc#OUr+4!p~v!`GoX&I84#>mVT)QQVG>PXSZG}tZP}dRH|Aks+JD)-9JK& z)sB<}>KiKupjlNa?k$4Sq-7n;g~)ZO5Vrxk^=mn)z8K#KkP(pt{UZWQe$Z?HQ+#| zDU+%9^74Y5`ulZi8>e}Jz0>19J-rMRu~1nkWwu;^Rn1O+oLuYF3juWQ>iHj=IcS*o zDFLPrIcI+k9-i^DA6hou2x_tBJxsQhi~XxW+b>8$(YAuGm-^Oj`a=PkmX_9&NwMCL z1BXnha6^wIgzBYUeyA^UKjch5*(+8BaBj=bd%1^KzzCdoxo*=hw zOLD8ODYKF28dr!eDB+i&N_N2%{}UCvexhQ-vSNQ#3F^jb#otn`TbfsOZ~P7P;IvAr zkzY@DtJj$vYAPaOruLDWJYX+p#kB43eWgKv&qB}PJ0i(RMx+~& zRF_wOsxE(RZRhZnuhoR#?p|j8A`fo81zy7X9Qc!w`Gb|Xv#?kexgr@jOs<3x#0U(y zpJkPS;t1a|4A>Rlh%hS=Nmk5qi@AK|L&o4aRoF`_C6?E7a+6Rt)}tq{=W zt6bo~O-b#7rVhwE$N_o~;DJs>4M9+f0L8$Zssl9G`XfWYjK5hXPks*kH8Q>`H$;C5 zfthpM1*l!a+#dm$(q`x^4i_Q2kch|#FahSiMaG4{$SDE<7V6fT1ozP&?bzt(qv`wp ze0y8{j^n(y*!y?tcQVNPXlI8czWUp5>P}9keQTiU1e`7=sRnP%X=fq)NqecEcc z!)WLQ=>~9iz}mSBP?rjw9h0rNGLs6Nd_hN+GGvC=u^ax7?n>^+`*A;HPG5@Nqm>dF z?o3jhuvp^G&Z^`=ybt`VfPB?}hWfB-cB!;(hJdE=2e=0)!-|Y=j!s|h9Sz@}p8f>t zAA$czR>~-U0UimHiDRu02j72}gt53yT;xd_+#>ACVNRzSsDq`Fk9$WY^U9#yOzSW;q=jApBH``u_OnZJy6~xT*u=iJHMYpNxY%0C-Z7d2gOi zk~Dl2%3d#nEF3B7PMKOK($_WFy!FH1)=t5glxx|eF(8+|J3j^Dd3NSopI#Pv*IB%Xu@8qO%~MfbgMoc}_Ebx;6uYI^EtTAaQRH#u(0o6MpkLAiEb+qWtx9f^(L4jv zfxEnrIqpAZG7E!sEhQ-hbvYDCT_zg(ZXy-m0$TNLu_8ZZI$0F%Rlu8V;SQQc{*Rfa zzC0Wy@g6vUMU5=Z?|y)(ra;||bRJP(1#fD}c&viEsNt7DiAyS>*^TC5k8j^aG3&tI^@SwRi6(P~|pO}7!4 zPSbSCp0y3r?mCfUx-G>vyRFv9v>nriKTfBiSc>U(8b=+oVJm+Pv(@bERr2$aSySV+ zrlK>$bo7^6Y#WH*!t8(Sq69XjhW7tq%N)jE2&vOy0KDQLM;l7>#;*j5}(ZO~rOi zr|VwXW>cP_9mRiUjY#T^I6`-JUulHhR62n?EKS2~zIDvD)y7t|u@x&-Z)Ga8f)!Air%(NNkG*X+7ZZ7JAY$86ZJOLSW> z?c7!+fdSJ3`WoYnwRkK5rq|T4ZN*vtgjJ+Ca?DoOQCfed(*=<1Q8LruBwiVm#*qw4rU;?#m!)&>&y_RWNP$&4y_fyyG z+HIx%)^^P99$S}e3Ao3uX)E2JiK&}K9!u$%?PgnXOuOThUs2#IPkh8Nt&Xd7AK4%V z^+9|$`<8#@)T~S!n2A^AOoXkyvYw=wRa-e$xuD9aX!8ukdcvZwU?x^SYc^RUGN} zoNb-*B!}j^wNAo6iqw%tr6bu#+K$;KYbZxWSZ0(i((oyV@ZCcn4+w3-4rqk`f{BdC++K|P7EP=dMy3Mxf zbR&P*C=}atn~qeV?JBK_xna6jNE2JJ?#$+%4SRnX=$cJf%kG^$vDZi>7H&3!S|;{( zSEh%qgYu0^E%^3ti{l%{#|HVa;+nZL^nH%6}6?E;+!{oH|tYP2%LH7%=Qw2U_VQy`*a zw3XHfmPw=4Fv3%%%Ct&w3i z9n)#K1{{rDv)zE%*JyPV+q5lyl*}vRNk<>|!*mqU*I^V@37eME;T_LLq!WLX`=;Ge ztf>L>6Z+i^%y!dkH;Wl)bxg}~j@o9U-B!9lSu0H>MA|U!YpH=MwpvFmv)hJFsg(zj z?XF>3P6Pw*7!9-At(IY0u(qrwpR(i4O{_Ezj`%&c!?O#HT^yXado`w(Vi)a)iDw1n zG=!_Nd^?!h7F50g9b#Gzv>tzYzU=~O?=%g^w7M?-ZT$VMhZL;+toc*9`PlNeL3HzU z>v2^*R@!S<+G~sUey5Wgxp26J)yj3NyR)rWSKZ2H=`1g3HZeKgI(LTsuLsw@2B)NR zRlSimVVi=Jv~sRn3arcKUAq?Az?cL%j1D;YXya|6XkH=6}YmMe{>omy3T_fV|40nyB;+y0|S_lWXw`<#jM0Pce zCQ>)uX3K1KflJnbO$&~f7I1aoTr;~i9IdwPz~R$`Bd2Y_ac8$0`)$)|G!)x(tQPRo z+78=sGc3`;9kAJKm~Fc?GMin;Y+{fNg*Z){6{WDEY&bxbrrCeC8$h~tSjnzpTXKb! zCTwQ`W9PtsEu%HEj0RkF4A-zNqh;9E1za>+ofb?~OHQot&F)t2qtBx#n4(zXub!B| zLkfV_fPV$=$?SHUk?8`t*Ru=Vn@>Jlu^<}h$o5_}JMfOC>;lZ`CPq6silCy9 z#AB_w`%-KEjfa2ZaT{xB+eH;^ab*(wlVTmWO<2O+Zd2|d+{sP6K0nbzaPZlR#V7T( z)WkPwIC~4Cq$JLR>PR_CN2-h5U9hz{Q_FC_81E|X|K$9vuOFnoK8$aYe?O7cF}p78 zpPi23m`)dN8HQA8O1HU7vOE6(S=(~%Yc1^Jvcy^ z8K&EA!iV6lU^NW0>AH`{T{vROo;50~0M-QS*~}~swG3xH=ei84`F}*t%3kj(*2FHS6?9rOf(iZBmEU0nytRL&`Dw#^t<=p{npGwfBhZ_Z4dOkg zMsC9W$la8?^RMK-Gr#TQY}!3a)9yG);)^^;vo$iLrI=2;6PWF`*%o^(+{io4j?y+A z{KsjJPMa;c5zxMtV=FFXg%s^J+|_OV{K;&Cn88AxRufoOb}KO3U9;U4ALzj1Y?*D> zY`cGAa#+BV?7Dj$IJ@x==V6*I5N)k}NZdfuNI~HsGIh*0lTA7(GO=N&GhG`zs$3mr%WQWW$f5^9oZW_hrF7Ckda46FFT3T8OuOxxO}k?@+g-Ekz}Kv9 zoBfS22_&wUV_``kxe*C$Xt*s}kGJxo7^8puUacU5R|r$E()Ma4DP5p=TV}KC^6wcN zYlKBbQi6IpSQl2W$735!y zkDwU_wx?wS%dFEXis4J_LRw;{^E{$;DT@mik#4s!f@8!mUAJSl;K=FX#DNbrx{XQO zwA;?ebh|D}db%CcZnjtgh&P*UCNZ(i4wMH{ID7=TS1U7Y(`h-d1aQ1WIGkzi7JL#8 zGbqO>N_^We8+Ky^Ox6bWPPc2oH;sR?Qu~hCZaKKnaAM*C)quO0*;G0ie$#01$WFt+ zZ%)}4npP`-L);ZyW7Fw&P1}Jb-0Z@s=`_1|7jMF}!Y3%w>UI^^Y(wi>c2jAXE=(w^ zWm;|5v|GSqZJGENyKH3I$PqQ+LTNQ!6LuP>Wj1X%zg?FV1GI(P=FeDUhO>XVYrv-r zDZEx^R@%gXPgS`~E#UL7OVHPO9Idq@a@ttu8hu;N7J_l`r^*t;0EnliE@6@47%WGP`yYbZUYP zQsD?#L&zVhaqu87-o)34PMj`vre>{Dj?$R|+sxXSeT^f!fyOr5%?9juuzuXG>2?by znS)Gx5X?6i8MJ4D99Dm@SAdiPU5ZvWa)Fi2HqBZg7RX_ncIWY{$`viM)oLhq)9iG* z68orYx^By~?UIKCqB_{Fn`XQ7I5iq(tJOqioV#ZOb53D@3pueh0;P!-Bc@!qmNijN z0*JVl)d~v!zX3XMRIrvh#YBJ+z( z+PAIh`^2eoeNi>N67qQ5Fm0Q4cEhxq-7DLzG1gi&z8WSw0xh17qck3mq3_&@ZC^PP z!xqe^!^(e1(Hf&4c&?`7RHcm_7g-x9Y?U=^aZo3w)$PEp&~Aaa->sy83q%*v*j-eg zZ6R;iX}78h1C6%Xb{cQnS9S|-QI6T1)Kw@Opc~V68b?fUC5TTPV~wOsmy8f_r~`1IR23ULoSGeCq;EjniNsw1I8S z)R66(UK?*6RJ_&2>&jc#bggcL-C{rs+0evbENgv$=+=({+_j1O#jYb-fy`y6!9{M8yxQ9bpPgw^_&bsKeA)F(GQ( zHClglm0O-r!H@Gfd>Z+EZfL90SOq7zh;*kO#J5qHP1F?;(SRFXM8Bf*JR8Z;H>ni; zIt}9g_x}mPOkEAgapW&(>=iy~yajmG@W9HNrr}5>%+-e!z+8Sv0pKV^&HxTWr1x>~ zAiXh1Al{a}T9GmR@A>Xc`rq>16?rB)n~s0NDOhqN>^+PgzN^qB_;ivmX!*4iWFwxv zXOWF~_MTOgczORPi*%kS(pixiO-D4&i<0=ex*{LLD7k*fX%WTX?%}Veibz!q{r~u0 zz=`-jy%$#GB=Wga@51^JtcLGKqtjWQEte7qcOP6BBsWT~189z)6reXNavu48759HT zb-=9=uOfdjih?Zj)Ht{^!kkW(ahlA=V5GDnZ_DWGJXWsr*oeaTqXKF1VfJ7+%IvdY z!Jp{XaEjbUj)bN%Hf|#NSqY->HpcQZ1DX$PtZ?k9*JC4}Q1FPvApfrCd5(6%K^g`I z{X7feF&(SIG7*6`0^>U4#jBoLRJVVO8*^U|o(cw-u*tCEqTrfFs;4p;MbJ5P@i&5Z_WS?^}cAQiC5q_g_%6J~%p2llUD5dedY~qboM6 z^1jV7+B{>WD*)lB4|UnHur}u$XZ~fZiIjCr6@=0$tDi6*2?T&?yx)IWu?F}_`JFJT zGGCFOH0)<-Cgi$Ky5u|p$O~dS9bF%gW}~4)9|j?*))M><3UE)%#Fc$fUWqI=*yMk@lV6wSM}MV2Qi9phK{I$i1ILIHrnl-|%(q$$A-aH2@L zd-VYCK8(l7J@XuQOvis=psCsnOUsNAn5i4t2oox>JL#&VPt+&2O*U$h@*#rTq*GKTFp+X<>`nR2XtfGM*#p@P4I?2j# zA(;b=r6RDZ6;>cv7DrGNs^lsneVS+mW%d@p;Swzxd^i(#NF|5?uoa< zSkpO?nQZDnHj=e>gy`KhIjgp9ydsyqoz)aHg9ozS9hwn)| z3SGA9FMOx5Df|cDX{|_@9RwLevMyZL@{No|T+DytaqytY`CreY=mQN>+`CKXuoMni z>8%9L;^?N#y({~=;Q)k^h=|LG9lPwe7l0`wUVez-OcHcf+11|9fE$0C%yaKeMgU$X zM3_8k&Qj1r$X_u)MNJHA3P?;T!$uOoO+aXbw{bGhzm%1IMyt<{4d_stz62iuo5FDP zv6z3TDZR|ccl}%9_Ck!xD#`*g>4_2*7g-?6*j$2)dY|1Z}fp`RGJmOOVl`uCb` zQ9=?I(}-k-O(J~-hs!S&?#y+ND(r{zMU3fKxd}hhv5_Y; z<@(mRPR0*LPSZRL(uew8Tio$in5M=}G!KEX@h0+N56or(+-w+QYN3Bl zgY-6xjR;_0Jk@NbbP6|7RXLr}_|@e*MI{EnKC;YW1Fcq`vux5eR?iwsXT--}jtkOT z3g7-;tgw^dZZFD@gY@HkCKw0Mm^6tpReW(%q75viO}wEvxC?I?X5EUN3cN=)Hc1=+ z1MG$Oa{scHh=D6xmeLzh%6<_D9I<~5RbP=`>odWKgFB^sG_p~eM3G>?OsN3`j{+}7 zHZ?$1NnMej*13J^iv0Lx9v~f6qLJ-aWAdqFg3b9~atki7MqzyWs$kGD%(17(U1H^J z9;+3}N1K^+m0^tN`1(P8x*{tQkNov>f0CBtTNFMH??NE&#Nei*FZNdQe^h^Og?~?P z1%J7>veBj)7nS7|te@&k#m_(iBdG;Aw?)5XG)O^$$GL;^kze78?9FCXUdTys_cB1% z$sf^QpxRMFUZK~)#M7iCYi zV1Hi-aAv?hWWEGUMx6B+8|>#4$YbDtS%gCpx|UpK$tX6$I0|EGTm#4eXAuLoA|77& zkj+N``>n3PEs!ZFwem|D`;z^ye<_S_68ub2cErkd{`IoKlL|Q827-V1O$7MdL(g83 z4(Y3WkkkR3l%yEX_k3{S685Wf3H#}r#+gkrKE|7dEKM|VA%V4T9))-GDx~+~DzdSH z(#7aUUZp@KbVMpeUC=xaH}2X{+`6 zdK3SKO6Id~tKj9?f4+Zm(@%N+O|?7z`SS8VU%fECqbbOyzNLzXJn?_Nni(C<(b@dB zRP%x-{?Au)oX@|hhOaK){WnyO-i0IjZS}*X$3Yz2!nye68pfl)v67=Cz6o!?sgnDc zlvNV=o&WZpyb06keUQ@U^Vw}0jK8fbU-L}wc%FZEC8q&6^oxIkctoG4^g2oMKQZFx zEW?-USgB_^Dh{Zmc0hr(eeG}x|Bk~62#Dv|L&oUYZ`cl4(tmfyW3~RmdHElz*tegN z|CtK?sdMpbRSHIbQ?20ckwo-c_a99ACss=~`mi26ru<4rMI(|Y+Pt{> z0ZSd2=TJe=08oF)h*fdCo}^$o_ewE*!EIvKkea?yGS87 znw%*LZUO{<6yx+&bzeC`&M0fC?j?z)ss!{1qzV>+-V;@|pQ@_32T64;6&b2^4P#N& zFn+S8!0*M1;!Ge-+bM+)e^!LWuF=I!nBQ~n@H$byx(a`d3KXK~FzA^+Agu;|8NJVp zN4}aTGnGt7zIvVHc`{YW&B#|%rUf+{`6}`p$zaALNCzSs34*pc>MWVfpxuw~Nf2Ywctlll zG5Y_Qd((gRwv8oG`1}186r#%jQIn#RIrGj7rOijSe`E5`@@t4h0koeHJ2Y7`f=oYxjo7Etbw(6>l6Ot z%AZ2!y$K|A^yy?O>s|X~GWVy>qnjn&4WGbju~~ndviHmjEOLXxhhOKgOtYM&{KdVh z_{IZ;(2-BACH^{;75R%7s^X_Kglt;JKDF-n&v&w_cy;R2$%#*Hd~)uSd!HQnE=$2nKU=4D5W0=3Wrr zLAu#~-)`wnzpwSCU&w?LuY4-RYM6uU4CGu$nMGIxwow75nxA_ab0x&gqDn$fxes8B z$qvkX!TFH!X=R4~zy4`N^C60^GMT zNg{`s14=iHBc~}eiy4cTwT1y*4v(`~AH9@AJ`}lL;t{f!Acp}J1IvChmpF$3Apu91 zT89B8e?%^GCzep0i(@%8vbVgfpqycrEI@NRu$;+gTEgl*^SQco>D`k*eAH?r8 zE}}rU_r!HOEwUst6LVM^JRY;Sx=gp1}f!(e?91 z2uYDJT2*8Qz9j;VnK2;)rB)2Ntd0kH~`B=@2l@o}Kmk z-q;iELIx0bqae@^>Qcy9G{g?z)j&VN}z#@_v6;j|HAVTvA5`pew zuOOT>bp5@6BfSN=cNgTr^-CiQm#+~ie{2|7XH=Grq_s6uIi&&8y(_>AIFdo+{7j$V zZi*6z73zLSReAB9?#=Ms1_nQ&D0qDZ$3vW^1i!fIb6VCp9pk{ z@n~b7DWnrYa}NpAWM;n+=1=VRKl*m$DC~F;#4Dcc8kmzaZ^^A5Q{%IgrbC{6e-mD< z*XeK`XL)%y4;N6el$isKWMn2{;q=5c%byDi9>hd|t#nQ;qyEIofo^~-l#r7$pIg&m z7zOP9bY>&T=scw({poAtQ(FLUjiz&%va@w09j8m`Hn_U!UriEfo-Z*Y=^NgI%LLkH z!Px?7q{ZlS>j(5kqELwP@sC7^I_rA7_s*b?~o zaQNPp@D1e$QNRBOmnDh;AObncmq&^LAb;vbel&2)(A()FAEX`82sZ&QUnrUIc@aLH z3T#D=^YiFH9u1rnvJz1oG1!xIhzW&33b9@rr3p*3grUvkdTk{j-h|Pn@)WMugC{!_ zvaQO`En*oT8KVG9xL9!424}s_arO()eqe+8CB93C{AIsyjD{Df#W{c#(6Ct=i+@&P zl3N4fqoQ&Sf%TSrw;m#c130$sE!TQ5(j2CRyah!M&j4xt>GOn~x?XI8eIpOt3UK>K z;P#Qk>?4lXM

2P*_xmt-MK#nZwB^(UdZhb5cyOE?%Kn7oauUm=?|uxA81^gK&}@ z?ha^&;6=?TaT3y~M%faNM#E(sgnzS;r5O-Jf?htw!<~_gzVz*Ahz9D2oydYlg`I)n zz$5{ghTeya?*qRsEZ4&O!vcT}`+bj(v7oJVXOC$mZ1fJDNHb2BmTYER_H|JDwfg zhgptQxB=?vElX#M_^!gmWJMPj^MrFTi-ICr5fVa|a173^^dv)*5$b&mC9asH4n9=Lu zJ(<(%;RD{Sx74fi!`8j!kgJyR2g@N3H1Wv=4SaG(wN%dM>-ZJUUIYiiG7p?Zp38zy z3YmSS6bKrV;^5@bSNa;SY_*#GzLsJ@+JESN-#()jH+?iZ;03$`V74d?{Po&~ABn$S z!{3?j^!p*D^vZWG=+Y|yfUX860q+TW`lZPq2RZp5~qxN0faDi1D zbK-0PR@T<=sVzj>GNK5z<#Z4SpSW&w>-J z-#7{WQHx|*4|?ZG>!?l9?1m+7R3aW^+-}pYE-z`Nq?Ngn5IL1J&{l!p?JNHiAO=;n}uH)$@D94z4pig?#IR< zL6$-37jx0B&~|E4nAG2huO$Kw`+dJ%0Nv(<$(yoBB)srRP@wBq*6%M`s`Cj|tDEBf zO`UV;_qX)l`bFCi${M813cBK?p-?;v5|ZnlB?*b_GSZoXWj092q<>1I3KwL1A6i&r zYis_|I**h6Ytkm?o)r@|1?fHvX_NKB);f+Gako#sOj`I?Lh$>@kIC`#00XBE;|jMJmB7#?ns zPeD&|VEgO!j&BMwbKrlAGa8%PfQ5QIj}s|X7s`)NY6U!U?`C^=a>#?bC`%iZ)QzLHI2&j^kWNy;~K;Z#lk2B^rtm5OwbeT@J`ACsPoG zy9T)gXi`7MWD-r$n|Csrl8lBOqGrArh>c)zw97XfvuC~?BM{>wP`muB{0ZgwQx?HH zamw%||E&t)HJ(pw5l;v&a8SC9-FqZ;RR5Vcco}bgknw+yGI{bc(eNO{@nyq%s;WFt zQK+~7q$Us3v?S27QubJvhsEQUU}}S^E4+KMLOlWldA}#{!~Ty_a$~~HpHld5U`LZ; z3JZjP9CNH!M!mgSy;3TK3=-=`k{?yV11wmC1{!4sdUs7}!PwY#mkSOw*J=n3#!QvF zT!e51ETNY^RKy442j2w&0!SEQZZ82<3SGq?U-*BnPXeuNc^*gLYe$y4QZT;r-GIzM zjIq$oB%;O+rL%s2M&_t1F*re@ z;1)54pxI6&a=8;f_HHFYL&EP4G0osa(ES|ma0>85;Hx|Sckh<}Ey+1$HK7A1oZu|+ zq4oRc{50Lu^GQ6dm=)e^wBno0z+e-`lqwlSQn+col(fdg>{Q=Zq z@?_lzLEgC%EzA~S@@=}t4#6GQI>NMf@`hq*c#++Ni8c%XLKk{CybrS$`2wB{eAg5( zM8lc4SX_Dj%{vJt{7}lnQkdsAsGrtl?byLFgf;jmMSqz(i6DJnulY&z7K`|f1rUE2 z$LRLj5a4MPbPJl(YdC<5_1eCrlwPmbJIQ#G6!x_ftFJ=Kxh# z!v%}3^Lf8-FKNKnU`CfdmH{Gv#mpIIahi7+cg`kkI+#sz6qML`#}JC@5CSy>pH#QV zO-SnopH(o-{=+1~drw>53p3u?3$&W&|9AzH89>eY#KSFM`J;hxROdCFJLPw>u|GwVI2&706I&# zDy2h?>CyOpJOiTu_a2M^0{`G*ayWf@H#wa4`}Q%M(PM|h$UFW(PU&PSY#&Z}h@lsg z^XboYgu*{nbPPr0#pE4-bvk6XEPZIVD+QtS>N&lEReZwfsq!nBaN9TJ)G5Q6edb8q z`i3@9_JauBszF!s^>bG><$Kq?!-$l-m-TchtI_jj_*+V92EoYcL+d{QJX$T8xXKh*cqAD#)ih8 zU&z)}NK&yGjN-1PllOJ0P0dzzb?<`Ca0CaGi-v&2P!^VvAt~a2bb?TS?g726Bu0Yz zke}_rXF}qHgom=~9NrtU$s<$b7oaoI68(vWS-fP`JvEHIIYJZn{h_+==ON@5=(_J8 zWACt?C_wY0v;{rHb@UK6^<35Nh$l{Rm7aDDC#VvH6ugpokau9S7WBTbfdM5x{L0Wx zj1=^!uorNv92K^I?;O-&Et*nItUedw1f?JnD!-%5_Yx*ceG+Ex zCQqjFa`u289g&RM*?K*4euzokD313p7<{h;g948{xLH}+kuqEh9qExlGU`nX%r+$p znho!XPqX2}qm{xN-Vg8h=z?sn@~}sJhct07^%MvlD-f!}MGGWxPrpZ@i?%aZ(ET6Nq zVxzTQFVVJdh3uo zYTW~Ia3NV)@^v%Vle3nK=6U_1IcdWH^YQ$zn3T?c$qkjV7uD9ucqRALJt1NT-IKCz zhodTuRHac#PQc75Zxtkr+lwoxf{QNiHJO6eYDjK{93+p(p^)WW2piWI>Y3ZP9;@fN ztdG2-Undt+a!TLrT~7|DFkK$@Lx@irw;i-A8?-gKMr;W9|q3`H|k8gZ)*kQ6MYyD zc3niAb;d{Ti2P39?R^^jM*e{5&c=WG_9=O{_hssE@7*hot+;T?0PVS~8Vgl~&nsApgnzOy@$*)r~ z62s5I^@Oo02^r;13;`#D<%N6e5Rb`OzHZL)Zt9F*yCae@8nSW9+?);0CJ~zsp5vE@ zn*lX{&&CVp#thc)Zc2Sd?)l$A&QK;_FcLKH!*ojTH?ubeJ9Anz+yHAne?SP+_n@gI z9{=0#w_Rghj)4*V;7c#G7N@_IzwUJLlT;LJW3*nwpR|O*WS}@jUjtMYdtjySQ`59# zWSDb|N>Ug|Pq-A39@9&_~jcuyf!0GBm0el$X4LAeR+}WYr2mzIPsWUeFvSXT> zrXNPvu~vVU?kxzZ9D?gOw(SB2pF>0q2Z*lRR9Y86W99m=IxY}y#qge9;g^D(0VM+8 z7?-J>0bvujTbQPyD#(XAhVEqAMq3)k?roPHodG(3z2}f1u>*t13rgUdQ-+pILUZ^= zu0Yj^V={FkVRenwPL;2b!Qt%|u1f6HVBe~^Dde5U#3NbjPQ+=zQWm^1H$)KEO>5`f z0(h$m4t2rCM_i$RTGO#d^)4l_Z0yi)8 z2nfxa7r`4t6VW^MAE{{AXbQoG$VS2T^H>2OR4#4XGmITlJ%4>2!iC^<1UnE;0h1h$ zxMJ2-0gal5`w=kxop2PX4N$5chjWcBRiT`Jocc`U&oXUuXZDUS!#yaMvy9{wixBNE1!I4I zTX1jCRmS-}^Dv#lJMI>wP1Ul7<-76<_yyEACCQ{9`Ux@AfyRegf%e#~QD9Id2AP27 zrU|2jJ??qTFW1pQ`g2Q%Liv*J?K!Z3AY~9UkQl7_(IlOM{mgI{p?#qQ6vIlw13>Xe zKw~b~X@QXt4Gu#MAQBuF98>}WR7z)mz0RDHEi%?K);FaC%)-h+1L!mZ9E?l_@6vb~ zX0_|H7#eV7JA~O84|mgS!&|Ryk9M{s3SnIF;+EMCB%F$jI?hVknGj~jJmnA^aN?B? z30Rh=@q-D;a3A<|XH+5wzw?$`zewFmp-BzQskaek7|(P9_m-reilM%+z)QY=HdyAY zz%(=(#?i|#&2qj#jRHY572Ja8V?o~+wnJXw-&LJta3{-x92cZ0$SZ$ZI^^Xe>X8=( zIWNe&f_McvET*L+#B)Lx88Vt^f;I$~OrZfj6!}t6?n;Put?0V(si2HLwRJke=a-(L0V{uu{jAs! zBK$+PIP`?EVO{yJF9qP^U$EMay#uixuHlF9K00^@2yXQFRro`i zt3qwfN&es)$sjh3+>0Gbp`GE;$rkCfQKj%RRD*<5INn>UPM{=rp z0gaeRa@E=c9gao7owneNMlA>mr1xt48W=$REJ9e4Uki$v&Pu`paUe}OT}ko)D+mPe z@DQB(Z3H#{>yRC=4w)PzdjDO(mKVX)OqEWeU3hkm@=ckqMf zaq@r^V()*)(UT{GA4fm_NAG;jjD`-2d>*Hn(NdVtiz{p+zq`uzRkQoosh7+#odU;W zahma7PC}nW84G$4wl?j(IX-_(b61P_3R4U0pBz6wdV6++jqSs3ECVXxW){%ZGA=TP zL3UgVe}fY!$easEF7DkjS&U)LlR(Pq?TJh>YXyH-3;7ePVgfn*B~oq)!Jo*cg>UZW zjDh3mi_FhZsWyrViB!`mj z2EN8rXj}!7W0SW>gPaGu^>iG$`Fb4*)cDn$QW79*6#|7BQqXsyn$(!cL!jf$b7R(e zn5uleRu>}lF$qDPF{M|5&CxWHe<{5%>dgWW4#9Ly15WNfkunJH5}f0$!V`rX1(5Pu zt_b5r{r*n9$cVdH{alHY_%dSxSqSXq*q$^OT~OS4ND(*5AfP||0re0#I|a5+JQ9v0 z9!wG@RG4&vT@2$03?r%h(eKCdQV#%%Jfc#c+lzo$5cGdZ!*N|fRMQ*ve^T&pmX|JT zX6(+n)Dx)?ej$4$jSs+fN~8%t@Tg}J!kV1QfSd|J5}JV&Q<*adb`0qx%#{U`eUhrV zodrNO+l<6R00JC3M2rSulI@X649N@74byYncJqA|^w3UK;q$;w8$E+QA!_!Vik>lU z3HHm#Bo+O7&cU+n0LdLBB1?VTJhGIV7n7wv;sNm|!IUmZOQemAEJX!3e~++DW&sBGX5w(21$3noS2!yJsq8$#OvJ4Hrudi7ukIy{CA>qUmtTH#P*E|}x(}xuUi>bKup?A>9w`&R*Ny&<5Q7ck0H!h!b=+n4#T5@ch1j2~L z%F_Y`b*vKBS_qiTa+Hq&EyFoW+*O_Ze?x`_kngY&{2n6v7K?@1mKHG8VNlgz2QYu} z@ax&>Th7Va0rTU4eRzNTTqc`E?vt;O;X7j33*7|mL3<{LgFmJq>1Fik0K#INLE;QCZhi7Kr-(x)bhyvjhXxKl6W3HLyls4-tJMWKze0*nx07%j1)|GLWd z5z&F5!k>HTVQ!CViD&ktThwGe&t#Q@^5&tWa%~IZ%?nj zz~v|3a4c|$szRuQ-nRG#4%qWK^KVqEgq3ae^$}|HLk!X6FCf7gp9jPGf8N+Ra_a~- z?n>G-u0TQu1M#(dawry=42MIh88NMlz<6mm9Cr2uXj0T^e9p$0ZKZ6^o8+`>AgEn= z3~ttPZ*EJ#hiD0#4a-%rqjG0_xx0$WOKE6>DqR?pLY|y|iNKx)aM=DoGkf9ie~`@tLu`ePqahLI!J!e@WA_kZ-FOj~*6@;tP>afvpR=Q&$L>L#f=ABinnO@l zBn(oP*^K^%YA5jdK^PV52XbozngwX<1CcxA1J`0!J|oR-Jv=|WXn1|}8Q(Zg2^V<` z6JC8N1m1mY6Alr4%9i>^A3+TS9~{)A+yVfuS#HGkh3s6J&4&m7Pr$kh)EXCm7OY^; zMUVh3(_VQ}MDT4Wf>g%+ml1}>f5$>)WAy!jD{p@*R3mEjj1N!K=a&tw0Um$epB zAIt2Bf`(k_CBEZf!Y>`L6YeATt?xid8I2?00G2ZCoZRWLso>!Dgp5Arx@ehhh*(n5 z_ac|gtpS&RV`t4TjgC-^4cyUlvE-&ycmmmsRlZOwj1rW0gUZ26nP*DKg++oztV!-& zp;BsUk?$uymvplr#C$uHb5_&urwXtU1#n9S`Q^W;HL#}6r3vl_CL%;}G60eZA3J{( zXl?U5KPgBbOVE3n#!ImV<^&H-8FAk4zX*l`)`4<=5&C#&M|=y-DC>~UcE*)vbR%zy zMII&#CK}7Wtu-4ByUUUs3+*rW_t(n7h+d+$2sra)m}S7b88{W4Q~`q@z?~i0^7xX7 zTuvk|6`xX0@k}p`xUF11GO-RVVjaFeI>az~0u$n<NJr#SRB9Rq zGJ+JdvUv=7Z3WRpyQ*t@ni-~77wuLlCR8KV6v$)Dbvt0xTP;DOiAqsPKGi<8)c&9ms$*5#X}+#D+ITO;NS`0a-qu-K z*6)7~mol*dHGlgOY!im{J_TjxN#!78@Y}7-^T)s`FA$p00kU&yWAPb09B1OyI~JV- zknwz;m1UBFkAHXES~&;HY%-e4D&+->0^R+$_c1Vw8+1-+I3X)H(!5 zmJ`en@rQhT&jaPX4yXmZpGAHRC7_Pr6NR2EM9VBum@VmLkb2kRcEqy9l3s#o6h7{*euFZ9DVP_tuvk({ zPdLY-y2XNtesL*^lkqmAtyuM4l1hk~&)@;PGdC-{k6~AvB zOSfr>GD|jXa~jpCi@jL3C|L9NjwO244Pbxg>QP?3S4)#n?6`Wl35mGy_^7$snwRm# z6wqnD2(zY3R<6oftix|DWMgFQAkPSvyOVp%c7 znsrL1$O(mE1JJ}4%YL78jSv39;5b^Z^y}r;EB$uaeWfe#EKy>k&^4SU`*q-8@uz<| zi#~Ga58GL36MG)r4Fwj}35)UbCQB4PRa^cwa7qLwRj#(F-~U{1POj2|dFip(d)q0+ z@~m3lif_0RW}{d^PzDld@lx}ajY-!wN`4Oay!UX$j?l6Bd38K`PZgD+COO92p{}M# zmqd?2%+3+ij(jZh*c7!W+VVJO%S?ZywSJllqGIr5y=FhF>pz^;$5FAN-##YnTUlg8 z)!vUm@@1={Lp+cob8%#t2`Kv$_B0y54%}aZ0~0h-0vwd7)oGkg0bXX2=FbVD3xi+Z zs121u4G9Ejkj{UCBDLknR<~CoNpA2~zwhIvao>w8h*JjwJyTjy*bMPZKtc@0mc~MJ zC&c0Dpffy|3$+0%f64gjHF;u33zJqA=1e<9*8~wC;P+xzFSuqtRYF>H-l@%e}|$dY=QkDM{1N z3*xAK6s51FAYCRTS27AIO06><^W}uWnVBBsXlB_Evp!}Ie~@IZAe}m)tnr}{MZ3Jn zBvv|>SG6HhN99}lguZf(dy1I$WNw+-S_Ex$-pQ=m{Ve?Wz5@;K^eShZ_*u2x^G(hh_DSaJ4K%w z`imTr$8K2SAU!);c8m9=qB*WkLyM-WB$e2z3cfRme{d!3>O730(@N9F$`CXiTa{a6 zz}Z!|R*F6{ezP|6l=aHUULM4?)8adX1+Y2wW3+Hc+$ieSSghhEE?5x1*$6+LEqH_Y zWZ|^2G@hL2cdx2%1y8y}cx;POYU6q?ILT$IvQqf6LudHSF|&Qfn|(B|6f&9zF!IdH`SSqPRv z-SP66YCRu{C$z+1`n_okhg3duI#*BVp{|y4r5~0;Iv6h&UXn43T!7ea9^dV*9+u@L zf4LKnk$C>Pq?dCP^5Nqde+-y>|6GgU9+ssNGhULDCFMG3asvW6)!(^(f+Iujm-N!j z{5WB~;qK~YS&BcGKl;OR-%>?FVxmhc9-guAEJ;8B#M7|xm zcSfCvVWd#NC3nc)UFEQj6~pm8f9)DqUTz?2w;-R+rpgh%~~i8ZMEFO)Hby++swGRk#pC|Rmicre~sa>;_coosTHJg z!eil_3QkX#yjfQ4Kz5hH@Z&>bk+<^U1y+5pxV}^1#6B8a9~Qh)Od~$N)J!8jKhsR% z>9X8iy@A3%O@k0!{DBvP(V{N=qL{}|ZA_q=(-b`bRGdEtA6@nMJ^8$Z8~f9;9PX~Z zK+C`2B8cw?JOt8je@lvjB@0scf&9|NUm_UL@8(-6^OQBoAM&k8<;OplDa&~auWNN= zBgQA>S6K)1{>d65B|2}0%|sQm)HOdK`HZe0Htln;Rt38gm7&S1#hA4S=xQPIniV`m z-ZJzp8O?PvB0tJd@Z5M(*omrGtkU+jBvGZq7F!fO0BOIXe;IK#0)Y?Y04&=B3Pv=t zmM7P-s)G`YJ$GRTC-7W!VkRYZJww88v{>8twdL3vK;n z6K(wxR=G6Tx|TiO`CFSTi8F`{kVBG93}jrlVc}<1Ptuh9X()VDS}a#&a)H$&juqL^ zgGy3RAK+>uY`*V?%}0abUj;A>CDaaKTNbeuf&_#1Wjj;btg;biMMM>coCm|UXdrqt zMC7EjHo)memw3GaCx7s6*%@j;C%>)78xWJ|-G%u)F7lq&6PcNUp4HmBB}nYyr4H>^ z)-Ym)Tz^)y4%kwGH^0w%lH$r_?>*j>4xTAM>IP$T}<`O5%cI6 z5-;H>e-J61P!>S`YJ&01mc)#W0m>#plm1=fjC8iuIO7pk7=O{eLVNh3Eevddj2iYM ziifyw>3S`q8*|~(10=?<&b=$1T;PU1bl5c7pDV&rlm_PYsjDe}N7~{7f_CQ$W)vgt z0JaLFf@!PoAx6Z?Sn$rv=QadQPmOI7+YIeii#4Tgg;53u5$!$p`#UkD4T0IVMoHvFll!F1rn6>kAY%7!8BV5P`Lgk=Wr7 z%XlbM2+&jGLPAGsg*OA))nO98=8~^h=tx+J+wfScUJ_C32otBMMSujIyG!zRX#`c^ zaE1_)V#-s)4Z(cwG{%WJHk5%uJ}o)>?waxm@=1#<{xYl(LFX5FoZDJ2j&1lCbMs z#~c`SWa2E36C;ErxPq6UzyTb8&n5Y^BZuFvmJ>zs|x~$Tt zEd_^CpeY51#jMkDz3#T&;gHU>xxirpWH$>M+D;z7Y?JJ6v0ZlOZ;;)^GcJQ;EjmB7 zI4wCpgnkak-DaktVt>?3$kitF`=A*I%vqHvc{3#mSQdbh`3C^iTuqGq!-YH-(v)6Zs?$H4(9e|w|q-rPJ=Og$(=*`}J_ zq7FCC8drQ91*rX&5N2A1$o1*eMt1<8(e_ZZ%OomgB%2WhV6_!85DVhR=0=UmCIB%R zU$8N(uBCRQ4|g!*x;wm(K!ReC<&0j6G!nb301)kLru1pU+`AH`i9pwPUK&_rF4Qyl zB_LF2Od>ZCe_^UpMWaEprV7R>vES{#75kxSyT*MBFU!xptGC{*G6CO?2j|=I;B_}1 zwDd}s=>JfW0E-c>*Hyq!t0twQ(vVTFoD#?jT%d03VAiz z&hppnVVuldYZj+?D%CN+f@6;9ga>(?ke>L>Wm}{-46^~2klrxML;vOhs#)X#J~In0 zX4Fc&e*na|0}-6zZbq%8mtKdFC=mzu>#TBGYhj;gMLAGjv-}g73)wBVOg$kRkDG$k zrT9#RM-fqU!YE=X))+8Hs)Z`ZTX!g^-I1yo5sgY@GuvvgSm0hb*d(NtDFBR%XvurC$aaLAkvV-waD|CG?_XB zI_yL|Dh;kEznN)MDl9mkNmD9(yq_ske=2-Fn$b(13QQCn&KdzbvHV4aAFsYUHb}+l zn|Cv1sdY9p-FHsm0zuv#R6-VcqZt2P^+pK2$=exSNe&-~Ohs)P0dh+6-mKMPDO4vw zn~`uN_4)A|jdp_2Tg2DZ05W_A{7pyX7&%y4_l$Yw`8O}pI8oP;q8=MnkxV*If4#(X z%7yLKA7!9K5B44{JB^=`iqNjwxNJe4)*I zT0KcRmG$Sdx=S`Io8}UVR9CA#>h#U1ueOv2)(j;oEb!?}hZp)%Wcdq(BfAbcd=-pf z5%8>|A2^O@lnHN@Uw=D2N`KOv5%ajyGyqI^)N}eBS^_HABa?cv3sf zu!q8P@@!ViBEXL_me)2xi*Gj0t!@Xz3I7jOYoO|Om#s)aDSp{qubA1FFUA2Ef8FiO zt}i&dmwS~ zws;y<>Zb7mGGhu<->8en{ya|Ts4dcnyeLriT)9ph45m_P!1+dRpEomStDlzul8T%` z190PLwp=EisN%j2m6p{@v+Yo}e+Ou4i->g0WfZHr-l`U>RmF2^UL(?_>6thrdUkE@ z)JIZ=S3!4%hf`&7w3kWcOm{?gdLic08n5e+vxPNoT6-ACuJoH+y|Jb7z)sB$K_Z56 zYpH1RTNVq6V*>%NHlEoij+nLm6pHiOatzzgH$Syo>*A^1>S?WTJT3#@fBUX`Tij8n zRW|t$H-E{)rZz|c!QpqGp_>_B)xQ?tgA3BjP5ZVnKK#k-vCfTEboUCKzn*Rp=I~mi zZrfgE;6EQo4qgcHwoQ6>2HAAAFtwvuB^cO@LU%V&9IGwl%C~%6A6{NVly$^eAGXph z*U-)C-@Z>8dbOa0L^OYjY{>`#8ybFi+@VAW4>2IDE1!5g*iC-0=(UT1iu(PoHhC*< zrHid?mMv$qbw7+&wR34}6&we(tWGl|^4L@YAOE%?(qHv&%iA5?-pxqUMT(=@>Gyvv zqRXT5b+nim$jcGfG!a_A$&9Eb&0NTOu3e5WO{vG}?n;7OBjjl@#pYyW#hH{U9G$&>AVRY|wxMFZWxUHbBv z$O)exC26V3$Q$C8swAYQ`R3cdUfW#0CC}{tCC;x(NPKuaFUE(hxQZHGE=iybOj`R0 z{+w-{C=7t#Tx<8_xpUKS_t2+9c&W5 z6%pDqIi_01U8xfNG;^GerkNw%y6cU;-j@%`0VscI!fl=trOHp}%KE`_Cl+>(Gz570 zgy4I+@PL39#Kr!$yFh@!xg#eBOto+@{lw9F| zDg5up3;riy@HYtX|L_+7;Hw~q|Jf4$pW`?4C;0!5`2SD%|9{~B|G@wM6aEkJzvZ<% zv9f>E2S26MN5?74Tja*TcvK)FqXJFI0>9*=O7OoFN0#A#OK*X1vc!wV5-PIE8pab*956W0wS3_cb?Va}Ipdu$8O}ZUxn9c~(@Ufi2egEi7m!k- z5|^(R3V4iXA_2MM46}I2MA}+xAOanj48fexmA6>LcW|?~aY96vR7l&-?(@{cFhhT? zWs!Nl1fGm!9^7VM(Ue~Pei?~inQWN=vFKVq2jT70pZDQ^Ft4xlBK=@hU#h_D(QZcS zz#$l%o*YTD=w&1$c^IGT(TQuOBt-%#>GGCPBI~-XeWlQ};w5PtEd_VTwinIu?d4RyMYq1?=Jg6p0Vv-@6=_F;q zg~@12WZ3(zO{pfjQ|C=n-VoWf$|DVjLzOfTDH}Al#Fm;4^A<(*rBq8<(%wr#>4Y_A&C2&Z}6>9a2@3!@W<(S^9Ay9U) zKAvIMZGQ?=6X0^g9>7_+5=w6ss#QFY?wX~}*lvt09D2%55dwzmm-)o{!J4kulPTf+ z$Yk0G&lWjZV&^1v*6Xmr&*y6Jj`VL4>!5(5tVan{wFq|MKeG)lt-B{4|R^J3T)?eIo;|S>)vCrL33={S4e4(NvWWX!H(B(kY2>E!Xijvcz+5U4%bif7v zbu(1kCc;nQT@bfPGV6OF0(*FuP)lr+)dLj2pe4x1whDZ&Wd;2IdxCri zER7Iih{TCKzQ-l`+D?S(&Cq1zrT}w#Q(77%qt2pO*{l{ymhYZQ)hHv#we6;baqAky z@Aj~EdPD8M8uad4DeNYVte2!geaj8&BD@`ACndoy2<@}Yx}0wfBqYG<8{!DHXp4AO zhCr(+a*^UoTZz5`mwL|u8h;1g*vN>KYEzoD>b11_a;Y6(VUHD#9&<@}YG z8_)q(f7e|>DCGp>yNIPPx}xrN8PIb7$EG2~PF~fV)z-h69PKb}hzw4@+9OT#M` zTs>GMs=-1CQNDN&D}7C-O~=h|Sb*C6^DqHZuT+wo(FTRrrVR?Cj6r5hOS`oSoZAwt zGiqiHOOkJh;AwrYLzCr-~z2I4Td5ve{T$66u<#7t~Wy#>p>3$A~1@mfv)i z5I7ihS>WYWHW1lL%xwbFt{edKe5tP()Ivij{rBBuplni9_$%)B&Zh7y(xTAS7B zKSf5i9uQ9G2x-0k2L*j+D3?7A3SXb8TTN??)98hWGA68_>KkQ(VXIQOe-@@p z$8C`f%gC1-&HbeeP)aJq%<@n=Y~*3mW)?q7Is?9L{WgJbD^!qyW*UJdrL2LA7D>&= zc?!1T7s({Ah8mvAllro#o3W7g__mSrKRu-Bw@6@(^=ZaJHB+l-Q4VW~d^%f%i4nE5 zMTAuK`f9xYOCeCnzb)=6m-gxJf5f7=m&5Lh20pp04C2fP5NiZP9g$@2K^BQCHj7jC z#tS2h%p#X5;-^j@W=%A~NrpjUv=1goIn#h|28BgTyFLDDF5<6NjJm3x8!Gq@rwz3Z zu9*c17-$t!GI!dH!!{9D{L>D>5Vt9-+cLLdM>FIfkAeW>T6$68EzqLQf0!Yq^h#LM zbqos0Wf7#=-BeKov9i!7<&o_oQ}dX1;{r0dw_4k6TCOTdwt3kO@;W)MrlcZ-jvo>i ziTn{n1{GqZF2};?zk6h`D2<`zhR8}sU8>j*V^~wcPlBagyDW<~?QAQqiDlcQA1Nj) z4f$q=qttba5DYVj*_-Mif1k7PuGqqA&>n^gOvD4P*T}lsB5bokdIurX{H(#4Nl9HP zp{bs#RL2Vt9N@Si0T{VvZx`!cQ8N!G2h0o8c{E{j6_US z8t}D^1l~E^kU%!YgX3%v=2rCpwJ*!|X>2mXsT@Q@v5m=n6p66;A}9LOztO-{T>} z@9`HEe+_=M^)tbof8mF|(xm$mg%}@;bdEKCLPyds54PFUm^&cMjm-N{Xm%#~6rwA^ zh#=u=WMnju*9#mtCaD!FuYDL{jU%oJX6%g#U<_TD7K6x-fP1M1JC%yBziqC5mi)gB z4qwKBeWuKZpMzaP5+{JL26N^GAVfSd3wbz+n*@*KSt9pMe_L6a$=Lp@qVX4&0_~c6 z(f&rwy&OYTb3gy7{$33H>CgM~pIRHhNz@hlQVYz9)RLUn=LjpS=gHO5omyq7M84Nb z|H`FerH?cG@;f6jLWAMc>}#f-t7P^&MmT=fS3B4q$E8o1#9)*)1E9^gjqfS!}bK< zT#~m5m5e#hp%};c8;r~8TBGE?2mb~?{^NgJBwNODK0lNGw0|q>jDO|`ELP9s#2pdtq;@950J$S!7?QFXtmL-nY}@hG`wjR?1AnRdYjFR3 zw%K76oT`=AmL+JP$`$Ho28^ipa%INVM^N+oaTH;;bGNpL3X8pEcc-GTk{Fliq~%R$ zkd(Bvr&e8xA8nD>TZh&3cifzun?nUsnj-#iE%p`R5r4}0*`jR7&)@iRcO`$9@=*UY z`rntXS)DYI<4_$oPIbyBYW&_N|K#C8F_;`sZ=JhY^IvZ`Td9}jsHbktJ-4X12-p>|DuZJ=%3vCHoOxW%r8L3s&+J1t{u;>ckQ zMd~!$YFiY3jeVA7HiBvy=a#**HhrwM669W8QkZhL~LVZ8>g*+!lwX1ag`4E!ZOyg#-DI;rV+ z4u3FHnPOQ@zy`%g%jZt#FXJT=5y&a6V0X9Ogg76rz@V* zOU%}|yGrM!b4kKEZN@x+<(ZORqdW;=#amv$34{0u-W+(`OU3o>uHtzMb1RA`LevDk zVbNttdb_J|US5)HPC+xw=4IFDI)C2EF?PJ>BfS)i>0;i=m@2kZ2`CY-bR=W_-k2ii z@6G9n<*Bx$kbDL~7^)HZC8FmwwV8yfQ&DK& zWgvAh?la|Cvcq(?!|I*D=8Z%u05(QuA9-PoUzSXw0YiD7zOMe^ToDk z6SJM#84Y?AkQ;+7LHmlMWhT%4sMB4!;e{B2GilunO z>gpqn3p@EGA-^T$_k{eBkk<+MHECq?)JxyYU)jk#8%J4V95VvN&L1L^FJxLrE9^-} z_YqJM@RL&dIvJ1LT&GP!3Ql=wPkw`GG}DnrGz;M9O1VLb;jBPI6^U>+D=`fz0UebN zS zjq{`XFyo}H2v8?4mln#h`*JyraIBD!WaE3Fb2TI!gEyu&My`QMZH~lnJvlA%%75C!&X~Ezf_QTj#Wh#n>`$`JQp40F zzJZBjf0x1RK(8v`+5aH+QiXT>l8|s-4dM28P@$NMWKN*b+Fkj;Oa-c}$VJg|ZX^Z8l9?>gf0dHC#oJE34af^1XI15e`^AihdH^J|BV3t@NwtT z+|DZ+&NF|`f?~m5RSgHL+Tr@TG`%9f$tg0~x;yRb^4AsxzPy@m4I+FoCl8y}Z9aee z>eRKcZNO&uTx}T{46ClOZFh;;lwjgvu7WOAwtUq{f8GmwtLU>9)F5qp+7W{iPP7<} z8XL92P!+X_ab&b9Fxg$0`*WKcY&$F8%b4Mg>Ke~QZn@o%o+b3 zK)|69L6;%dg17}D{tYg%uJfK{0FMFGEs`WofAj2}mu2BC!@!%eC=Ou>?A}?GuxdARaz3Z4jO9i0;`EJclGQx&T#`#3#aY1@`POiYOD~K>;SQ~4lpa^! ze;VPvhdjo^Gj#kyIgwG%ajV|?wYt@pvoOuL6sPGBEm*mCDVv4&dsdRCAQvzqq@kqo zjhEgOi49TO(d^dVzqok!{^;fL?-v*Q*QC-2ir>@<(VVn-AP(W2hdJ;d{1=E;V23yi zuGhAH=v-ZVZ<*hB$egQ}pWp#n0^jzR```goe|}D9 zO{e0M`7klB1r^xJMA~Kr@PYPI#-h(I_@WdbSelHcvalpaMn4=j*nuqQ<+i}w5zIl} zfl3e1GC*c1ETnl#={S?o^x$MZHQq6en^GpIhhX?!E-*Bm(Sq=909kN?BM}0Rf3QbR6GnYeph~c9mOfD)Ob3= ze5XFpgA2Z@c~hvi8T*R%B=NLo0Wz)?A`<~Wp(EwcV2)&;kMUgEPdYqE$uf*={^pH$ zUm3cABPzR6>-lM|-EWt1;sH2+znMGaW=^|q>v~J=NAG+-6vPG!Udca3qepK*9U>fa zZ`JwV0`yEY306cL;=cu|Bw3@eVApy`qCj20mABIQsk$0?v9}Pd`7oa|j4uv_~fw8W7k;E=(xM4FnJ`gLsoHg z4&NO`ftwQXfB@s>@)00^#*IiE3A>1x2%lTmIFf&TQ#c4eZoxOzt?uEbnr*w&TEBhi zyQ|jrvb$o-HnmpPJ5`n?Ih@l~^ymw!3+gAF8|KC?3vsqqZ4auTp`5>3^lQ>C}HmAl( z+Ei5)N`fRd6seMwWJTt>mVGVz%D!g?00HnI%SpPP{k{8LZ)1_fX>b?}1~b3eb^_}d z{LPjevJr2TeN*3m2|DdYgQN) zH|A~W@Hnib6TigYel;lY;{_yLvta%((Hc-eQxUNN0XB4bD5EY2Vl}4r3nqODi z^Xp1D893Djsqp`=TTSyEGV%3P055sWj@RZ90>0S2$Zbl|kT* z@GW>gRT}tn%D=IvIE_Kx;a@6^dMhFoIqC2POH8lhn&Y_>zf&0^KncFas8XGZ)oA<+ zpM`s7r@}6v@g+wK?o`Gey~a0K5L!7z>}`$uBjt;59R%;Az&=FWtY3J2@1ENK|K zL&w9hVGOSl1wxZtL4Ns%?}@9I%FyvEvGa*y`uZP#{`ZG^X9RQJagedbZZZ%lWOG<2~5(XgXRjt#;h)#HC8lYDM4Xe8Hn68m=s=OHM z1w*{cgo~@d83A5V{`A=?(2}G_PK(gPX1#4Dq(J1#*C_P-tg}BP33)WNyN0qyfF3v9 z2*56XOxr7r9(ktgm6+VWNnHe0RG0@-vpx>O$NF`TZ*Bk?#-?4rcXRV#+Vy)V!DmEs zy8!G&PjQib(RnbC$X z$hbsaD;DYB;=10UidC1HkXT@vs4BHGbrjxzJW)-rh8Bvi;L`M5eGN{$7-imqIui0) zQ`w~iFvSh~t$?dY0#ntoROA))EtQ$!LXEu2%ouT3$8>d@gi#XP&!%7lC`J9WmiZzA zy%&j-b49a9ZC(S2x38HV=HyBB$Sm#$dxWdeL$irpEoiZdu+<=;!SJUdIK(H*A?C{?AUR*T>PDJak*(r6a zT&L%FlOBglM7wPARL(OM4lE5L?$H^4__Z^>YrxHKLKeOx@bwf;0Li_n&0-684A{E_ zE=@vS$`62KlaM#^#W3({(X3`!Mnc|C?ae0&0eCVWjf6Y}-pjz%dgMtqA<8vOpfM8i zP!=D}@d}_e2tow+0Sn#^MkB`XHGj*LOE+3Lp0}=XNNf?z@!iyZ=~Z7$Nw;f%gw)4U z^w?h{Bmr^7&AelKe$^;gL54{pd*N48u%C4Oz5{QJA47ICOL&7mR5wP7qE)-T_KeOR zt*c9gDBs84!W|hMJwak}dSUt*3F*>+*Ku#EJiJsN-tdR1#V@S8VNm^9AZ?ikM#Uh^ zUqf9+7uA#o2sLUDZFoLZH-Ybexjuyh0}WX#bITWil$1n!#i?NOVY`~9*$INGfvz__ zvnSfbmZ{PpB5mmpjxWFCbJMmGkkz~x zVJl0>&%m71yH=w|e%dm|bh6tRvo4D#r6ZdXJo-js3^nFQcIZW~M1=AA|`n3Rv8JIRcolYJ3oUwGLU!%6v|FLoUCx2wQN$mqW~=uqR`2c5NZ zw#%T8GlPy5e%+w6Ht#g(^Xw>1w~W$c*HM~=6bfJdH^&L<{DP6TY9<6U_H+a7OV2>#)>z4r=j|k z87h8YcsqduL+(iJMD(>Yp`CYA!VU-Ezvjm44)xfJhl@$a}f|4qA6ViO3@G6e3S3yb*>;Jo)zF0*-=1`2w0_m}J3!-f&7X=a*l?>Vj-kS=Si^ zi@5#({(S|vyzv4=-i#hm<6lXHKWGC5kDoQ!c) zJ2DawoWr1M{Gb1A90)m>HA$C$QgN81b^oR|Xr&XK)_9`Q>QC-S>+3K8CF3guGB2VvoG7#gle^QZ)c~|w3bF2l zRrOT`HkAUMZ&6@NDKPpL1s*8{dXwDv4kvPa7n$)LY2!O%;~VWfzAx!2dWY(~sAzP@ z>gbk8eke$G&NrBQjws2uLjk=$swh@uj9^!&>ylm%y~`y^&*&ooKDV9c?WY5ifH1u_5=VrW2!4S?eLdkE%}z@3lm4eah$6Y^q0?qz!TWmyluQF{0l^zbX_ zVfD&H_(xgvnE19uTO_m%c@K-mCjsDMf`KEy;=8=MQh(QJG<4?d%7sezi0`W1KmUk- zjPL&4J1`F?q>4U(_I<$Jl$n6gj|lN<5*Mn0{~Ou+hGlUkG=G05l51tt=s0%uw6T0Ep3UId$TJz!|drPz}u$s&cV+5!f>UfLy7xHH(17xD)1ruI8 z0q^C}vSccgjMG6ZL)EQ2aqJAIS7?l+#T^9$-6H3<360+b0d}3**UD=p%qLhkp=Cud zuCO{F(FOCXOufTOQo#XGj?)DomC8ppO%pO_@u(6q@H1y9%tv;A ze@}NI%=9upv-?4fEh)pi(DJkOv9!BAGQc_wkKc&ciu+?oozeP=xri)N%JvNTzAF0y2I^srzt>ed=5HNx1_+L0aaksFdeHhaLm*2Nf3ssf)| zdUpd%1tM>ceTXc?{{nx1(%p>-|5Cq{C>^$Z>jx0~#&d@;HnJ+p2}!Ob<`GO*05TLq zANw{1x{4w_Qi$vvn;oBgwwdO_-fNhA#`~HV`%2wB+QhX%3^@o83eB-o#x=C78xe>h zfs%aS1pE6{hsAa)vKtM_&?GB_v_3BwSp1xW3UF?2JlNw+;zGoKG+QWMR9Gd!ud$IX zx}K%BCXBWkgivxX_Wb?*@Pc-On$_#z22gbmZ2!XQ)GPuqd|bQg9t7sU0+Y}#Gh}kE zs|}LcG`S?GS*F=DlXDbgU7!Ah-2O}`g5k(T zY>x?5qy>Ay9$(UbYXz4LK3uU6Z*w97AZ&?55li&$*(2LwKiTZDCwTGrh_gO>hL0lG z5TD+>eQvMu*L$`#*&_m6Jeb>a0*(yX1!xZlWxKuDAMJp7GkC$uo&vUA02_eq%D_sp z`H%gP?GY}fu^ts0UX_xo^=uG$snGV;nUDS5b%DARUiW_*r;1FyM4xc}}eEjn2F&1{13i0n1A^rsj@h?D#zgKWf zQg*7t?w|GE_tgvgk9z%k^N;#X{rf}W+lTOYNJ9It)A?g`aPN==U}48rp5P7+A`zF~Ju`jDBg=mAY zM?d<91G`4mvq1L~ zD1v)~LBK|X!(p59Wa=?U7$HFI?|;0viIYy}-o`yRNIrt(xp3JQb8`c)(wswentO^} zpUh#$B!B>M@XyT+Sju+eo-ARZ)vY+&hUv?Hbu|RlyBatffPjf#4}AJ+Jl@|QU}O!z*gDV0r9!8R3ZV>Ck7Vqdz%DTsX;n5HZ79Yb?} zUo_oGdxSUzU8mH)RJFW{7S4ykAPxhxh`Nr=Rx$Dt_WOR=WS&@|ob&`>owPsBxr2fPS>{_)^A2)d*t zM%E4R{1nu=N)J2^15k8}X1Jh4#SlRXPWM#|fl)^gk&JzNHk;9r3$a&<5_vYHd6Yp8 ztePoWsJ&K{i9Xp376XXe({YIO;b#!qQs3$A?+^2M4H0O+bT#GBB*PqR;r_n6zwhSr z4^=h#8V~84#FSbJ*hGqd;@FxdKwHsn*s}wUO68lIvhz_$HVim#5pvSlwS#Hj4-y4d zi2oQQRnX|0rXv00wZPK}c`_jnC*<$U&A_{|n}NTSL&Z}#R6K=4#k<10moyXQTNyLo zL;zf_JZvyM8Y=75_o!)v&7NtMK^Ra12a8yN;u{u%`EPCvn67GCoH&7Ygk_cIKah=n#1EW<6RrBX5?SlIkc}SRy1m<~gy2}0|8Hm?epML76JQiifKmKiq# zY!di%YmFeK^0c{P{0n~{cA6yL4m1*_NW=YhVy?BU z`uqxwt}1WnattHo0cP464V$JBR|$Ien+{YK{xUxVH%tqcRVJ zr8}ad3eXKN-NdL27IB4M&(Rep{SYKZ{UhA(ryQ#ayD{Ng``>?Cc@Maz)Y1w3{|FEI zpzxrFlZ!`b4|S>!fg3$4=0^H?%)ov!IOw{YVy^odZd!B*p(!>sptu%2oG@U*rN9=v_f|M~F;{9>vI)MCfqwFFQ^$)LBT$sfOmTU0Wv04+D7XYh)4`5W4<(%TaScBky|M<<-vsW+W{13Fv zlea7V{BaP~IsXElm3&lFr$a82i^QTJnE}s3FzRx2&?bL}XGqIRKBkP!O{!qhMc~4M z{3=-*9#MXrkY6U`x6JwE_p^g7lngg0MSSw5#y5ndsI&LWL0ruL@cz*UlP ziE;fo-ZjB>pA%9?y`ADlPX;l%A`Vv1Aph=iFdRB^W7G&Ul|%2}y);-nawBFeC3600 z8#RBUXMR*?`?<=F@gl+}%E>kcJ16B%om+XPx92!#81@mLjmky6n`y1bq~d zkwTLzxUrPtcv{7!T&RV}B3>-OAKnut1<_r~f`YoeM21~YHA=tVa^&R_XHD?k=>HMf z^Cx8^`UN(kUvv}Nte6QmXFRfyG;(oyG+uwuNY30S0(P(LpkSVg`}KLO1%-0S$23MP z&uJU?Hcn#nJ}R-Im~LmMa8U)wwxKrrQNYv8mO&+6GI4pBDJENT{s$t3lY9ik{30RA zHM>ENp|_k<4v_kYk@l8pn(g2#%}IVsXEKvSepU`wzipl6jz`1);b$r?=P&K+N%eoh zPRAzf6oa@dkQaY)U_u8Gg=6kV67i=Id-O!W=OGWQ#m`)e^vp#!Wig^EL4rjwP~EsB zmXCAy_(S{tB3}RXhdQ=FthQ6Cn;R5lJnO(kgUYhoO zZt(6#(3Kb$7h)U6#9t!P6ST5pq)Yk%TL2u}iF4)W;2Wz$JP9`$;}Tn$5S|o-iVP}} z3Y`QCXUpeVFrpq>lJi7ek&02#%i%C>DQpRTy?$KMZr2|NMv@S~XNfvpgSUSlZGQOg z+7M8UYTWFJ$8(TP1Q-NB)T&alfl?s2a4I9NaPMJ%0{c_ImQ{7r{fI0E?^bvNlaIYl z`oM^yby*EzEUKn-FhKh#kE~bKJc)=8?Andg>Wv$br*Zx4+yzrZ29^c6z9FT{`;Chj zJR~A!r=6O-gOPy-v*v>T2K#?(fqebK3=x!H4&1ZLN0{q0Jp>z&8NhpisGs*RLtHIbOK66L zu?pOLL^j@OwOV2li?&RLB`CNPA}`Sq!ze45-AV&4itFC9%uewah~uu&17LH=Bl!WU zGbDT`>-Y?9fJU7l0mpy*a0w^H6k8yL1PGP@3E(ep_uNl4cY2*!)F%$F71D7hy&*`! zwz@*X;hros>Ion)_8}qNLSQEtI)%wx6rg3XdJ8eL040lUU=lxpmu85XRpbSQrQvQ< zh^WOJ1?>B2Qa|j75>T@ zjjra-P;NjBc8(j7inlw#G;D`|>l#BR92v|q46D2_W^t`;bPVv&&ASAPEGYsAtt;(^ zEQsSquVa(66!+q6cIHpaej|!9OeB@a3k`2iG^cn!VGa{Oct_8Ik>foZZ2^+J z4QLXl+gli~k~Du3l+ggI?wR9x>9}#{pLE2?ko&rIHs~$KifffSis zWOnpKfCY)tVbs}(gMbRF$Ynm{9}(~mcLd-{%M<_wyGdXv8wnX^KczT}i|iNX&Yh4^ zmM@W6HYvw}8ZWmZE6$g@A}dap<;aR;lC6Cl1XzKv?8JXH=`6o)39+&f2nTlrm~2F7 z);b2lN)|36)~1o`c)`RVU0TP9b72^wx!%O+=}XXCDi1^cFl`%Tu_O{b5s?#v+rS66 zGg3}ehLNf>_7+BpMFan2voXUqM@tQQUab%9lJmrYv+kNj{M#f1ckLH6ilC8(!JdELVn5?)3ePUA!FyK9QTG~TF;5>X zI9g7?OQq8w@9;eyoS{7Sc(oV~;q#tF#x5}Y1pk8W0)B&zVljW>di?$eGJ;P@5K{GJ zYQzMHkfs8_i`+Ix9*WlQ$5rFIvGNwgJr>Y0%{ig*6W$td*G!pt;H$;q=#*9_V$%~n z(Jw2WMJZrSNh>df7cM)SarC<}ja7>p=$=W}LUZ~dn^AU4 zmujQR8pLNTm=YIs3qb1|WTSS&*U>MgxXts4=Q^t}5{4Ps>2DXNhB$+u*x`hyv{>ks z3nw^u#Z3pyO#y)u8ec%EwsBBcZ#nI>4!YT@PNUE(a2M=%Z=Ul=sZ1CC?#*+sVDNuS z4mc zX2SOCUP#AmH1JX#K?vZ`i?3h|Q^f>-WnM5HL`QfDoA(>(B|8?W@JP8X*dRHhnd{eI z29k0-ac7%sA?gI1K7#q$4X*sfG3I}bfl!g+b`7Ze?Zxw_aXf!R|6b52J`W4r-I*U9 zJEb=p9P0(8z6+)GoTb})4G0#qSDc8E__6!+?c3Lt$8X-edef`i+lZ7XJb&!u(uZ}y zAW+!J)1Dz4m_f)>6vd+A2@N8bW||rOtmAOu535WHqN78wkd7Vid#lkfS=oOpH=bUG zbVU8wb-ajqoC_w*4H@Mcm$r@Iqc*2LL|g*iqOC^OUTs0<_B@loN|b>GxCsIDAdUGL zKE*YQwg#-R5wt3XZ4Q)pg7>K%Z1BcrMv`}|z8%7;WxZ74-7Aw?ON|U84-fri2KC)Y z$g1GoXE?1axxV$PAii7s2M!=fhwx99-;ulm@{nPik{IBShs1Y0fQ~~R0n^A%B(Fos%!OJ-j^rw zd;?PV7NMD7K!-~VmVl>uZq}r&z|_{MWE|wN8pk0oz=G-obO{i6K$%Ss;f$XZY;jwb ziiec#!1ARi@^Y@j9@u%DNy2C3^AaNF603P?nmy9CnxK_+0QV!?sGbhFc(v5=9XAgKNJ;KJ&o%7lemt zk%!A{fRRpyz}G5GuhfSZU&(#iZ1~!o`{bl%WIy+o8aK!#%CLi%yn)>C^Uab?LnuN` z_Wtf%4>ai5&oOVpz#xeMM*H}ij<1d#f#Pf5uz&w3{ePSDivWRghyo1p5z)*^9AvRv~uI&6oN1nrQtVeWDNHb&LR#tQddHL)9PS>C;+f3A-~HnFhF+qw(a@@j4Am^jzM^KwW`HA1=mY zigzz=^dO}Z5>S5xwvR9A3Id&hJ)N1o3UPmZH)a9fs=|g-Rsyw!Px*CdqtgRu-8c7oz4fG&N{ItS6jeYY(Vr$aUnE-uNL4xO{-?dFwBEkARqw57t~7NpMzXt zUXfm=?X$NyeNi(3^F${fw4|DUTq1ws!3VyI?`F%OmHNEntAiK6krj&dvcIp{H0BeS zB>xV~*2Hf>KMfXPR5eWwVNq+E2S&#@kO^M6KDyIyNpfC=H}Tmd zb>pDQOYl555@`;>_^%!ZPuy!dsXt9oGkpg#O4PmlbtRHDO|c z0t;U$C4pVFj_MYMN6qx4NV?t7C3=b;KYDTT;}=a?hh6s=eFbEW!gxh$5B#1zgl-ip zb4OBsGUKC^Zza@Kn3ErLH#;!zFmGJOlpx^4-` zo9JP2#px_(ofi!BgjaxZD^_26R&fT!;nVe~ZA99BH52I47p|{-6F;Mv&Ql9nnE%2*MuaU8Er8qTv76?bhg5l5 zxg%-%%p+OzZ`!#84iBk4EmDnUwbGh4WxGOR0+KHF_JrMz=%&nb0v3Y zw)`M5Cj@t(XCv7iY)fJ?v(oH@jA%3rUA*;B)5Ghc{V{pr; zFx%vTb|#+C=&xFD09VP zaWJ^bnRS1_0OK_F$SAF4Hc0Q^$7zLX*)YlV^3>*t)At;?mNWSJsERwpc8@r7<#KwuBBn~Tm*4f!kF-O zU!*b8e+fa9LXb?CTtt!R;0z_4SOktP#d_F;up4)g1<1G7tVn^-^zCzP6n$%7C0Lg z>TDqCxD^kO^#udO%7YOa=_6F`wZjb9nUFJ;%pcK9YTN|RU4kXmB=fbx(o7+~0sjqB z8v6W0)M((=f3SdnB*BIUgF9NsgCXVv5WG2j* zUc3H-C~_x0CScMhGg-~}9xXzE%Goiw>RmK+Vmb*B08qd*b$eVvSWqYeDrs=0WhuD_ zaGVxbNE2!VbLAB#dZe?4#}ZnoR_mswi-H#b#5GD{NjB%w$WfNe`0{NX5TRbi$ayrhQ-Wxl@U^AVJ7)6 z;9*MJvNs{yU*j#-X$p&q0ndQpFw1Qkx)BTU6Vh$icBpLO@klyuv)!+gDUe^xusyqQ ziUN8UIU`Gb6GzfViio}8+cdv4?jRxHJbpwv&R48Kr5D~Ca^7SIug$;S+ ze<s^lzRpV&$wa_Le@vj3gtDRhCXJ&yEYia3yyFs zN0M_VG=z;!&Y_Se%`_54Gjt^bC_->?=eNKGiJ8DATy&U)@((foxu{tk%Pgz{u~3J! z+8_*AFJHi#%SLfAI2DJa9^?ZhgY5p10({c zZJUESRweiL&C?H<5-fb0JK~O=-)1+7v%3j+_(=ti?<_06v#iC=AYymq{7GD&gWw6S z(h2EnxoU9suQ*I*5+-K@EuN-Xkvt8UMt4+Xf&v!FTR`Z9(WG@G>_7Z;D6YePL%j%@JA9*H4Oj z;-T8te-{@2ZkOW!DlYziSGD*Lg~k84OYy%H7yp}D{O^Uu|Ex{VM{E#awS`U;?=VMy zDz2HXcbX)OY3w*dBxlm6N4(BX4}V=aJ;HT*uNzfs2wjv$nd#1!*f`Q1!jO(1EtNE>ceyO;gkCCR<}sFF0N5%XT^q?aqxLO*v(}R zcu8(=-SOsr>j`jwt4FbZq}Fx5(w~?qgxHCkpc**!Cr;0oaAIXg_M-L5;k$Vc@%*5z zeK08VLf%GHua_C#M+4G*rdZ0o9+c6{jhiq$hptaxfvfTKf*r>qG?zERG*hc6zmB(e zMCdT8#h`nsD-7abRAv-xR)ks|+78wlAh3WdZ0MERqw@HF;6{JoIsPX@_v?N&(Dbre zH+`MAnYNzUSZ4gqUJQiqU-6akH|X=?%jMEsia}0clfciapg==tuw~!fQ(41H_2JEc z`HGSaGfgb$Er@Y><%|(RLfu(%t>*G$eGWPLku#>HOh7u-7c+r;QLr0j4ZDS%;+95d z2DvL3qQ3us2=gxHJ-DSau2828+f2A~cSJHzX@5wU+U@8gJz@-cS_HN?$2#|UE9+J6mDBDEXzVV5!iP%m)(RETty>So=P;YWZ z9`3$H6trDz72D?|@9#&85U+f8o|n9$5zn`)!V(`MWS^l>^%inF4?!?VkcBcRJl`~!92W$X+K!U&T zbdT46+ceKz(|z-){_EYRx@de(B>JEa!H>m5ATBwxegDf>_Punf-`@8juYae$S8rs$ zdEftb_r4d6f6w*(?|R?=T-^6kKd~9|pRgj4;Ib-3!b7XkAjX`ALp)CVk@#6PUlXzeToQ&y@d@MANk*l*Q_U zzrIaE%iMTmH-M50iPDZZV;ynEJEHVXEhm0#XS+ROw7xa`UCZj2|_KyXH$lt%J|`oQVwxHbcMu>X3 zt;{Hqe+U63T1~T;?Br(`tuw&)s*}rFRlL3o$FH5|bP4n4)+sV9R$eaFine#etF{WN zo@rHI&f?vx9v4?Vm#-E3aZ%s2@T;^g2TwS6SaU|Bzz6*N32%F|pzX_S+b;nc0?zig zwtZ0C_O;q=eO`5|pz4*VdN_-pfE_mVhr87Kf4R8ctF3)@39ZUGGWE#wJM<;OmaYrY zB;T%Yg9o`4I)1yYe383ix$)X4Ug$<@emI^x9!$z2Q>+)zFQWm3TR6?xWIcxq2KKR; zGtGMHPNw|pan7>r1#~ogL+i3TS;ZAwc?qh(l1nkwSEi2^45T8>jp7C4{4bt<2E&Z$ ze`n^hc+cmC9cq8ohqJgp79}29^cb8jhVIPq-mZd2?!=AZSORTGm-=n>jvj&zd^DJ) z^;+MT)Xc|8RPe&}Q|dA`Wb(3w+aJ+!x1?yEq*n*(`ZG{UK@HCJKUITsjWs-rUM=ER zjp9)JA0O zyHvXW|83lLXYYuT+t$qOs#oH7J1oFFS>$hG3s8>B{dIo{v#7=?w|x{Zu4o*)fBq!; z5^R+Jfmg0Y(1O}J5*My=VQHw3n){2|&n&UjR^HhOhTYO=r(se26%Ad-%c(#$CKq9v zo$n_L@|LJJEpaY8lmC~~Z|W`mlIw{w+Fu8rClH{v_rN^xJeQ~3`HH5rgj;%pt9dhO zTiFde>TFd_ZKg4PIPSC1yQ8dAe|fal^a9?RsKAD4E0nWeifaf?&P?x8$S`D<+!zY) zdD899GJvzgbzU`@s;UZ3pgZX+vzJG?&f)Umc0Hvo?|-R^QglzMp9B%M6W2!rrT|&2 zOEr~c%@*1N)iJ6-uql}VBYg(UN@jp^1mATA$Sil60b^CSH@|}hh#PKJe^hnQuJ>eR zCk9x$xLDyl%$93YRi3Jfi30C;U2DLRsW(%EW9%3UA0SkdA|+{AdwLpT@pR1`=CmV| z3W5AZ6JnxIh-LYN5PFR}ObDLkE)!y~jU^OlSH1)+yYwV+%hg5M;eb@p->{TJ_?$cV z1)MOwU z-okoPVsz&7JY_zbriig0f**1^ii>SvF9n86x7jQe&6Z!C&_{gHf0VjU>zrq+`dAEc zzR0QFMaOK@x$mTMzn1#Z+ej7=bB}M+h2H(}S!$c>=t~m42jD!?r<;V}lqce+Ds54PuB5t{y{h5;2GY12i^z=c3FCN9;qZVf1Yxp-sD1 zj|4XDHhaXiX}8ro5AA9dz=Ep*{0Bc=_z!;g@ZW)54flha|AhOlc>o95+VN@Q^yK)g zb$rt>fB50##=n36ezR>Jgf|UyKfL*mzaKzKt5<8Cp0$sUf7+)krPaJ~A!)-r@FA&x z^B;FVgrv>h!Exi*YP3(SGnVw^Cb)n9zSTAlTuAEP{3qD=A!(~uvl>T7?Pj~xYGB%C z^CpC}4f7zlX^=b8bJVL@C&x$D>FL?Yc6!>q1FP9+G)@~w&D#h%?$xZLlUB23wT`S? zC_3rYj@l=UfA-mF>l@H>+B>jLP8y9?`^dUwfX;d~>#TixWE~w@C$|t}HF~w9X5+YZ z3o(`jg`PEz8b{V~^Bd4(HG2ovY2)a)(Ko$5|+}n+2qtR@h zHEtschrV^(Znay@vo9vfy$SaJm9U@Pmhj{k6Nd-3~`+&Vk9ZlMPjv31;P zx2!gw$zMkhEDY=D=;-*=IyyetPExa1Yg)lWa@%~fn!pyC$H$G6 zlh)CfQuJr=rNY&4tglcOWSv@Ax6#VGmH|1(`S z)@kdk(Q33g$FhzY9mkB0KixllG1#qkt8vzDGtSyCogIcGJtrWF-2Ue?bX0@&3qpu?gXGOc+Y&A|A+b9AyXEhs5tI-lGdMBEiy@M0$ z^yKuU$$PAXq_bYFb<~2XB36-35S)5!{{a5rlhw1s1Neha&7K_`z#n{S^=$V5{@gT@ z^b8fICo_Vug5w~p#&+X8{^9uOe>^_0k4(QC-@o7N*$j2=G8oYZaWys}6o&fZz%qaM z;WYoci7X=jz6Hb_r6*cnHYwP>N!_@2P^I1Y!0PeJyYWG@Cu;1*2d!RNHLk2CATDu8 zL_9LIMaXlqu)Eets|m-4vlgi}Ppp$x>-6M|)Y8{SM7%u}&4ecYU=N|4f5%6rUtN$8 z0LIeLHgWCvh{UF8V)_8ky|bT9fw4why9=l~TmIK0tY-qK&@x@tF*n)4`r(Ia{I8=Q zerTBx0X|J-HBaRi65nsPO=uOg+BJE})^S>N&u)l*bnL2QpMF0C074$($2vCaGeko= ztk%E3Fst=VgPbPjy+gPye@}5x95|U<$8cG$ytDA#%}s;2z>@6<{@)`!|)=L$n1d8#XHxJPZ{=7<4eXf%#LR_?){G%#7GRH zPayaW_Ns3;&ix;b&V6WVcwom}djJ0E_x{1rK8PLS5gt+`Qdy#xe@+X9V~9z?$cJ9E zBj5|Ylq1mkUxDja0r5U9Y6wNK?}yz#7N8NS{jqrRk$MiXpbr35L zX{*9IL`KNhKng#^pqo}oA^t_!3F`ywCE2L4zF#PPt4xHjX2Be2ntpx0h^AEqmx*!T zYD}6Y%-^)J<^9CtO!!LA%!T1t2tfs_X2J{cs&3VoQqK#(f3wF}_#--YeL6C18}2uP zu{!cMH_AgDAF~gRL^%=@GpCH|QSh}ebO-Wc)G8M@Y zDk+%CF3nIxShi>QLL#A33<>P;4ttog?6Pu6SSpuje`JhZ-;JhK7e>~%J*7WPLJ{eC z3=rN#U2(+M3B%X6vS#ufWfp17~#EfVZhrwE^M@`QJO)4c{I))ka`@p?K>#-*wxTJ{a$U% zj*?FxrJPVmKCb#sN0d$vn5Ed%PYrwfBlv2O||0!~fTw*?&q>jTa*%GM8y zql3#qVXErbX*-pkY&+lNVz)h0xf0uu2}Pp>e>Y_nj16QMqOx@ew&xbHy`UNv3Eu_G z4y17RBC|0FDSdxGKRy3)pID9>E?9SY{?q ze*sew%)u!TxQm!Yk~$7NTHDu?s=UTG$+NY+d+=xGt@R)bgVl37jyr*;ym>?C)QLMT ze}^u(+^K8C-#xw_dd>_@s@az(uIF_oy33a*-Mtp`^udBqDU%SKSR3Tk+XrY70jB4# z-afcm42SSx&*CP&5H)<|gw!Aap@Z?Me}jkx;FU3|6H_{hK(ZH)+URylSC!XcFmoeX z6$WJ7Xd|?rxE{?))g-W+jk6Q0-6XD!viOj?9vna1&-5@Acpt6RVdWApl=lW*EHD&@ zXE)BHdZ2_8cE?pmcz59XUwE-eyaU@J-Vd%hteWT4i)b2C)&bU5HP2%V-b$b_e?*?f z;LWfGgfYvIl`s5Byzc|z+wnng-)0TJV1GLCL2w>nB;f~-){Aj?OiuyFHe;Q~oM3@g!v(d0^+voV+7pwv~t-wo&MK8|IAuR+m ziy#7@-_ThQ)2BfcKk!G)HiD#xjE05p2dG)4OxwjF? zz@RJn*wGXFb>S}qpGmf-Ee_Akmy8X*F~sL73c5oiG>8ICmNe}o`zfit3` zLx{%~JKpzJqhYeLS8hDL4C#pavFmuU*Z9(s!=||erJhSK94_1B!mse#qe|0@tVf{-gEK*uLr4=5F6)YMnBN{ud7uAiOhASZ`32{h4z#leq`ax2JA#Ul=3=@3@{HO7YMF})N^7M0_~NwWlTRigDCJ8a6}8{wRWQt z#ZDN*k2rMv$a7*^f1A0Zkq7x-$o$b9?K^8MvTqudnhk0~?#Yx(H(Ef{f!fp^jWB%1 zOZjnXApjpB@UHC*H+(gg5Nl@Wcs0j!C%(}!(iZS+GY|8;Un45MqpugBW6;n@6uagfd#o#sbxV5=L;6Dfe}73b&;*)!`p;v4GK|sP z|5s(G1i!{)9ijW6velvSM9kO`%__hU>4?hZk#gB=4!L8|tT-=$kvzbn@}JIdNau0Ks1ID9z(4iLXG4YHk$#ub?>3b^|8M(Theb4TO@btH@3Q;Ff6NuskvHc!QhHo!|P)>VGq~j?=^Y1-i=c0 z7D`zVOra7&h0DSn=$sqmB_q<&iG0Hto8Uu8%KJ5Wy4H}PHyPMm4;)%>dU)>eljb=t zX9tQrFf*7$s(N8ZCJBS8Q85n6`uCwT{3KZF)|VhG0vCUV#v&47$%HQTtsuW-oKI>1 zvjDqBLVn8%e7T^uMnZlUi4yX2F>cRK#l9*ztezCo;Og&%6VguKOJmZG=n_6Kq@7(t z%K~R{;`cO++`va=4plS^c#0f*K@e8yp+%2C-Ko5*(waq&zK=~nB$G<41EX#n`1r3{ z083l58jXMNeG^+eTA)btft_~vt*L3>Z#Jx>19>SdLeB`wgrB{2Y=Z6NzSY)>`<2p9 zT5-FU)POLc`X$5{U{mOWBSo(aW<@sKyKs->7z6;^06`el#x5Pt9rZ!$R>mWyM+W(Y zGYPqQL?BtSwef%cw_F?mLWfZ|0~VuzuT<3&QA>aEJdh%ivPdxK1M{6PY^xg2l7$rg zjc;;V?9GCZqN5B07tIk+3^MMgnleAU3Jcn?hSy;*f>~KsM|SRhSq(5zZ?Az(=eySc z$it$O42Ynt^~`kDN7u|gg463EM}J@EI#NpxMXKTF(=er0AviiThAUopn^H%>pV#Ic zsd|4zV}vVz;anpUcUk`lywH?_nRDGo%&9rB{ z>JQR#`b(DP3x-(;q+i*Th3k#JK`S0YIge0_cl+v0>jkx6?9S-j>wW5kBdGVa6Y_Xx zyHq?~_2*6)_Zh0lm)CkrpJ%ERy<2p~#6W+s=)#%W645@}Wb8f5Oz$%AQo@O8)uOHM zW7w;n2SdlBZ`~Q?yB3AR_Kf}>__SlZdwXe+X|M>bCIZ=k-hR*L5#zd{u-xk^`*eJW zxGE+Idt5;u_tkhmIl;#7+nZ<@AYf|%n22HaMUZ{rW?qQcJAEMlad8HLw4Mi(-Q|B7 zApM#DG+2Z(K3@f5+4b4xf6ui}ebhjDlu)zV=6WkObmh3SHMCRtFB>{&MawOf#gqc&->_)c-J)$V8L3K{U0G#3xZ7YAJG#~e^?z^Ke%g7}n6@nI5GMm#;c1|-bW9MNI zc+~L;#1rh}Ns`TPZq2C9Z!&hg=+;D75L>2NA|WW|4%wpIUGnhr>X)q*O_Tbe+BF8w z2v8!rB0TQ}tt+jkb){Vx7+WUX7N`3jn2)C2X)3|FbGfWcF4@6KwjOD+lb?Urx4-02 zMwqVSArRY%`so>M61pNR(ssT^l$39r)$U9%bXGj3qIM2q(51OD>!Gu%&zyO+s+wKE z)i&PfDyO0JglAWcM#DIWO<}HEYnlXr9J%>+1!TOb6TOF10NfX6Y+6bh5z^6ONM+39 zgV-bu^L_)T-LPMJ{$6a~phka6&fzc!M+heaXdvmM2*Q+*SwZ3&LsWzn)3>|X7@67d zmpwA?vq*D2GA$~NN35G{Z}qZYa^W$!ot#H>3E;;?mAf&WL8#TOWUdOEjp`eKK=zz@ zL`T_xp!txZB57er~5i>jFt^0{v#ppjNouHAp)Yo(w$b0=hK zx9>82ceQ2UF8c~h^Egl@;`4r?V&_voqaVV+g{-$LDO*OoI)+U6wx0p`Rx&`?(S8O) zyQn`Ji|Xf|c@|j?3sTR!@KEqUkhRx1}hl5sAkEZvr(;8nc%J3tG60HNGwi(ds$Tz}Q8@9t zK4nwdSqY_b@@=Z8VSh)}6Y&2zBYBJZDWB?A6_nc2VqpQrJ0^d;FZy4mhtk^R%u3I? znT@_KbSJwVHq}~gIcR1p%AGVN%-5)2F79WGi6~CJr~s2qkDI4&E*Lh$Pb8+%tgi>d z{D-%ekLlKOZ{sH)cime)YPsM>QO08&_w|5*?kJP|t9KnKfXK_hXf7zBZ5?AdXBv0eCvN|zbk8Z7v@zQ{@9zk~o^$2K^A z9J0V#aH*VzwE6#-;piFaNk~iZ$cj7*S?(1 z{mg7$^$VBmkMj4`Vh)D8+WDJd?x}TL^>wz}NCBD+ar%GS!g?>ig!S&y#!XhGyS)I$ zq0>=#-cLC-zp`{@bA@xMCl$%AdSbfd^|q!Aryk;+(4X@OyK6@cck6#B5fYh~zO}!KOazM7lIXMC8oPVH zWtz9W`y|VH=7SL>4Z#q{ryiaGbe-UkeF3R;i#mv>!Z0=EdO)6v-G=aV>_KN-U!uuV z#XeBv!MW2Q8-v+VvLk25cyNh^aY}QENFfio9pl-{_t|s_#MNS_M>uniyrs+FJd=<>Fr-}XtDU8O1mhC<>k(osJ7?%K&h zD%ijqWaBSpowR&5%m-GYYB6;pAKXgh_4fXIJ#|2q-Bz(?JDEm&*M->!y9={g_Z6H) zQnB_Utfm?l-@68Mw6SOV`}^^hqu|sak4k?jh5~6Aufd*0KYKmvlOnMfai`tcV)i6- za6PdIO#o95z{SO1lEP!1E;y^OyID!Lsk524;N+n)8C68meZx{I2OUGUv!6Q)8k+I& zc_Ax#tqQ=eqzH>faa=G2f3fYxf{GoU7(b*3=PUNc2bX#y{8bLco1*V3M;h)igX@1* zma)r#m*gy)1!c(-eFuzYj^`QV1)F?@Sr+BSZCc#+f4H+BIqR?N>POD{i#i(e?cB|n zxVV#F7IrdZ*s+V>{BvE*O<%sWe=+-AJC|kJrDu7{;%cQ8IgXT{&WTErfW zJ6HS>3FtiMPgiqFN5PCj=)_O_-CNXJy&8`@kM%cx=K+85n^N~{?prG%DgX(6GNB{p z%p{(k`Bjrd!SEBsu!>Xagt2s$#UdU|Xy%O)n9*~Wl=1w8xX}|gjN&KY9QJ=;%x0@k zhcToR7vOm8-Xuh~%21w_n2$`=EpljPsq#^#S5%K8Pwtwu_&!ums1i4GV=n89BGq?F zQtlM?9KMP|nKH5L)eG0}G_)7abqAvIDz8Ac!fBHROq&KXFhS-nU}iRP$I`-5;(IMN zC4Z9%P|4ncuQklW48`g@W;TBbC8}?geo$*?lUl8vzsc0j-tJk1bFa%_Hgo(@n%#|l zr2{rc`HTW-!h-ASTu9Alz6)-s?7o=!;<>&XUtw7en}`*GFy4&TBngIBC97ny%bc`4FmmWfQ8 z`5@9~7lF)>{UUN?Cy~sN{Q}#M>K@5_sl6bgV!#r@JbKfwWlW|d&ZO{<9|gk&NRf5) z?YzPOH9=T4zFXGjuIGW~f6y62$+h9s^+px0nl+`DI=!U@_MUYPlzP)DH8E-)fcuzw zXjg?zJm8+18M}roCy{?vr}rBdT9DUVN*s{Ba1j0emf~0l@O&Uxyie5_n&3CtfUvu@ zqcGSk5mHHkWrx^P>LN`fu#)Og5XREBqs|lpG5f2B0F|q%^GifBoPpYV4Rb>UsDK_C zRF9Dfje+md!TGBLgvNpo2tw_c6t~4PxkEUZ`0}30uMrZ_7}S4p!6H^&)-yEkSq1zs zF%h8vhC{w&i)Mpx7sJ5oP(;z_#EC7uu8D7dytgr?aXgPYhHcyQ!dOLMT(OFbq;q(9 zZ}S0q=iVkx(jNSH&LVeWEyxpor2}54&)dd6)&rm0#UXr!JFXg=gqfc^u_Lh}Fy1LT zW>op7b9U&gJ5hh+PWbdprxVJHBxZk2vMnj<>PXD~3Nt+4E}NcX*B?FeNAwye0u_N6 z?O3MiHhRL9z&x*6w(VbRYe1ThC}o(Pow9WO9?@$yi(=1mAA6AaP{ysA2|$We3MN=K zrPVN#%zyzVgH7cQm*28&v@p~%VJ)`0F&1UB65;h9_yd1B4nn$6kWvcv3^twB9bSe2 zR-{=C#zWnn71qSJ74?-wm0+j_P-@?q(1Qb5tYSeay+!Tz8E=6mU+?duOPpA``2=dm zZ)vn;+qT-T0NS!fAfpTQHNsmMb97SflyHLYDMCjmow-qqz%SLJX6;H#`R3V3NZ?;h zgVkehbDV!7$z;w3+33x}MwxTy3)f%T#sNJr_L+&cZ5-syw!dMRXZ0u}03&lvwo(C6 z73YMMz$xd(Jt^V(87CLDnt2jr$o`i5lXB#;V^ZSymIk)YG*(iq2%#B*6|<$3==@PE zOSp1-{0|bJIXl{MX&XXkg-&YUt`W4L5&X0&y1Qton=qWXSuE#gdj7d0yGLo?>j%Pg zkH{Qk#lQFv&k5u4Uz1fV1vx?IF6Q z(Q!TR>9R>=eD$Lh^eq_ z4s`|-Zf7ulhMTaV*_oS!-$>HL5iNTXgEgWtHtbDe;B$W*U{Z}(x4Lj%=w9~!FBG@ zr2?zx+xHo?GXUSBn#=}BW=Pfbd-Z1_*l1z7=EX8_N0mk=1bzU($~cShM)*mYm&!xk zn18ZHqy4|-Ti;n$e21&>q=kPpt69vOMbYjFU?DIbaHG`aCJ0^X$Jj=rlOmNF33u`)pj&Ej5O@P8tjuf8bbQz=@B!A-GElS`f0aq`4fqDHz@ti-??U8gH1INrKwly5 zu0=Vw78UJUlm|uS^AC5;zbMW>D#)MBIxEgPUfqHhDBuFEMjkJ)f*zNZFo1{`BV1RN ztZOHkueRU>PFLEbPJP%`+A7DH3llq};DbQrR8HS8=bhy$>k)+xH-lAnE&R2JVt2e2 ze*j4w)USeGMd)r2Nu0m+sss|Tf;=Sy^4H%+?nS~ zkcA*@7Yp*9kgHXeksN_9*-G&xn=j&;mRb{>ULNm9exaTe2DX$b61Mna5#8OVh{ z74);H`fXX&*I!Fd{>N`y~2FfJ>XQ zphWRA>MttVQ;qttO}$^~-izevu0!`EH*`XPRl79bn-5nRkGsEB1^`-+%EX!1f0~%{ zxH7xO?K$=|4jqZ9Nvz#S&Wm(KaS)_#Z<&F$xMFW z<~?Mr6f|ELCYj3@%m~FW$yB~mj8zPiOf=#Y<72}l%VL}VK?cZ^tnr2LRy0h~#}|qv zj$x7&zT*l|!z7>aSVg_0 z?f`V1xDYINb6`#2G}rsPQzLB1!7tJA}N0j{UUe*$MC5OD;O zXBRXg`n3uRLvgVgxzXHn)*ZtSd}<_}O~UXSMCpHTaX(ZGL`ZXK;+uL7!d@n>=!iT> zpt*j9Z}!~OT(DIp=Y-Vxq#rBbgvSaxFxVL*&tV~Dq4RpakPNp@&npE^?Ee^e|Y^04{&WXoPykYQGjd8eeF^ifV*ITzigUse}7MajupYq zTG_lBJg!noVId2myQ}95QSBx_FbhHHEepPS@A{hEGdWUS)TBc~>Ps+^zZxod$F8+GWE&{eYh&kC6BcUAIrH)a?rio^D0?0 z9PT%@w|-Y2e(ozif5nzEe}9tx2e>&grCbVUhEB8+$x!B_E6q;?^J<3@=2svt7{qDu ze$Bewl~>P_ngP8`t$+|auX2O-y;&axk}pXZ6fwi4ISq&C7LfHejSEQPwgA$lSnDeA zCjt#HSh(^U#ihQfSRl){_gjrdDg8#}kXKQxw^Ir)cFxdEf582fGXCL;bXR`0U{@15 z3&2_swojS;By(nnX81?I2n(+`9}TMtE(zrX%85dd_zLV27;EA)hau{YNU9SW2Yy_G z=rW)WI+|Y_NrH?ySef7#@~Nj&?*3Pa)z#%&h<&>V{Z~FpnT*2O?G!r26h@^K?m`SR zqb(ps*b&?^e_~>|n8?j&8Q)HeS4_+BpP@xH5x9jO?XiFP3{^;)dtBY70jom#=%LbOOHD4uSoSi$@r3qC2x)Lktw&3vVAEAMngys!Yu3Zi2yIow)w&f8~1un;rA?ObCDd;F7<6a4BCt z5-%TJ8cDCtm4#K4z%@lRcdBDXy?#9y{B@x>LNN1(epS8uz@*#+)s)3E*fVP1KLCDS z4f&NwP;i{Yf0BJFQFB6;DgvF`uHz0T*yNL-HdEK!1 zDki|If0(TA=5mGeXhJlPVo{LhK+IZ_@fHr1r@ zM!1bQs4mm<=B(CYZUrC&PMzr03NTU7`?87?$lMw@VW7^<^-2x=(a)>{e!v$TR1-o9 z!Uk;1!`zjc39%ZjrsDd|*VT{ep@lSa{!OUbeV#|ms4oa4c1CX zSP=~mUjyV-2vFYMz5Q9oi#!dw9!!#uHv%gm<1@)YM&_`Zid>^;fd!gEcBTY_82RLP zfA5c+UkNGDy(yBZ)+GhDRtedRg8-cAv^Bq*e3F$IGNU&uY1F6_d;XY`_uBvG2@^f6ye|#$kn4z?R7`z5UmFHHRpR;QN@;SrM3(mX} zRY~0!E}59C2H0Uk$1b!AgYAH(rQo`cX;wxw8iwwGt5*I$_TGKFZJSFJ|NG2S*qn|Q zS;MmAG@X_X;&EiVN$Xr{I!#=U#~&?07Hf)BNXi#ldYAJozbE^xy#bH_FA`-ZfBnv! zGhM5(h&whmHa7MRB|Tg%ob%Wv2jf| z(T8SDpcqYqDVks{t7ci`SwZOXMqjEAwtCUWH#ieU#e?_HgYF|;Xg1&#bB`e$kvZyNgUjLaR&(A9V ztxJ&kB>?c#sSI4F2GmsK%GIykt#~`=3MRbUSunn?9A9sR@qyHPf8_y6tWifO+1KtO z2$CF#N6iL#%q`ZnXIK}JtUSWrU$@lgbhn@V@chSr|LNzwSH1m%!(U$i`rFalcOQNf_|~5J zfW`C39Q>Ikpyl}&e@Kc&+zogJA&W%Yuh+W!7O2Ugb0_Sfm9A95+zKy+j7b-LEf-0R}M+g*$I z`1;q)cgLHjAI=|soNs0+A2$Cye|V-2ggf$p-Pvmv4bcECo{mdH#8SLxD0dRR4)4uG z20|%B;#di2e=Hha;PZb5(fm%Z<2gu%r7y-REqVcO(;_7l5HxhfR;S{GTF{^xlcEe! z1<#GwZx4@t`SkpA?^#ZUe*W{ve?L7w*>--)3DW)_2fzOC+wUJf?y02d-7A9xcu-!( z;t{Vlnqs^kn#QJKZFDUXY55(Zm#|1k|GLeSx?tEwf1h<*dr{=xw}(+UHY2Ozffb0Y z81=#d+e?to?O4QZP*3SA!CF#d5>RS%jg92Y+X&A$k~4QBJcl4E1w_j|Rf3&i7@6F& zsk0Nj_<1MT*svaw8GYql!JC~*R}36eG%{jYFJ6dpKWuM4|326>gBNsrw_|Tx)?AX7 z@{^vYf49f&$Z38xRC>g#pt7xU!!B3eV^DDeBs*if^Zm4d<<8qI@!C0iUHMGj zzifT~{rbPGeZS^}5r4U)+wMMZZ$r|5xYJ}5e@6f7czWr6cGuR{UL=tlkNzW?Ub=T~ zJB+Sgs>Q-v7L3=R82s9P_U!rd|F{~vp5N|=V_p&>*aR_Kd;8|(U)H|g%HHVv{T6ow zDh&gwIa}cgCsM9hqNuZ+`|)My$GlcV7nOl#l2*%%Xw)8he!?Qm_3GMDAFWl$~gE7Uz z5N;=d?aBeb)v{TMv*dhN{jeYAJITLo@BV%(00W_8);o$%nmV`NZuxFIOU~iUu$N3S zmLXXNZ0H3{6z~{06fUa@E{wK1s+u8HfAw&Wqg=KE0ZGukUjmusK>ibfEMDpQ^OhPi z2e_mA2@8j792;6KBgP-a40!Aza+5IHRe3N;{CG(=w<|JPtF@pz@PG{BB$`6FElO#Y zzAk^aG0qUWUdplAS%;R%tbCesIrD+7dBGant%vCY%AxFlbiAXEsxT(j3uMtlp8;BA8HMi4HQz6qMOuSmxt~Yqd;v z7M)Xghf_fe^6RbGmB5#ftSbeUEahZJgg2v<2`>6!zJmjV6N;*b*0Qn?0kTNHU|tcp zlD_aH2%?P*E1(|O5+vEF99S=De`hu`!?WO=!V#Sve&HPZZbk{crIc{R0hJa)Mst1> zA7mKCmxGZ2mLs3RJexrV^dtowfolsm(E+v&;9V4s?L5X&t0iI`1@4V^1v^r}dVM`h zX-{Jo?Oj1QyD{9iV3XiFq%=v*EpyjCiM#=1e;?hPt=Z?h-R^(3XXDsoe>0Db!%NR+ zvrpJfy!K%f2F%*>EE4hR2ZJjHpCSJodJ!9jcaY-ol-M1*k+w$Tm4ZKj#>Wg z27@T{22jK0HM|d|JD7!oS=Lv~GnA@@ntn2@)h3G|#5Y+F(pJk)e-*T!Dpevj4sTeKT7!JE@PCb$5cLdrD~Q`h6NRk?IWx18 z=pM>3zCbLNSJW)QBNm45A2R9a7stIAAE5=HE6ty0aj87Y-EaIZP$be~YkV)AHCu#2Uz#b0YSi&`zr%chSY!W4AbnBR^tz7WNEE)_=wuTUR-5U1f*@t+iT-g@8wH7{i|^zt4hQ!ySO11xG3492N?KsR{lJpQ#WP z;I%I>w1XSb#CQA5e;lPm`-Z{^+Tua-j#7`B(S{l9VgcJQtWq1wD&Axh;A23GF*FSX z`0%CRhF~i2vN)N7-3GCJt5uZ>G`1%MNe(o-8_-7OOhydDvNOdTCW6r#)srle4KLXZ zY+&Mc@W78|v(4^0kFF=~svO}B3obm=*yNw!pJ0&)PxB4am+wsi3x71IU8X8GZWt(I zD%NCHKpC#@+8`9*MQ|JFGz~WrxV^Pn+=XeK(g^1lq49ZO35;9^WM@>xam&I3HKgJY z*nj`_t(!#N-E1aOKC#Q+yrkzih4gaDTPI7)4%1c(LPdmTpb)ov4bwApfprt1_v2Uh zC+-y>AV_yY+94i|NZ#OWn%}JXRA2_Lz+K%7u6jO%c{pbMWHtlZz+oh~2I7*K z_@qy!WPrqDT^2xRq-=FK6@-!Rnl3dxTGNFIAx6Rpi77A{!+(VM)N5acNfM4ppL%VK zi9n{*YmXTI98j-)>kX2TWt%STkeGIeFL8kD>v+MwmiP&(kZD!HLAjts%w#qLCPkf} z8PaezQ)db1pJj2P9JX3v^&EjtOYFW$%}~Sr*(`vY287k<=LMwUvtd~r@M9X*0dU9N zq+`*gio-vHJwaXAcM;v^||G6f4cPZ+~`)peK+92F)RbZD9fFjCLfC ze4Cn)brzh%oYuhsb^_;ZFq;|BKmOhgUsBHwUr^7ICqslFpdA1C$-09*MFis1W_(08 zFh@D|<yMsR_v>;A!aH#bBt|M@SXD0h9yEHTO0)H1;5yOHUjNavpG2`ED$! zP!FcE6Cjb#$2ObsnNZvS0WriRV1Fd7*7_jjclsSGph0^Qu^TU(#-d5by97#@$wX%} zeMCpvLDj*vWV6}2cAD_Yn_ay2?BG#T-HT*RqOG-KE+;<=j zXuz^bz&K{JU}IyaEdIGp<&l!m4uQ~O5h4(hSxcOrA?zXs(&n)35|@VU0R$9c;y^fJ z!{mF91bNCyIQia{e4RL6O9!bqa}p9L$IeO4$qR5*5+I2gI8?FV1mccKX0_eNvPf88 z1b<>!_%)l!OiqfkT>fc^83RV};Nta70gc6A1(b$u`Iyz`+05W&^ki#E$CZtpRyTIa zH_3WXcZ33(XGb!`anS$vSj9No2ltlTjN(&)~Zs z6N^%+q$QZvYH7uQn3c+5Vxb_Xs->bLxqncYAJfIMJT)Kwfj<09jKaY>NlQYY9SGtZ zNFR71Qb6HN0P&l_1|^s01Qwa3RE_$rbTCON=bPsiNi6lG*i-Jl`Oeya=;Qgm+Mvqr z-B)(6;3qzW{{5ImB#;Sz{SFcJGp_SbHc2wy7l_K=9)#mv{1?LFBjtc~2m1&+*nbVO zL`XR6oWs39Et3!CP2ocGFqhpZil+Qnsgl2j8p8*8Is`r;DIO86)_T}>18)p3542k0 zI^2-3Y9CHd;A|0-I1-G#K-^w|CyXSH@l{x-)Z6v!?pBBNc@BvHTgz2l8VNMcYP!3w zs0kS);+&(X>fU8lV=A)L%JFV&Z-4UIJdZ?z2`x9^Q<;P3jf1(6Bw8IXvhyXir?5hE%>78>S3nX93wkUs)@^kiX9^3?!jv=VI-p zjA#!R%L@aHdu=QG!GzLWKO0W)Eo!l{;0o1hot^W?1@ax&X8uAXVmgz;FMod7(aw29 z$pTmp34eI?f!p%gj70Dv4svC4d9-}KHQ{ZTvqbFw%`UjF^`)=}29?cke;Vy6uq;jPjUtwk<9`+ixf76aQxvO$pj&KvZbbvfsN{3zW zzG7aIK@UKStWynsh@Q0A=0Y%GR%ktfRx2>GJN$YVc-@_(8i#V8NE&()mj>BFkr*gV z0M`}H8y?DxzDPI;@pDQUA`2M+E(C}RgI3FoIW7#^LlO&I7_=EE?SB%iNMra}g<7rF zU26Iy1~Pb;nlAn_r`R60YHgWQ-efFKS<}_BT`e1Bq(%L=ezwP7qQt~slkk?G=bL!a zlWl85NyldS*-o}CYpb?$P**v~Dt{m4D?K--C;^Bh5?B_wOU;_kc>reG%W8MP%6bmJ^;pm3Mf4Y_t(%`M_tc5ODE3fXqI%jm|Xo{F4~ z3nM@nNs7`tNZiS;CgN1U3knEC@QwmM1|+6BURhhFH=A`~+kdKv2wMV@cbHs|;OaA7 zUyq5Ba7x$L{fePvkr9^ca{Rc$*Q4r9iz z8gnFMWoTqQuNfQBhB1zf4O63z%#9&34+^2n=_r<7GKmi9@PSKLh+ML&a9ujiWEmF8 zf7jtq^F>x0TYv8oeR;Zt)qw}-BVehKd3l=xWAR-|A3Xldj+e4m_u_qLwCH@7(g%Fp z%C1xq!n2xja}{rLPQOdRiecw%T5VT=N9V+!UU>m(3g8x>e_F{1 zBGpP>%U3#eWIZHjkurSL&*>>= z;W=^?g)|emuvh{Jyj+1&OYAN-TC884R@EjC>71j?2doiS5UfmS28d8MT@DwuW6m_n zafj!#S%1{7z)&d-1g0C!dq`Hv+VR?Q)RYlVrqiqhVQ1kv^~kTcW9zcJj*<><1-_dSGG+x(Yy<++@er*-_4D=U2u$a8?{I z0vq$DTP>V+DP3G4bSxl@zsV86yMFl;0AfI$zpFRO!qI<))&n~WbuJ!<;M=F-=W7h(YyXlGXxss(0yeN?hk!|= z+|kS-@Ph3P%To58Hcug8vOsokf z>)~~(QaA3;Sep18_Gf5WirvO*6f3QlZ$m62J9sVPtA=LF7-%HSu1W_!0qvVuG*l-f z;ek(pL;){*;1eIHm4G?vLlXkNcw;K902%Nih14;WRVPG-aIoe$Kqi#6*{KB))CdD(BSI5uG@sTb?i-P?EHS&*&bn0fPU9pTV|TMz zkj`dNx@(Sdc`Sbw74t>hRj?F*BY>wxgnQQ5BP9Mxb7n*yE~l55K8w*z4iDhWFpiAc zNl4wC*FPboP~$uBASJtd!0@@GnZOUbV(VGcr`0ov-T;lJ0{C(ljDq}W@jO5m#Zc0*zMkyfcuF;R)kyo3>g;CXRnKJjEnM;5FA=aYsIj^c<>7*DoAysF+H-x$aKE z%%ITDU2bVF3}o8KP|X$5 zOidjK&B3OUB3@1a*?d*{G0Jk)46fyd@}EkL@jR>}i~vH4irEfa+CBce3%`HyN(*Xp z2x;x%PY;#ltyb2a+a9~ol^5XHHcH8SXd5UY=Mw|{x>7%`t{<1`d!jy?-#}PU)JjV& zuvl4$wmKaLJPQi#dmsq&LQrM%Jo#imMVa0Fu@BoAg^`zrK>`;G=n}%%RCT!)U_hJ` zHlDy;wuha>C;?4OG~(ji&I^CqUDSTqYPoI1e#J&eq;OoB(o7jI(sy}+ZSWU01j?>^ zvzc)jCL?G8-oa#U)M|+z>y%D+)xOmSPI8kV)3}7F*t{<>-`Ok!yPC!&^@T(UEG5l< zNi*{&0OMKeoX=)nF{#z^H~KFls}5Fka_wk>jNFnI?QKi?HT0?>E{A_%tVrZAn7#;{ z7FMQKm8Fzc72e4Wp+V?Ao5j*fV!fl}&1UPM)nhp!g1lDCz@x!XvbfD+1?Qd>NKBSZ zjs#Zr8)=(Q&0sgOL#t}6a$pokRJLW~){{Qi0R0tu0a!dqvpU~;x!L8y4!AoZBZe)b zZCi-8(FUb|-kK3eNh5#yfGl7%2XP?6FXD__z?}&{09+C?!Xq5*Q?iUiq@yGeH*L~G zP9jpji0GdNb8?~){`H>IYJEwOY<)>{eGK6UXU)l7!}5kT-^q@n?93>c7aM*4xJpqk zG<0$^T#%x&AQ2aC6K=+F5>9{zaj%4^y_A#{dovz02-(Lf%2$8onU*-VQBDPF09d#K zeFJ$z^Dwm@7&6RLFc03%exdethjZd)i0n0J{67m;8-AaXiPMh5F{{#LFyL}?ao~88 z+`kE8h@3ErSUd`SG_C`qksey&ibL^M9E#yfDwI}9s};VW0aq_>fGWg;Jdma)t(FH_ zT*J7LnFs>W+jW1bWCBqR;iCRbz5HwHCT6iJU6TpE6coHFterqyH()Htx6woh$Ma;4Qj0Trot^eCwux59j2|;VXBx&@cLf-ewxJIfSruI_`uKN zsHhjKeWz&MA}zFMGq9M=ze@tae=z?aYvXC0tX;A-H&}mz03m`!B5NQj%%p=4^UN1g z$O!iRap}%t7rbTcXToNeO1Gln``USZ5!Hpvi?As-A5=M+?kKAD3i{BY< zXm9=3*)%-&zuWS-<8p4cWbGO+eb2Csm)}E>U3ZdUpxqTO&pWs71bPwS)CZ1M3VY(J zPb56=gV%rFfbkIu{z1Jl3nV;pOnv_F!Wvx6d*VeQ0qsppG)R9G#w;ji@>BySTFiqq zgLqc~fW~minm4{O2zTq_gBHm7J~-rw>m8D*fb0$tLV6B#hHqpSBxUdz0j8S8+4@LxLLrRL9xNin~NfLkA{M7^SksUC2(xJouz7I}-;=B+1 zd#Hfq2PG`|0+?L6+m<}yu81xwj4!ivQ{0K+G2}k`;N3C*80Jd49cwo0LimF|J9K^j z((PYEXG8J32w9nKWF_7Qui$M#rOrv{vk30Qb|)`|8WsED&h8M58XHe1lMtfF;GMu; z?{YeG06n?YGCnAm2Ds)(gWPp5B~`T3eqNwo@6vWjip=eUfHfk4wQdTvI+uY9FR>j> ztbFZAhkeVqFqe#90wjNGIS#2RDH_j?k_rT&7>`{C>Mr^N$vYc=)d<_U2!j)Mf(=-< z$xDsHY0PTM5MBhk<>5riWvsf9^SW|d@S3tkNEJ^Pa6rLvih7hx#hA2_?t$SH{s75^ zc!VA!4=c$W*t6N$xdnoFTzasQQEjFi)!iJ-p7?2q#_s&UCIf#u%^)2HZ^N66XIvz2 z@|(+>4D^YSLMOi@bQ1ZCBu_p_o)Y47u@fV{H1wrG{u|ZR5%Jp?Sep$%B$ec!nC;DG zw<(~!-@f&HpYJhzh@Qs8FG!nQ!U_@~-y&dJ(We7`^nuZU3;OLG$vtLAAgP zLGF!_hcbBfqk=2nIf|-SQLHkeTH(D@OBK}m^L;0Gm81`f=dZo*kRk_Djtm2aS8(wI zLdJx(vB;RsOy5Cs18Yt$u2`~PK8F>H%NoL!;dyq7?^b^&EB2SLe-2a2t}Zwy3Q@2N zoHlFccr2M@Zonz0Ma0vwQXY(&JpBYm?i-(ob#ta2?g2U1j{X~*>H%-pYH`xVMO0b_ z1A_$@{1v3lk)O%b+wsU9gty0Gh<-`!i3=9U{IBQ)!yd(K@Ihp$-zHdZTC-V9`dOp_ zw6IiQa_@f#Jf)o-iR+baMk2O_rJy#qVMpAST4nHIi1-Z>3=lh6MmldfnX0 zNc~JX8bq)g@I37xXUt<)fwELGp`eBL+g>cT*};H2iObWeK>BC1buXRG*5ldCa1M?R zdM8{YBldD3<_HYTjOh?ej3UMSG(Zmpkl;wOgxP=WQ%Wg~xk>i#DLqQf*dl-OA8KWb zRG@tytMxU6j>YLCc5y+CBP7(mh)>B^(#VC_F#%PJ9T2W)v4goq{z%QdAVmG<_IGF} z7b2!)8-c&?q+g7!IXct9K#{ukr9_PwR`I$iEO`KQKA6%*-q^GR+!jtKs-KXYiI`da zgy4TLva)^}Pj4q`^D9iNwJyfZzxlXlGtOR2A_ljSF4m)tw1IrCotei z!F+V3kfa5i(Zbt+Mf=k4mP?~>mjt?>ttYqyZh{B}Kc>_puTp9rrZlirZ?V5pdYHCB ztty;J$fuMB|N>VU|On+IVlz*z^ zpIX*SZY@0YLkQ-u1Y@_%YW%8^mw<7qCALS!`kKIXmy=Z)7#t%X9FQ*S*1n=i4d~+X zynql^cE>Z-PPt}RULN%0ZJOWnVI$P_Xpw1t+sD1osHEHu%L*$yLKIzuo=OKR*3ovs z@>+9JIz0*(DYRY5E)u1C#D97<51i;1sR3hRT$Y5u^>8klFY@ufVTvf ze~bbZA z+%mB8_$3|%HSZ;=dDk&CDSUgame+PkoPP`;;)c^|4V+f%I)60>4hCxo^P@V0+D6pA zlMWplroZc-bz&;c9&ejQ-1`Y;jz1f*?ls!r`%R}K#eg*p)oTRB7h#R6WRA=;k#e>9u&S=;d@97 z3tV=%HlN4;Imtae2I&0(S>rIRPLC^-6D=`vXrF`*osz(z;e6S>ACO+5e+4SF{w38I z7`eN2{nM`IW)F6Q3EavL0h!6I1OGCM6sWg1G;bX)Qhy#elRIzz-$$GQ5L_H~DjX zA_&c8cnX&n;P!3?QQ8kDml0?J99hKedkGk}blN}V1IX5&+&a64e4`m`GQB&9A}-6N zXl!Z_quYW1)CLrr$IQ%zNXKXT1q1>d`7t^Q5R($nHu-^X5YO4lG`e4e5Jt@K`);SR zVf@>gvEf#ig=hjRe>y?i0m*+BtIcMnd79FF*vwYO9wtfTT}~4afx|MKYEp+Td{;}T zBsUAr0NAaG2o2W9Dsj=IJY5_Vx7a>S(HaYWzZuX1y9*QIMr)v1u9!#$xpv8DVmA8- zifYiay<)>KVlbvd>`fc2#u^4vY=xc9x$_CcOXJ*USlFYL&+861j2 zU?I#cKow0q&*4w8qEsPZQiLEi~pvpk%nr*H%qo1FpFbzyqw< z8^7KN?mVzae~b-!+-^>z`ow^sq?^1#Mh~oPH|lJMBmC+Slo9e>QAiaG~rcV72#T4p`sHZua46 z7u3ArrLhmDjG0ag9Qk(#Xsr1vU8nS8YT3*YdRFja2aqdH74+9Mx83CWqdoy)FG!30 z%z9JL5fQd>qh6`U)fBg&-LND#qrvbG@k+x&KxW}iU#0Y8O7={vf&w0x1~(ryD#_Mg z>28rYe;tAcg=K$CwZjYd;l3%2Do3=Jnx2XXoM|7a!Jt(K*xS_XrDgya9%$DnUKyqSV%s za56_Io(ymupE-l}(z2d@w+R7tuOtFBOs&Uk6)h?DJ7Nesq z2Pj2WU8ffzw`f(D=>-B-S)&&M`1vBe2+`QJP^+}KFZkjrYs9B;1#TT4=@1+=EXh^% ze@gM?pjQjlqpK}P8TwbX@O4W%MX~z0g>J|txhEG?lW%~3FS(`b)$0=MGKtKf;tMfB zLQaI&^e~afOO(tZ#8uhlY_y$-z?_i4t0uClkBPjhWTP{i86dE;8AJ_?XR~O-+uDYd z4S#EU4kSG%-iN7(uL8D5I#rkH0SPUVf6{OlnSNXF`3_CtaN!pL{MVYzQgO)VX{1M; zUFCu#z23Z`>BF@PcG)F(SE!C#w_s3^+r7?a7Z$mpRWFh#CSo+o@c7iujlugg+6@GF zY~jB|rkDspEJb*g0Vaq*mco*ojOAJEf@gT#YE7LN{qK3S*7cFoYMJ0K(RX(Jf0xsZ z{`cP2w(Y+dY~n94VuT5GEHZMeF=I33~f^aA27TDx5vgAC#3F~@W~o4GJB=o&`}o-cC1vQ6xFYa8oI zeCo_mw;zy@-0pVmxUCr}6YfAFf0wLaoV;5oZP%0|SK!%)oB;ub=Z3HA5#=+2NQOt7 z^oHEiS7{dT>g?!TNb&Bo2(Mf+aIeL$yr-0YNi7r;fosVPPbx4WzC&Hocjygn`D^O7 zx%;&mdyCcsV5=xHGWGo)Y)Yi|w$^ zv5Ii6$d1r{8_1HK2+1#ge*|EG3C;ob0F7fZKY_8754-q^oX zY(RKaD+Ym72*17#>^F1bt7fZvruCrkgIQFe9uM|rSY+6)4EzQ@Fzb|#Tdi?j0VTSj z^8Ag`TTjl*AP*cHz{(I6E66?fH5SwEw=dx3BK|qd`ZxJal{vf2f63jla%X^kMWUi# zi3|D_i!h+>5LuYplk@w}E_3M69#SQ#)nY(NnXr~5V~W+xx|@Rwz|4cBY4`rhH4xyz zMW}9)M}^8kF;?5R$aq0D%Oa+W%nDj2DOPZh>VboB&VvHwmD4P0C4hC}8Vp`lURGx_ za~0T5s<0%`bHzj#R@RLRDn(72WCnSEA!-RH6~-x2f4oxA6I5Vfpkg5xgNBo$ z1QZXwf|nXu)>y^;d#r>O;M^*jj%0#6p|V1WUL&)TcMi`Ga?CtIm4MZ{ZcNL9HZHoI zre?CtazLi3d&pE$C8EJCs)78>sFd(Qz%q#Kh!lRAxN051FMYKt36n)Y{rM%;-Z;=c zr93wi;e#b8f2FU|*$j>uA)p0unxl%1tXAtZh5xmdPitES=`&zdjnUGy(x5|F8y(h* z!~st0V@HaOYY$^hL|%CwFBGEJSv5=pX?BSe`;`*XgGW^?5p7H^+h~3SQQfO_*ZiEm zq@7mFgdT-{9$LlT;02njBVp0}vEmS@9d0D4x@e=ie`c{;Xz6MF>MfM&0qX-yO*?jP zz;xdJ4AA&JZM9TrRKg!$#auR_M-53~w?q)-&#AbNd`@RG^K(jnhp;R3&$PZ5j?1-! z<4#|E(pE-bR5KXy+b*bTM6%o`AAwraS1J9H(!WxAn$k}x{UxP|Lj#A#4*i_c-(bA- z&y@NOIPE)>IUrY847W4AUZyb04So=5@11JfU?%-DDB8}G+yx|ZXz0++TzL1E!%M}t z9zJe{-Y?ce-^U?g-I3mVMX za_ywo&fR>i*$iFSG7qz4&K|NgvV-_cs4sYU-HDOg=hKaJIa}rJ}9Oo*G0M!@5^4A>B*!d0@ z$i7SUu#M7R&f1rFasm&3TCHmbq9Bj~i<2n4w@Z!{m@Ej`j>eDg@IV4cbf(K)d8f-= zd3DCTuAzFKvBKSVTI24^Unl zG}P=8mfBonY!4pGed6SAb4$1ZjD{12Tx_Eh#ak9tm5*Ab#SL5rB z+It0wm6RVVz(CBwIl7(Z5##0rBB>;a&+LOVC($WM!mBHv@%zP<2r6ptrv&$veZ(V? zo_PIh`#VSYyT6ku?;J8<3F|9f&PNUexy5C-opg?dRxGK1jPeci5er~+XIjNJl)|%; z1l{uaNsI=Cot@wX+X*%{ESRtovNbs0fwwse4?AN`{unCw0h6Tm<&1Tf6F~l8RYne# zCQAWo-oj+qKnAPMLe;D(l`{QJlaMuxqgJbBW>}U5jg%?C6!0GyMG^suFLzj2hDSc2 zl?O6DX2Rot@nBhqLkCfT4F1Tx!Q<#U=%cJkNm}<2dU0=Wg+wBtBisTBOW#DOs#VlOLuvp zQ?Tz5Z95E2D{s8yp7?p{gs}nXb34x|z~c`Pfx|0*YkxR2AOI9{>}2J^_f`Bngl8}) zrNvkWkAB`4YENjTX7@qT~T zyruVlFC66+FM3q{CRic;#)~}>v`@(Z1@y&>UUhS)HO-yYHFpZebdMR|qhRPqi=?o-q#x6wed9Pk?hUspu}S0do~ z@Wf}h16YZB_ZiN%{HXgAKR;{1Xz=o<2w4Xe7UIg<72`2; zQAOSeP3A<5v9bZNw1rjQ$>KP{SFIodLr>RaimSIHUZu&}Vm>JhQIABo6YUm%E9m!z zghV39jao##ZOBV-mal1banQQJ1H@zY@6H^eQ+;;GL;j$EEzC0Ca}XMHi~OlA*Xpnp zw<x49DX zG+V-Iht?YUSlAf_Im>R1^QyRzqWm}qPa#&*iayA^H3?#^)r_U>FH~~Zx6ogxN3X3n zCnejIrVv#nf$tULh$J(PsD_~_BR#5uoyBW3U2Tob>0=jSiH$+SddjhhK{ER0+J2hbN#5%osXf?5zLg z%w}JlRtv-suboy4taJ~5!SW6gap}G|8ar$jgzz3Tws<1kkXqQ+y3|8#un3MY(1r)k zPIfHv+My9SbU^Fncufi~YiId2k?D=3s7~YVc z8x4e6r|sR4HxRu|eorp>x5FaO_3dA7$=h(?`um>ihgbH^4SDsZ9|muNVc7GczR&D? zpC^4_LErQ3p7=b8+<0W~i?6*X3UA^09y?M~K2B^`eE$-8lh>KaqkZYmF*L1hTJ)v) zForR}b3^!Jp8-`mg-_Jbqcv@W!Eqnvglp*7pqMVKZll;fJIAJvLKj|Wz?i!TnFp>2 zL!R{kt_x!>#FDfW`$oarA48WddIA@J3b0*_4j7j)OGe=!wjcBiURW7ATp@=Hw64aM zJMp$;M_axJ0gE3-%pJglKS8TjI~lRSG#P#QIhNS@9UFTIge~M>Q8-OlWLZjC-f0Fr z%Cg`5C0^6wkxosdTl6KIYv4ee@V`>8p^dsLx|1?h(}Y##5h$hV=(~swv0sRP;jZ%= zG?V4Q7LviJMbv2n56?~hWhl{jJY)z@4La>RRVf%;vED7JmEMg=EDkb zJrBcZ3<$Lo2wvO7pveYA@~T`3V}p?gzMi)6;k|QW5Oq}ux`_u*TO1urcxl9F8Q&W} z+G7^S?v(^@VjAwmifgP65{4TxR}c3}`fGIGaiGi^|waDxE6MollrULw)44s_hW zzVipmXkwE} zXQ-M4Nd(Vv+#B{DHty1i;`S~B9g_<;)68Z71p#x5{OZt$J`d@Qj^$j}JSVJIou1_B zV}BE5k82Q7cauM^GABWgKEO4`Hey^QgNV|XNw9XA1e=q{8@ti{+W3BR+nAH6N6!o| z2v`Jx^Nn*7<{j_=<6>ifcsIzqEWen8_*#R9y2akch!+)_2%SWLuf1rallpV5MgwUIl0gaPuzg54c!4-1A0=2 zAIfl`LbiIod&zu%pA9bWjS759+)J)-H9E$e+$u92mq1gO6x`cWIBq0rUTG{VC9wH&vtf zppC+qllS=$-qsD_ySgE~TQ~#(xB-j%k#~uZJ157Y$~pOilizrHDe3Qo)8E%JV;a{YGiP6LgTQ+#3yeiw@G1tEK@Vy0CirzGr~WILpg!i-tu8 zh2)RmRXffT@tz6ZO?eW(i5lR@gK6R}Y=F8rE0o`04U1gme|M$Zq>A(cegEab{2S~y6bZt5QzYUX3Y9w zFaQ)6G#`^eMkr2?x}Edd0~(gCUevSB$+K;JUn01c??X9jyuB>hhyZ= zm1Lk!LbIRYpejR~XCu*;C7!rJhP0>VnQ`e3z}@vBLyB8B3VvchNc0p8d>M0+ac2) zpVDfR=!fI68w{4rqpH;6sW{*m!{?z_&Zjb{oywqBTA$y*4iND=$2>I;NLO_Z`tFo+ zaJ^j+D5E6fUo#r(_2F?>nEwEO3{F6QSUX8BPRmd9IilQ{Jb*X~%xD9C<|^VYRSt)S znb}z+>cBxnej?+ZMWWC&{=fgdX7Hic4R+B0LC9Ae-f;Qw77c6&YXd*}J@g6dZvSLi z=Xk8>C"ay%@OUlBQ^ER$23@~NPwq1cIk7#WW$j*T0_ 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 T&)^uQqyPFpwov}mLlO)Cz{Av* diff --git a/webapp_dist/zones.json.gz b/webapp_dist/zones.json.gz index 0b291d94629868d4d51380f8a53aedf4eda3f736..feeaebf73d23faf85e065cbe0267bc1aef21bc9c 100644 GIT binary patch literal 4023 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 literal 4030 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 Date: Thu, 25 Apr 2024 09:18:55 +0200 Subject: [PATCH 65/70] Introducing defines for RX2/TX2 --- src/PinMapping.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 7c8bec186..516e6de1d 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -94,6 +94,14 @@ #define VICTRON_PIN_RX -1 #endif +#ifndef VICTRON_PIN_TX2 +#define VICTRON_PIN_TX2 -1 +#endif + +#ifndef VICTRON_PIN_RX2 +#define VICTRON_PIN_RX2 -1 +#endif + #ifndef BATTERY_PIN_RX #define BATTERY_PIN_RX -1 #endif @@ -284,8 +292,8 @@ bool PinMappingClass::init(const String& deviceMapping) // OpenDTU-OnBattery-specific pins below _pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX; _pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX; - _pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX; - _pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX; + _pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX2; + _pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX2; _pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX; _pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN; From 84e83f2dbb2945e10f7a7ad40e65b53462c8fb47 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Mon, 29 Apr 2024 14:21:28 +0200 Subject: [PATCH 66/70] adopt WebApiClass::parseRequestData() method saves redundant code, reducing flash usage. --- src/WebApi_Huawei.cpp | 63 +++++-------------------------------- src/WebApi_battery.cpp | 31 ++---------------- src/WebApi_powerlimiter.cpp | 27 ++-------------- src/WebApi_powermeter.cpp | 56 ++++----------------------------- src/WebApi_vedirect.cpp | 31 ++---------------- 5 files changed, 22 insertions(+), 186 deletions(-) diff --git a/src/WebApi_Huawei.cpp b/src/WebApi_Huawei.cpp index e1e32a5e3..8bef19ff4 100644 --- a/src/WebApi_Huawei.cpp +++ b/src/WebApi_Huawei.cpp @@ -72,40 +72,16 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - JsonDocument root; - DeserializationError error = deserializeJson(root, json); float value; uint8_t online = true; float minimal_voltage; - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (root.containsKey("online")) { online = root["online"].as(); @@ -203,40 +179,15 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) if (!WebApi.checkCredentials(request)) { return; } - - AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } + AsyncJsonResponse* response = new AsyncJsonResponse(); JsonDocument root; - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("enabled")) || !(root.containsKey("can_controller_frequency")) || !(root.containsKey("auto_power_enabled")) || diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index 1076ec229..798957d3b 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -59,38 +59,13 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - JsonDocument root; - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!root.containsKey("enabled") || !root.containsKey("provider")) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 1b72d48dc..114ced773 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -122,34 +122,13 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - response->setLength(); - request->send(response); - return; - } - JsonDocument root; - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + // we were not actually checking for all the keys we (unconditionally) // access below for a long time, and it is technically not needed if users // use the web application to submit settings. the web app will always diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index c10b0274d..8ca492b01 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -98,35 +98,13 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 4096) { - retMsg["message"] = "Data too large!"; - response->setLength(); - request->send(response); - return; - } - JsonDocument root; - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!(root.containsKey("enabled") && root.containsKey("source"))) { retMsg["message"] = "Values are missing!"; response->setLength(); @@ -217,35 +195,13 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) } AsyncJsonResponse* asyncJsonResponse = new AsyncJsonResponse(); - auto& retMsg = asyncJsonResponse->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - asyncJsonResponse->setLength(); - request->send(asyncJsonResponse); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 2048) { - retMsg["message"] = "Data too large!"; - asyncJsonResponse->setLength(); - request->send(asyncJsonResponse); - return; - } - JsonDocument root; - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - asyncJsonResponse->setLength(); - request->send(asyncJsonResponse); + if (!WebApi.parseRequestData(request, asyncJsonResponse, root)) { return; } + auto& retMsg = asyncJsonResponse->getRoot(); + if (!root.containsKey("url") || !root.containsKey("auth_type") || !root.containsKey("username") || !root.containsKey("password") || !root.containsKey("header_key") || !root.containsKey("header_value") || !root.containsKey("timeout") || !root.containsKey("json_path")) { diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp index c1e0c8349..2499ebed3 100644 --- a/src/WebApi_vedirect.cpp +++ b/src/WebApi_vedirect.cpp @@ -66,38 +66,13 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - JsonDocument root; - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); + if (!WebApi.parseRequestData(request, response, root)) { return; } + auto& retMsg = response->getRoot(); + if (!root.containsKey("vedirect_enabled") || !root.containsKey("verbose_logging") || !root.containsKey("vedirect_updatesonly") ) { From d3b306e2fc365760131b3d296fe3f7dc4cafb4b3 Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Mon, 29 Apr 2024 20:17:18 +0200 Subject: [PATCH 67/70] appease eslint --- webapp/src/components/VedirectView.vue | 4 +-- webapp/src/views/PowerLimiterAdminView.vue | 32 +++++++++++----------- webapp/src/views/PowerMeterAdminView.vue | 4 +-- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/webapp/src/components/VedirectView.vue b/webapp/src/components/VedirectView.vue index e79b265fe..a1ed9bc3c 100644 --- a/webapp/src/components/VedirectView.vue +++ b/webapp/src/components/VedirectView.vue @@ -164,7 +164,7 @@ export default defineComponent({ this.socket.onmessage = (event) => { console.log(event); - var root = JSON.parse(event.data); + const root = JSON.parse(event.data); this.dplData = root["dpl"]; if (root["vedirect"]["full_update"] === true) { this.vedirect = root["vedirect"]; @@ -192,7 +192,7 @@ export default defineComponent({ clearTimeout(this.dataAgeTimers[serial]); } - var nextMs = 1000 - (this.vedirect.instances[serial].data_age_ms % 1000); + const nextMs = 1000 - (this.vedirect.instances[serial].data_age_ms % 1000); this.dataAgeTimers[serial] = setTimeout(() => { this.doDataAging(serial); }, nextMs); diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index fec3f4ff8..500614c6c 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -244,8 +244,8 @@ export default defineComponent({ }, watch: { 'powerLimiterConfigList.inverter_serial'(newVal) { - var cfg = this.powerLimiterConfigList; - var meta = this.powerLimiterMetaData; + const cfg = this.powerLimiterConfigList; + const meta = this.powerLimiterMetaData; if (newVal === "") { return; } // do not try to convert the placeholder value @@ -271,9 +271,9 @@ export default defineComponent({ }, methods: { getConfigHints() { - var cfg = this.powerLimiterConfigList; - var meta = this.powerLimiterMetaData; - var hints = []; + const cfg = this.powerLimiterConfigList; + const meta = this.powerLimiterMetaData; + const hints = []; if (meta.power_meter_enabled !== true) { hints.push({severity: "optional", subject: "PowerMeterDisabled"}); @@ -284,7 +284,7 @@ export default defineComponent({ this.configAlert = true; } else { - var inv = meta.inverters[cfg.inverter_serial]; + const inv = meta.inverters[cfg.inverter_serial]; if (inv !== undefined && !(inv.poll_enable && inv.command_enable && inv.poll_enable_night && inv.command_enable_night)) { hints.push({severity: "requirement", subject: "InverterCommunication"}); } @@ -309,19 +309,19 @@ export default defineComponent({ return this.powerLimiterMetaData.power_meter_enabled; }, canUseSolarPassthrough() { - var cfg = this.powerLimiterConfigList; - var meta = this.powerLimiterMetaData; - var canUse = this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered; + const cfg = this.powerLimiterConfigList; + const meta = this.powerLimiterMetaData; + const canUse = this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered; if (!canUse) { cfg.solar_passthrough_enabled = false; } return canUse; }, canUseSoCThresholds() { - var cfg = this.powerLimiterConfigList; - var meta = this.powerLimiterMetaData; + const cfg = this.powerLimiterConfigList; + const meta = this.powerLimiterMetaData; return this.isEnabled() && meta.battery_enabled && !cfg.is_inverter_solar_powered; }, canUseVoltageThresholds() { - var cfg = this.powerLimiterConfigList; + const cfg = this.powerLimiterConfigList; return this.isEnabled() && !cfg.is_inverter_solar_powered; }, isSolarPassthroughEnabled() { @@ -331,10 +331,10 @@ export default defineComponent({ return Array.from(Array(end).keys()); }, needsChannelSelection() { - var cfg = this.powerLimiterConfigList; - var meta = this.powerLimiterMetaData; + const cfg = this.powerLimiterConfigList; + const meta = this.powerLimiterMetaData; - var reset = function() { + const reset = function() { cfg.inverter_channel_id = 0; return false; }; @@ -343,7 +343,7 @@ export default defineComponent({ if (cfg.is_inverter_solar_powered) { return reset(); } - var inverter = meta.inverters[cfg.inverter_serial]; + const inverter = meta.inverters[cfg.inverter_serial]; if (inverter === undefined) { return reset(); } if (cfg.inverter_channel_id >= inverter.channels) { diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index 4bc9e76c3..8371d4651 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -281,7 +281,7 @@ export default defineComponent({ this.powerMeterConfigList = data; this.dataLoading = false; - for (var i = 0; i < this.powerMeterConfigList.http_phases.length; i++) { + for (let i = 0; i < this.powerMeterConfigList.http_phases.length; i++) { this.testHttpRequestAlert.push({ message: "", type: "", @@ -312,7 +312,7 @@ export default defineComponent({ ); }, testHttpRequest(index: number) { - var phaseConfig:PowerMeterHttpPhaseConfig; + let phaseConfig:PowerMeterHttpPhaseConfig; if (this.powerMeterConfigList.http_individual_requests) { phaseConfig = this.powerMeterConfigList.http_phases[index]; From 4e36c8c9ea33db0081e08fc5f607b2c3ba98639d Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Mon, 29 Apr 2024 20:43:35 +0200 Subject: [PATCH 68/70] Feature: battery interface: use HW serial 0 on ESP32-C3 or S3 (#933) this allows to use two VE.Direct interfaces, as there is no conflict regarding HW serial port 2 after making the battery interfaces use serial port 0 on devices with USB CDC. on those chips HW serial 0 is free to be used since serial messages are written through the USB interface directly. --- include/JkBmsController.h | 4 +++- include/VictronSmartShunt.h | 4 +++- lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp | 1 + lib/VeDirectFrameHandler/VeDirectShuntController.cpp | 3 ++- src/JkBmsController.cpp | 3 ++- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/include/JkBmsController.h b/include/JkBmsController.h index b21744d3f..bbc5a5ac0 100644 --- a/include/JkBmsController.h +++ b/include/JkBmsController.h @@ -19,7 +19,9 @@ class Controller : public BatteryProvider { void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } - bool usesHwPort2() const final { return true; } + bool usesHwPort2() const final { + return ARDUINO_USB_CDC_ON_BOOT != 1; + } private: enum class Status : unsigned { diff --git a/include/VictronSmartShunt.h b/include/VictronSmartShunt.h index 42b65774e..97b421325 100644 --- a/include/VictronSmartShunt.h +++ b/include/VictronSmartShunt.h @@ -9,7 +9,9 @@ class VictronSmartShunt : public BatteryProvider { void deinit() final { } void loop() final; std::shared_ptr getStats() const final { return _stats; } - bool usesHwPort2() const final { return true; } + bool usesHwPort2() const final { + return ARDUINO_USB_CDC_ON_BOOT != 1; + } private: uint32_t _lastUpdate = 0; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 074285dbd..44435dacf 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -65,6 +65,7 @@ template void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { _vedirectSerial = std::make_unique(hwSerialPort); + _vedirectSerial->end(); // make sure the UART will be re-initialized _vedirectSerial->begin(19200, SERIAL_8N1, rx, tx); _vedirectSerial->flush(); _canSend = (tx != -1); diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp index 54229b93d..2d8b85a71 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -5,7 +5,8 @@ VeDirectShuntController VeDirectShunt; void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) { - VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, verboseLogging, 2); + VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, verboseLogging, + ((ARDUINO_USB_CDC_ON_BOOT != 1)?2:0)); } bool VeDirectShuntController::processTextDataDerived(std::string const& name, std::string const& value) diff --git a/src/JkBmsController.cpp b/src/JkBmsController.cpp index 3f924030f..94f80cb08 100644 --- a/src/JkBmsController.cpp +++ b/src/JkBmsController.cpp @@ -198,7 +198,7 @@ class DummySerial { }; DummySerial HwSerial; #else -HardwareSerial HwSerial(2); +HardwareSerial HwSerial((ARDUINO_USB_CDC_ON_BOOT != 1)?2:0); #endif namespace JkBms { @@ -220,6 +220,7 @@ bool Controller::init(bool verboseLogging) return false; } + HwSerial.end(); // make sure the UART will be re-initialized HwSerial.begin(115200, SERIAL_8N1, pin.battery_rx, pin.battery_tx); HwSerial.flush(); From 686b5df64efaee90ead58fed01ee025cc1f773f2 Mon Sep 17 00:00:00 2001 From: eu-gh <141823622+eu-gh@users.noreply.github.com> Date: Thu, 2 May 2024 21:19:25 +0200 Subject: [PATCH 69/70] Feature: Publish Huawei AC charger mode via MQTT (#876) --- include/Huawei_can.h | 11 +++++---- src/Huawei_can.cpp | 50 ++++++++++++++++++---------------------- src/MqttHandleHuawei.cpp | 3 ++- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/include/Huawei_can.h b/include/Huawei_can.h index 2b8edc98e..3a699cd7d 100644 --- a/include/Huawei_can.h +++ b/include/Huawei_can.h @@ -105,14 +105,14 @@ class HuaweiCanCommClass { SPIClass *SPI; MCP_CAN *_CAN; uint8_t _huaweiIrq; // IRQ pin - uint32_t _nextRequestMillis = 0; // When to send next data request to PSU + uint32_t _nextRequestMillis = 0; // When to send next data request to PSU std::mutex _mutex; uint32_t _recValues[12]; uint16_t _txValues[5]; bool _hasNewTxValue[5]; - + uint8_t _errorCode; bool _completeUpdateReceived; }; @@ -125,8 +125,9 @@ class HuaweiCanClass { void setMode(uint8_t mode); RectifierParameters_t * get(); - uint32_t getLastUpdate(); - bool getAutoPowerStatus(); + uint32_t getLastUpdate() const { return _lastUpdateReceivedMillis; }; + bool getAutoPowerStatus() const { return _autoPowerEnabled; }; + uint8_t getMode() const { return _mode; }; private: void loop(); @@ -154,4 +155,4 @@ class HuaweiCanClass { }; extern HuaweiCanClass HuaweiCan; -extern HuaweiCanCommClass HuaweiCanComm; \ No newline at end of file +extern HuaweiCanCommClass HuaweiCanComm; diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index 6cd41b4fa..a288981a7 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -68,10 +68,10 @@ bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t // Public methods need to obtain semaphore -void HuaweiCanCommClass::loop() -{ +void HuaweiCanCommClass::loop() +{ std::lock_guard lock(_mutex); - + INT32U rxId; unsigned char len = 0; unsigned char rxBuf[8]; @@ -122,7 +122,7 @@ void HuaweiCanCommClass::loop() if ( _hasNewTxValue[i] == true) { uint8_t data[8] = {0x01, i, 0x00, 0x00, 0x00, 0x00, (uint8_t)((_txValues[i] & 0xFF00) >> 8), (uint8_t)(_txValues[i] & 0xFF)}; - // Send extended message + // Send extended message byte sndStat = _CAN->sendMsgBuf(0x108180FE, 1, 8, data); if (sndStat == CAN_OK) { _hasNewTxValue[i] = false; @@ -137,10 +137,10 @@ void HuaweiCanCommClass::loop() _nextRequestMillis = millis() + HUAWEI_DATA_REQUEST_INTERVAL_MS; } -} +} -uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter) -{ +uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter) +{ std::lock_guard lock(_mutex); uint32_t v = 0; if (parameter < HUAWEI_OUTPUT_CURRENT1_IDX) { @@ -149,8 +149,8 @@ uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter) return v; } -bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear) -{ +bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear) +{ std::lock_guard lock(_mutex); bool b = false; b = _completeUpdateReceived; @@ -160,8 +160,8 @@ bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear) return b; } -uint8_t HuaweiCanCommClass::getErrorCode(bool clear) -{ +uint8_t HuaweiCanCommClass::getErrorCode(bool clear) +{ std::lock_guard lock(_mutex); uint8_t e = 0; e = _errorCode; @@ -171,7 +171,7 @@ uint8_t HuaweiCanCommClass::getErrorCode(bool clear) return e; } -void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType) +void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType) { std::lock_guard lock(_mutex); if (parameterType < HUAWEI_OFFLINE_CURRENT) { @@ -185,7 +185,7 @@ void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType) void HuaweiCanCommClass::sendRequest() { uint8_t data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - //Send extended message + //Send extended message byte sndStat = _CAN->sendMsgBuf(0x108040FE, 1, 8, data); if(sndStat != CAN_OK) { _errorCode |= HUAWEI_ERROR_CODE_RX; @@ -242,10 +242,6 @@ RectifierParameters_t * HuaweiCanClass::get() return &_rp; } -uint32_t HuaweiCanClass::getLastUpdate() -{ - return _lastUpdateReceivedMillis; -} void HuaweiCanClass::processReceivedParameters() { @@ -284,7 +280,7 @@ void HuaweiCanClass::loop() MessageOutput.println("[HuaweiCanClass::loop] Data request error"); } if (com_error & HUAWEI_ERROR_CODE_TX) { - MessageOutput.println("[HuaweiCanClass::loop] Data set error"); + MessageOutput.println("[HuaweiCanClass::loop] Data set error"); } // Print updated data @@ -298,7 +294,7 @@ void HuaweiCanClass::loop() if (_rp.output_current > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT) { _outputCurrentOnSinceMillis = millis(); } - if (_outputCurrentOnSinceMillis + HUAWEI_AUTO_MODE_SHUTDOWN_DELAY < millis() && + if (_outputCurrentOnSinceMillis + HUAWEI_AUTO_MODE_SHUTDOWN_DELAY < millis() && (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { digitalWrite(_huaweiPower, 1); } @@ -321,7 +317,7 @@ void HuaweiCanClass::loop() _batteryEmergencyCharging = true; // Set output current - float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); + float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); float outputCurrent = efficiency * (config.Huawei.Auto_Power_Upper_Power_Limit / _rp.output_voltage); MessageOutput.printf("[HuaweiCanClass::loop] Emergency Charge Output current %f \r\n", outputCurrent); _setValue(outputCurrent, HUAWEI_ONLINE_CURRENT); @@ -343,7 +339,7 @@ void HuaweiCanClass::loop() if (_mode == HUAWEI_MODE_AUTO_INT ) { - // Check if we should run automatic power calculation at all. + // Check if we should run automatic power calculation at all. // We may have set a value recently and still wait for output stabilization if (_autoModeBlockedTillMillis > millis()) { return; @@ -368,7 +364,7 @@ void HuaweiCanClass::loop() if (inverter != nullptr) { if(inverter->isProducing()) { _setValue(0.0, HUAWEI_ONLINE_CURRENT); - // Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus + // Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus _autoModeBlockedTillMillis = millis() + 1000; MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n"); return; @@ -384,7 +380,7 @@ void HuaweiCanClass::loop() // Calculate new power limit float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); - float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); + float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); // Powerlimit is the requested output power + permissable Grid consumption factoring in the efficiency factor newPowerLimit += _rp.output_power + config.Huawei.Auto_Power_Target_Power_Consumption / efficiency; @@ -449,7 +445,7 @@ void HuaweiCanClass::loop() _setValue(0.0, HUAWEI_ONLINE_CURRENT); } } - } + } } void HuaweiCanClass::setValue(float in, uint8_t parameterType) @@ -476,7 +472,7 @@ void HuaweiCanClass::_setValue(float in, uint8_t parameterType) } // Start PSU if needed - if (in > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT && parameterType == HUAWEI_ONLINE_CURRENT && + if (in > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT && parameterType == HUAWEI_ONLINE_CURRENT && (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { digitalWrite(_huaweiPower, 0); _outputCurrentOnSinceMillis = millis(); @@ -524,7 +520,5 @@ void HuaweiCanClass::setMode(uint8_t mode) { } } -bool HuaweiCanClass::getAutoPowerStatus() { - return _autoPowerEnabled; -} + diff --git a/src/MqttHandleHuawei.cpp b/src/MqttHandleHuawei.cpp index 4330dc7c2..1f0f7ddb7 100644 --- a/src/MqttHandleHuawei.cpp +++ b/src/MqttHandleHuawei.cpp @@ -75,6 +75,7 @@ void MqttHandleHuaweiClass::loop() MqttSettings.publish("huawei/input_temp", String(rp->input_temp)); MqttSettings.publish("huawei/output_temp", String(rp->output_temp)); MqttSettings.publish("huawei/efficiency", String(rp->efficiency)); + MqttSettings.publish("huawei/mode", String(HuaweiCan.getMode())); yield(); @@ -158,4 +159,4 @@ void MqttHandleHuaweiClass::onMqttMessage(Topic t, } break; } -} \ No newline at end of file +} From 6620ab487a898c102158351941450ac0c8a6fbd6 Mon Sep 17 00:00:00 2001 From: SW-Nico Date: Thu, 11 Apr 2024 10:03:55 +0200 Subject: [PATCH 70/70] Fix: VE.Direct: take the load current into account when calculating efficiency, we need to take into account that the load might also sink a significant amount of current and power, which adds to the total output of the charge controller. --- .../VeDirectMpptController.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index de3a7a59b..f1bd00dec 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -84,15 +84,23 @@ bool VeDirectMpptController::processTextDataDerived(std::string const& name, std * This function is called at the end of the received frame. */ void VeDirectMpptController::frameValidEvent() { - _tmpFrame.batteryOutputPower_W = static_cast(_tmpFrame.batteryVoltage_V_mV * _tmpFrame.batteryCurrent_I_mA / 1000000); + // power into the battery, (+) means charging, (-) means discharging + _tmpFrame.batteryOutputPower_W = static_cast((_tmpFrame.batteryVoltage_V_mV / 1000.0f) * (_tmpFrame.batteryCurrent_I_mA / 1000.0f)); + // calculation of the panel current if ((_tmpFrame.panelVoltage_VPV_mV > 0) && (_tmpFrame.panelPower_PPV_W >= 1)) { - _tmpFrame.panelCurrent_mA = static_cast(_tmpFrame.panelPower_PPV_W * 1000000) / _tmpFrame.panelVoltage_VPV_mV; + _tmpFrame.panelCurrent_mA = static_cast(_tmpFrame.panelPower_PPV_W * 1000000.0f / _tmpFrame.panelVoltage_VPV_mV); + } else { + _tmpFrame.panelCurrent_mA = 0; } + // calculation of the MPPT efficiency + float totalPower_W = (_tmpFrame.loadCurrent_IL_mA / 1000.0f + _tmpFrame.batteryCurrent_I_mA / 1000.0f) * _tmpFrame.batteryVoltage_V_mV /1000.0f; if (_tmpFrame.panelPower_PPV_W > 0) { - _efficiency.addNumber(static_cast(_tmpFrame.batteryOutputPower_W * 100) / _tmpFrame.panelPower_PPV_W); + _efficiency.addNumber(totalPower_W * 100.0f / _tmpFrame.panelPower_PPV_W); _tmpFrame.mpptEfficiency_Percent = _efficiency.getAverage(); + } else { + _tmpFrame.mpptEfficiency_Percent = 0.0f; } if (!_canSend) { return; }