diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index e3d7d0f8d..29af34c69 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -33,6 +33,9 @@ For more details go to [docs.emsesp.org](https://docs.emsesp.org/). - pumpmode enum for HT3 boilers, add commands for manual defrost, chimneysweeper [#2727](https://github.com/emsesp/EMS-ESP32/issues/2727) - pid settings [#2735](https://github.com/emsesp/EMS-ESP32/issues/2735) - refresh MQTT button added to MQTT Settings page +- added LWT (Last Will and Testament) to MQTT entities in Home Assistant +- added api/metrics endpoint for prometheus integration by @gr3enk + [#2774](https://github.com/emsesp/EMS-ESP32/pull/2774) ## Fixed diff --git a/interface/package.json b/interface/package.json index 9741c12a2..a17afeaf2 100644 --- a/interface/package.json +++ b/interface/package.json @@ -23,14 +23,14 @@ "standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\"" }, "dependencies": { - "@alova/adapter-xhr": "2.2.1", + "@alova/adapter-xhr": "2.3.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", "@preact/compat": "^18.3.1", "@table-library/react-table-library": "4.1.15", - "alova": "3.3.4", + "alova": "3.4.0", "async-validator": "^4.2.5", "etag": "^1.8.1", "formidable": "^3.5.4", @@ -59,7 +59,7 @@ "concurrently": "^9.2.1", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", - "prettier": "^3.6.2", + "prettier": "^3.7.3", "rollup-plugin-visualizer": "^6.0.5", "terser": "^5.44.1", "typescript-eslint": "^8.48.0", @@ -67,5 +67,5 @@ "vite-plugin-imagemin": "^0.6.1", "vite-tsconfig-paths": "^5.1.4" }, - "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b" + "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a" } diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index 69a2f7ef1..a2a71bdbd 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@alova/adapter-xhr': - specifier: 2.2.1 - version: 2.2.1(alova@3.3.4) + specifier: 2.3.0 + version: 2.3.0(alova@3.4.0) '@emotion/react': specifier: ^11.14.0 version: 11.14.0(@types/react@19.2.7)(react@19.2.0) @@ -30,8 +30,8 @@ importers: specifier: 4.1.15 version: 4.1.15(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) alova: - specifier: 3.3.4 - version: 3.3.4 + specifier: 3.4.0 + version: 3.4.0 async-validator: specifier: ^4.2.5 version: 4.2.5 @@ -86,7 +86,7 @@ importers: version: 2.10.2(@babel/core@7.28.5)(preact@10.27.2)(vite@7.2.4(@types/node@24.10.1)(terser@5.44.1)) '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.0 - version: 6.0.0(prettier@3.6.2) + version: 6.0.0(prettier@3.7.3) '@types/node': specifier: ^24.10.1 version: 24.10.1 @@ -109,8 +109,8 @@ importers: specifier: ^10.1.8 version: 10.1.8(eslint@9.39.1) prettier: - specifier: ^3.6.2 - version: 3.6.2 + specifier: ^3.7.3 + version: 3.7.3 rollup-plugin-visualizer: specifier: ^6.0.5 version: 6.0.5(rollup@4.53.3) @@ -132,8 +132,8 @@ importers: packages: - '@alova/adapter-xhr@2.2.1': - resolution: {integrity: sha512-0aPVdFmmMn4Z4KvG+DOyWhzQKaBGCe8yPQ4mJz1hQNPzbrIfqq+0flVF6ArFL4EtPbOJVnKropJNE691sjtq5A==} + '@alova/adapter-xhr@2.3.0': + resolution: {integrity: sha512-IegkchjfXFxXgn6JUZuVEHFQn+jojzrnNdzrGhX5ecEOIC8M/CQvLQzXjLeT6PbGiwnXwvZWL2ya4eqQz51+uQ==} peerDependencies: alova: ^3.0.20 @@ -475,8 +475,8 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.39.1': @@ -960,8 +960,8 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - alova@3.3.4: - resolution: {integrity: sha512-UKKqXdvf8aQ4C7m3brO77YWe5CDz8N59PdAUz7M8gowKUUXTutbk0Vk5DRBrCe0hMUyyNMUhdCZ38llGxCViyQ==} + alova@3.4.0: + resolution: {integrity: sha512-/vSvVbA45CHg34Y5erx+wVxy1B/n4UoGX7dKqSpLVz9cDSDSOhqCnRD/dV+AErjMmQeVpJrjmDT7SCkhQbnUeQ==} engines: {node: '>= 18.0.0'} ansi-regex@2.1.1: @@ -1027,8 +1027,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.31: - resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==} + baseline-browser-mapping@2.8.32: + resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} hasBin: true bin-build@3.0.0: @@ -1185,8 +1185,8 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie@1.1.0: - resolution: {integrity: sha512-vXiThu1/rlos7EGu8TuNZQEg2e9TvhH9dmS4T4ZVzB7Ao1agEZ6EG3sn5n+hZRYUgduISd1HpngFzAZiDGm5vQ==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} core-util-is@1.0.3: @@ -1337,8 +1337,8 @@ packages: duplexer3@0.1.5: resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} - electron-to-chromium@1.5.260: - resolution: {integrity: sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==} + electron-to-chromium@1.5.262: + resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2478,8 +2478,8 @@ packages: resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} engines: {node: '>=4'} - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + prettier@3.7.3: + resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==} engines: {node: '>=14'} hasBin: true @@ -3098,10 +3098,10 @@ packages: snapshots: - '@alova/adapter-xhr@2.2.1(alova@3.3.4)': + '@alova/adapter-xhr@2.3.0(alova@3.4.0)': dependencies: '@alova/shared': 1.3.1 - alova: 3.3.4 + alova: 3.4.0 '@alova/shared@1.3.1': {} @@ -3423,7 +3423,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 debug: 4.4.3 @@ -3716,7 +3716,7 @@ snapshots: react-virtualized-auto-sizer: 1.0.26(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-window: 1.8.11(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@trivago/prettier-plugin-sort-imports@6.0.0(prettier@3.6.2)': + '@trivago/prettier-plugin-sort-imports@6.0.0(prettier@3.7.3)': dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 @@ -3726,7 +3726,7 @@ snapshots: lodash-es: 4.17.21 minimatch: 9.0.5 parse-imports-exports: 0.2.4 - prettier: 3.6.2 + prettier: 3.7.3 transitivePeerDependencies: - supports-color @@ -3911,7 +3911,7 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - alova@3.3.4: + alova@3.4.0: dependencies: '@alova/shared': 1.3.1 rate-limiter-flexible: 5.0.5 @@ -3962,7 +3962,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.8.31: {} + baseline-browser-mapping@2.8.32: {} bin-build@3.0.0: dependencies: @@ -4019,9 +4019,9 @@ snapshots: browserslist@4.28.0: dependencies: - baseline-browser-mapping: 2.8.31 + baseline-browser-mapping: 2.8.32 caniuse-lite: 1.0.30001757 - electron-to-chromium: 1.5.260 + electron-to-chromium: 1.5.262 node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) @@ -4151,7 +4151,7 @@ snapshots: convert-source-map@2.0.0: {} - cookie@1.1.0: {} + cookie@1.1.1: {} core-util-is@1.0.3: {} @@ -4366,7 +4366,7 @@ snapshots: duplexer3@0.1.5: {} - electron-to-chromium@1.5.260: {} + electron-to-chromium@1.5.262: {} emoji-regex@8.0.0: {} @@ -4529,7 +4529,7 @@ snapshots: '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 + '@eslint/eslintrc': 3.3.3 '@eslint/js': 9.39.1 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 @@ -5487,7 +5487,7 @@ snapshots: prepend-http@2.0.0: {} - prettier@3.6.2: {} + prettier@3.7.3: {} process-nextick-args@2.0.1: {} @@ -5533,7 +5533,7 @@ snapshots: react-router@7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - cookie: 1.1.0 + cookie: 1.1.1 react: 19.2.0 set-cookie-parser: 2.7.2 optionalDependencies: diff --git a/interface/src/app/settings/MqttSettings.tsx b/interface/src/app/settings/MqttSettings.tsx index f03d906a0..2d02f94b3 100644 --- a/interface/src/app/settings/MqttSettings.tsx +++ b/interface/src/app/settings/MqttSettings.tsx @@ -266,6 +266,7 @@ const MqttSettings = () => { label={LL.CERT()} variant="outlined" value={data.rootCA} + sx={{ width: '50ch' }} onChange={updateFormValue} margin="normal" /> diff --git a/mock-api/package.json b/mock-api/package.json index ee779efb5..99ddbc963 100644 --- a/mock-api/package.json +++ b/mock-api/package.json @@ -13,7 +13,7 @@ "@trivago/prettier-plugin-sort-imports": "^6.0.0", "formidable": "^3.5.4", "itty-router": "^5.0.22", - "prettier": "^3.6.2" + "prettier": "^3.7.3" }, - "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b" + "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a" } diff --git a/mock-api/pnpm-lock.yaml b/mock-api/pnpm-lock.yaml index c408890a4..fe35f976c 100644 --- a/mock-api/pnpm-lock.yaml +++ b/mock-api/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 3.1.2 '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.0 - version: 6.0.0(prettier@3.6.2) + version: 6.0.0(prettier@3.7.3) formidable: specifier: ^3.5.4 version: 3.5.4 @@ -21,8 +21,8 @@ importers: specifier: ^5.0.22 version: 5.0.22 prettier: - specifier: ^3.6.2 - version: 3.6.2 + specifier: ^3.7.3 + version: 3.7.3 packages: @@ -167,8 +167,8 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + prettier@3.7.3: + resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==} engines: {node: '>=14'} hasBin: true @@ -246,7 +246,7 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 - '@trivago/prettier-plugin-sort-imports@6.0.0(prettier@3.6.2)': + '@trivago/prettier-plugin-sort-imports@6.0.0(prettier@3.7.3)': dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 @@ -256,7 +256,7 @@ snapshots: lodash-es: 4.17.21 minimatch: 9.0.5 parse-imports-exports: 0.2.4 - prettier: 3.6.2 + prettier: 3.7.3 transitivePeerDependencies: - supports-color @@ -311,6 +311,6 @@ snapshots: picocolors@1.1.1: {} - prettier@3.6.2: {} + prettier@3.7.3: {} wrappy@1.0.2: {} diff --git a/mock-api/restServer.ts b/mock-api/restServer.ts index 196ab665e..b2129a961 100644 --- a/mock-api/restServer.ts +++ b/mock-api/restServer.ts @@ -569,14 +569,15 @@ let mqtt_settings = { publish_time_heartbeat: 60, publish_time_water: 60, mqtt_qos: 0, - rootCA: '', mqtt_retain: false, ha_enabled: true, nested_format: 1, discovery_type: 0, discovery_prefix: 'homeassistant', send_response: true, - publish_single: false + publish_single: false, + enableTLS: true, + rootCA: '' }; const mqtt_status = { enabled: true, diff --git a/platformio.ini b/platformio.ini index 8591c64d9..3ad7826df 100644 --- a/platformio.ini +++ b/platformio.ini @@ -106,7 +106,7 @@ board_build.filesystem = littlefs lib_deps = bblanchon/ArduinoJson @ 7.4.2 ESP32Async/AsyncTCP @ 3.4.9 - ESP32Async/ESPAsyncWebServer @ 3.9.0 + ESP32Async/ESPAsyncWebServer @ 3.9.2 https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8 @@ -214,23 +214,20 @@ lib_ldf_mode = off lib_deps = ; unit tests -; The code is in ./test/test_api.* +; The test code is in ./test/test_api.cpp and the test_api.h file is created by the native-test-create environment. ; to run use `platformio run -e native-test -t exec`. All tests should PASS. -; to update the test results, compile with -DEMSESP_UNITY_CREATE by uncommenting the line below -; then re-run and capture the output between "START - CUT HERE" and "END - CUT HERE" into the test_api.h file ; tip: use https://jsondiff.com/ to compare the expected and actual responses. [env:native-test] platform = native test_build_src = true build_flags = - ; -DEMSESP_UNITY_CREATE - -DARDUINOJSON_ENABLE_ARDUINO_STRING=1 - -DEMSESP_STANDALONE -DEMSESP_TEST - -DEMSESP_UNITY - -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev.0\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\" - -std=gnu++17 -Og -ggdb build_type = debug build_src_flags = + -DEMSESP_STANDALONE -DEMSESP_TEST + -DEMSESP_UNITY + -DARDUINOJSON_ENABLE_ARDUINO_STRING=1 + -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev.0\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\" + -std=gnu++17 -Og -ggdb -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces -Wno-vla-cxx-extension -Wno-tautological-constant-out-of-range-compare @@ -260,6 +257,12 @@ lib_deps = Unity test_testing_command = ${platformio.build_dir}/${this.__env__}/program +; builds the test cases and creates the test_api.h file +; run with `pio run -e native-test-create -t exec` and capture the output between "START - CUT HERE" and "END - CUT HERE" and paste it into the test_api.h file +[env:native-test-create] +extends = env:native-test +build_flags = + -DEMSESP_UNITY_CREATE ; ; Building and testing locally on OS, which we call "standalone" without an ESP32. ; See https://docs.platformio.org/en/latest/platforms/native.html diff --git a/src/ESP32React/MqttSettingsService.cpp b/src/ESP32React/MqttSettingsService.cpp index 7550262cf..d616f3491 100644 --- a/src/ESP32React/MqttSettingsService.cpp +++ b/src/ESP32React/MqttSettingsService.cpp @@ -173,7 +173,7 @@ bool MqttSettingsService::configureMqtt() { // only connect if WiFi is connected and MQTT is enabled if (_state.enabled && emsesp::EMSESP::system_.network_connected() && !_state.host.isEmpty()) { - // create last will topic with the base prefixed. It has to be static because the client destroys the reference + // create the Last Will Testament topic (LWT) with the base prefixed. It has to be static because the client destroys the reference static char will_topic[FACTORY_MQTT_MAX_TOPIC_LENGTH]; if (_state.base.isEmpty()) { snprintf(will_topic, sizeof(will_topic), "status"); diff --git a/src/ESP32React/MqttSettingsService.h b/src/ESP32React/MqttSettingsService.h index 2c4871a50..b844e2ea1 100644 --- a/src/ESP32React/MqttSettingsService.h +++ b/src/ESP32React/MqttSettingsService.h @@ -39,7 +39,7 @@ #endif #ifndef FACTORY_MQTT_KEEP_ALIVE -#define FACTORY_MQTT_KEEP_ALIVE 16 +#define FACTORY_MQTT_KEEP_ALIVE 60 #endif #ifndef FACTORY_MQTT_CLEAN_SESSION diff --git a/src/core/console.cpp b/src/core/console.cpp index ee7ff0532..f7141baf0 100644 --- a/src/core/console.cpp +++ b/src/core/console.cpp @@ -87,8 +87,8 @@ static void setup_commands(std::shared_ptr const & commands) { Command::show_all(shell); } else if (command == F_(system)) { EMSESP::system_.show_system(shell); - } else if (command == F_(users) && (shell.has_flags(CommandFlags::ADMIN))) { - EMSESP::system_.show_users(shell); // admin only + } else if (command == F_(users)) { + EMSESP::system_.show_users(shell); } else if (command == F_(devices)) { EMSESP::show_devices(shell); } else if (command == F_(log)) { diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index f29e7aa1f..5c4b1b7bb 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -1706,7 +1706,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { // generate Prometheus metrics format from device values std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { - std::string result; + std::string result; std::unordered_map seen_metrics; for (auto & dv : devicevalues_) { @@ -1715,43 +1715,42 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { } // only process number and boolean types for now - if (dv.type != DeviceValueType::BOOL && dv.type != DeviceValueType::UINT8 && dv.type != DeviceValueType::INT8 - && dv.type != DeviceValueType::UINT16 && dv.type != DeviceValueType::INT16 && dv.type != DeviceValueType::UINT24 - && dv.type != DeviceValueType::UINT32 && dv.type != DeviceValueType::TIME) { + if (dv.type != DeviceValueType::BOOL && dv.type != DeviceValueType::UINT8 && dv.type != DeviceValueType::INT8 && dv.type != DeviceValueType::UINT16 + && dv.type != DeviceValueType::INT16 && dv.type != DeviceValueType::UINT24 && dv.type != DeviceValueType::UINT32 && dv.type != DeviceValueType::TIME) { continue; } - bool has_value = false; + bool has_value = false; double metric_value = 0.0; switch (dv.type) { case DeviceValueType::BOOL: if (Helpers::hasValue(*(uint8_t *)(dv.value_p), EMS_VALUE_BOOL)) { - has_value = true; + has_value = true; metric_value = (bool)*(uint8_t *)(dv.value_p) ? 1.0 : 0.0; } break; case DeviceValueType::UINT8: if (Helpers::hasValue(*(uint8_t *)(dv.value_p))) { - has_value = true; + has_value = true; metric_value = *(uint8_t *)(dv.value_p); } break; case DeviceValueType::INT8: if (Helpers::hasValue(*(int8_t *)(dv.value_p))) { - has_value = true; + has_value = true; metric_value = *(int8_t *)(dv.value_p); } break; case DeviceValueType::UINT16: if (Helpers::hasValue(*(uint16_t *)(dv.value_p))) { - has_value = true; + has_value = true; metric_value = *(uint16_t *)(dv.value_p); } break; case DeviceValueType::INT16: if (Helpers::hasValue(*(int16_t *)(dv.value_p))) { - has_value = true; + has_value = true; metric_value = *(int16_t *)(dv.value_p); } break; @@ -1759,7 +1758,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { case DeviceValueType::UINT32: case DeviceValueType::TIME: if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) { - has_value = true; + has_value = true; metric_value = *(uint32_t *)(dv.value_p); } break; @@ -1793,7 +1792,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { } } - auto fullname = dv.get_fullname(); + auto fullname = dv.get_fullname(); std::string help_text; if (!fullname.empty()) { help_text = fullname; @@ -1850,9 +1849,9 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { } result += " "; - char val_str[30]; + char val_str[30]; double final_value = metric_value; - + if (dv.numeric_operator != 0) { if (dv.numeric_operator > 0) { final_value = metric_value / dv.numeric_operator; @@ -1860,7 +1859,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { final_value = metric_value * (-dv.numeric_operator); } } - + double rounded = (final_value >= 0) ? (double)((int64_t)(final_value + 0.5)) : (double)((int64_t)(final_value - 0.5)); if (dv.type == DeviceValueType::BOOL || (final_value == rounded)) { snprintf(val_str, sizeof(val_str), "%.0f", final_value); diff --git a/src/core/mqtt.cpp b/src/core/mqtt.cpp index fa5d8d379..32aa5f303 100644 --- a/src/core/mqtt.cpp +++ b/src/core/mqtt.cpp @@ -509,14 +509,14 @@ void Mqtt::on_connect() { queue_subscribe_message(discovery_prefix_ + "/+/" + Mqtt::basename() + "/#"); } - // send initial MQTT messages for some of our services - EMSESP::system_.send_heartbeat(); // send heartbeat - // re-subscribe to all custom registered MQTT topics resubscribe(); // publish to the last will topic (see Mqtt::start() function) to say we're alive queue_publish_retain("status", "online"); // retain: https://github.com/emsesp/EMS-ESP32/discussions/2086 + + // send initial MQTT messages for some of our services + EMSESP::system_.send_heartbeat(); // send heartbeat } // Home Assistant Discovery - the main master Device called EMS-ESP @@ -532,9 +532,10 @@ void Mqtt::ha_status() { strcpy(uniq, "system_status"); } + doc["~"] = Mqtt::base(); doc["uniq_id"] = uniq; doc["def_ent_id"] = (std::string) "binary_sensor." + uniq; - doc["stat_t"] = Mqtt::base() + "/status"; + doc["stat_t"] = "~/status"; doc["name"] = "System status"; doc["pl_on"] = "online"; doc["pl_off"] = "offline"; @@ -981,8 +982,9 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev return queue_remove_topic(topic); } - // build the full payload + // build the full topic's payload JsonDocument doc; + doc["~"] = Mqtt::base(); doc["uniq_id"] = uniq_id; // set the entity_id. This is breaking change in HA 2025.10.0 - see https://github.com/home-assistant/core/pull/151775 @@ -1000,9 +1002,9 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev char command_topic[MQTT_TOPIC_MAX_SIZE]; // add command topic if (tag >= DeviceValueTAG::TAG_HC1) { - snprintf(command_topic, sizeof(command_topic), "%s/%s/%s/%s", Mqtt::base().c_str(), device_name, EMSdevice::tag_to_mqtt(tag), entity); + snprintf(command_topic, sizeof(command_topic), "~/%s/%s/%s", device_name, EMSdevice::tag_to_mqtt(tag), entity); } else { - snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), device_name, entity); + snprintf(command_topic, sizeof(command_topic), "~/%s/%s", device_name, entity); } doc["cmd_t"] = command_topic; @@ -1063,9 +1065,9 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev // This is where we determine which MQTT topic to pull the data from // There is one exception for DeviceType::SYSTEM, which uses the heartbeat topic, and when fetching the version we want to take this from the info topic instead if ((device_type == EMSdevice::DeviceType::SYSTEM) && (strncmp(entity, "version", 7) == 0)) { - snprintf(stat_t, sizeof(stat_t), "%s/%s", Mqtt::base().c_str(), F_(info)); + snprintf(stat_t, sizeof(stat_t), "~/%s", F_(info)); } else { - snprintf(stat_t, sizeof(stat_t), "%s/%s", Mqtt::base().c_str(), tag_to_topic(device_type, tag).c_str()); + snprintf(stat_t, sizeof(stat_t), "~/%s", tag_to_topic(device_type, tag).c_str()); } doc["stat_t"] = stat_t; @@ -1484,6 +1486,11 @@ void Mqtt::add_ha_avail_section(JsonObject doc, const char * state_t, const bool avty.add(avty_json); // returns 0 if no mem } + // add LWT (Last Will and Testament) + avty_json.clear(); + avty_json["t"] = "~/status"; // as a topic + avty.add(avty_json); + doc["avty_mode"] = "all"; } diff --git a/src/core/system.cpp b/src/core/system.cpp index 7689ac501..0d57db613 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -731,11 +731,6 @@ void System::heartbeat_json(JsonObject output) { // send periodic MQTT message with system information void System::send_heartbeat() { - // don't send heartbeat if WiFi or MQTT is not connected - if (!Mqtt::connected()) { - return; - } - refreshHeapMem(); // refresh free heap and max alloc heap JsonDocument doc; @@ -997,6 +992,11 @@ int8_t System::wifi_quality(int8_t dBm) { // print users to console void System::show_users(uuid::console::Shell & shell) { + if (!shell.has_flags(CommandFlags::ADMIN)) { + shell.printfln("Unauthorized. You need to be an admin to view users."); + return; + } + shell.printfln("Users:"); #ifndef EMSESP_STANDALONE diff --git a/src/emsesp_version.h b/src/emsesp_version.h index 008f29b1b..453b4062d 100644 --- a/src/emsesp_version.h +++ b/src/emsesp_version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.7.3-dev.32" +#define EMSESP_APP_VERSION "3.7.3-dev.33" diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index 0a6037ccf..12647383e 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -144,6 +144,8 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) { return; } + api_count_++; + // send the json that came back from the command call // sequence matches CommandRet in command.h (FAIL, OK, NOT_FOUND, ERROR, NOT_ALLOWED, INVALID, NO_VALUE) // 400 (bad request) @@ -153,16 +155,23 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) { // 400 (invalid) int ret_codes[7] = {400, 200, 404, 400, 401, 400, 404}; + response->setCode(ret_codes[return_code]); + response->setLength(); + response->setContentType("application/json; charset=utf-8"); + request->send(response); + // serialize JSON to string to ensure correct content-length and avoid HTTP parsing errors (issue #2752) - std::string output_str; - serializeJson(output, output_str); - request->send(ret_codes[return_code], "application/json; charset=utf-8", output_str.c_str()); + // std::string output_str; + // serializeJson(output, output_str); + // request->send(ret_codes[return_code], "application/json; charset=utf-8", output_str.c_str()); #if defined(EMSESP_UNITY) // store the result so we can test with Unity later storeResponse(output); #endif #if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY) + std::string output_str; + serializeJson(output, output_str); Serial.printf("%sweb output: %s[%s] %s(%d)%s %s%s", COLOR_WHITE, COLOR_BRIGHT_CYAN, @@ -175,9 +184,6 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) { Serial.println(); EMSESP::logger().debug("web output: %s %s", request->url().c_str(), output_str.c_str()); #endif - - api_count_++; - delete response; } #if defined(EMSESP_UNITY) diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index 32a5a1af3..ea35e128d 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -422,7 +422,7 @@ void WebSettings::set_board_profile(WebSettings & settings) { // load the board profile into the data vector // 0=led, 1=dallas, 2=rx, 3=tx, 4=button, 5=phy_type, 6=eth_power, 7=eth_phy_addr, 8=eth_clock_mode, 9=led_type - std::vector data(99, 0); // initialize with 99 for all values, just as a safe guard to catch bad gpios + std::vector data(10, 99); // initialize with 99 for all values, just as a safe guard to catch bad gpios if (settings.board_profile != "default") { if (!System::load_board_profile(data, settings.board_profile.c_str())) { #if defined(EMSESP_DEBUG) diff --git a/test/test_api/api_test.js b/test/test_api/api_test.js index 9fb4b8edc..9d8a74f3c 100644 --- a/test/test_api/api_test.js +++ b/test/test_api/api_test.js @@ -29,7 +29,7 @@ async function testAPI(ip = "ems-esp.local", apiPath = "system") { } // Run the test -testAPI("192.168.1.223", "system") +testAPI("192.168.1.65", "system") .then(() => { console.log('Test completed successfully'); process.exit(0); diff --git a/test/test_api/test_api.cpp b/test/test_api/test_api.cpp index 033270c02..66b90b754 100644 --- a/test/test_api/test_api.cpp +++ b/test/test_api/test_api.cpp @@ -290,30 +290,29 @@ void manual_test7() { void manual_test8() { const char * response = call_url("/api/boiler/metrics"); - + TEST_ASSERT_NOT_NULL(response); TEST_ASSERT_TRUE(strlen(response) > 0); - + TEST_ASSERT_TRUE(strstr(response, "# HELP") != nullptr); TEST_ASSERT_TRUE(strstr(response, "# TYPE") != nullptr); TEST_ASSERT_TRUE(strstr(response, "emsesp_") != nullptr); TEST_ASSERT_TRUE(strstr(response, " gauge") != nullptr); - - TEST_ASSERT_TRUE(strstr(response, "emsesp_tapwateractive") != nullptr || - strstr(response, "emsesp_selflowtemp") != nullptr || - strstr(response, "emsesp_curflowtemp") != nullptr); + + TEST_ASSERT_TRUE(strstr(response, "emsesp_tapwateractive") != nullptr || strstr(response, "emsesp_selflowtemp") != nullptr + || strstr(response, "emsesp_curflowtemp") != nullptr); } void manual_test9() { const char * response = call_url("/api/thermostat/metrics"); - + TEST_ASSERT_NOT_NULL(response); TEST_ASSERT_TRUE(strlen(response) > 0); - + TEST_ASSERT_TRUE(strstr(response, "# HELP") != nullptr); TEST_ASSERT_TRUE(strstr(response, "# TYPE") != nullptr); TEST_ASSERT_TRUE(strstr(response, "emsesp_") != nullptr); - + if (strstr(response, "circuit=") != nullptr) { TEST_ASSERT_TRUE(strstr(response, "{circuit=") != nullptr); }