From 693054a92a53f91b241bec76717f24def8e650fc Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 14:47:27 +0100 Subject: [PATCH 01/27] package update --- interface/package.json | 4 ++-- interface/pnpm-lock.yaml | 48 ++++++++++++++++++++-------------------- mock-api/package.json | 4 ++-- mock-api/pnpm-lock.yaml | 16 +++++++------- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/interface/package.json b/interface/package.json index 9741c12a2..2b7980b99 100644 --- a/interface/package.json +++ b/interface/package.json @@ -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.2", "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..8fd9207d4 100644 --- a/interface/pnpm-lock.yaml +++ b/interface/pnpm-lock.yaml @@ -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.2) '@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.2 + version: 3.7.2 rollup-plugin-visualizer: specifier: ^6.0.5 version: 6.0.5(rollup@4.53.3) @@ -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': @@ -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.2: + resolution: {integrity: sha512-n3HV2J6QhItCXndGa3oMWvWFAgN1ibnS7R9mt6iokScBOC0Ul9/iZORmU2IWUMcyAQaMPjTlY3uT34TqocUxMA==} engines: {node: '>=14'} hasBin: true @@ -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.2)': 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.2 transitivePeerDependencies: - supports-color @@ -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.2: {} 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/mock-api/package.json b/mock-api/package.json index ee779efb5..8eff6718c 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.2" }, - "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..fa8648aa6 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.2) 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.2 + version: 3.7.2 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.2: + resolution: {integrity: sha512-n3HV2J6QhItCXndGa3oMWvWFAgN1ibnS7R9mt6iokScBOC0Ul9/iZORmU2IWUMcyAQaMPjTlY3uT34TqocUxMA==} 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.2)': 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.2 transitivePeerDependencies: - supports-color @@ -311,6 +311,6 @@ snapshots: picocolors@1.1.1: {} - prettier@3.6.2: {} + prettier@3.7.2: {} wrappy@1.0.2: {} From 48b4bf02a36d3d271061cd442bcddce3ec74565b Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 14:47:35 +0100 Subject: [PATCH 02/27] wider box for TLS --- interface/src/app/settings/MqttSettings.tsx | 1 + 1 file changed, 1 insertion(+) 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" /> From 6569b8c038b4c4fb8a5661d09ba7daec93c94e47 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 14:47:42 +0100 Subject: [PATCH 03/27] enable TLS for test data --- mock-api/restServer.ts | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/mock-api/restServer.ts b/mock-api/restServer.ts index 196ab665e..b2b776699 100644 --- a/mock-api/restServer.ts +++ b/mock-api/restServer.ts @@ -276,10 +276,10 @@ function updateMask(entity: any, de: any, dd: any) { const old_custom_name = dd.nodes[dd_objIndex].cn; console.log( 'comparing names, old (' + - old_custom_name + - ') with new (' + - new_custom_name + - ')' + old_custom_name + + ') with new (' + + new_custom_name + + ')' ); if (old_custom_name !== new_custom_name) { changed = true; @@ -375,15 +375,15 @@ function check_upgrade(version: string) { console.log( 'Upgrade this version (' + - THIS_VERSION + - ') to dev (' + - dev_version + - ') is ' + - (DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') + - ' and to stable (' + - stable_version + - ') is ' + - (STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') + THIS_VERSION + + ') to dev (' + + dev_version + + ') is ' + + (DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') + + ' and to stable (' + + stable_version + + ') is ' + + (STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') ); data = { emsesp_version: THIS_VERSION, @@ -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, From a2baa5053030da6baeec58d5378305c833ef0565 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 14:48:13 +0100 Subject: [PATCH 04/27] show users reports error if not admin --- src/core/console.cpp | 4 ++-- src/core/system.cpp | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) 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/system.cpp b/src/core/system.cpp index 7689ac501..9e7994c90 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -997,6 +997,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 From d6c460e7fd23bc35613020ebc00ed86cf889b15e Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 14:48:24 +0100 Subject: [PATCH 05/27] ESP32Async/ESPAsyncWebServer @ 3.9.2 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 8591c64d9..4c5250014 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 From 3eb581142a0eb51de3d7b5cf54537b88f0a020e9 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 14:48:42 +0100 Subject: [PATCH 06/27] default keep alive 60 seconds --- src/ESP32React/MqttSettingsService.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5cb9f3b014d08c8662c391050d991985d7f6ead7 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 14:48:57 +0100 Subject: [PATCH 07/27] init board profile correctly --- src/web/WebSettingsService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 0cf932f57ed780124e49a6477c25ce20f08f6d86 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 14:49:08 +0100 Subject: [PATCH 08/27] comment change --- src/ESP32React/MqttSettingsService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"); From 9c4beba3b1dec4a5274158585415ce2a021c960f Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 15:12:07 +0100 Subject: [PATCH 09/27] add LWT to HA discovery config topic --- src/core/mqtt.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/core/mqtt.cpp b/src/core/mqtt.cpp index fa5d8d379..5ee46cf6c 100644 --- a/src/core/mqtt.cpp +++ b/src/core/mqtt.cpp @@ -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"; } From 80e5d307815e9ac7f3db91b083925f1b1bc1eab6 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 15:14:26 +0100 Subject: [PATCH 10/27] dev-33 --- src/emsesp_version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 0a36f1df7a0391f0dd68b5facfbfca0222bd028b Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 15:14:33 +0100 Subject: [PATCH 11/27] updated --- CHANGELOG_LATEST.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index e3d7d0f8d..f994a6eed 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -33,6 +33,7 @@ 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 ## Fixed From da7a06646abeedb0df8aea960424e8a77b7546fd Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 15:35:57 +0100 Subject: [PATCH 12/27] rollback AsyncWS fix #2752 --- src/web/WebAPIService.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index 0a6037ccf..613723673 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,24 @@ 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()); + // delete response; #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 +185,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) From 8ca9f7ee3082f5be92cc0dc98b0a37ea9c57fe62 Mon Sep 17 00:00:00 2001 From: proddy Date: Sat, 29 Nov 2025 15:36:24 +0100 Subject: [PATCH 13/27] change ip --- test/test_api/api_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 76c827257ec2f7a46c604b91a2bb62cd2c5d88ac Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 10:22:08 +0100 Subject: [PATCH 14/27] mqtt heartbeat, no need to check if connected --- src/core/mqtt.cpp | 6 +++--- src/core/system.cpp | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/core/mqtt.cpp b/src/core/mqtt.cpp index 5ee46cf6c..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 diff --git a/src/core/system.cpp b/src/core/system.cpp index 9e7994c90..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; From 3fcd656bb65bed70b9b965270eec4e540f0d703e Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 10:22:20 +0100 Subject: [PATCH 15/27] package update with new alova --- interface/package.json | 6 +++--- interface/pnpm-lock.yaml | 38 +++++++++++++++++++------------------- mock-api/package.json | 2 +- mock-api/pnpm-lock.yaml | 16 ++++++++-------- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/interface/package.json b/interface/package.json index 2b7980b99..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.7.2", + "prettier": "^3.7.3", "rollup-plugin-visualizer": "^6.0.5", "terser": "^5.44.1", "typescript-eslint": "^8.48.0", diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml index 8fd9207d4..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.7.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.7.2 - version: 3.7.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 @@ -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: @@ -2478,8 +2478,8 @@ packages: resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} engines: {node: '>=4'} - prettier@3.7.2: - resolution: {integrity: sha512-n3HV2J6QhItCXndGa3oMWvWFAgN1ibnS7R9mt6iokScBOC0Ul9/iZORmU2IWUMcyAQaMPjTlY3uT34TqocUxMA==} + 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': {} @@ -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.7.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.7.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 @@ -5487,7 +5487,7 @@ snapshots: prepend-http@2.0.0: {} - prettier@3.7.2: {} + prettier@3.7.3: {} process-nextick-args@2.0.1: {} diff --git a/mock-api/package.json b/mock-api/package.json index 8eff6718c..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.7.2" + "prettier": "^3.7.3" }, "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a" } diff --git a/mock-api/pnpm-lock.yaml b/mock-api/pnpm-lock.yaml index fa8648aa6..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.7.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.7.2 - version: 3.7.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.7.2: - resolution: {integrity: sha512-n3HV2J6QhItCXndGa3oMWvWFAgN1ibnS7R9mt6iokScBOC0Ul9/iZORmU2IWUMcyAQaMPjTlY3uT34TqocUxMA==} + 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.7.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.7.2 + prettier: 3.7.3 transitivePeerDependencies: - supports-color @@ -311,6 +311,6 @@ snapshots: picocolors@1.1.1: {} - prettier@3.7.2: {} + prettier@3.7.3: {} wrappy@1.0.2: {} From 8af7cde2d613abc48546f302331e82510ac4e051 Mon Sep 17 00:00:00 2001 From: Jakob Date: Sun, 30 Nov 2025 10:35:19 +0100 Subject: [PATCH 16/27] feat: add api/metrics endpoint --- src/core/emsdevice.cpp | 178 +++++++++++++++++++++++++++++++++++++++ src/core/emsdevice.h | 1 + src/core/locale_common.h | 1 + 3 files changed, 180 insertions(+) diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index b6d4fd981..f29e7aa1f 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -1533,6 +1533,14 @@ bool EMSdevice::get_value_info(JsonObject output, const char * cmd, const int8_t } return true; } + if (!strcmp(cmd, F_(metrics))) { + std::string metrics = get_metrics_prometheus(tag); + if (!metrics.empty()) { + output["api_data"] = metrics; + return true; + } + return false; + } // search device value with this tag // make a copy of cmd and split attribute (leave cmd untouched for other devices) @@ -1696,6 +1704,176 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { json["visible"] = !dv.has_state(DeviceValueState::DV_WEB_EXCLUDE); } +// generate Prometheus metrics format from device values +std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { + std::string result; + std::unordered_map seen_metrics; + + for (auto & dv : devicevalues_) { + if (tag >= 0 && tag != dv.tag) { + continue; + } + + // 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) { + continue; + } + + 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; + 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; + metric_value = *(uint8_t *)(dv.value_p); + } + break; + case DeviceValueType::INT8: + if (Helpers::hasValue(*(int8_t *)(dv.value_p))) { + 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; + metric_value = *(uint16_t *)(dv.value_p); + } + break; + case DeviceValueType::INT16: + if (Helpers::hasValue(*(int16_t *)(dv.value_p))) { + has_value = true; + metric_value = *(int16_t *)(dv.value_p); + } + break; + case DeviceValueType::UINT24: + case DeviceValueType::UINT32: + case DeviceValueType::TIME: + if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) { + has_value = true; + metric_value = *(uint32_t *)(dv.value_p); + } + break; + default: + break; + } + + if (!has_value) { + continue; + } + + std::string metric_name = dv.short_name; + size_t last_dot = metric_name.find_last_of('.'); + if (last_dot != std::string::npos) { + metric_name = metric_name.substr(last_dot + 1); + } + + for (char & c : metric_name) { + if (!isalnum(c) && c != '_') { + c = '_'; + } + } + + std::string full_metric_name = "emsesp_" + metric_name; + + std::string circuit_label; + if (dv.tag != DeviceValueTAG::TAG_NONE) { + const char * circuit = tag_to_mqtt(dv.tag); + if (circuit && strlen(circuit) > 0) { + circuit_label = circuit; + } + } + + auto fullname = dv.get_fullname(); + std::string help_text; + if (!fullname.empty()) { + help_text = fullname; + } else { + help_text = metric_name; + } + + std::string uom_str; + if (dv.type == DeviceValueType::BOOL) { + uom_str = "boolean"; + } else if (dv.uom != DeviceValueUOM::NONE) { + uom_str = uom_to_string(dv.uom); + } + + std::string help_line = help_text; + if (!uom_str.empty()) { + help_line += ", " + uom_str; + } + + bool readable = dv.type != DeviceValueType::CMD && !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE); + bool writeable = dv.has_cmd && !dv.has_state(DeviceValueState::DV_READONLY); + bool visible = !dv.has_state(DeviceValueState::DV_WEB_EXCLUDE); + + if (readable) { + help_line += ", readable"; + } + if (writeable) { + help_line += ", writeable"; + } + if (visible) { + help_line += ", visible"; + } + + std::string escaped_help; + for (char c : help_line) { + if (c == '\\') { + escaped_help += "\\\\"; + } else if (c == '\n') { + escaped_help += "\\n"; + } else { + escaped_help += c; + } + } + + if (seen_metrics.find(full_metric_name) == seen_metrics.end()) { + result += "# HELP " + full_metric_name + " " + escaped_help + "\n"; + result += "# TYPE " + full_metric_name + " gauge\n"; + seen_metrics[full_metric_name] = true; + } + + result += full_metric_name; + if (!circuit_label.empty()) { + result += "{circuit=\"" + circuit_label + "\"}"; + } + result += " "; + + 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; + } else { + 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); + } else { + snprintf(val_str, sizeof(val_str), "%.2f", final_value); + } + result += val_str; + result += "\n"; + } + + return result; +} + // mqtt publish all single values from one device (used for time schedule) void EMSdevice::publish_all_values() { for (const auto & dv : devicevalues_) { diff --git a/src/core/emsdevice.h b/src/core/emsdevice.h index 44a963ef4..b971cb9d9 100644 --- a/src/core/emsdevice.h +++ b/src/core/emsdevice.h @@ -251,6 +251,7 @@ class EMSdevice { std::string get_value_uom(const std::string & shortname) const; bool get_value_info(JsonObject root, const char * cmd, const int8_t id); void get_value_json(JsonObject output, DeviceValue & dv); + std::string get_metrics_prometheus(const int8_t tag = -1); void get_dv_info(JsonObject json); enum OUTPUT_TARGET : uint8_t { API_VERBOSE, API_SHORTNAMES, MQTT, CONSOLE }; diff --git a/src/core/locale_common.h b/src/core/locale_common.h index cd85c1a96..3705a9d6e 100644 --- a/src/core/locale_common.h +++ b/src/core/locale_common.h @@ -86,6 +86,7 @@ MAKE_WORD(info) MAKE_WORD(settings) MAKE_WORD(value) MAKE_WORD(entities) +MAKE_WORD(metrics) MAKE_WORD(coldshot) // device types - lowercase, used in MQTT From b44a0d68136718269df81a4beaef8b471a3a6431 Mon Sep 17 00:00:00 2001 From: Jakob Date: Sun, 30 Nov 2025 10:37:34 +0100 Subject: [PATCH 17/27] test: add tests for api/metrics endpoint --- src/core/command.cpp | 2 ++ src/core/locale_translations.h | 1 + test/test_api/test_api.cpp | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/core/command.cpp b/src/core/command.cpp index e71be6b88..a13468fdf 100644 --- a/src/core/command.cpp +++ b/src/core/command.cpp @@ -763,6 +763,8 @@ void Command::show_all(uuid::console::Shell & shell) { shell.println(COLOR_RESET); shell.printf(" entities \t\t\t%slist all entities %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN); shell.println(COLOR_RESET); + shell.printf(" metrics \t\t\t%slist all prometheus metrics %s*", COLOR_BRIGHT_CYAN, COLOR_BRIGHT_GREEN); + shell.println(COLOR_RESET); // show system ones first show(shell, EMSdevice::DeviceType::SYSTEM, true); diff --git a/src/core/locale_translations.h b/src/core/locale_translations.h index f5ce7ce07..c3c125b0f 100644 --- a/src/core/locale_translations.h +++ b/src/core/locale_translations.h @@ -63,6 +63,7 @@ MAKE_WORD_TRANSLATION(pool_device, "Pool Module", "Poolmodul", "", "Poolmodul", MAKE_WORD_TRANSLATION(info_cmd, "list all values (verbose)", "Liste aller Werte", "lijst van alle waardes", "lista alla värden", "wyświetl wszystkie wartości", "Viser alle verdier", "", "Tüm değerleri listele", "elenca tutti i valori", "zobraziť všetky hodnoty", "vypsat všechny hodnoty (podrobně)") // TODO translate MAKE_WORD_TRANSLATION(commands_cmd, "list all commands", "Liste aller Kommandos", "lijst van alle commando's", "lista alla kommandon", "wyświetl wszystkie komendy", "Viser alle kommandoer", "", "Tüm komutları listele", "elencaa tutti i comandi", "zobraziť všetky príkazy", "vypsat všechny příkazy") // TODO translate MAKE_WORD_TRANSLATION(entities_cmd, "list all entities", "Liste aller Entitäten", "lijst van alle entiteiten", "lista all entiteter", "wyświetl wszsytkie encje", "Viser alle enheter", "", "Tüm varlıkları listele", "elenca tutte le entità", "zobraziť všetky entity", "vypsat všechny entity") // TODO translate +MAKE_WORD_TRANSLATION(metrics_cmd, "list all prometheus metrics", "Liste aller Prometheus Metriken", "lijst van alle Prometheus metriken", "lista alla Prometheus metriker", "wyświetl wszystkie Prometheus metryki", "Viser alle Prometheus metrikker", "", "Tüm Prometheus metriklerini listele", "elenca tutte le metriche Prometheus", "zobraziť všetky Prometheus metriky", "vypsat všechny Prometheus metriky") // TODO translate MAKE_WORD_TRANSLATION(send_cmd, "send a telegram", "Sende EMS-Telegramm", "stuur een telegram", "skicka ett telegram", "wyślij telegram", "send et telegram", "", "Bir telegram gönder", "invia un telegramma", "poslať telegram", "odeslat telegram") // TODO translate MAKE_WORD_TRANSLATION(read_cmd, "send read request", "", "", "skicka en läsförfrågan", "", "", "", "", "", "odoslať žiadosť o prečítanie", "") // TODO translate MAKE_WORD_TRANSLATION(setiovalue_cmd, "set I/O value", "Setze Werte E/A", "instellen standaardwaarde", "sätt ett I/O-värde", "ustaw wartość", "sett en io verdi", "", "Giriş/Çıkış değerlerini ayarla", "imposta valore io", "nastaviť hodnotu io", "nastavit hodnotu I/O") // TODO translate diff --git a/test/test_api/test_api.cpp b/test/test_api/test_api.cpp index 14a3c1601..033270c02 100644 --- a/test/test_api/test_api.cpp +++ b/test/test_api/test_api.cpp @@ -288,6 +288,37 @@ void manual_test7() { TEST_ASSERT_EQUAL_STRING(expected_response, call_url("/api/custom/test_ram", data)); } +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); +} + +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); + } +} + void run_manual_tests() { RUN_TEST(manual_test1); RUN_TEST(manual_test2); @@ -296,6 +327,8 @@ void run_manual_tests() { RUN_TEST(manual_test5); RUN_TEST(manual_test6); RUN_TEST(manual_test7); + RUN_TEST(manual_test8); + RUN_TEST(manual_test9); } const char * run_console_command(const char * command) { @@ -353,6 +386,7 @@ void create_tests() { capture("/api/boiler/values"); capture("/api/boiler/info"); // capture("/api/boiler/entities"); // skipping since payload is too large + capture("/api/boiler/metrics"); capture("/api/boiler/comfort"); capture("/api/boiler/comfort/value"); capture("/api/boiler/comfort/fullname"); @@ -364,6 +398,7 @@ void create_tests() { // thermostat capture("/api/thermostat"); capture("/api/thermostat/hc1/values"); + capture("/api/thermostat/metrics"); capture("/api/thermostat/hc1/seltemp"); capture("/api/thermostat/hc2/seltemp"); From 91020abc900e6ace29ca954d5b26b82ee737a388 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 14:04:05 +0100 Subject: [PATCH 18/27] formatting --- mock-api/restServer.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/mock-api/restServer.ts b/mock-api/restServer.ts index b2b776699..b2129a961 100644 --- a/mock-api/restServer.ts +++ b/mock-api/restServer.ts @@ -276,10 +276,10 @@ function updateMask(entity: any, de: any, dd: any) { const old_custom_name = dd.nodes[dd_objIndex].cn; console.log( 'comparing names, old (' + - old_custom_name + - ') with new (' + - new_custom_name + - ')' + old_custom_name + + ') with new (' + + new_custom_name + + ')' ); if (old_custom_name !== new_custom_name) { changed = true; @@ -375,15 +375,15 @@ function check_upgrade(version: string) { console.log( 'Upgrade this version (' + - THIS_VERSION + - ') to dev (' + - dev_version + - ') is ' + - (DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') + - ' and to stable (' + - stable_version + - ') is ' + - (STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') + THIS_VERSION + + ') to dev (' + + dev_version + + ') is ' + + (DEV_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') + + ' and to stable (' + + stable_version + + ') is ' + + (STABLE_VERSION_IS_UPGRADEABLE ? 'YES' : 'NO') ); data = { emsesp_version: THIS_VERSION, From 0f4963d91ecd55008fbb7c5c606bc896a4a3bb96 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 14:04:14 +0100 Subject: [PATCH 19/27] formatting --- src/core/emsdevice.cpp | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) 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); From 26ea8320ce1947d49e2d32820365602b475f0653 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 14:04:45 +0100 Subject: [PATCH 20/27] rollback for #2752 --- src/web/WebAPIService.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index 613723673..56ca09e61 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -155,24 +155,25 @@ 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); + // TOOD add back when AsyncWebServer 3.9.3+ is released + // 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()); - // delete response; + 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); + // add back when AsyncWebServer 3.9.3+ is released + // std::string output_str; + // serializeJson(output, output_str); Serial.printf("%sweb output: %s[%s] %s(%d)%s %s%s", COLOR_WHITE, COLOR_BRIGHT_CYAN, @@ -185,6 +186,8 @@ 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 + + delete response; // TODO remove when AsyncWebServer 3.9.3+ is released } #if defined(EMSESP_UNITY) From fb2294c94572414777e86ad0e94c0e4aff3df592 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 14:05:02 +0100 Subject: [PATCH 21/27] added Prometheus metrics --- CHANGELOG_LATEST.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index f994a6eed..29af34c69 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -34,6 +34,8 @@ For more details go to [docs.emsesp.org](https://docs.emsesp.org/). - 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 From dd06882860c76a1451554e845b44ae5496f95770 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 14:05:50 +0100 Subject: [PATCH 22/27] auto-formatting --- test/test_api/test_api.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) 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); } From ff8566498f625f2028785e77d960a7c4c5bfdde4 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 15:15:03 +0100 Subject: [PATCH 23/27] typo --- src/web/WebAPIService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index 56ca09e61..e7c21c90d 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -155,7 +155,7 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) { // 400 (invalid) int ret_codes[7] = {400, 200, 404, 400, 401, 400, 404}; - // TOOD add back when AsyncWebServer 3.9.3+ is released + // TODO add back when AsyncWebServer 3.9.3+ is released // response->setCode(ret_codes[return_code]); // response->setLength(); // response->setContentType("application/json; charset=utf-8"); From 40fc0fd2f97fc38573c66669314a1d14450c3c26 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 15:39:13 +0100 Subject: [PATCH 24/27] anti-rollback! --- src/web/WebAPIService.cpp | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index e7c21c90d..12647383e 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -155,25 +155,23 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) { // 400 (invalid) int ret_codes[7] = {400, 200, 404, 400, 401, 400, 404}; - // TODO add back when AsyncWebServer 3.9.3+ is released - // response->setCode(ret_codes[return_code]); - // response->setLength(); - // response->setContentType("application/json; charset=utf-8"); - // request->send(response); + 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) - // add back when AsyncWebServer 3.9.3+ is released - // std::string output_str; - // serializeJson(output, output_str); + std::string output_str; + serializeJson(output, output_str); Serial.printf("%sweb output: %s[%s] %s(%d)%s %s%s", COLOR_WHITE, COLOR_BRIGHT_CYAN, @@ -186,8 +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 - - delete response; // TODO remove when AsyncWebServer 3.9.3+ is released } #if defined(EMSESP_UNITY) From 9c3521caf259943e901deb5d17c547124df6e5f2 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 15:39:36 +0100 Subject: [PATCH 25/27] add target native-test-create --- platformio.ini | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/platformio.ini b/platformio.ini index 4c5250014..3ad7826df 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 From 19e343e51740b70f68751e805410a4695463b8a7 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 17:23:57 +0100 Subject: [PATCH 26/27] loop tests --- test/test_api/api_test.js | 58 +++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/test/test_api/api_test.js b/test/test_api/api_test.js index 9d8a74f3c..8d21d6240 100644 --- a/test/test_api/api_test.js +++ b/test/test_api/api_test.js @@ -2,34 +2,52 @@ // node api_test.js const axios = require('axios'); -async function testAPI(ip = "ems-esp.local", apiPath = "system") { +async function testAPI(ip = "ems-esp.local", apiPath = "system", loopCount = 1, delayMs = 1000) { const baseUrl = `http://${ip}/api`; const url = `${baseUrl}/${apiPath}`; + const results = []; - try { - const response = await axios.get(url, { - timeout: 5000, - headers: { - 'Content-Type': 'application/json' - } - }); - - console.log('Status:', response.status); - console.log('Data:', JSON.stringify(response.data, null, 2)); - - return response.data; - } catch (error) { - console.error('Error:', error.message); - if (error.response) { - console.error('Response status:', error.response.status); - console.error('Response data:', error.response.data); + for (let i = 0; i < loopCount; i++) { + if (loopCount > 1) { + console.log(`\n--- Request ${i + 1} of ${loopCount} ---`); + } + + try { + const response = await axios.get(url, { + timeout: 5000, + headers: { + 'Content-Type': 'application/json' + } + }); + + console.log('Status:', response.status); + console.log('Data:', JSON.stringify(response.data, null, 2)); + + results.push(response.data); + + // Delay before next request (except for the last one) + if (i < loopCount - 1) { + await new Promise(resolve => setTimeout(resolve, delayMs)); + } + } catch (error) { + console.error('Error:', error.message); + if (error.response) { + console.error('Response status:', error.response.status); + console.error('Response data:', error.response.data); + } + throw error; } - throw error; } + + return loopCount === 1 ? results[0] : results; } // Run the test -testAPI("192.168.1.65", "system") +// Examples: +// testAPI("192.168.1.65", "system") - single call +// testAPI("192.168.1.65", "system", 5) - 5 calls with 1000ms delay +// testAPI("192.168.1.65", "system", 10, 2000) - 10 calls with 2000ms delay +testAPI("192.168.1.65", "system", 1000) .then(() => { console.log('Test completed successfully'); process.exit(0); From 748a2f5fcf0e9da1fea76f638ded69a7e9412686 Mon Sep 17 00:00:00 2001 From: proddy Date: Sun, 30 Nov 2025 20:51:17 +0100 Subject: [PATCH 27/27] update to show heap --- test/test_api/api_test.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/test/test_api/api_test.js b/test/test_api/api_test.js index 8d21d6240..8705d4006 100644 --- a/test/test_api/api_test.js +++ b/test/test_api/api_test.js @@ -8,8 +8,9 @@ async function testAPI(ip = "ems-esp.local", apiPath = "system", loopCount = 1, const results = []; for (let i = 0; i < loopCount; i++) { + let logMessage = ''; if (loopCount > 1) { - console.log(`\n--- Request ${i + 1} of ${loopCount} ---`); + logMessage = `--- Request ${i + 1} of ${loopCount} ---`; } try { @@ -20,10 +21,17 @@ async function testAPI(ip = "ems-esp.local", apiPath = "system", loopCount = 1, } }); - console.log('Status:', response.status); - console.log('Data:', JSON.stringify(response.data, null, 2)); + // console.log('Status:', response.status); + // console.log('Data:', JSON.stringify(response.data, null, 2)); - results.push(response.data); + // Extract and print freeMem + const freeMem = response.data?.freeMem || response.data?.system?.freeMem; + if (freeMem !== undefined) { + logMessage += (logMessage ? ' ' : '') + `System Free Memory: ${freeMem}`; + } else { + logMessage += (logMessage ? ' ' : '') + 'freeMem not found in response'; + } + console.log(logMessage); // Delay before next request (except for the last one) if (i < loopCount - 1) { @@ -47,7 +55,7 @@ async function testAPI(ip = "ems-esp.local", apiPath = "system", loopCount = 1, // testAPI("192.168.1.65", "system") - single call // testAPI("192.168.1.65", "system", 5) - 5 calls with 1000ms delay // testAPI("192.168.1.65", "system", 10, 2000) - 10 calls with 2000ms delay -testAPI("192.168.1.65", "system", 1000) +testAPI("192.168.1.65", "system", 20000, 5) .then(() => { console.log('Test completed successfully'); process.exit(0);