Merge pull request #2773 from proddy/dev

a collection of changes
This commit is contained in:
Proddy
2025-11-30 15:43:18 +01:00
committed by GitHub
19 changed files with 131 additions and 112 deletions

View File

@@ -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) - 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) - pid settings [#2735](https://github.com/emsesp/EMS-ESP32/issues/2735)
- refresh MQTT button added to MQTT Settings page - 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 ## Fixed

View File

@@ -23,14 +23,14 @@
"standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\"" "standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\""
}, },
"dependencies": { "dependencies": {
"@alova/adapter-xhr": "2.2.1", "@alova/adapter-xhr": "2.3.0",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.5", "@mui/icons-material": "^7.3.5",
"@mui/material": "^7.3.5", "@mui/material": "^7.3.5",
"@preact/compat": "^18.3.1", "@preact/compat": "^18.3.1",
"@table-library/react-table-library": "4.1.15", "@table-library/react-table-library": "4.1.15",
"alova": "3.3.4", "alova": "3.4.0",
"async-validator": "^4.2.5", "async-validator": "^4.2.5",
"etag": "^1.8.1", "etag": "^1.8.1",
"formidable": "^3.5.4", "formidable": "^3.5.4",
@@ -59,7 +59,7 @@
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"prettier": "^3.6.2", "prettier": "^3.7.3",
"rollup-plugin-visualizer": "^6.0.5", "rollup-plugin-visualizer": "^6.0.5",
"terser": "^5.44.1", "terser": "^5.44.1",
"typescript-eslint": "^8.48.0", "typescript-eslint": "^8.48.0",
@@ -67,5 +67,5 @@
"vite-plugin-imagemin": "^0.6.1", "vite-plugin-imagemin": "^0.6.1",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
}, },
"packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b" "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
} }

View File

@@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@alova/adapter-xhr': '@alova/adapter-xhr':
specifier: 2.2.1 specifier: 2.3.0
version: 2.2.1(alova@3.3.4) version: 2.3.0(alova@3.4.0)
'@emotion/react': '@emotion/react':
specifier: ^11.14.0 specifier: ^11.14.0
version: 11.14.0(@types/react@19.2.7)(react@19.2.0) version: 11.14.0(@types/react@19.2.7)(react@19.2.0)
@@ -30,8 +30,8 @@ importers:
specifier: 4.1.15 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) 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: alova:
specifier: 3.3.4 specifier: 3.4.0
version: 3.3.4 version: 3.4.0
async-validator: async-validator:
specifier: ^4.2.5 specifier: ^4.2.5
version: 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)) 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': '@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0(prettier@3.6.2) version: 6.0.0(prettier@3.7.3)
'@types/node': '@types/node':
specifier: ^24.10.1 specifier: ^24.10.1
version: 24.10.1 version: 24.10.1
@@ -109,8 +109,8 @@ importers:
specifier: ^10.1.8 specifier: ^10.1.8
version: 10.1.8(eslint@9.39.1) version: 10.1.8(eslint@9.39.1)
prettier: prettier:
specifier: ^3.6.2 specifier: ^3.7.3
version: 3.6.2 version: 3.7.3
rollup-plugin-visualizer: rollup-plugin-visualizer:
specifier: ^6.0.5 specifier: ^6.0.5
version: 6.0.5(rollup@4.53.3) version: 6.0.5(rollup@4.53.3)
@@ -132,8 +132,8 @@ importers:
packages: packages:
'@alova/adapter-xhr@2.2.1': '@alova/adapter-xhr@2.3.0':
resolution: {integrity: sha512-0aPVdFmmMn4Z4KvG+DOyWhzQKaBGCe8yPQ4mJz1hQNPzbrIfqq+0flVF6ArFL4EtPbOJVnKropJNE691sjtq5A==} resolution: {integrity: sha512-IegkchjfXFxXgn6JUZuVEHFQn+jojzrnNdzrGhX5ecEOIC8M/CQvLQzXjLeT6PbGiwnXwvZWL2ya4eqQz51+uQ==}
peerDependencies: peerDependencies:
alova: ^3.0.20 alova: ^3.0.20
@@ -475,8 +475,8 @@ packages:
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/eslintrc@3.3.1': '@eslint/eslintrc@3.3.3':
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.39.1': '@eslint/js@9.39.1':
@@ -960,8 +960,8 @@ packages:
ajv@6.12.6: ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
alova@3.3.4: alova@3.4.0:
resolution: {integrity: sha512-UKKqXdvf8aQ4C7m3brO77YWe5CDz8N59PdAUz7M8gowKUUXTutbk0Vk5DRBrCe0hMUyyNMUhdCZ38llGxCViyQ==} resolution: {integrity: sha512-/vSvVbA45CHg34Y5erx+wVxy1B/n4UoGX7dKqSpLVz9cDSDSOhqCnRD/dV+AErjMmQeVpJrjmDT7SCkhQbnUeQ==}
engines: {node: '>= 18.0.0'} engines: {node: '>= 18.0.0'}
ansi-regex@2.1.1: ansi-regex@2.1.1:
@@ -1027,8 +1027,8 @@ packages:
base64-js@1.5.1: base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.8.31: baseline-browser-mapping@2.8.32:
resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==} resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==}
hasBin: true hasBin: true
bin-build@3.0.0: bin-build@3.0.0:
@@ -1185,8 +1185,8 @@ packages:
convert-source-map@2.0.0: convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie@1.1.0: cookie@1.1.1:
resolution: {integrity: sha512-vXiThu1/rlos7EGu8TuNZQEg2e9TvhH9dmS4T4ZVzB7Ao1agEZ6EG3sn5n+hZRYUgduISd1HpngFzAZiDGm5vQ==} resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
core-util-is@1.0.3: core-util-is@1.0.3:
@@ -1337,8 +1337,8 @@ packages:
duplexer3@0.1.5: duplexer3@0.1.5:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
electron-to-chromium@1.5.260: electron-to-chromium@1.5.262:
resolution: {integrity: sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==} resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2478,8 +2478,8 @@ packages:
resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
engines: {node: '>=4'} engines: {node: '>=4'}
prettier@3.6.2: prettier@3.7.3:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
@@ -3098,10 +3098,10 @@ packages:
snapshots: snapshots:
'@alova/adapter-xhr@2.2.1(alova@3.3.4)': '@alova/adapter-xhr@2.3.0(alova@3.4.0)':
dependencies: dependencies:
'@alova/shared': 1.3.1 '@alova/shared': 1.3.1
alova: 3.3.4 alova: 3.4.0
'@alova/shared@1.3.1': {} '@alova/shared@1.3.1': {}
@@ -3423,7 +3423,7 @@ snapshots:
dependencies: dependencies:
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
'@eslint/eslintrc@3.3.1': '@eslint/eslintrc@3.3.3':
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
debug: 4.4.3 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-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) 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: dependencies:
'@babel/generator': 7.28.5 '@babel/generator': 7.28.5
'@babel/parser': 7.28.5 '@babel/parser': 7.28.5
@@ -3726,7 +3726,7 @@ snapshots:
lodash-es: 4.17.21 lodash-es: 4.17.21
minimatch: 9.0.5 minimatch: 9.0.5
parse-imports-exports: 0.2.4 parse-imports-exports: 0.2.4
prettier: 3.6.2 prettier: 3.7.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3911,7 +3911,7 @@ snapshots:
json-schema-traverse: 0.4.1 json-schema-traverse: 0.4.1
uri-js: 4.4.1 uri-js: 4.4.1
alova@3.3.4: alova@3.4.0:
dependencies: dependencies:
'@alova/shared': 1.3.1 '@alova/shared': 1.3.1
rate-limiter-flexible: 5.0.5 rate-limiter-flexible: 5.0.5
@@ -3962,7 +3962,7 @@ snapshots:
base64-js@1.5.1: {} base64-js@1.5.1: {}
baseline-browser-mapping@2.8.31: {} baseline-browser-mapping@2.8.32: {}
bin-build@3.0.0: bin-build@3.0.0:
dependencies: dependencies:
@@ -4019,9 +4019,9 @@ snapshots:
browserslist@4.28.0: browserslist@4.28.0:
dependencies: dependencies:
baseline-browser-mapping: 2.8.31 baseline-browser-mapping: 2.8.32
caniuse-lite: 1.0.30001757 caniuse-lite: 1.0.30001757
electron-to-chromium: 1.5.260 electron-to-chromium: 1.5.262
node-releases: 2.0.27 node-releases: 2.0.27
update-browserslist-db: 1.1.4(browserslist@4.28.0) update-browserslist-db: 1.1.4(browserslist@4.28.0)
@@ -4151,7 +4151,7 @@ snapshots:
convert-source-map@2.0.0: {} convert-source-map@2.0.0: {}
cookie@1.1.0: {} cookie@1.1.1: {}
core-util-is@1.0.3: {} core-util-is@1.0.3: {}
@@ -4366,7 +4366,7 @@ snapshots:
duplexer3@0.1.5: {} duplexer3@0.1.5: {}
electron-to-chromium@1.5.260: {} electron-to-chromium@1.5.262: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
@@ -4529,7 +4529,7 @@ snapshots:
'@eslint/config-array': 0.21.1 '@eslint/config-array': 0.21.1
'@eslint/config-helpers': 0.4.2 '@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0 '@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.1 '@eslint/eslintrc': 3.3.3
'@eslint/js': 9.39.1 '@eslint/js': 9.39.1
'@eslint/plugin-kit': 0.4.1 '@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.7 '@humanfs/node': 0.16.7
@@ -5487,7 +5487,7 @@ snapshots:
prepend-http@2.0.0: {} prepend-http@2.0.0: {}
prettier@3.6.2: {} prettier@3.7.3: {}
process-nextick-args@2.0.1: {} 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): react-router@7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
dependencies: dependencies:
cookie: 1.1.0 cookie: 1.1.1
react: 19.2.0 react: 19.2.0
set-cookie-parser: 2.7.2 set-cookie-parser: 2.7.2
optionalDependencies: optionalDependencies:

View File

@@ -266,6 +266,7 @@ const MqttSettings = () => {
label={LL.CERT()} label={LL.CERT()}
variant="outlined" variant="outlined"
value={data.rootCA} value={data.rootCA}
sx={{ width: '50ch' }}
onChange={updateFormValue} onChange={updateFormValue}
margin="normal" margin="normal"
/> />

View File

@@ -13,7 +13,7 @@
"@trivago/prettier-plugin-sort-imports": "^6.0.0", "@trivago/prettier-plugin-sort-imports": "^6.0.0",
"formidable": "^3.5.4", "formidable": "^3.5.4",
"itty-router": "^5.0.22", "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"
} }

View File

@@ -13,7 +13,7 @@ importers:
version: 3.1.2 version: 3.1.2
'@trivago/prettier-plugin-sort-imports': '@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0(prettier@3.6.2) version: 6.0.0(prettier@3.7.3)
formidable: formidable:
specifier: ^3.5.4 specifier: ^3.5.4
version: 3.5.4 version: 3.5.4
@@ -21,8 +21,8 @@ importers:
specifier: ^5.0.22 specifier: ^5.0.22
version: 5.0.22 version: 5.0.22
prettier: prettier:
specifier: ^3.6.2 specifier: ^3.7.3
version: 3.6.2 version: 3.7.3
packages: packages:
@@ -167,8 +167,8 @@ packages:
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
prettier@3.6.2: prettier@3.7.3:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
@@ -246,7 +246,7 @@ snapshots:
dependencies: dependencies:
'@noble/hashes': 1.8.0 '@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: dependencies:
'@babel/generator': 7.28.5 '@babel/generator': 7.28.5
'@babel/parser': 7.28.5 '@babel/parser': 7.28.5
@@ -256,7 +256,7 @@ snapshots:
lodash-es: 4.17.21 lodash-es: 4.17.21
minimatch: 9.0.5 minimatch: 9.0.5
parse-imports-exports: 0.2.4 parse-imports-exports: 0.2.4
prettier: 3.6.2 prettier: 3.7.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -311,6 +311,6 @@ snapshots:
picocolors@1.1.1: {} picocolors@1.1.1: {}
prettier@3.6.2: {} prettier@3.7.3: {}
wrappy@1.0.2: {} wrappy@1.0.2: {}

View File

@@ -569,14 +569,15 @@ let mqtt_settings = {
publish_time_heartbeat: 60, publish_time_heartbeat: 60,
publish_time_water: 60, publish_time_water: 60,
mqtt_qos: 0, mqtt_qos: 0,
rootCA: '',
mqtt_retain: false, mqtt_retain: false,
ha_enabled: true, ha_enabled: true,
nested_format: 1, nested_format: 1,
discovery_type: 0, discovery_type: 0,
discovery_prefix: 'homeassistant', discovery_prefix: 'homeassistant',
send_response: true, send_response: true,
publish_single: false publish_single: false,
enableTLS: true,
rootCA: ''
}; };
const mqtt_status = { const mqtt_status = {
enabled: true, enabled: true,

View File

@@ -106,7 +106,7 @@ board_build.filesystem = littlefs
lib_deps = lib_deps =
bblanchon/ArduinoJson @ 7.4.2 bblanchon/ArduinoJson @ 7.4.2
ESP32Async/AsyncTCP @ 3.4.9 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 https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8
@@ -214,23 +214,20 @@ lib_ldf_mode = off
lib_deps = lib_deps =
; unit tests ; 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 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. ; tip: use https://jsondiff.com/ to compare the expected and actual responses.
[env:native-test] [env:native-test]
platform = native platform = native
test_build_src = true test_build_src = true
build_flags = 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_type = debug
build_src_flags = 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 -Wall -Wextra
-Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces -Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces
-Wno-vla-cxx-extension -Wno-tautological-constant-out-of-range-compare -Wno-vla-cxx-extension -Wno-tautological-constant-out-of-range-compare
@@ -260,6 +257,12 @@ lib_deps = Unity
test_testing_command = test_testing_command =
${platformio.build_dir}/${this.__env__}/program ${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. ; Building and testing locally on OS, which we call "standalone" without an ESP32.
; See https://docs.platformio.org/en/latest/platforms/native.html ; See https://docs.platformio.org/en/latest/platforms/native.html

View File

@@ -173,7 +173,7 @@ bool MqttSettingsService::configureMqtt() {
// only connect if WiFi is connected and MQTT is enabled // only connect if WiFi is connected and MQTT is enabled
if (_state.enabled && emsesp::EMSESP::system_.network_connected() && !_state.host.isEmpty()) { 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]; static char will_topic[FACTORY_MQTT_MAX_TOPIC_LENGTH];
if (_state.base.isEmpty()) { if (_state.base.isEmpty()) {
snprintf(will_topic, sizeof(will_topic), "status"); snprintf(will_topic, sizeof(will_topic), "status");

View File

@@ -39,7 +39,7 @@
#endif #endif
#ifndef FACTORY_MQTT_KEEP_ALIVE #ifndef FACTORY_MQTT_KEEP_ALIVE
#define FACTORY_MQTT_KEEP_ALIVE 16 #define FACTORY_MQTT_KEEP_ALIVE 60
#endif #endif
#ifndef FACTORY_MQTT_CLEAN_SESSION #ifndef FACTORY_MQTT_CLEAN_SESSION

View File

@@ -87,8 +87,8 @@ static void setup_commands(std::shared_ptr<Commands> const & commands) {
Command::show_all(shell); Command::show_all(shell);
} else if (command == F_(system)) { } else if (command == F_(system)) {
EMSESP::system_.show_system(shell); EMSESP::system_.show_system(shell);
} else if (command == F_(users) && (shell.has_flags(CommandFlags::ADMIN))) { } else if (command == F_(users)) {
EMSESP::system_.show_users(shell); // admin only EMSESP::system_.show_users(shell);
} else if (command == F_(devices)) { } else if (command == F_(devices)) {
EMSESP::show_devices(shell); EMSESP::show_devices(shell);
} else if (command == F_(log)) { } else if (command == F_(log)) {

View File

@@ -1706,7 +1706,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) {
// generate Prometheus metrics format from device values // generate Prometheus metrics format from device values
std::string EMSdevice::get_metrics_prometheus(const int8_t tag) { std::string EMSdevice::get_metrics_prometheus(const int8_t tag) {
std::string result; std::string result;
std::unordered_map<std::string, bool> seen_metrics; std::unordered_map<std::string, bool> seen_metrics;
for (auto & dv : devicevalues_) { 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 // only process number and boolean types for now
if (dv.type != DeviceValueType::BOOL && dv.type != DeviceValueType::UINT8 && dv.type != DeviceValueType::INT8 if (dv.type != DeviceValueType::BOOL && dv.type != DeviceValueType::UINT8 && dv.type != DeviceValueType::INT8 && dv.type != DeviceValueType::UINT16
&& dv.type != DeviceValueType::UINT16 && dv.type != DeviceValueType::INT16 && dv.type != DeviceValueType::UINT24 && dv.type != DeviceValueType::INT16 && dv.type != DeviceValueType::UINT24 && dv.type != DeviceValueType::UINT32 && dv.type != DeviceValueType::TIME) {
&& dv.type != DeviceValueType::UINT32 && dv.type != DeviceValueType::TIME) {
continue; continue;
} }
bool has_value = false; bool has_value = false;
double metric_value = 0.0; double metric_value = 0.0;
switch (dv.type) { switch (dv.type) {
case DeviceValueType::BOOL: case DeviceValueType::BOOL:
if (Helpers::hasValue(*(uint8_t *)(dv.value_p), EMS_VALUE_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; metric_value = (bool)*(uint8_t *)(dv.value_p) ? 1.0 : 0.0;
} }
break; break;
case DeviceValueType::UINT8: case DeviceValueType::UINT8:
if (Helpers::hasValue(*(uint8_t *)(dv.value_p))) { if (Helpers::hasValue(*(uint8_t *)(dv.value_p))) {
has_value = true; has_value = true;
metric_value = *(uint8_t *)(dv.value_p); metric_value = *(uint8_t *)(dv.value_p);
} }
break; break;
case DeviceValueType::INT8: case DeviceValueType::INT8:
if (Helpers::hasValue(*(int8_t *)(dv.value_p))) { if (Helpers::hasValue(*(int8_t *)(dv.value_p))) {
has_value = true; has_value = true;
metric_value = *(int8_t *)(dv.value_p); metric_value = *(int8_t *)(dv.value_p);
} }
break; break;
case DeviceValueType::UINT16: case DeviceValueType::UINT16:
if (Helpers::hasValue(*(uint16_t *)(dv.value_p))) { if (Helpers::hasValue(*(uint16_t *)(dv.value_p))) {
has_value = true; has_value = true;
metric_value = *(uint16_t *)(dv.value_p); metric_value = *(uint16_t *)(dv.value_p);
} }
break; break;
case DeviceValueType::INT16: case DeviceValueType::INT16:
if (Helpers::hasValue(*(int16_t *)(dv.value_p))) { if (Helpers::hasValue(*(int16_t *)(dv.value_p))) {
has_value = true; has_value = true;
metric_value = *(int16_t *)(dv.value_p); metric_value = *(int16_t *)(dv.value_p);
} }
break; break;
@@ -1759,7 +1758,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) {
case DeviceValueType::UINT32: case DeviceValueType::UINT32:
case DeviceValueType::TIME: case DeviceValueType::TIME:
if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) { if (Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
has_value = true; has_value = true;
metric_value = *(uint32_t *)(dv.value_p); metric_value = *(uint32_t *)(dv.value_p);
} }
break; 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; std::string help_text;
if (!fullname.empty()) { if (!fullname.empty()) {
help_text = fullname; help_text = fullname;
@@ -1850,7 +1849,7 @@ std::string EMSdevice::get_metrics_prometheus(const int8_t tag) {
} }
result += " "; result += " ";
char val_str[30]; char val_str[30];
double final_value = metric_value; double final_value = metric_value;
if (dv.numeric_operator != 0) { if (dv.numeric_operator != 0) {

View File

@@ -509,14 +509,14 @@ void Mqtt::on_connect() {
queue_subscribe_message(discovery_prefix_ + "/+/" + Mqtt::basename() + "/#"); 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 // re-subscribe to all custom registered MQTT topics
resubscribe(); resubscribe();
// publish to the last will topic (see Mqtt::start() function) to say we're alive // 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 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 // Home Assistant Discovery - the main master Device called EMS-ESP
@@ -532,9 +532,10 @@ void Mqtt::ha_status() {
strcpy(uniq, "system_status"); strcpy(uniq, "system_status");
} }
doc["~"] = Mqtt::base();
doc["uniq_id"] = uniq; doc["uniq_id"] = uniq;
doc["def_ent_id"] = (std::string) "binary_sensor." + 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["name"] = "System status";
doc["pl_on"] = "online"; doc["pl_on"] = "online";
doc["pl_off"] = "offline"; doc["pl_off"] = "offline";
@@ -981,8 +982,9 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
return queue_remove_topic(topic); return queue_remove_topic(topic);
} }
// build the full payload // build the full topic's payload
JsonDocument doc; JsonDocument doc;
doc["~"] = Mqtt::base();
doc["uniq_id"] = uniq_id; 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 // 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]; char command_topic[MQTT_TOPIC_MAX_SIZE];
// add command topic // add command topic
if (tag >= DeviceValueTAG::TAG_HC1) { 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 { } 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; 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 // 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 // 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)) { 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 { } 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; 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 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"; doc["avty_mode"] = "all";
} }

View File

@@ -731,11 +731,6 @@ void System::heartbeat_json(JsonObject output) {
// send periodic MQTT message with system information // send periodic MQTT message with system information
void System::send_heartbeat() { 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 refreshHeapMem(); // refresh free heap and max alloc heap
JsonDocument doc; JsonDocument doc;
@@ -997,6 +992,11 @@ int8_t System::wifi_quality(int8_t dBm) {
// print users to console // print users to console
void System::show_users(uuid::console::Shell & shell) { 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:"); shell.printfln("Users:");
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.7.3-dev.32" #define EMSESP_APP_VERSION "3.7.3-dev.33"

View File

@@ -144,6 +144,8 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
return; return;
} }
api_count_++;
// send the json that came back from the command call // 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) // sequence matches CommandRet in command.h (FAIL, OK, NOT_FOUND, ERROR, NOT_ALLOWED, INVALID, NO_VALUE)
// 400 (bad request) // 400 (bad request)
@@ -153,16 +155,23 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
// 400 (invalid) // 400 (invalid)
int ret_codes[7] = {400, 200, 404, 400, 401, 400, 404}; 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) // serialize JSON to string to ensure correct content-length and avoid HTTP parsing errors (issue #2752)
std::string output_str; // std::string output_str;
serializeJson(output, output_str); // serializeJson(output, output_str);
request->send(ret_codes[return_code], "application/json; charset=utf-8", output_str.c_str()); // request->send(ret_codes[return_code], "application/json; charset=utf-8", output_str.c_str());
#if defined(EMSESP_UNITY) #if defined(EMSESP_UNITY)
// store the result so we can test with Unity later // store the result so we can test with Unity later
storeResponse(output); storeResponse(output);
#endif #endif
#if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY) #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", Serial.printf("%sweb output: %s[%s] %s(%d)%s %s%s",
COLOR_WHITE, COLOR_WHITE,
COLOR_BRIGHT_CYAN, COLOR_BRIGHT_CYAN,
@@ -175,9 +184,6 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) {
Serial.println(); Serial.println();
EMSESP::logger().debug("web output: %s %s", request->url().c_str(), output_str.c_str()); EMSESP::logger().debug("web output: %s %s", request->url().c_str(), output_str.c_str());
#endif #endif
api_count_++;
delete response;
} }
#if defined(EMSESP_UNITY) #if defined(EMSESP_UNITY)

View File

@@ -422,7 +422,7 @@ void WebSettings::set_board_profile(WebSettings & settings) {
// load the board profile into the data vector // 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 // 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<int8_t> data(99, 0); // initialize with 99 for all values, just as a safe guard to catch bad gpios std::vector<int8_t> data(10, 99); // initialize with 99 for all values, just as a safe guard to catch bad gpios
if (settings.board_profile != "default") { if (settings.board_profile != "default") {
if (!System::load_board_profile(data, settings.board_profile.c_str())) { if (!System::load_board_profile(data, settings.board_profile.c_str())) {
#if defined(EMSESP_DEBUG) #if defined(EMSESP_DEBUG)

View File

@@ -29,7 +29,7 @@ async function testAPI(ip = "ems-esp.local", apiPath = "system") {
} }
// Run the test // Run the test
testAPI("192.168.1.223", "system") testAPI("192.168.1.65", "system")
.then(() => { .then(() => {
console.log('Test completed successfully'); console.log('Test completed successfully');
process.exit(0); process.exit(0);

View File

@@ -299,9 +299,8 @@ void manual_test8() {
TEST_ASSERT_TRUE(strstr(response, "emsesp_") != nullptr); TEST_ASSERT_TRUE(strstr(response, "emsesp_") != nullptr);
TEST_ASSERT_TRUE(strstr(response, " gauge") != nullptr); TEST_ASSERT_TRUE(strstr(response, " gauge") != nullptr);
TEST_ASSERT_TRUE(strstr(response, "emsesp_tapwateractive") != nullptr || TEST_ASSERT_TRUE(strstr(response, "emsesp_tapwateractive") != nullptr || strstr(response, "emsesp_selflowtemp") != nullptr
strstr(response, "emsesp_selflowtemp") != nullptr || || strstr(response, "emsesp_curflowtemp") != nullptr);
strstr(response, "emsesp_curflowtemp") != nullptr);
} }
void manual_test9() { void manual_test9() {