From 452679e90b9c63ad002a8f7c0ecb77be4e0f994e Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Wed, 9 Aug 2023 16:53:55 +0200 Subject: [PATCH 01/10] make vite proxy target easily configurable the current proxy target IP address is probably only working for a single developer at a time. this change introduces a vite.user.ts, which is ingored by GIT, and which can define a proxy_target that works for the respective developer. this does not change the default behavior, as the fallback value is still the old IP address. if the new vite.user.ts file does not exist, or if it does not export proxy_target, the fallback value is used. file vite.config.ts adds an example in a comment of how to bootstrap a vite.user.ts. --- webapp/.gitignore | 1 + webapp/vite.config.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/webapp/.gitignore b/webapp/.gitignore index 38adffa6..9077710c 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -13,6 +13,7 @@ dist dist-ssr coverage *.local +vite.user.ts /cypress/videos/ /cypress/screenshots/ diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts index e6f43197..08955939 100644 --- a/webapp/vite.config.ts +++ b/webapp/vite.config.ts @@ -9,6 +9,14 @@ import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' const path = require('path') +// example 'vite.user.ts': export const proxy_target = '192.168.16.107' +let proxy_target; +try { + proxy_target = require('./vite.user.ts').proxy_target; +} catch (error) { + proxy_target = '192.168.178.87'; +} + // https://vitejs.dev/config/ export default defineConfig({ plugins: [ @@ -52,15 +60,15 @@ export default defineConfig({ server: { proxy: { '^/api': { - target: 'http://192.168.20.110/' + target: 'http://' + proxy_target }, '^/livedata': { - target: 'ws://192.168.20.110/', + target: 'ws://' + proxy_target, ws: true, changeOrigin: true }, '^/console': { - target: 'ws://192.168.20.110/', + target: 'ws://' + proxy_target, ws: true, changeOrigin: true } From 94f6078c9e592000c49a9505e034833b202cba67 Mon Sep 17 00:00:00 2001 From: "Stefan Schultheis, OE1SCS" Date: Sun, 6 Aug 2023 18:06:58 +0200 Subject: [PATCH 02/10] Update de.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bessere Übersetzung --- webapp/src/locales/de.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 9c06289f..8ea7bd32 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -538,9 +538,9 @@ "DefaultProfile": "(Standardeinstellungen)", "ProfileHint": "Ihr Gerät reagiert möglicherweise nicht mehr, wenn Sie ein inkompatibles Profil wählen. In diesem Fall müssen Sie eine Löschung über das serielle Interface durchführen.", "Display": "Display", - "PowerSafe": "Power Safe aktivieren:", + "PowerSafe": "Stromsparen aktivieren:", "PowerSafeHint": "Schaltet das Display aus, wenn kein Wechselrichter Strom erzeugt", - "Screensaver": "Screensaver aktivieren:", + "Screensaver": "Bildschirmschoner aktivieren:", "ScreensaverHint": "Bewegt die Ausgabe bei jeder Aktualisierung um ein Einbrennen zu verhindern (v. a. für OLED-Displays nützlich)", "Contrast": "Kontrast ({contrast}):", "Rotation": "Rotation:", From 629ede3cbd48d5c8a0ec130464e4b834316d2c09 Mon Sep 17 00:00:00 2001 From: "Stefan Schultheis, OE1SCS" Date: Sun, 6 Aug 2023 18:08:08 +0200 Subject: [PATCH 03/10] Update en.json typo. power safe != power save --- webapp/src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 081818f9..1edb88d8 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -538,7 +538,7 @@ "DefaultProfile": "(Default settings)", "ProfileHint": "Your device may stop responding if you select an incompatible profile. In this case, you must perform a deletion via the serial interface.", "Display": "Display", - "PowerSafe": "Enable Power Safe:", + "PowerSafe": "Enable Power Save:", "PowerSafeHint": "Turn off the display if no inverter is producing.", "Screensaver": "Enable Screensaver:", "ScreensaverHint": "Move the display a little bit on each update to prevent burn-in. (Useful especially for OLED displays)", From 4bf094c3ef01c8f13a5a479e8c31285bd759e25e Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 9 Aug 2023 18:23:50 +0200 Subject: [PATCH 04/10] webapp: Update default proxy ip --- webapp/vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts index 08955939..e418141d 100644 --- a/webapp/vite.config.ts +++ b/webapp/vite.config.ts @@ -14,7 +14,7 @@ let proxy_target; try { proxy_target = require('./vite.user.ts').proxy_target; } catch (error) { - proxy_target = '192.168.178.87'; + proxy_target = '192.168.20.110'; } // https://vitejs.dev/config/ From 6eb3c632883c17c01ee9c378ea8a71f1479326b5 Mon Sep 17 00:00:00 2001 From: Bernhard Roth Date: Mon, 21 Aug 2023 11:14:24 +0200 Subject: [PATCH 05/10] Fix issue #1256 (HMS-2000-4T reactive power should be signed) --- lib/Hoymiles/src/inverters/HMS_4CH.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.cpp b/lib/Hoymiles/src/inverters/HMS_4CH.cpp index b0ca6207..e2947d8f 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_4CH.cpp @@ -36,7 +36,7 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_UAC, UNIT_V, 50, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 58, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_PAC, UNIT_W, 54, 2, 10, false, 1 }, - { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 56, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 56, 2, 10, true, 1 }, { TYPE_AC, CH0, FLD_F, UNIT_HZ, 52, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 60, 2, 1000, false, 3 }, From 0c5aef8599bd0343a8d8d94dd3c1d4f0fc64bde8 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 22 Aug 2023 10:43:31 +0200 Subject: [PATCH 06/10] webapp: Update dependencies --- webapp/package.json | 12 +++---- webapp/yarn.lock | 78 ++++++++++++++++++++++----------------------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index 6b0ca9b7..d4d9b246 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -22,18 +22,18 @@ "vue-router": "^4.2.4" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^0.12.2", + "@intlify/unplugin-vue-i18n": "^0.12.3", "@rushstack/eslint-patch": "^1.3.3", - "@tsconfig/node18": "^18.2.0", + "@tsconfig/node18": "^18.2.1", "@types/bootstrap": "^5.2.6", - "@types/node": "^20.4.8", + "@types/node": "^20.5.1", "@types/sortablejs": "^1.15.1", "@types/spark-md5": "^3.0.2", - "@vitejs/plugin-vue": "^4.2.3", + "@vitejs/plugin-vue": "^4.3.3", "@vue/eslint-config-typescript": "^11.0.3", "@vue/tsconfig": "^0.4.0", - "eslint": "^8.46.0", - "eslint-plugin-vue": "^9.16.1", + "eslint": "^8.47.0", + "eslint-plugin-vue": "^9.17.0", "npm-run-all": "^4.1.5", "sass": "^1.64.2", "terser": "^5.19.2", diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 97ea93fc..14593bac 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -151,10 +151,10 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== -"@eslint/eslintrc@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.1.tgz#18d635e24ad35f7276e8a49d135c7d3ca6a46f93" - integrity sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA== +"@eslint/eslintrc@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" + integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -166,10 +166,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^8.46.0": - version "8.46.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6" - integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA== +"@eslint/js@^8.47.0": + version "8.47.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d" + integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og== "@humanwhocodes/config-array@^0.11.10": version "0.11.10" @@ -249,10 +249,10 @@ resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.3.0-beta.24.tgz#23e08af9fc904fe3ef896786f9e659da6bb567b5" integrity sha512-AKxJ8s7eKIQWkNaf4wyyoLRwf4puCuQgjSChlDJm5JBEt6T8HGgnYTJLRXu6LD/JACn3Qwu6hM/XRX1c9yvjmQ== -"@intlify/unplugin-vue-i18n@^0.12.2": - version "0.12.2" - resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.12.2.tgz#64f7aad79cff1c4e8ff199cc059ea2bb9c36b2bb" - integrity sha512-IIgzLRSPUKZM1FBdUAZ9NwVPiLUr4ea5g/HLWe2lB7gNtPDz4FOfUNUllIT504hT+3pDoJmjaYJ6pyqT7F4Wuw== +"@intlify/unplugin-vue-i18n@^0.12.3": + version "0.12.3" + resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.12.3.tgz#fae7d92d3e7bfe9e710fb332b28d22e0f8d999f2" + integrity sha512-0riPtSfTM58JmGNMmJho/aHD2z3K24BESYAmkLvKlo61/LbaPvnjYU1DbSbJEm6bSjE2oEjUj+di3QaYxXei/w== dependencies: "@intlify/bundle-utils" "^7.0.2" "@intlify/shared" "9.3.0-beta.24" @@ -360,10 +360,10 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69" integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw== -"@tsconfig/node18@^18.2.0": - version "18.2.0" - resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.0.tgz#d6b5358b3fa85fe89b13b46cb1e996e4d79d6a07" - integrity sha512-yhxwIlFVSVcMym3O31HoMnRXpoenmpIxcj4Yoes2DUpe+xCJnA7ECQP1Vw889V0jTt/2nzvpLQ/UuMYCd3JPIg== +"@tsconfig/node18@^18.2.1": + version "18.2.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.1.tgz#ebf5e6b8d94e9de072e712bc197d6441a325ed61" + integrity sha512-RDDZFuofwkcKpl8Vpj5wFbY+H53xOtqK7ckEL1sXsbPwvKwDdjQf3LkHbtt9sxIHn9nWIEwkmCwBRZ6z5TKU2A== "@types/bootstrap@^5.2.6": version "5.2.6" @@ -382,10 +382,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== -"@types/node@^20.4.8": - version "20.4.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85" - integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg== +"@types/node@^20.5.1": + version "20.5.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" + integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== "@types/semver@^7.3.12": version "7.3.13" @@ -486,10 +486,10 @@ "@typescript-eslint/types" "5.59.1" eslint-visitor-keys "^3.3.0" -"@vitejs/plugin-vue@^4.2.3": - version "4.2.3" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz#ee0b6dfcc62fe65364e6395bf38fa2ba10bb44b6" - integrity sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw== +"@vitejs/plugin-vue@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.3.3.tgz#3b2337f64495f95cfea5b1497d2d3f4a0b3382b2" + integrity sha512-ssxyhIAZqB0TrpUg6R0cBpCuMk9jTIlO1GNSKKQD6S8VjnXi6JXKfUXjSsxey9IwQiaRGsO1WnW9Rkl1L6AJVw== "@volar/language-core@1.10.0", "@volar/language-core@~1.10.0": version "1.10.0" @@ -1104,10 +1104,10 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-plugin-vue@^9.16.1: - version "9.16.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.16.1.tgz#3508d9279d797b40889db76da2fd26524e9144e6" - integrity sha512-2FtnTqazA6aYONfDuOZTk0QzwhAwi7Z4+uJ7+GHeGxcKapjqWlDsRWDenvyG/utyOfAS5bVRmAG3cEWiYEz2bA== +eslint-plugin-vue@^9.17.0: + version "9.17.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.17.0.tgz#4501547373f246547083482838b4c8f4b28e5932" + integrity sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" natural-compare "^1.4.0" @@ -1163,20 +1163,20 @@ 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-visitor-keys@^3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" - integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== +eslint-visitor-keys@^3.4.3: + 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== -eslint@^8.46.0: - version "8.46.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552" - integrity sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg== +eslint@^8.47.0: + version "8.47.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.47.0.tgz#c95f9b935463fb4fad7005e626c7621052e90806" + integrity sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.1" - "@eslint/js" "^8.46.0" + "@eslint/eslintrc" "^2.1.2" + "@eslint/js" "^8.47.0" "@humanwhocodes/config-array" "^0.11.10" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -1187,7 +1187,7 @@ eslint@^8.46.0: doctrine "^3.0.0" escape-string-regexp "^4.0.0" eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.2" + eslint-visitor-keys "^3.4.3" espree "^9.6.1" esquery "^1.4.2" esutils "^2.0.2" From 4bf9083b238383d626e98c1d85f8218063e0f964 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 22 Aug 2023 11:45:14 +0200 Subject: [PATCH 07/10] Prometheus Endpoint: Publish only relevant amount of digits Implemented method to return the correctly formatted field value as string --- lib/Hoymiles/src/parser/StatisticsParser.cpp | 7 +++++++ lib/Hoymiles/src/parser/StatisticsParser.h | 1 + src/MqttHandleInverter.cpp | 6 +----- src/WebApi_prometheus.cpp | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index 2218ca4b..ac61be81 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -150,6 +150,13 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch return 0; } +String StatisticsParser::getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +{ + return String( + getChannelFieldValue(type, channel, fieldId), + static_cast(getChannelFieldDigits(type, channel, fieldId))); +} + bool StatisticsParser::hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index 436dba27..13d7d4f4 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -119,6 +119,7 @@ public: fieldSettings_t* getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); float getChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + String getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); bool hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); const char* getChannelFieldUnit(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index a42803cd..c9621e33 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -126,11 +126,7 @@ void MqttHandleInverterClass::publishField(std::shared_ptr inv return; } - String value = String( - inv->Statistics()->getChannelFieldValue(type, channel, fieldId), - static_cast(inv->Statistics()->getChannelFieldDigits(type, channel, fieldId))); - - MqttSettings.publish(topic, value); + MqttSettings.publish(topic, inv->Statistics()->getChannelFieldValueString(type, channel, fieldId)); } String MqttHandleInverterClass::getTopic(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index 18664946..968438cb 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -114,14 +114,14 @@ void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId)); stream->printf("# TYPE opendtu_%s %s\n", chanName, _metricTypes[_fieldMetricAssignment[fieldId]]); } - stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %f\n", + stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n", chanName, serial.c_str(), idx, inv->name(), inv->Statistics()->getChannelTypeName(type), channel, - inv->Statistics()->getChannelFieldValue(type, channel, fieldId)); + inv->Statistics()->getChannelFieldValueString(type, channel, fieldId).c_str()); } } From 6429d64062a7f4997d4d65e853fe0511098bf645 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 22 Aug 2023 13:00:52 +0200 Subject: [PATCH 08/10] Prometheus Endpoint: Simplify code by looping over fields instead of duplicated code --- include/WebApi_prometheus.h | 45 ++++++++++++++++++++----------------- src/WebApi_prometheus.cpp | 27 +++++++--------------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h index 4a77acdd..b03f8178 100644 --- a/include/WebApi_prometheus.h +++ b/include/WebApi_prometheus.h @@ -13,33 +13,38 @@ public: private: void onPrometheusMetricsGet(AsyncWebServerRequest* request); - void addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* channelName = NULL); + void addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* metricName, const char* channelName = NULL); void addPanelInfo(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel); AsyncWebServer* _server; - enum { - METRIC_TYPE_NONE = 0, - METRIC_TYPE_GAUGE, - METRIC_TYPE_COUNTER, + enum MetricType_t { + NONE = 0, + GAUGE, + COUNTER, }; const char* _metricTypes[3] = { 0, "gauge", "counter" }; - std::map _fieldMetricAssignment { - { FLD_UDC, METRIC_TYPE_GAUGE }, - { FLD_IDC, METRIC_TYPE_GAUGE }, - { FLD_PDC, METRIC_TYPE_GAUGE }, - { FLD_YD, METRIC_TYPE_COUNTER }, - { FLD_YT, METRIC_TYPE_COUNTER }, - { FLD_UAC, METRIC_TYPE_GAUGE }, - { FLD_IAC, METRIC_TYPE_GAUGE }, - { FLD_PAC, METRIC_TYPE_GAUGE }, - { FLD_F, METRIC_TYPE_GAUGE }, - { FLD_T, METRIC_TYPE_GAUGE }, - { FLD_PF, METRIC_TYPE_GAUGE }, - { FLD_EFF, METRIC_TYPE_GAUGE }, - { FLD_IRR, METRIC_TYPE_GAUGE }, - { FLD_Q, METRIC_TYPE_GAUGE } + struct publish_type_t { + FieldId_t field; + MetricType_t type; + }; + + const publish_type_t _publishFields[14] = { + { FLD_PAC, MetricType_t::GAUGE }, + { FLD_UAC, MetricType_t::GAUGE }, + { FLD_IAC, MetricType_t::GAUGE }, + { FLD_PDC, MetricType_t::GAUGE }, + { FLD_UDC, MetricType_t::GAUGE }, + { FLD_IDC, MetricType_t::GAUGE }, + { FLD_YD, MetricType_t::COUNTER }, + { FLD_YT, MetricType_t::COUNTER }, + { FLD_F, MetricType_t::GAUGE }, + { FLD_T, MetricType_t::GAUGE }, + { FLD_PF, MetricType_t::GAUGE }, + { FLD_Q, MetricType_t::GAUGE }, + { FLD_EFF, MetricType_t::GAUGE }, + { FLD_IRR, MetricType_t::GAUGE }, }; }; \ No newline at end of file diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index 968438cb..3decacf6 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -74,24 +74,13 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques for (auto& t : inv->Statistics()->getChannelTypes()) { for (auto& c : inv->Statistics()->getChannelsByType(t)) { addPanelInfo(stream, serial, i, inv, t, c); - addField(stream, serial, i, inv, t, c, FLD_PAC); - addField(stream, serial, i, inv, t, c, FLD_UAC); - addField(stream, serial, i, inv, t, c, FLD_IAC); - if (t == TYPE_AC) { - addField(stream, serial, i, inv, t, c, FLD_PDC, "PowerDC"); - } else { - addField(stream, serial, i, inv, t, c, FLD_PDC); + for (uint8_t f = 0; f < sizeof(_publishFields) / sizeof(_publishFields[0]); f++) { + if (t == TYPE_AC && _publishFields[f].field == FLD_PDC) { + addField(stream, serial, i, inv, t, c, _publishFields[f].field, _metricTypes[_publishFields[f].type], "PowerDC"); + } else { + addField(stream, serial, i, inv, t, c, _publishFields[f].field, _metricTypes[_publishFields[f].type]); + } } - addField(stream, serial, i, inv, t, c, FLD_UDC); - addField(stream, serial, i, inv, t, c, FLD_IDC); - addField(stream, serial, i, inv, t, c, FLD_YD); - addField(stream, serial, i, inv, t, c, FLD_YT); - addField(stream, serial, i, inv, t, c, FLD_F); - addField(stream, serial, i, inv, t, c, FLD_T); - addField(stream, serial, i, inv, t, c, FLD_PF); - addField(stream, serial, i, inv, t, c, FLD_Q); - addField(stream, serial, i, inv, t, c, FLD_EFF); - addField(stream, serial, i, inv, t, c, FLD_IRR); } } } @@ -106,13 +95,13 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques } } -void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* channelName) +void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* metricName, const char* channelName) { if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { const char* chanName = (channelName == NULL) ? inv->Statistics()->getChannelFieldName(type, channel, fieldId) : channelName; if (idx == 0 && type == TYPE_AC && channel == 0) { stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId)); - stream->printf("# TYPE opendtu_%s %s\n", chanName, _metricTypes[_fieldMetricAssignment[fieldId]]); + stream->printf("# TYPE opendtu_%s %s\n", chanName, metricName); } stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n", chanName, From e2594ac843e093b53adb4481bc9be00864bfb36b Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 22 Aug 2023 17:05:46 +0200 Subject: [PATCH 09/10] Fix: Calculate the json buffer for the inverter list based on INV_MAX_COUNT --- src/WebApi_inverter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index a8165925..c6e692b0 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -34,7 +34,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096U); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT); JsonObject root = response->getRoot(); JsonArray data = root.createNestedArray("inverter"); From 08ca2214102ea0c3fd9cd5fbc496d391f0e7ed3f Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Tue, 22 Aug 2023 18:55:25 +0200 Subject: [PATCH 10/10] webapp: add app.js.gz --- webapp_dist/js/app.js.gz | Bin 169188 -> 169199 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 cc4ebae522750d11d9b73cf4aea09ec5eb31ebff..2221e53abbe3d97ba24633df86d9dd0f2a16218a 100644 GIT binary patch delta 16721 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 delta 16710 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