diff --git a/interface/package.json b/interface/package.json
index 73372e725..07dcf3af2 100644
--- a/interface/package.json
+++ b/interface/package.json
@@ -26,8 +26,8 @@
"@alova/adapter-xhr": "2.3.1",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
- "@mui/icons-material": "^7.3.9",
- "@mui/material": "^7.3.9",
+ "@mui/icons-material": "^9.0.0",
+ "@mui/material": "^9.0.0",
"@preact/compat": "^18.3.2",
"@table-library/react-table-library": "4.1.15",
"alova": "^3.5.1",
@@ -38,10 +38,10 @@
"magic-string": "^0.30.21",
"mime-types": "^3.0.2",
"preact": "^10.29.1",
- "react": "^19.2.4",
- "react-dom": "^19.2.4",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5",
"react-icons": "^5.6.0",
- "react-router": "^7.14.0",
+ "react-router": "^7.14.1",
"react-toastify": "^11.0.5",
"typesafe-i18n": "^5.27.1",
"typescript": "^6.0.2"
@@ -52,18 +52,18 @@
"@preact/compat": "^18.3.2",
"@preact/preset-vite": "^2.10.5",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
- "@types/node": "^25.5.2",
+ "@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
- "axe-core": "^4.11.2",
+ "axe-core": "^4.11.3",
"concurrently": "^9.2.1",
- "eslint": "^10.1.0",
+ "eslint": "^10.2.0",
"eslint-config-prettier": "^10.1.8",
- "prettier": "^3.8.1",
+ "prettier": "^3.8.3",
"rollup-plugin-visualizer": "^7.0.1",
"terser": "^5.46.1",
- "typescript-eslint": "^8.58.0",
- "vite": "^8.0.3",
+ "typescript-eslint": "^8.58.2",
+ "vite": "^8.0.8",
"vite-plugin-imagemin": "^0.6.1"
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
diff --git a/interface/pnpm-lock.yaml b/interface/pnpm-lock.yaml
index f773ee0e2..4ff491e52 100644
--- a/interface/pnpm-lock.yaml
+++ b/interface/pnpm-lock.yaml
@@ -13,22 +13,22 @@ importers:
version: 2.3.1(alova@3.5.1)
'@emotion/react':
specifier: ^11.14.0
- version: 11.14.0(@types/react@19.2.14)(react@19.2.4)
+ version: 11.14.0(@types/react@19.2.14)(react@19.2.5)
'@emotion/styled':
specifier: ^11.14.1
- version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
+ version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@mui/icons-material':
- specifier: ^7.3.9
- version: 7.3.9(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
+ specifier: ^9.0.0
+ version: 9.0.0(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@mui/material':
- specifier: ^7.3.9
- version: 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ specifier: ^9.0.0
+ version: 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@preact/compat':
specifier: ^18.3.2
version: 18.3.2(preact@10.29.1)
'@table-library/react-table-library':
specifier: 4.1.15
- version: 4.1.15(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 4.1.15(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
alova:
specifier: ^3.5.1
version: 3.5.1
@@ -54,20 +54,20 @@ importers:
specifier: ^10.29.1
version: 10.29.1
react:
- specifier: ^19.2.4
- version: 19.2.4
+ specifier: ^19.2.5
+ version: 19.2.5
react-dom:
- specifier: ^19.2.4
- version: 19.2.4(react@19.2.4)
+ specifier: ^19.2.5
+ version: 19.2.5(react@19.2.5)
react-icons:
specifier: ^5.6.0
- version: 5.6.0(react@19.2.4)
+ version: 5.6.0(react@19.2.5)
react-router:
- specifier: ^7.14.0
- version: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ specifier: ^7.14.1
+ version: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react-toastify:
specifier: ^11.0.5
- version: 11.0.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ version: 11.0.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
typesafe-i18n:
specifier: ^5.27.1
version: 5.27.1(typescript@6.0.2)
@@ -80,16 +80,16 @@ importers:
version: 7.29.0
'@eslint/js':
specifier: ^10.0.1
- version: 10.0.1(eslint@10.1.0)
+ version: 10.0.1(eslint@10.2.0)
'@preact/preset-vite':
specifier: ^2.10.5
- version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1))
+ version: 2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
'@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.2
- version: 6.0.2(prettier@3.8.1)
+ version: 6.0.2(prettier@3.8.3)
'@types/node':
- specifier: ^25.5.2
- version: 25.5.2
+ specifier: ^25.6.0
+ version: 25.6.0
'@types/react':
specifier: ^19.2.14
version: 19.2.14
@@ -97,35 +97,35 @@ importers:
specifier: ^19.2.3
version: 19.2.3(@types/react@19.2.14)
axe-core:
- specifier: ^4.11.2
- version: 4.11.2
+ specifier: ^4.11.3
+ version: 4.11.3
concurrently:
specifier: ^9.2.1
version: 9.2.1
eslint:
- specifier: ^10.1.0
- version: 10.1.0
+ specifier: ^10.2.0
+ version: 10.2.0
eslint-config-prettier:
specifier: ^10.1.8
- version: 10.1.8(eslint@10.1.0)
+ version: 10.1.8(eslint@10.2.0)
prettier:
- specifier: ^3.8.1
- version: 3.8.1
+ specifier: ^3.8.3
+ version: 3.8.3
rollup-plugin-visualizer:
specifier: ^7.0.1
- version: 7.0.1(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(rollup@4.59.0)
+ version: 7.0.1(rolldown@1.0.0-rc.15)(rollup@4.59.0)
terser:
specifier: ^5.46.1
version: 5.46.1
typescript-eslint:
- specifier: ^8.58.0
- version: 8.58.0(eslint@10.1.0)(typescript@6.0.2)
+ specifier: ^8.58.2
+ version: 8.58.2(eslint@10.2.0)(typescript@6.0.2)
vite:
- specifier: ^8.0.3
- version: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1)
+ specifier: ^8.0.8
+ version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
vite-plugin-imagemin:
specifier: ^0.6.1
- version: 0.6.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1))
+ version: 0.6.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
packages:
@@ -234,14 +234,14 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
- '@emnapi/core@1.9.1':
- resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==}
+ '@emnapi/core@1.9.2':
+ resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
- '@emnapi/runtime@1.9.1':
- resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
+ '@emnapi/runtime@1.9.2':
+ resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
- '@emnapi/wasi-threads@1.2.0':
- resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==}
+ '@emnapi/wasi-threads@1.2.1':
+ resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@emotion/babel-plugin@11.13.5':
resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
@@ -469,16 +469,16 @@ packages:
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
- '@eslint/config-array@0.23.3':
- resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==}
+ '@eslint/config-array@0.23.5':
+ resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/config-helpers@0.5.3':
- resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==}
+ '@eslint/config-helpers@0.5.5':
+ resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/core@1.1.1':
- resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==}
+ '@eslint/core@1.2.1':
+ resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/js@10.0.1':
@@ -490,12 +490,12 @@ packages:
eslint:
optional: true
- '@eslint/object-schema@3.0.3':
- resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==}
+ '@eslint/object-schema@3.0.5':
+ resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/plugin-kit@0.6.1':
- resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==}
+ '@eslint/plugin-kit@0.7.1':
+ resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@humanfs/core@0.19.1':
@@ -533,27 +533,27 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
- '@mui/core-downloads-tracker@7.3.9':
- resolution: {integrity: sha512-MOkOCTfbMJwLshlBCKJ59V2F/uaLYfmKnN76kksj6jlGUVdI25A9Hzs08m+zjBRdLv+sK7Rqdsefe8X7h/6PCw==}
+ '@mui/core-downloads-tracker@9.0.0':
+ resolution: {integrity: sha512-uwQNGkhv0lf7ufxw6QXev77BW6pWbW+7uxYjU5+rfp4lBkFtMEgJCsarTM3Tn+i0lGx6+Ol2u88JdGXr0GDskA==}
- '@mui/icons-material@7.3.9':
- resolution: {integrity: sha512-BT+zPJXss8Hg/oEMRmHl17Q97bPACG4ufFSfGEdhiE96jOyR5Dz1ty7ZWt1fVGR0y1p+sSgEwQT/MNZQmoWDCw==}
+ '@mui/icons-material@9.0.0':
+ resolution: {integrity: sha512-oDwyvI6LgjWRC9MBcSGvLkPud9S9ELgSBQFYxa1rYcZn6Br55dn22SyvsPDMsn0G8OndFk53iMT45W5mNqrogw==}
engines: {node: '>=14.0.0'}
peerDependencies:
- '@mui/material': ^7.3.9
+ '@mui/material': ^9.0.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
- '@mui/material@7.3.9':
- resolution: {integrity: sha512-I8yO3t4T0y7bvDiR1qhIN6iBWZOTBfVOnmLlM7K6h3dx5YX2a7rnkuXzc2UkZaqhxY9NgTnEbdPlokR1RxCNRQ==}
+ '@mui/material@9.0.0':
+ resolution: {integrity: sha512-+VP/oQCDhDR87NQQgXnNBG8dwy6GNuQLnenS1pZvkbn2dKFSxRSRMybTpH9xUxXP+316mlYDy5CSbYtusnCWtw==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
- '@mui/material-pigment-css': ^7.3.9
+ '@mui/material-pigment-css': ^9.0.0
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
react: ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -567,8 +567,8 @@ packages:
'@types/react':
optional: true
- '@mui/private-theming@7.3.9':
- resolution: {integrity: sha512-ErIyRQvsiQEq7Yvcvfw9UDHngaqjMy9P3JDPnRAaKG5qhpl2C4tX/W1S4zJvpu+feihmZJStjIyvnv6KDbIrlw==}
+ '@mui/private-theming@9.0.0':
+ resolution: {integrity: sha512-JtuZoaiCqwD6vjgYu6Xp3T7DZkrxJlgtDz5yESzhI34fEX5hHMh2VJUbuL9UOg8xrfIFMrq6dcYoH/7Zi4G0RA==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -577,8 +577,8 @@ packages:
'@types/react':
optional: true
- '@mui/styled-engine@7.3.9':
- resolution: {integrity: sha512-JqujWt5bX4okjUPGpVof/7pvgClqh7HvIbsIBIOOlCh2u3wG/Bwp4+E1bc1dXSwkrkp9WUAoNdI5HEC+5HKvMw==}
+ '@mui/styled-engine@9.0.0':
+ resolution: {integrity: sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
@@ -590,8 +590,8 @@ packages:
'@emotion/styled':
optional: true
- '@mui/system@7.3.9':
- resolution: {integrity: sha512-aL1q9am8XpRrSabv9qWf5RHhJICJql34wnrc1nz0MuOglPRYF/liN+c8VqZdTvUn9qg+ZjRVbKf4sJVFfIDtmg==}
+ '@mui/system@9.0.0':
+ resolution: {integrity: sha512-YnC5Zg6j04IxiLc/boAKs0464jfZlLFVa7mf5E8lF0XOtZVUvG6R6gJK50lgUYdaaLdyLfxF6xR7LaPuEpeT/g==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
@@ -606,16 +606,16 @@ packages:
'@types/react':
optional: true
- '@mui/types@7.4.12':
- resolution: {integrity: sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==}
+ '@mui/types@9.0.0':
+ resolution: {integrity: sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg==}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@types/react':
optional: true
- '@mui/utils@7.3.9':
- resolution: {integrity: sha512-U6SdZaGbfb65fqTsH3V5oJdFj9uYwyLE2WVuNvmbggTSDBb8QHrFsqY8BN3taK9t3yJ8/BPHD/kNvLNyjwM7Yw==}
+ '@mui/utils@9.0.0':
+ resolution: {integrity: sha512-bQcqyg/gjULUqTuyUjSAFr6LQGLvtkNtDbJerAtoUn9kGZ0hg5QJiN1PLHMLbeFpe3te1831uq7GFl2ITokGdg==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -624,8 +624,8 @@ packages:
'@types/react':
optional: true
- '@napi-rs/wasm-runtime@1.1.2':
- resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==}
+ '@napi-rs/wasm-runtime@1.1.3':
+ resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==}
peerDependencies:
'@emnapi/core': ^1.7.1
'@emnapi/runtime': ^1.7.1
@@ -646,8 +646,8 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
- '@oxc-project/types@0.122.0':
- resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
+ '@oxc-project/types@0.124.0':
+ resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==}
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
@@ -683,103 +683,103 @@ packages:
preact: ^10.4.0 || ^11.0.0-0
vite: '>=2.0.0'
- '@rolldown/binding-android-arm64@1.0.0-rc.12':
- resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==}
+ '@rolldown/binding-android-arm64@1.0.0-rc.15':
+ resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
- '@rolldown/binding-darwin-arm64@1.0.0-rc.12':
- resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==}
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.15':
+ resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@rolldown/binding-darwin-x64@1.0.0-rc.12':
- resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==}
+ '@rolldown/binding-darwin-x64@1.0.0-rc.15':
+ resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@rolldown/binding-freebsd-x64@1.0.0-rc.12':
- resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==}
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.15':
+ resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
- resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==}
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
+ resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
- resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==}
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
+ resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
- resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==}
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
+ resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
- resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==}
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
+ resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
- resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==}
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
+ resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
- resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==}
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
+ resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
- resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==}
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
+ resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
- '@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
- resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==}
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
+ resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
- '@rolldown/binding-wasm32-wasi@1.0.0-rc.12':
- resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==}
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
+ resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
- '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
- resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==}
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
+ resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
- resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==}
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
+ resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
- '@rolldown/pluginutils@1.0.0-rc.12':
- resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==}
+ '@rolldown/pluginutils@1.0.0-rc.15':
+ resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==}
'@rollup/pluginutils@4.2.1':
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
@@ -1005,8 +1005,8 @@ packages:
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
- '@types/node@25.5.2':
- resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==}
+ '@types/node@25.6.0':
+ resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -1033,63 +1033,63 @@ packages:
'@types/svgo@2.6.4':
resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==}
- '@typescript-eslint/eslint-plugin@8.58.0':
- resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==}
+ '@typescript-eslint/eslint-plugin@8.58.2':
+ resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.58.0
+ '@typescript-eslint/parser': ^8.58.2
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/parser@8.58.0':
- resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==}
+ '@typescript-eslint/parser@8.58.2':
+ resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/project-service@8.58.0':
- resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==}
+ '@typescript-eslint/project-service@8.58.2':
+ resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/scope-manager@8.58.0':
- resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==}
+ '@typescript-eslint/scope-manager@8.58.2':
+ resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.58.0':
- resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==}
+ '@typescript-eslint/tsconfig-utils@8.58.2':
+ resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/type-utils@8.58.0':
- resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==}
+ '@typescript-eslint/type-utils@8.58.2':
+ resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/types@8.58.0':
- resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==}
+ '@typescript-eslint/types@8.58.2':
+ resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.58.0':
- resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==}
+ '@typescript-eslint/typescript-estree@8.58.2':
+ resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/utils@8.58.0':
- resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==}
+ '@typescript-eslint/utils@8.58.2':
+ resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/visitor-keys@8.58.0':
- resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==}
+ '@typescript-eslint/visitor-keys@8.58.2':
+ resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
acorn-jsx@5.3.2:
@@ -1158,8 +1158,8 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
- axe-core@4.11.2:
- resolution: {integrity: sha512-byD6KPdvo72y/wj2T/4zGEvvlis+PsZsn/yPS3pEO+sFpcrqRpX/TJCxvVaEsNeMrfQbCr7w163YqoD9IYwHXw==}
+ axe-core@4.11.3:
+ resolution: {integrity: sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==}
engines: {node: '>=4'}
babel-plugin-macros@3.1.0:
@@ -1181,8 +1181,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
- baseline-browser-mapping@2.10.14:
- resolution: {integrity: sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==}
+ baseline-browser-mapping@2.10.19:
+ resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -1212,11 +1212,11 @@ packages:
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
- brace-expansion@1.1.13:
- resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
+ brace-expansion@1.1.14:
+ resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==}
- brace-expansion@2.0.3:
- resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
+ brace-expansion@2.1.0:
+ resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
brace-expansion@5.0.5:
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
@@ -1260,8 +1260,8 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
- call-bind@1.0.8:
- resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
+ call-bind@1.0.9:
+ resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==}
engines: {node: '>= 0.4'}
call-bound@1.0.4:
@@ -1280,8 +1280,8 @@ packages:
resolution: {integrity: sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==}
engines: {node: '>=0.10.0'}
- caniuse-lite@1.0.30001784:
- resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==}
+ caniuse-lite@1.0.30001788:
+ resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==}
caw@2.0.1:
resolution: {integrity: sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==}
@@ -1516,8 +1516,8 @@ packages:
duplexer3@0.1.5:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
- electron-to-chromium@1.5.331:
- resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==}
+ electron-to-chromium@1.5.336:
+ resolution: {integrity: sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -1710,8 +1710,8 @@ packages:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- eslint@10.1.0:
- resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==}
+ eslint@10.2.0:
+ resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
@@ -2711,8 +2711,8 @@ packages:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
- postcss@8.5.8:
- resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
+ postcss@8.5.9:
+ resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==}
engines: {node: ^10 || ^12 || >=14}
powershell-utils@0.1.0:
@@ -2734,8 +2734,8 @@ packages:
resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
engines: {node: '>=4'}
- prettier@3.8.1:
- resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
+ prettier@3.8.3:
+ resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
engines: {node: '>=14'}
hasBin: true
@@ -2768,10 +2768,10 @@ packages:
rate-limiter-flexible@5.0.5:
resolution: {integrity: sha512-+/dSQfo+3FYwYygUs/V2BBdwGa9nFtakDwKt4l0bnvNB53TNT++QSFewwHX9qXrZJuMe9j+TUaU21lm5ARgqdQ==}
- react-dom@19.2.4:
- resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
+ react-dom@19.2.5:
+ resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
peerDependencies:
- react: ^19.2.4
+ react: ^19.2.5
react-icons@5.6.0:
resolution: {integrity: sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==}
@@ -2781,11 +2781,11 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
- react-is@19.2.4:
- resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==}
+ react-is@19.2.5:
+ resolution: {integrity: sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==}
- react-router@7.14.0:
- resolution: {integrity: sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==}
+ react-router@7.14.1:
+ resolution: {integrity: sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
@@ -2819,8 +2819,8 @@ packages:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- react@19.2.4:
- resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
+ react@19.2.5:
+ resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
engines: {node: '>=0.10.0'}
read-pkg-up@1.0.1:
@@ -2854,8 +2854,8 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
- resolve@1.22.11:
- resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
+ resolve@1.22.12:
+ resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
engines: {node: '>= 0.4'}
hasBin: true
@@ -2871,8 +2871,8 @@ packages:
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
- rolldown@1.0.0-rc.12:
- resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==}
+ rolldown@1.0.0-rc.15:
+ resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -3136,8 +3136,8 @@ packages:
resolution: {integrity: sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==}
engines: {node: '>=0.10.0'}
- tinyglobby@0.2.15:
- resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ tinyglobby@0.2.16:
+ resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
to-buffer@1.2.2:
@@ -3190,8 +3190,8 @@ packages:
peerDependencies:
typescript: '>=3.5.1'
- typescript-eslint@8.58.0:
- resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==}
+ typescript-eslint@8.58.2:
+ resolution: {integrity: sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -3205,8 +3205,8 @@ packages:
unbzip2-stream@1.4.3:
resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==}
- undici-types@7.18.2:
- resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+ undici-types@7.19.2:
+ resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
@@ -3254,14 +3254,14 @@ packages:
peerDependencies:
vite: 5.x || 6.x || 7.x || 8.x
- vite@8.0.3:
- resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==}
+ vite@8.0.8:
+ resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.1.0
- esbuild: ^0.27.0
+ esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
@@ -3513,18 +3513,18 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
- '@emnapi/core@1.9.1':
+ '@emnapi/core@1.9.2':
dependencies:
- '@emnapi/wasi-threads': 1.2.0
+ '@emnapi/wasi-threads': 1.2.1
tslib: 2.8.1
optional: true
- '@emnapi/runtime@1.9.1':
+ '@emnapi/runtime@1.9.2':
dependencies:
tslib: 2.8.1
optional: true
- '@emnapi/wasi-threads@1.2.0':
+ '@emnapi/wasi-threads@1.2.1':
dependencies:
tslib: 2.8.1
optional: true
@@ -3561,17 +3561,17 @@ snapshots:
'@emotion/memoize@0.9.0': {}
- '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4)':
+ '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@emotion/babel-plugin': 11.13.5
'@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3
- '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4)
+ '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.5)
'@emotion/utils': 1.4.2
'@emotion/weak-memoize': 0.4.0
hoist-non-react-statics: 3.3.2
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
transitivePeerDependencies:
@@ -3587,16 +3587,16 @@ snapshots:
'@emotion/sheet@1.4.0': {}
- '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)':
+ '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@emotion/babel-plugin': 11.13.5
'@emotion/is-prop-valid': 1.4.0
- '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
+ '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
'@emotion/serialize': 1.3.3
- '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.4)
+ '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.5)
'@emotion/utils': 1.4.2
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
transitivePeerDependencies:
@@ -3604,9 +3604,9 @@ snapshots:
'@emotion/unitless@0.10.0': {}
- '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.4)':
+ '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.5)':
dependencies:
- react: 19.2.4
+ react: 19.2.5
'@emotion/utils@1.4.2': {}
@@ -3693,38 +3693,38 @@ snapshots:
'@esbuild/win32-x64@0.27.4':
optional: true
- '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0)':
+ '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0)':
dependencies:
- eslint: 10.1.0
+ eslint: 10.2.0
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
- '@eslint/config-array@0.23.3':
+ '@eslint/config-array@0.23.5':
dependencies:
- '@eslint/object-schema': 3.0.3
+ '@eslint/object-schema': 3.0.5
debug: 4.4.3
minimatch: 10.2.5
transitivePeerDependencies:
- supports-color
- '@eslint/config-helpers@0.5.3':
+ '@eslint/config-helpers@0.5.5':
dependencies:
- '@eslint/core': 1.1.1
+ '@eslint/core': 1.2.1
- '@eslint/core@1.1.1':
+ '@eslint/core@1.2.1':
dependencies:
'@types/json-schema': 7.0.15
- '@eslint/js@10.0.1(eslint@10.1.0)':
+ '@eslint/js@10.0.1(eslint@10.2.0)':
optionalDependencies:
- eslint: 10.1.0
+ eslint: 10.2.0
- '@eslint/object-schema@3.0.3': {}
+ '@eslint/object-schema@3.0.5': {}
- '@eslint/plugin-kit@0.6.1':
+ '@eslint/plugin-kit@0.7.1':
dependencies:
- '@eslint/core': 1.1.1
+ '@eslint/core': 1.2.1
levn: 0.4.1
'@humanfs/core@0.19.1': {}
@@ -3762,47 +3762,47 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
- '@mui/core-downloads-tracker@7.3.9': {}
+ '@mui/core-downloads-tracker@9.0.0': {}
- '@mui/icons-material@7.3.9(@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)':
+ '@mui/icons-material@9.0.0(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
- '@mui/material': 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react: 19.2.4
+ '@mui/material': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
- '@mui/material@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
- '@mui/core-downloads-tracker': 7.3.9
- '@mui/system': 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
- '@mui/types': 7.4.12(@types/react@19.2.14)
- '@mui/utils': 7.3.9(@types/react@19.2.14)(react@19.2.4)
+ '@mui/core-downloads-tracker': 9.0.0
+ '@mui/system': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
+ '@mui/types': 9.0.0(@types/react@19.2.14)
+ '@mui/utils': 9.0.0(@types/react@19.2.14)(react@19.2.5)
'@popperjs/core': 2.11.8
'@types/react-transition-group': 4.4.12(@types/react@19.2.14)
clsx: 2.1.1
csstype: 3.2.3
prop-types: 15.8.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- react-is: 19.2.4
- react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-is: 19.2.5
+ react-transition-group: 4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
optionalDependencies:
- '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
- '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
+ '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
+ '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@types/react': 19.2.14
- '@mui/private-theming@7.3.9(@types/react@19.2.14)(react@19.2.4)':
+ '@mui/private-theming@9.0.0(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
- '@mui/utils': 7.3.9(@types/react@19.2.14)(react@19.2.4)
+ '@mui/utils': 9.0.0(@types/react@19.2.14)(react@19.2.5)
prop-types: 15.8.1
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
- '@mui/styled-engine@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)':
+ '@mui/styled-engine@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
'@emotion/cache': 11.14.0
@@ -3810,49 +3810,49 @@ snapshots:
'@emotion/sheet': 1.4.0
csstype: 3.2.3
prop-types: 15.8.1
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
- '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
- '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
+ '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
+ '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
- '@mui/system@7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)':
+ '@mui/system@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
- '@mui/private-theming': 7.3.9(@types/react@19.2.14)(react@19.2.4)
- '@mui/styled-engine': 7.3.9(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
- '@mui/types': 7.4.12(@types/react@19.2.14)
- '@mui/utils': 7.3.9(@types/react@19.2.14)(react@19.2.4)
+ '@mui/private-theming': 9.0.0(@types/react@19.2.14)(react@19.2.5)
+ '@mui/styled-engine': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)
+ '@mui/types': 9.0.0(@types/react@19.2.14)
+ '@mui/utils': 9.0.0(@types/react@19.2.14)(react@19.2.5)
clsx: 2.1.1
csstype: 3.2.3
prop-types: 15.8.1
- react: 19.2.4
+ react: 19.2.5
optionalDependencies:
- '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
- '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4)
+ '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
+ '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)
'@types/react': 19.2.14
- '@mui/types@7.4.12(@types/react@19.2.14)':
+ '@mui/types@9.0.0(@types/react@19.2.14)':
dependencies:
'@babel/runtime': 7.29.2
optionalDependencies:
'@types/react': 19.2.14
- '@mui/utils@7.3.9(@types/react@19.2.14)(react@19.2.4)':
+ '@mui/utils@9.0.0(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
- '@mui/types': 7.4.12(@types/react@19.2.14)
+ '@mui/types': 9.0.0(@types/react@19.2.14)
'@types/prop-types': 15.7.15
clsx: 2.1.1
prop-types: 15.8.1
- react: 19.2.4
- react-is: 19.2.4
+ react: 19.2.5
+ react-is: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
- '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)':
+ '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
dependencies:
- '@emnapi/core': 1.9.1
- '@emnapi/runtime': 1.9.1
+ '@emnapi/core': 1.9.2
+ '@emnapi/runtime': 1.9.2
'@tybys/wasm-util': 0.10.1
optional: true
@@ -3870,7 +3870,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
- '@oxc-project/types@0.122.0': {}
+ '@oxc-project/types@0.124.0': {}
'@paralleldrive/cuid2@2.3.1':
dependencies:
@@ -3882,19 +3882,19 @@ snapshots:
dependencies:
preact: 10.29.1
- '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1))':
+ '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.1)(rollup@4.59.0)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))':
dependencies:
'@babel/core': 7.29.0
'@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0)
'@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0)
- '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1))
+ '@prefresh/vite': 2.4.12(preact@10.29.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
'@rollup/pluginutils': 5.3.0(rollup@4.59.0)
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0)
debug: 4.4.3
magic-string: 0.30.21
picocolors: 1.1.1
- vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1)
- vite-prerender-plugin: 0.5.13(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1))
+ vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
+ vite-prerender-plugin: 0.5.13(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))
zimmerframe: 1.1.4
transitivePeerDependencies:
- preact
@@ -3909,7 +3909,7 @@ snapshots:
'@prefresh/utils@1.2.1': {}
- '@prefresh/vite@2.4.12(preact@10.29.1)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1))':
+ '@prefresh/vite@2.4.12(preact@10.29.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1))':
dependencies:
'@babel/core': 7.29.0
'@prefresh/babel-plugin': 0.5.3
@@ -3917,61 +3917,60 @@ snapshots:
'@prefresh/utils': 1.2.1
'@rollup/pluginutils': 4.2.1
preact: 10.29.1
- vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1)
+ vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
transitivePeerDependencies:
- supports-color
- '@rolldown/binding-android-arm64@1.0.0-rc.12':
+ '@rolldown/binding-android-arm64@1.0.0-rc.15':
optional: true
- '@rolldown/binding-darwin-arm64@1.0.0-rc.12':
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.15':
optional: true
- '@rolldown/binding-darwin-x64@1.0.0-rc.12':
+ '@rolldown/binding-darwin-x64@1.0.0-rc.15':
optional: true
- '@rolldown/binding-freebsd-x64@1.0.0-rc.12':
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.15':
optional: true
- '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12':
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
optional: true
- '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12':
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
optional: true
- '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12':
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
optional: true
- '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12':
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
optional: true
- '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12':
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
optional: true
- '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12':
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
optional: true
- '@rolldown/binding-linux-x64-musl@1.0.0-rc.12':
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
optional: true
- '@rolldown/binding-openharmony-arm64@1.0.0-rc.12':
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
optional: true
- '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)':
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
dependencies:
- '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
- transitivePeerDependencies:
- - '@emnapi/core'
- - '@emnapi/runtime'
+ '@emnapi/core': 1.9.2
+ '@emnapi/runtime': 1.9.2
+ '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
optional: true
- '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12':
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
optional: true
- '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12':
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
optional: true
- '@rolldown/pluginutils@1.0.0-rc.12': {}
+ '@rolldown/pluginutils@1.0.0-rc.15': {}
'@rollup/pluginutils@4.2.1':
dependencies:
@@ -4063,16 +4062,16 @@ snapshots:
'@sindresorhus/is@0.7.0': {}
- '@table-library/react-table-library@4.1.15(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ '@table-library/react-table-library@4.1.15(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
- '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4)
+ '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5)
clsx: 1.1.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
- react-virtualized-auto-sizer: 1.0.26(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
- react-window: 1.8.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
+ react-virtualized-auto-sizer: 1.0.26(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+ react-window: 1.8.11(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
- '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1)':
+ '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3)':
dependencies:
'@babel/generator': 7.29.1
'@babel/parser': 7.29.2
@@ -4082,7 +4081,7 @@ snapshots:
lodash-es: 4.18.1
minimatch: 9.0.9
parse-imports-exports: 0.2.4
- prettier: 3.8.1
+ prettier: 3.8.3
transitivePeerDependencies:
- supports-color
@@ -4098,7 +4097,7 @@ snapshots:
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 6.0.0
- '@types/node': 25.5.2
+ '@types/node': 25.6.0
'@types/imagemin-gifsicle@7.0.4':
dependencies:
@@ -4127,21 +4126,21 @@ snapshots:
'@types/imagemin@7.0.1':
dependencies:
- '@types/node': 25.5.2
+ '@types/node': 25.6.0
'@types/json-schema@7.0.15': {}
'@types/keyv@3.1.4':
dependencies:
- '@types/node': 25.5.2
+ '@types/node': 25.6.0
'@types/minimatch@6.0.0':
dependencies:
minimatch: 10.2.5
- '@types/node@25.5.2':
+ '@types/node@25.6.0':
dependencies:
- undici-types: 7.18.2
+ undici-types: 7.19.2
'@types/parse-json@4.0.2': {}
@@ -4161,21 +4160,21 @@ snapshots:
'@types/responselike@1.0.3':
dependencies:
- '@types/node': 25.5.2
+ '@types/node': 25.6.0
'@types/svgo@2.6.4':
dependencies:
- '@types/node': 25.5.2
+ '@types/node': 25.6.0
- '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0)(typescript@6.0.2))(eslint@10.1.0)(typescript@6.0.2)':
+ '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.58.0(eslint@10.1.0)(typescript@6.0.2)
- '@typescript-eslint/scope-manager': 8.58.0
- '@typescript-eslint/type-utils': 8.58.0(eslint@10.1.0)(typescript@6.0.2)
- '@typescript-eslint/utils': 8.58.0(eslint@10.1.0)(typescript@6.0.2)
- '@typescript-eslint/visitor-keys': 8.58.0
- eslint: 10.1.0
+ '@typescript-eslint/parser': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
+ '@typescript-eslint/scope-manager': 8.58.2
+ '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
+ '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
+ '@typescript-eslint/visitor-keys': 8.58.2
+ eslint: 10.2.0
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.2)
@@ -4183,79 +4182,79 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.58.0(eslint@10.1.0)(typescript@6.0.2)':
+ '@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.2)':
dependencies:
- '@typescript-eslint/scope-manager': 8.58.0
- '@typescript-eslint/types': 8.58.0
- '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2)
- '@typescript-eslint/visitor-keys': 8.58.0
+ '@typescript-eslint/scope-manager': 8.58.2
+ '@typescript-eslint/types': 8.58.2
+ '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2)
+ '@typescript-eslint/visitor-keys': 8.58.2
debug: 4.4.3
- eslint: 10.1.0
+ eslint: 10.2.0
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.58.0(typescript@6.0.2)':
+ '@typescript-eslint/project-service@8.58.2(typescript@6.0.2)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2)
- '@typescript-eslint/types': 8.58.0
+ '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2)
+ '@typescript-eslint/types': 8.58.2
debug: 4.4.3
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.58.0':
+ '@typescript-eslint/scope-manager@8.58.2':
dependencies:
- '@typescript-eslint/types': 8.58.0
- '@typescript-eslint/visitor-keys': 8.58.0
+ '@typescript-eslint/types': 8.58.2
+ '@typescript-eslint/visitor-keys': 8.58.2
- '@typescript-eslint/tsconfig-utils@8.58.0(typescript@6.0.2)':
+ '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.2)':
dependencies:
typescript: 6.0.2
- '@typescript-eslint/type-utils@8.58.0(eslint@10.1.0)(typescript@6.0.2)':
+ '@typescript-eslint/type-utils@8.58.2(eslint@10.2.0)(typescript@6.0.2)':
dependencies:
- '@typescript-eslint/types': 8.58.0
- '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2)
- '@typescript-eslint/utils': 8.58.0(eslint@10.1.0)(typescript@6.0.2)
+ '@typescript-eslint/types': 8.58.2
+ '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2)
+ '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
debug: 4.4.3
- eslint: 10.1.0
+ eslint: 10.2.0
ts-api-utils: 2.5.0(typescript@6.0.2)
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/types@8.58.0': {}
+ '@typescript-eslint/types@8.58.2': {}
- '@typescript-eslint/typescript-estree@8.58.0(typescript@6.0.2)':
+ '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2)':
dependencies:
- '@typescript-eslint/project-service': 8.58.0(typescript@6.0.2)
- '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@6.0.2)
- '@typescript-eslint/types': 8.58.0
- '@typescript-eslint/visitor-keys': 8.58.0
+ '@typescript-eslint/project-service': 8.58.2(typescript@6.0.2)
+ '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.2)
+ '@typescript-eslint/types': 8.58.2
+ '@typescript-eslint/visitor-keys': 8.58.2
debug: 4.4.3
minimatch: 10.2.5
semver: 7.7.4
- tinyglobby: 0.2.15
+ tinyglobby: 0.2.16
ts-api-utils: 2.5.0(typescript@6.0.2)
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.58.0(eslint@10.1.0)(typescript@6.0.2)':
+ '@typescript-eslint/utils@8.58.2(eslint@10.2.0)(typescript@6.0.2)':
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0)
- '@typescript-eslint/scope-manager': 8.58.0
- '@typescript-eslint/types': 8.58.0
- '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2)
- eslint: 10.1.0
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0)
+ '@typescript-eslint/scope-manager': 8.58.2
+ '@typescript-eslint/types': 8.58.2
+ '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2)
+ eslint: 10.2.0
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.58.0':
+ '@typescript-eslint/visitor-keys@8.58.2':
dependencies:
- '@typescript-eslint/types': 8.58.0
+ '@typescript-eslint/types': 8.58.2
eslint-visitor-keys: 5.0.1
acorn-jsx@5.3.2(acorn@8.16.0):
@@ -4308,13 +4307,13 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
- axe-core@4.11.2: {}
+ axe-core@4.11.3: {}
babel-plugin-macros@3.1.0:
dependencies:
'@babel/runtime': 7.29.2
cosmiconfig: 7.1.0
- resolve: 1.22.11
+ resolve: 1.22.12
babel-plugin-transform-hook-names@1.0.2(@babel/core@7.29.0):
dependencies:
@@ -4326,7 +4325,7 @@ snapshots:
base64-js@1.5.1: {}
- baseline-browser-mapping@2.10.14: {}
+ baseline-browser-mapping@2.10.19: {}
bin-build@3.0.0:
dependencies:
@@ -4368,12 +4367,12 @@ snapshots:
boolbase@1.0.0: {}
- brace-expansion@1.1.13:
+ brace-expansion@1.1.14:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
- brace-expansion@2.0.3:
+ brace-expansion@2.1.0:
dependencies:
balanced-match: 1.0.2
@@ -4387,9 +4386,9 @@ snapshots:
browserslist@4.28.2:
dependencies:
- baseline-browser-mapping: 2.10.14
- caniuse-lite: 1.0.30001784
- electron-to-chromium: 1.5.331
+ baseline-browser-mapping: 2.10.19
+ caniuse-lite: 1.0.30001788
+ electron-to-chromium: 1.5.336
node-releases: 2.0.37
update-browserslist-db: 1.2.3(browserslist@4.28.2)
@@ -4430,7 +4429,7 @@ snapshots:
es-errors: 1.3.0
function-bind: 1.1.2
- call-bind@1.0.8:
+ call-bind@1.0.9:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
@@ -4451,7 +4450,7 @@ snapshots:
camelcase@2.1.1: {}
- caniuse-lite@1.0.30001784: {}
+ caniuse-lite@1.0.30001788: {}
caw@2.0.1:
dependencies:
@@ -4753,7 +4752,7 @@ snapshots:
duplexer3@0.1.5: {}
- electron-to-chromium@1.5.331: {}
+ electron-to-chromium@1.5.336: {}
emoji-regex@10.6.0: {}
@@ -4899,9 +4898,9 @@ snapshots:
escape-string-regexp@4.0.0: {}
- eslint-config-prettier@10.1.8(eslint@10.1.0):
+ eslint-config-prettier@10.1.8(eslint@10.2.0):
dependencies:
- eslint: 10.1.0
+ eslint: 10.2.0
eslint-scope@9.1.2:
dependencies:
@@ -4914,14 +4913,14 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
- eslint@10.1.0:
+ eslint@10.2.0:
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0)
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0)
'@eslint-community/regexpp': 4.12.2
- '@eslint/config-array': 0.23.3
- '@eslint/config-helpers': 0.5.3
- '@eslint/core': 1.1.1
- '@eslint/plugin-kit': 0.6.1
+ '@eslint/config-array': 0.23.5
+ '@eslint/config-helpers': 0.5.5
+ '@eslint/core': 1.2.1
+ '@eslint/plugin-kit': 0.7.1
'@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
@@ -5697,11 +5696,11 @@ snapshots:
minimatch@3.1.5:
dependencies:
- brace-expansion: 1.1.13
+ brace-expansion: 1.1.14
minimatch@9.0.9:
dependencies:
- brace-expansion: 2.0.3
+ brace-expansion: 2.1.0
minimist@1.2.8: {}
@@ -5728,7 +5727,7 @@ snapshots:
normalize-package-data@2.5.0:
dependencies:
hosted-git-info: 2.8.9
- resolve: 1.22.11
+ resolve: 1.22.12
semver: 5.7.2
validate-npm-package-license: 3.0.4
@@ -5909,7 +5908,7 @@ snapshots:
possible-typed-array-names@1.1.0: {}
- postcss@8.5.8:
+ postcss@8.5.9:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
@@ -5925,7 +5924,7 @@ snapshots:
prepend-http@2.0.0: {}
- prettier@3.8.1: {}
+ prettier@3.8.3: {}
process-nextick-args@2.0.1: {}
@@ -5956,55 +5955,55 @@ snapshots:
rate-limiter-flexible@5.0.5: {}
- react-dom@19.2.4(react@19.2.4):
+ react-dom@19.2.5(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
scheduler: 0.27.0
- react-icons@5.6.0(react@19.2.4):
+ react-icons@5.6.0(react@19.2.5):
dependencies:
- react: 19.2.4
+ react: 19.2.5
react-is@16.13.1: {}
- react-is@19.2.4: {}
+ react-is@19.2.5: {}
- react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
cookie: 1.1.1
- react: 19.2.4
+ react: 19.2.5
set-cookie-parser: 2.7.2
optionalDependencies:
- react-dom: 19.2.4(react@19.2.4)
+ react-dom: 19.2.5(react@19.2.5)
- react-toastify@11.0.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ react-toastify@11.0.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
clsx: 2.1.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ react-transition-group@4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@babel/runtime': 7.29.2
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- react-virtualized-auto-sizer@1.0.26(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ react-virtualized-auto-sizer@1.0.26(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- react-window@1.8.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
+ react-window@1.8.11(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@babel/runtime': 7.29.2
memoize-one: 5.2.1
- react: 19.2.4
- react-dom: 19.2.4(react@19.2.4)
+ react: 19.2.5
+ react-dom: 19.2.5(react@19.2.5)
- react@19.2.4: {}
+ react@19.2.5: {}
read-pkg-up@1.0.1:
dependencies:
@@ -6042,8 +6041,9 @@ snapshots:
resolve-from@4.0.0: {}
- resolve@1.22.11:
+ resolve@1.22.12:
dependencies:
+ es-errors: 1.3.0
is-core-module: 2.16.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
@@ -6058,38 +6058,35 @@ snapshots:
dependencies:
glob: 7.2.3
- rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1):
+ rolldown@1.0.0-rc.15:
dependencies:
- '@oxc-project/types': 0.122.0
- '@rolldown/pluginutils': 1.0.0-rc.12
+ '@oxc-project/types': 0.124.0
+ '@rolldown/pluginutils': 1.0.0-rc.15
optionalDependencies:
- '@rolldown/binding-android-arm64': 1.0.0-rc.12
- '@rolldown/binding-darwin-arm64': 1.0.0-rc.12
- '@rolldown/binding-darwin-x64': 1.0.0-rc.12
- '@rolldown/binding-freebsd-x64': 1.0.0-rc.12
- '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12
- '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12
- '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12
- '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12
- '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12
- '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12
- '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12
- '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12
- '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
- '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12
- '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12
- transitivePeerDependencies:
- - '@emnapi/core'
- - '@emnapi/runtime'
+ '@rolldown/binding-android-arm64': 1.0.0-rc.15
+ '@rolldown/binding-darwin-arm64': 1.0.0-rc.15
+ '@rolldown/binding-darwin-x64': 1.0.0-rc.15
+ '@rolldown/binding-freebsd-x64': 1.0.0-rc.15
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15
+ '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15
+ '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15
+ '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15
- rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1))(rollup@4.59.0):
+ rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-rc.15)(rollup@4.59.0):
dependencies:
open: 11.0.0
picomatch: 4.0.4
source-map: 0.7.6
yargs: 18.0.0
optionalDependencies:
- rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
+ rolldown: 1.0.0-rc.15
rollup: 4.59.0
rollup@4.59.0:
@@ -6344,7 +6341,7 @@ snapshots:
timed-out@4.0.1: {}
- tinyglobby@0.2.15:
+ tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
@@ -6393,13 +6390,13 @@ snapshots:
dependencies:
typescript: 6.0.2
- typescript-eslint@8.58.0(eslint@10.1.0)(typescript@6.0.2):
+ typescript-eslint@8.58.2(eslint@10.2.0)(typescript@6.0.2):
dependencies:
- '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0)(typescript@6.0.2))(eslint@10.1.0)(typescript@6.0.2)
- '@typescript-eslint/parser': 8.58.0(eslint@10.1.0)(typescript@6.0.2)
- '@typescript-eslint/typescript-estree': 8.58.0(typescript@6.0.2)
- '@typescript-eslint/utils': 8.58.0(eslint@10.1.0)(typescript@6.0.2)
- eslint: 10.1.0
+ '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.2))(eslint@10.2.0)(typescript@6.0.2)
+ '@typescript-eslint/parser': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
+ '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.2)
+ '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.2)
+ eslint: 10.2.0
typescript: 6.0.2
transitivePeerDependencies:
- supports-color
@@ -6411,7 +6408,7 @@ snapshots:
buffer: 5.7.1
through: 2.3.8
- undici-types@7.18.2: {}
+ undici-types@7.19.2: {}
universalify@2.0.1: {}
@@ -6444,7 +6441,7 @@ snapshots:
spdx-correct: 3.2.0
spdx-expression-parse: 3.0.1
- vite-plugin-imagemin@0.6.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1)):
+ vite-plugin-imagemin@0.6.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)):
dependencies:
'@types/imagemin': 7.0.1
'@types/imagemin-gifsicle': 7.0.4
@@ -6469,11 +6466,11 @@ snapshots:
imagemin-webp: 6.1.0
jpegtran-bin: 6.0.1
pathe: 0.2.0
- vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1)
+ vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
transitivePeerDependencies:
- supports-color
- vite-prerender-plugin@0.5.13(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1)):
+ vite-prerender-plugin@0.5.13(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)):
dependencies:
kolorist: 1.8.0
magic-string: 0.30.21
@@ -6481,28 +6478,25 @@ snapshots:
simple-code-frame: 1.3.0
source-map: 0.7.6
stack-trace: 1.0.0-pre2
- vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1)
+ vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1)
- vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.2)(esbuild@0.27.4)(terser@5.46.1):
+ vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.4)(terser@5.46.1):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
- postcss: 8.5.8
- rolldown: 1.0.0-rc.12(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)
- tinyglobby: 0.2.15
+ postcss: 8.5.9
+ rolldown: 1.0.0-rc.15
+ tinyglobby: 0.2.16
optionalDependencies:
- '@types/node': 25.5.2
+ '@types/node': 25.6.0
esbuild: 0.27.4
fsevents: 2.3.3
terser: 5.46.1
- transitivePeerDependencies:
- - '@emnapi/core'
- - '@emnapi/runtime'
which-typed-array@1.1.20:
dependencies:
available-typed-arrays: 1.0.7
- call-bind: 1.0.8
+ call-bind: 1.0.9
call-bound: 1.0.4
for-each: 0.3.5
get-proto: 1.0.1
diff --git a/interface/src/SignIn.tsx b/interface/src/SignIn.tsx
index c113fff17..7b26d94dc 100644
--- a/interface/src/SignIn.tsx
+++ b/interface/src/SignIn.tsx
@@ -3,6 +3,7 @@ import { toast } from 'react-toastify';
import ForwardIcon from '@mui/icons-material/Forward';
import { Box, Button, Paper, Typography } from '@mui/material';
+import type { Theme } from '@mui/material/styles';
import * as AuthenticationApi from 'components/routing/authentication';
import { useRequest } from 'alova/client';
@@ -36,7 +37,7 @@ const SignIn = memo(() => {
{
immediate: false
}
- ).onSuccess((response) => {
+ ).onSuccess((response: { data: { access_token: string } }) => {
if (response.data) {
authenticationContext.signIn(response.data.access_token);
}
@@ -78,7 +79,6 @@ const SignIn = memo(() => {
}
}, [signInRequest, signIn, LL]);
- // Memoize callback to prevent recreation on every render
const submitOnEnter = useMemo(() => onEnterCallback(signIn), [signIn]);
// get rid of scrollbar
@@ -92,13 +92,15 @@ const SignIn = memo(() => {
return (
theme.breakpoints.values.sm}
+ sx={(theme: Theme) => ({
+ display: 'flex',
+ height: '100vh',
+ margin: 'auto',
+ padding: 2,
+ justifyContent: 'center',
+ flexDirection: 'column',
+ maxWidth: theme.breakpoints.values.sm
+ })}
>
({
@@ -111,16 +113,18 @@ const SignIn = memo(() => {
width: '100%'
})}
>
-
+
{PROJECT_NAME}
{
return (
{blocker ? : null}
-
- {LL.ENTITIES_HELP_1()}.
-
+
+ {LL.ENTITIES_HELP_1()}.
+
{renderEntity()}
@@ -361,8 +361,8 @@ const CustomEntities = () => {
/>
)}
-
-
+
+
{numChanges > 0 && (
)}
-
+
}
variant="outlined"
diff --git a/interface/src/app/main/CustomEntitiesDialog.tsx b/interface/src/app/main/CustomEntitiesDialog.tsx
index 1ec409d6e..170c5e888 100644
--- a/interface/src/app/main/CustomEntitiesDialog.tsx
+++ b/interface/src/app/main/CustomEntitiesDialog.tsx
@@ -7,7 +7,7 @@ import DoneIcon from '@mui/icons-material/Done';
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
-import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
+import RemoveIcon from '@mui/icons-material/RemoveCircleOutlined';
import {
Box,
Button,
@@ -178,7 +178,7 @@ const CustomEntitiesDialog = ({
onChange={updateFormValue}
/>
-
+
-
+
{!creating && (
-
+
}
variant="outlined"
diff --git a/interface/src/app/main/Customizations.tsx b/interface/src/app/main/Customizations.tsx
index 53fe71cfb..435c333bb 100644
--- a/interface/src/app/main/Customizations.tsx
+++ b/interface/src/app/main/Customizations.tsx
@@ -470,10 +470,10 @@ const Customizations = () => {
const renderDeviceList = () => (
<>
-
- {LL.CUSTOMIZATIONS_HELP_1()}.
-
-
+
+ {LL.CUSTOMIZATIONS_HELP_1()}.
+
+
{rename ? (
<>
{
const renderDeviceData = () => {
return (
<>
-
-
- ={LL.CUSTOMIZATIONS_HELP_2()}
-
- ={LL.CUSTOMIZATIONS_HELP_3()}
-
- =
- {LL.CUSTOMIZATIONS_HELP_4()}
- =
- {LL.CUSTOMIZATIONS_HELP_5()}
- ={LL.CUSTOMIZATIONS_HELP_6()}
-
-
+
+ ={LL.CUSTOMIZATIONS_HELP_2()}
+
+ ={LL.CUSTOMIZATIONS_HELP_3()}
+
+ =
+ {LL.CUSTOMIZATIONS_HELP_4()}
+ ={LL.CUSTOMIZATIONS_HELP_5()}
+
+ ={LL.CUSTOMIZATIONS_HELP_6()}
+
{
) : (
-
-
+
+
{numChanges !== 0 && (
-
+
-
+
©
{
return (
<>
-
- {LL.MODULES_DESCRIPTION()}.
-
+
+ {LL.MODULES_DESCRIPTION()}.
+
-
-
+
+
{numChanges !== 0 && (
-
+
{
return (
{blocker ? : null}
-
- {LL.SCHEDULER_HELP_1()}.
-
+
+ {LL.SCHEDULER_HELP_1()}.
+
{renderSchedule()}
{selectedScheduleItem && (
@@ -375,8 +375,8 @@ const Scheduler = () => {
/>
)}
-
-
+
+
{numChanges !== 0 && (
{
)}
-
+
}
diff --git a/interface/src/app/main/SchedulerDialog.tsx b/interface/src/app/main/SchedulerDialog.tsx
index 881aa14d0..8277da6e3 100644
--- a/interface/src/app/main/SchedulerDialog.tsx
+++ b/interface/src/app/main/SchedulerDialog.tsx
@@ -4,7 +4,7 @@ import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
-import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
+import RemoveIcon from '@mui/icons-material/RemoveCircleOutlined';
import {
Box,
Button,
@@ -338,11 +338,13 @@ const SchedulerDialog = ({
onChange={updateFormValue}
/>
{isTimerSchedule && (
-
-
- {LL.SCHEDULER_HELP_2()}
-
-
+
+ {LL.SCHEDULER_HELP_2()}
+
)}
>
) : (
@@ -391,7 +393,7 @@ const SchedulerDialog = ({
{!creating && (
-
+
}
variant="outlined"
diff --git a/interface/src/app/main/Sensors.tsx b/interface/src/app/main/Sensors.tsx
index 747034730..578140a43 100644
--- a/interface/src/app/main/Sensors.tsx
+++ b/interface/src/app/main/Sensors.tsx
@@ -591,7 +591,14 @@ const Sensors = () => {
/>
)}
{sensorData?.analog_enabled === true && me.admin && (
-
+
{fieldErrors && Object.keys(fieldErrors).length > 0 && (
-
+
{Object.values(fieldErrors).map((errArr, idx) =>
Array.isArray(errArr)
? errArr.map((err, j) => (
@@ -487,7 +487,7 @@ const SensorsAnalogDialog = ({
key={`${idx}-${j}`}
color="error"
variant="caption"
- display="block"
+ sx={{ display: 'block' }}
>
{err.message}
@@ -498,7 +498,7 @@ const SensorsAnalogDialog = ({
)}
{editItem.s && (
-
+
{!creating && (
-
+
}
disabled={editItem.s}
diff --git a/interface/src/app/main/SensorsTemperatureDialog.tsx b/interface/src/app/main/SensorsTemperatureDialog.tsx
index bc6ad2a09..31cdad43b 100644
--- a/interface/src/app/main/SensorsTemperatureDialog.tsx
+++ b/interface/src/app/main/SensorsTemperatureDialog.tsx
@@ -4,7 +4,6 @@ import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done';
import WarningIcon from '@mui/icons-material/Warning';
import {
- Box,
Button,
Dialog,
DialogActions,
@@ -111,11 +110,9 @@ const SensorsTemperatureDialog = ({
);
@@ -367,7 +385,9 @@ const InstallPartitionDialog = memo(
{LL.INSTALL()} {LL.STORED_VERSIONS()}
- {LL.INSTALL_VERSION(LL.INSTALL(), version)}
+
+ {LL.INSTALL_VERSION(LL.INSTALL(), version)}
+
{
const [stableUpgradeAvailable, setStableUpgradeAvailable] =
useState(false);
const [internetLive, setInternetLive] = useState(false);
+ const [downloadOnly, setDownloadOnly] = useState(false);
const [showVersionInfo, setShowVersionInfo] = useState(0); // 1 = stable, 2 = dev, 3 = partition
const [firmwareSize, setFirmwareSize] = useState(0);
@@ -444,6 +465,9 @@ const Version = () => {
error
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
const systemData = event.data as VersionData;
+ if (systemData.arduino_version.startsWith('Tasmota')) {
+ setDownloadOnly(true);
+ }
setUsingDevVersion(systemData.emsesp_version.includes('dev'));
});
@@ -459,6 +483,26 @@ const Version = () => {
immediate: false
});
+ const [upgradeImportantMessageType, setUpgradeImportantMessageType] =
+ useState(0);
+
+ const { send: checkUpgradeImportantMessages } = useRequest(
+ (version: string) =>
+ callAction({ action: 'upgradeImportantMessages', param: version }),
+ {
+ immediate: false
+ }
+ )
+ .onSuccess((event) => {
+ const upgradeImportantMessageType_n = (
+ event.data as { upgradeImportantMessageType: number }
+ ).upgradeImportantMessageType;
+ setUpgradeImportantMessageType(upgradeImportantMessageType_n);
+ })
+ .onError((error) => {
+ toast.error(String(error.error?.message || 'An error occurred'));
+ });
+
// Memoized values
const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]);
@@ -524,10 +568,16 @@ const Version = () => {
[]
);
- const showFirmwareDialog = useCallback((useDevVersion: boolean) => {
- setFetchDevVersion(useDevVersion);
- setOpenInstallDialog(true);
- }, []);
+ const showFirmwareDialog = useCallback(
+ (useDevVersion: boolean) => {
+ setFetchDevVersion(useDevVersion);
+ void checkUpgradeImportantMessages(
+ useDevVersion ? latestDevVersion?.name : latestVersion?.name
+ );
+ setOpenInstallDialog(true);
+ },
+ [latestDevVersion, latestVersion, fetchDevVersion]
+ );
const closeInstallDialog = useCallback(() => {
setOpenInstallDialog(false);
@@ -629,8 +679,8 @@ const Version = () => {
return (
<>
-
-
+
+
{LL.THIS_VERSION()}
@@ -695,7 +745,7 @@ const Version = () => {
{internetLive ? (
<>
-
+
{LL.AVAILABLE_VERSION()}
@@ -717,7 +767,7 @@ const Version = () => {
{otherPartitions.map((partition) => (
-
+
{partition.version}
@@ -783,7 +833,7 @@ const Version = () => {
>
) : (
-
+
{LL.INTERNET_CONNECTION_REQUIRED()}
@@ -807,6 +857,8 @@ const Version = () => {
fetchDevVersion={fetchDevVersion}
latestVersion={latestVersion}
latestDevVersion={latestDevVersion}
+ upgradeImportantMessageType={upgradeImportantMessageType}
+ downloadOnly={downloadOnly}
platform={platform}
LL={LL}
onClose={closeInstallDialog}
@@ -823,7 +875,7 @@ const Version = () => {
{LL.UPLOAD()}
-
+
>
)}
@@ -842,6 +894,7 @@ const Version = () => {
locale,
openInstallDialog,
fetchDevVersion,
+ downloadOnly,
me.admin,
showButtons,
handleVersionInfoClose,
diff --git a/interface/src/components/MessageBox.tsx b/interface/src/components/MessageBox.tsx
index d53e86776..aa17e6215 100644
--- a/interface/src/components/MessageBox.tsx
+++ b/interface/src/components/MessageBox.tsx
@@ -53,12 +53,16 @@ const MessageBox: FC> = ({
return (
{(message || children) && (
diff --git a/interface/src/components/layout/LayoutDrawer.tsx b/interface/src/components/layout/LayoutDrawer.tsx
index 8c85f666e..c89fb396b 100644
--- a/interface/src/components/layout/LayoutDrawer.tsx
+++ b/interface/src/components/layout/LayoutDrawer.tsx
@@ -29,7 +29,7 @@ const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
() => (
<>
-
+
{PROJECT_NAME}
diff --git a/interface/src/components/layout/LayoutMenu.tsx b/interface/src/components/layout/LayoutMenu.tsx
index d37c96b5c..1fa3830b5 100644
--- a/interface/src/components/layout/LayoutMenu.tsx
+++ b/interface/src/components/layout/LayoutMenu.tsx
@@ -51,9 +51,7 @@ const LayoutMenuComponent = () => {
sx={{ my: 0 }}
slotProps={{
primary: {
- fontWeight: '600',
- mb: '2px',
- color: 'lightblue'
+ sx: { fontWeight: 600, mb: '2px', color: 'lightblue' }
}
}}
/>
diff --git a/interface/src/components/loading/FormLoader.tsx b/interface/src/components/loading/FormLoader.tsx
index 762909bc4..42be38bad 100644
--- a/interface/src/components/loading/FormLoader.tsx
+++ b/interface/src/components/loading/FormLoader.tsx
@@ -32,8 +32,16 @@ const FormLoaderComponent = ({ errorMessage, onRetry }: FormLoaderProps) => {
);
}
return (
-
-
+
+
diff --git a/interface/src/components/loading/LoadingSpinner.tsx b/interface/src/components/loading/LoadingSpinner.tsx
index c979f8c27..d4e052c5d 100644
--- a/interface/src/components/loading/LoadingSpinner.tsx
+++ b/interface/src/components/loading/LoadingSpinner.tsx
@@ -15,12 +15,14 @@ const circularProgressStyles: SxProps = (theme: Theme) => ({
const LoadingSpinner = ({ height = '100%' }: LoadingSpinnerProps) => {
return (
diff --git a/interface/src/components/upload/DragNdrop.tsx b/interface/src/components/upload/DragNdrop.tsx
index 1449f5e6c..4bc865fc3 100644
--- a/interface/src/components/upload/DragNdrop.tsx
+++ b/interface/src/components/upload/DragNdrop.tsx
@@ -1,4 +1,4 @@
-// Code inspired by Prince Azubuike from https://medium.com/@dprincecoder/creating-a-drag-and-drop-file-upload-component-in-react-a-step-by-step-guide-4d93b6cc21e0
+// drag/drop code inspired by Prince Azubuike from https://medium.com/@dprincecoder/creating-a-drag-and-drop-file-upload-component-in-react-a-step-by-step-guide-4d93b6cc21e0
import {
type ChangeEvent,
type DragEvent,
@@ -6,12 +6,28 @@ import {
useRef,
useState
} from 'react';
+import { Link } from 'react-router';
+import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import UploadIcon from '@mui/icons-material/Upload';
-import { Box, Button, Typography, styled } from '@mui/material';
+import WarningIcon from '@mui/icons-material/Warning';
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Typography,
+ styled
+} from '@mui/material';
+import { callAction } from 'api/app';
+
+import { dialogStyle } from '@/CustomTheme';
+import { useRequest } from 'alova/client';
import { useI18nContext } from 'i18n/i18n-react';
const DocumentUploader = styled(Box)<{ active?: boolean }>(({ theme, active }) => ({
@@ -58,6 +74,29 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
const [dragged, setDragged] = useState(false);
const inputRef = useRef(null);
const { LL } = useI18nContext();
+ const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
+ const [upgradeImportantMessageType, setUpgradeImportantMessageType] =
+ useState(0);
+
+ const { send: checkUpgradeImportantMessages } = useRequest(
+ (version: string) =>
+ callAction({ action: 'upgradeImportantMessages', param: version }),
+ {
+ immediate: false
+ }
+ )
+ .onSuccess((event) => {
+ const upgradeImportantMessageType_n = (
+ event.data as { upgradeImportantMessageType: number }
+ ).upgradeImportantMessageType;
+ setUpgradeImportantMessageType(upgradeImportantMessageType_n);
+ if (upgradeImportantMessageType_n === 0) {
+ onFileSelected(file);
+ }
+ })
+ .onError((error) => {
+ toast.error(String(error.error?.message || 'An error occurred'));
+ });
const checkFileExtension = (file: File) => {
const validExtensions = ['.json', '.bin', '.md5'];
@@ -97,9 +136,8 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
const handleUploadClick = (event: MouseEvent) => {
event.stopPropagation();
- if (file) {
- onFileSelected(file);
- }
+ void checkUpgradeImportantMessages(file?.name || '');
+ setShowUpgradeDialog(true);
};
const handleBrowseClick = () => {
@@ -158,6 +196,55 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
{LL.UPLOAD()}
+ {showUpgradeDialog && upgradeImportantMessageType > 0 && (
+ setShowUpgradeDialog(false)}
+ >
+
+
+
+ {LL.UPGRADE_IMPORTANT_MESSAGES()}
+
+
+ {upgradeImportantMessageType === 1 &&
+ LL.UPGRADE_IMPORTANT_MESSAGES_1()}
+ {upgradeImportantMessageType === 2 &&
+ LL.UPGRADE_IMPORTANT_MESSAGES_2()}
+
+
+ {LL.ONLINE_HELP()}
+
+
+
+
+ }
+ variant="outlined"
+ onClick={() => setShowUpgradeDialog(false)}
+ color="secondary"
+ >
+ {LL.CANCEL()}
+
+ }
+ variant="outlined"
+ onClick={() => onFileSelected(file)}
+ color="primary"
+ >
+ {LL.UPLOAD()}
+
+
+
+ )}
>
)}
diff --git a/interface/src/components/upload/SingleUpload.tsx b/interface/src/components/upload/SingleUpload.tsx
index 5ad6941f2..0363cd445 100644
--- a/interface/src/components/upload/SingleUpload.tsx
+++ b/interface/src/components/upload/SingleUpload.tsx
@@ -13,11 +13,10 @@ import DragNdrop from './DragNdrop';
import { LinearProgressWithLabel } from './LinearProgressWithLabel';
interface SingleUploadProps {
- text: string;
doRestart: () => void;
}
-const SingleUpload = ({ text, doRestart }: SingleUploadProps) => {
+const SingleUpload = ({ doRestart }: SingleUploadProps) => {
const [md5, setMd5] = useState();
const [file, setFile] = useState();
const { LL } = useI18nContext();
@@ -58,7 +57,7 @@ const SingleUpload = ({ text, doRestart }: SingleUploadProps) => {
<>
{isUploading ? (
<>
-
+
{
>
) : (
-
+
)}
{md5 && (
-
+
{'MD5: ' + md5}
)}
diff --git a/interface/src/i18n/cz/index.ts b/interface/src/i18n/cz/index.ts
index 2f0774454..9bc067dfb 100644
--- a/interface/src/i18n/cz/index.ts
+++ b/interface/src/i18n/cz/index.ts
@@ -359,7 +359,12 @@ const cz: Translation = {
NO_DATA: 'Žádná data',
USER_PROFILE: 'Uživatelský profil',
STORED_VERSIONS: 'Uložené verze',
- ONLINE_HELP: 'online nápověda'
+ ONLINE_HELP: 'online nápověda',
+ UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovat důležité zprávy',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že jste vytvořili zálohu své konfigurace a nastavení před pokračováním a nahrajte ji po instalaci nové verze.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete se na novou hlavní verzi. Ujistěte se, že jste přečetli ChangeLog pro jakékoliv závažné změny.',
+ WARNING_SYSTEM_BACKUP: 'Toto vytvoří zálohu vašich celých systémových konfigurací a nastavení. Všechna hesla budou v zálohovém souboru čitelná. Buďte opatrní při sdílení! Opravdu chcete pokračovat?'
+
};
export default cz;
diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts
index 2411acaab..12c1e6ed7 100644
--- a/interface/src/i18n/de/index.ts
+++ b/interface/src/i18n/de/index.ts
@@ -359,7 +359,12 @@ const de: Translation = {
NO_DATA: 'Keine Daten',
USER_PROFILE: 'Benutzerprofil',
STORED_VERSIONS: 'Gespeicherte Versionen',
- ONLINE_HELP: 'Online-Hilfe'
+ ONLINE_HELP: 'Online-Hilfe',
+ UPGRADE_IMPORTANT_MESSAGES: 'Wichtige Nachrichten aktualisieren',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'Diese Aktualisierung erfordert eine Werkseinstellung. Stellen Sie sicher, dass Sie eine Sicherung Ihrer Konfiguration und Einstellungen vor dem Fortfahren erstellt haben und diese nach der Installation der neuen Version hochladen.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'Sie aktualisieren auf eine neue Hauptversion. Stellen Sie sicher, dass Sie den ChangeLog für alle wichtigen Änderungen gelesen haben.',
+ WARNING_SYSTEM_BACKUP: 'Dies wird eine Sicherung Ihrer vollständigen Systemkonfiguration und -einstellungen erstellen. Alle Passwörter werden im Sicherungsdatei lesbar sein. Seien Sie vorsichtig beim Teilen! Möchten Sie fortfahren?'
+
};
export default de;
diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts
index 6549cee7a..8c8f8dcbd 100644
--- a/interface/src/i18n/en/index.ts
+++ b/interface/src/i18n/en/index.ts
@@ -359,7 +359,12 @@ const en: Translation = {
NO_DATA: 'No data',
USER_PROFILE: 'User Profile',
STORED_VERSIONS: 'Stored Versions',
- ONLINE_HELP: 'online help'
+ ONLINE_HELP: 'online help',
+ UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Important Messages',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'This upgrade requires a factory reset. Make sure you have made a backup of your configuration and settings before continuing, and upload this after the new version is installed.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'You are upgrading to a new major version. Make sure you have read the ChangeLog for any breaking changes.',
+ WARNING_SYSTEM_BACKUP: 'This will create a backup of your full system configuration and settings. All passwords will be readable in the backup file. Be careful with sharing! Do you want to continue?'
+
};
export default en;
diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts
index 261ef5ac9..e576a5950 100644
--- a/interface/src/i18n/fr/index.ts
+++ b/interface/src/i18n/fr/index.ts
@@ -359,7 +359,12 @@ const fr: Translation = {
NO_DATA: 'Aucune donnée',
USER_PROFILE: 'Profil utilisateur',
STORED_VERSIONS: 'Versions stockées',
- ONLINE_HELP: 'aide en ligne'
+ ONLINE_HELP: 'aide en ligne',
+ UPGRADE_IMPORTANT_MESSAGES: 'Mettre à jour les messages importants',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous d\'avoir créé une sauvegarde de vos configurations et paramètres avant de continuer et de la charger après l\'installation de la nouvelle version.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'Vous mettez à jour vers une nouvelle version majeure. Assurez-vous de lire le ChangeLog pour tout changement important.',
+ WARNING_SYSTEM_BACKUP: 'Cela créera une sauvegarde de votre configuration et paramètres complets. Tous les mots de passe seront lisibles dans le fichier de sauvegarde. Soyez prudent avec le partage ! Voulez-vous continuer ?'
+
};
export default fr;
diff --git a/interface/src/i18n/it/index.ts b/interface/src/i18n/it/index.ts
index 042415c95..4e86cbc03 100644
--- a/interface/src/i18n/it/index.ts
+++ b/interface/src/i18n/it/index.ts
@@ -359,7 +359,12 @@ const it: Translation = {
NO_DATA: 'Nessun dato',
USER_PROFILE: 'Profilo utente',
STORED_VERSIONS: 'Versioni memorizzate',
- ONLINE_HELP: 'aiuto online'
+ ONLINE_HELP: 'aiuto online',
+ UPGRADE_IMPORTANT_MESSAGES: 'Aggiorna Messaggi Importanti',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di aver creato un backup delle tue configurazioni e impostazioni prima di continuare e di caricarlo dopo l\'installazione della nuova versione.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'Stai aggiornando a una nuova versione principale. Assicurati di aver letto il ChangeLog per qualsiasi cambiamento importante.',
+ WARNING_SYSTEM_BACKUP: 'Questo creerà un backup delle tue configurazioni e impostazioni complete. Tutte le password saranno leggibili nel file di backup. Sei sicuro di voler continuare?'
+
};
export default it;
diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts
index 308c21075..f246aa684 100644
--- a/interface/src/i18n/nl/index.ts
+++ b/interface/src/i18n/nl/index.ts
@@ -359,7 +359,12 @@ const nl: Translation = {
NO_DATA: 'Geen data',
USER_PROFILE: 'Gebruikersprofiel',
STORED_VERSIONS: 'Opgeslagen versies',
- ONLINE_HELP: 'online help'
+ ONLINE_HELP: 'online help',
+ UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Belangrijke Berichten',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u een back-up van uw configuratie en instellingen hebt gemaakt voordat u doorgaat en upload deze na de installatie van de nieuwe versie.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'U updatet naar een nieuwe grote versie. Zorg ervoor dat u de ChangeLog hebt gelezen voor alle brekende wijzigingen.',
+ WARNING_SYSTEM_BACKUP: 'Dit zal een back-up van uw volledige systeemconfiguratie en instellingen maken. Alle wachtwoorden zijn leesbaar in het back-upbestand. Wees voorzichtig bij delen! Wilt u doorgaan?'
+
};
export default nl;
diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts
index 5afd33c4e..84f56b6ae 100644
--- a/interface/src/i18n/no/index.ts
+++ b/interface/src/i18n/no/index.ts
@@ -359,7 +359,12 @@ const no: Translation = {
NO_DATA: 'Ingen data',
USER_PROFILE: 'Brukerprofil',
STORED_VERSIONS: 'Lagret versjoner',
- ONLINE_HELP: 'online hjelp'
+ ONLINE_HELP: 'online hjelp',
+ UPGRADE_IMPORTANT_MESSAGES: 'Oppdater viktige meldinger',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du har laget en sikkerhetskopi av din konfigurasjon og innstillinger før du fortsetter, og last denne opp etter at den nye versjonen er installert.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'Du oppdaterer til en ny hovedversjon. Sørg for at du har lest ChangeLog for eventuelle bruddende endringer.',
+ WARNING_SYSTEM_BACKUP: 'Dette vil lage en sikkerhetskopi av din fullstendige systemkonfigurasjon og innstillinger. Alle passord vil være lesbare i sikkerhetskopien. Vær forsiktig med deling! Vil du fortsette?'
+
};
export default no;
diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts
index 9ba19d73c..da445c469 100644
--- a/interface/src/i18n/pl/index.ts
+++ b/interface/src/i18n/pl/index.ts
@@ -359,7 +359,12 @@ const pl: BaseTranslation = {
NO_DATA: 'Brak danych',
USER_PROFILE: 'Profil użytkownika',
STORED_VERSIONS: 'Zapisane wersje',
- ONLINE_HELP: 'pomoc online'
+ ONLINE_HELP: 'pomoc online',
+ UPGRADE_IMPORTANT_MESSAGES: 'Aktualizuj ważne wiadomości',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że masz utworzoną kopię swoich ustawień i konfiguracji przed kontynuowaniem i przesuń ją po zainstalowaniu nowej wersji.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujesz się do nowej głównej wersji. Upewnij się, że przeczytałeś ChangeLog dla wszelkich istotnych zmian.',
+ WARNING_SYSTEM_BACKUP: 'To spowoduje utworzenie kopii zapasowej całej konfiguracji i ustawień systemu. Wszystkie hasła będą widoczne w pliku kopii zapasowej. Bądź ostrożny przy udostępnianiu! Chcesz kontynuować?'
+
};
export default pl;
diff --git a/interface/src/i18n/sk/index.ts b/interface/src/i18n/sk/index.ts
index 56aa89dfe..0f4697fe0 100644
--- a/interface/src/i18n/sk/index.ts
+++ b/interface/src/i18n/sk/index.ts
@@ -4,7 +4,7 @@ const sk: Translation = {
LANGUAGE: 'Jazyk',
RETRY: 'Opakovať',
LOADING: 'Načítanie',
- IS_REQUIRED: '{0} je požadovaných',
+ IS_REQUIRED: '{0} je požadovaná',
SIGN_IN: 'Prihlásiť sa',
SIGN_OUT: 'Odhlásiť sa',
USERNAME: 'Užívateľské meno',
@@ -276,11 +276,11 @@ const sk: Translation = {
NETWORK_SUBNET: 'Maska podsiete',
NETWORK_DNS: 'DNS servery',
ADDRESS_OF: '{0} adresa',
- ADMINISTRATOR: 'Administrator',
+ ADMINISTRATOR: 'Administrátor',
GUEST: 'Hosť',
- NEW: 'Nová',
+ NEW: 'Novú',
NEW_NAME_OF: 'Nový názov {0}',
- ENTITY: 'entita',
+ ENTITY: 'entitu',
MIN: 'min',
MAX: 'max',
BLOCK_NAVIGATE_1: 'Máte neuložené zmeny',
@@ -359,7 +359,12 @@ const sk: Translation = {
NO_DATA: 'Žiadne dáta',
USER_PROFILE: 'Profil používateľa',
STORED_VERSIONS: 'Uložené verzie',
- ONLINE_HELP: 'online pomoc'
+ ONLINE_HELP: 'online pomoc',
+ UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovať dôležité správy',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že ste vytvorili zálohu svojich konfigurácií a nastavení pred pokračovaním a nahrajte ju po instalácii novej verzie.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'Aktualizujete sa na novú hlavnú verziu. Uistite sa, že ste prečítali ChangeLog pre akékoľvek dôležité zmeny.',
+ WARNING_SYSTEM_BACKUP: 'Toto vytvorí zálohu všetkých vašich celých systémových konfigurácií a nastavení. Všetky hesla budú čitateľné v zálohovom súbore. Buďte opatrní pri zdieľaní! Chcete pokračovať?'
+
};
export default sk;
diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts
index 4641ce56a..3cff59b90 100644
--- a/interface/src/i18n/sv/index.ts
+++ b/interface/src/i18n/sv/index.ts
@@ -359,7 +359,12 @@ const sv: Translation = {
NO_DATA: 'Ingen data',
USER_PROFILE: 'Användarprofil',
STORED_VERSIONS: 'Lagrad versioner',
- ONLINE_HELP: 'online hjälp'
+ ONLINE_HELP: 'online hjälp',
+ UPGRADE_IMPORTANT_MESSAGES: 'Uppdatera viktiga meddelanden',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du har gjort en säkerhetskopia av din konfiguration och inställningar innan du fortsätter och ladda upp denna efter att den nya versionen är installerad.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'Du uppdaterar till en ny huvudversion. Se till att du har läst ChangeLog för eventuella brkande ändringar.',
+ WARNING_SYSTEM_BACKUP: 'Detta kommer att skapa en säkerhetskopia av din fullständiga systemkonfiguration och inställningar. Alla lösenord kommer att vara läsbara i säkerhetskopien. Var försiktig med att dela! Vill du fortsätta?'
+
};
export default sv;
diff --git a/interface/src/i18n/tr/index.ts b/interface/src/i18n/tr/index.ts
index 29c7450b8..4661b0b2f 100644
--- a/interface/src/i18n/tr/index.ts
+++ b/interface/src/i18n/tr/index.ts
@@ -359,7 +359,12 @@ const tr: Translation = {
NO_DATA: 'Hiçbir veri yok',
USER_PROFILE: 'Kullanıcı Profili',
STORED_VERSIONS: 'Kaydedilmiş Sürümler',
- ONLINE_HELP: 'online yardım'
+ ONLINE_HELP: 'online yardım',
+ UPGRADE_IMPORTANT_MESSAGES: 'Önemli Mesajları Güncelle',
+ UPGRADE_IMPORTANT_MESSAGES_1: 'Bu güncelleme továrnı ayarlarını gerektirir. Yapılandırmanızı ve ayarlarınızı önce yedekleyin ve ardından yeni sürüm yüklendikten sonra yükleyin.',
+ UPGRADE_IMPORTANT_MESSAGES_2: 'Yeni bir büyük sürüme yükselteceksiniz. Değişiklikleri ChangeLogı okuduğunuzdan emin olun.',
+ WARNING_SYSTEM_BACKUP: 'Bu, sistem yapılandırmanızı ve ayarlarınızın bir yedeklemesi oluşturacaktır. Tüm şifreler yedekleme dosyasında okunabilir olacaktır. Paylaşırken dikkatli olun! Devam etmek istediğinize emin misiniz?'
+
};
export default tr;
diff --git a/lib/espMqttClient/src/Transport/ClientPosix.cpp b/lib/espMqttClient/src/Transport/ClientPosix.cpp
index d5ceac48f..dd4eebfd3 100644
--- a/lib/espMqttClient/src/Transport/ClientPosix.cpp
+++ b/lib/espMqttClient/src/Transport/ClientPosix.cpp
@@ -8,7 +8,7 @@ the LICENSE file.
#include "ClientPosix.h"
-#if defined(__linux__)
+#if defined(__linux__) || defined(__APPLE__)
namespace espMqttClientInternals {
diff --git a/lib/espMqttClient/src/Transport/ClientPosix.h b/lib/espMqttClient/src/Transport/ClientPosix.h
index af0dd4bff..ca11bd1f2 100644
--- a/lib/espMqttClient/src/Transport/ClientPosix.h
+++ b/lib/espMqttClient/src/Transport/ClientPosix.h
@@ -8,7 +8,7 @@ the LICENSE file.
#pragma once
-#if defined(__linux__)
+#if defined(__linux__) || defined(__APPLE__)
#include
#include
diff --git a/lib/espMqttClient/src/espMqttClient.cpp b/lib/espMqttClient/src/espMqttClient.cpp
index 74d0ace67..6649487cc 100644
--- a/lib/espMqttClient/src/espMqttClient.cpp
+++ b/lib/espMqttClient/src/espMqttClient.cpp
@@ -69,7 +69,7 @@ espMqttClientSecure & espMqttClientSecure::setPreSharedKey(const char * pskIdent
#endif
-#if defined(__linux__)
+#if defined(__linux__) || defined(__APPLE__)
espMqttClient::espMqttClient()
: MqttClientSetup(espMqttClientTypes::UseInternalTask::NO)
, _client() {
diff --git a/lib/espMqttClient/src/espMqttClient.h b/lib/espMqttClient/src/espMqttClient.h
index 128a801be..b31a8ea64 100644
--- a/lib/espMqttClient/src/espMqttClient.h
+++ b/lib/espMqttClient/src/espMqttClient.h
@@ -14,7 +14,7 @@ the LICENSE file.
#if defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
#include "Transport/ClientSync.h"
#include "Transport/ClientSecureSync.h"
-#elif defined(__linux__)
+#elif defined(__linux__) || defined(__APPLE__)
#include "Transport/ClientPosix.h"
#endif
@@ -74,7 +74,7 @@ class espMqttClientSecure : public MqttClientSetup {
#endif
-#if defined(__linux__)
+#if defined(__linux__) || defined(__APPLE__)
class espMqttClient : public MqttClientSetup {
public:
espMqttClient();
diff --git a/mock-api/package.json b/mock-api/package.json
index d5681fd0a..4ba91519e 100644
--- a/mock-api/package.json
+++ b/mock-api/package.json
@@ -13,7 +13,7 @@
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"formidable": "^3.5.4",
"itty-router": "^5.0.23",
- "prettier": "^3.8.1"
+ "prettier": "^3.8.3"
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
}
diff --git a/mock-api/pnpm-lock.yaml b/mock-api/pnpm-lock.yaml
index 21e29fa74..d1f155ffa 100644
--- a/mock-api/pnpm-lock.yaml
+++ b/mock-api/pnpm-lock.yaml
@@ -13,7 +13,7 @@ importers:
version: 3.1.3
'@trivago/prettier-plugin-sort-imports':
specifier: ^6.0.2
- version: 6.0.2(prettier@3.8.1)
+ version: 6.0.2(prettier@3.8.3)
formidable:
specifier: ^3.5.4
version: 3.5.4
@@ -21,8 +21,8 @@ importers:
specifier: ^5.0.23
version: 5.0.23
prettier:
- specifier: ^3.8.1
- version: 3.8.1
+ specifier: ^3.8.3
+ version: 3.8.3
packages:
@@ -112,8 +112,8 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
- brace-expansion@2.0.3:
- resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
+ brace-expansion@2.1.0:
+ resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
@@ -145,8 +145,8 @@ packages:
engines: {node: '>=6'}
hasBin: true
- lodash-es@4.17.23:
- resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
+ lodash-es@4.18.1:
+ resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
minimatch@9.0.9:
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
@@ -167,8 +167,8 @@ packages:
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
- prettier@3.8.1:
- resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
+ prettier@3.8.3:
+ resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
engines: {node: '>=14'}
hasBin: true
@@ -246,17 +246,17 @@ snapshots:
dependencies:
'@noble/hashes': 1.8.0
- '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.1)':
+ '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.8.3)':
dependencies:
'@babel/generator': 7.29.1
'@babel/parser': 7.29.2
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
javascript-natural-sort: 0.7.1
- lodash-es: 4.17.23
+ lodash-es: 4.18.1
minimatch: 9.0.9
parse-imports-exports: 0.2.4
- prettier: 3.8.1
+ prettier: 3.8.3
transitivePeerDependencies:
- supports-color
@@ -264,7 +264,7 @@ snapshots:
balanced-match@1.0.2: {}
- brace-expansion@2.0.3:
+ brace-expansion@2.1.0:
dependencies:
balanced-match: 1.0.2
@@ -291,11 +291,11 @@ snapshots:
jsesc@3.1.0: {}
- lodash-es@4.17.23: {}
+ lodash-es@4.18.1: {}
minimatch@9.0.9:
dependencies:
- brace-expansion: 2.0.3
+ brace-expansion: 2.1.0
ms@2.1.3: {}
@@ -311,6 +311,6 @@ snapshots:
picocolors@1.1.1: {}
- prettier@3.8.1: {}
+ prettier@3.8.3: {}
wrappy@1.0.2: {}
diff --git a/mock-api/restServer.ts b/mock-api/restServer.ts
index fe079638d..e7e924c33 100644
--- a/mock-api/restServer.ts
+++ b/mock-api/restServer.ts
@@ -388,13 +388,34 @@ function custom_support() {
'',
"For help and questions please contact your installer."
],
- img_url: 'https://emsesp.org/_media/images/designer.png'
+ img_url: 'https://emsesp.org/media/images/designer.png'
// img_url: 'https://picsum.photos/200/300'
}
};
}
-// called by Action endpoint
+// called by Action endpoint upgradeImportantMessages
+function upgradeImportantMessages(version: string) {
+ // 0 is do nothing
+ // 1 means 3.9 and factory reset required
+ // 2 means a major version upgrade
+ let upgradeImportantMessageType_n = 0;
+
+ // see if its a filename with a .bin extension
+ if (version.endsWith('.bin')) {
+ upgradeImportantMessageType_n = 1; // 1 means 3.9 and factory reset required
+ } else if (version.endsWith('.md')) {
+ upgradeImportantMessageType_n = 0;
+ } else {
+ // this is a version string like "3.9.0"
+ upgradeImportantMessageType_n = 2;
+ }
+
+ console.log('upgradeImportantMessageType: ' + upgradeImportantMessageType_n);
+ return { upgradeImportantMessageType: upgradeImportantMessageType_n };
+}
+
+// called by Action endpoint checkUpgrade
function check_upgrade(version: string) {
let data = {};
if (version) {
@@ -5170,6 +5191,9 @@ router
// set partition
console.log('setting partition to', content.param);
return status(200);
+ } else if (action === 'upgradeImportantMessages') {
+ // check upgrade important messages
+ return upgradeImportantMessages(content.param);
}
}
return status(404); // cmd not found
diff --git a/platformio.ini b/platformio.ini
index f9ba5a97b..1b2348bca 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -59,7 +59,7 @@ framework = arduino
board_build.partitions = partitions/esp32_partition_4M.csv
board_upload.flash_size = 4MB
board_build.app_partition_name = app0
-platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.03.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.7 based on IDF 5.5.3.260313
+platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.04.50/platform-espressif32.zip ; Platform 2026.04.50 Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
; 16MB Flash variants
[espressif32_base_16M]
@@ -67,7 +67,7 @@ framework = arduino
board_build.partitions = partitions/esp32_partition_16M.csv
board_upload.flash_size = 16MB
board_build.app_partition_name = app0
-platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.03.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.7 based on IDF 5.5.3.260313
+platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.04.50/platform-espressif32.zip ; Platform 2026.04.50 Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
; 32MB Flash variants
[espressif32_base_32M]
@@ -75,7 +75,7 @@ framework = arduino
board_build.partitions = partitions/esp32_partition_32M.csv
board_upload.flash_size = 32MB
board_build.app_partition_name = app0
-platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.03.50/platform-espressif32.zip ; Tasmota Arduino Core 3.3.7 based on IDF 5.5.3.260313
+platform = https://github.com/tasmota/platform-espressif32/releases/download/2026.04.50/platform-espressif32.zip ; Platform 2026.04.50 Tasmota Arduino Core 3.3.8 based on IDF 5.5.4.260407
[env]
build_flags =
diff --git a/src/ESP32React/UploadFileService.cpp b/src/ESP32React/UploadFileService.cpp
index 4aa634c2a..3d4e99152 100644
--- a/src/ESP32React/UploadFileService.cpp
+++ b/src/ESP32React/UploadFileService.cpp
@@ -184,7 +184,7 @@ void UploadFileService::handleError(AsyncWebServerRequest * request, int code) {
}
void UploadFileService::handleEarlyDisconnect() {
- emsesp::EMSESP::logger().info("Upload completed");
+ emsesp::EMSESP::logger().info("Upload finished");
emsesp::EMSESP::system_.uart_init(); // re-enable UART
_is_firmware = false;
diff --git a/src/core/system.cpp b/src/core/system.cpp
index 2acd6e622..19d037877 100644
--- a/src/core/system.cpp
+++ b/src/core/system.cpp
@@ -1434,299 +1434,6 @@ bool System::check_restore() {
bool reboot_required = false; // true if we need to reboot
#ifndef EMSESP_STANDALONE
- File new_file = LittleFS.open(TEMP_FILENAME_PATH);
- if (new_file) {
- JsonDocument jsonDocument;
- DeserializationError error = deserializeJson(jsonDocument, new_file);
- if (error == DeserializationError::Ok && jsonDocument.is()) {
- JsonObject input = jsonDocument.as();
- // see what type of file it is, either settings or customization. anything else is ignored
- std::string settings_type = input["type"];
- LOG_INFO("Restoring '%s' settings...", settings_type.c_str());
-
- // system backup, which is a consolidated json object with all the settings files
- if (settings_type == "systembackup") {
- reboot_required = true;
- JsonArray sections = input["systembackup"].as();
- for (JsonObject section : sections) {
- std::string section_type = section["type"];
- LOG_DEBUG("Restoring '%s' section...", section_type.c_str());
- if (section_type == "settings") {
- saveSettings(NETWORK_SETTINGS_FILE, section);
- saveSettings(AP_SETTINGS_FILE, section);
- saveSettings(MQTT_SETTINGS_FILE, section);
- saveSettings(NTP_SETTINGS_FILE, section);
- saveSettings(SECURITY_SETTINGS_FILE, section);
- saveSettings(EMSESP_SETTINGS_FILE, section);
- }
- if (section_type == "schedule") {
- saveSettings(EMSESP_SCHEDULER_FILE, section);
- }
- if (section_type == "customizations") {
- saveSettings(EMSESP_CUSTOMIZATION_FILE, section);
- }
- if (section_type == "entities") {
- saveSettings(EMSESP_CUSTOMENTITY_FILE, section);
- }
- if (section_type == "modules") {
- saveSettings(EMSESP_MODULES_FILE, section);
- }
- if (section_type == "customSupport") {
- // it's a custom support, extract json and write to /config/customSupport.json file
- File customSupportFile = LittleFS.open(EMSESP_CUSTOMSUPPORT_FILE, "w");
- if (customSupportFile) {
- serializeJson(section, customSupportFile);
- customSupportFile.close();
- LOG_INFO("Custom support file updated");
- } else {
- LOG_ERROR("Failed to save custom support file");
- }
- }
-
- if (section_type == "nvs") {
- // Restore NVS values
- JsonArray nvs_entries = section["nvs"].as();
- for (JsonObject entry : nvs_entries) {
- std::string key = entry["key"] | "";
- int type = entry["type"] | NVS_TYPE_ANY;
-
- switch (type) {
- case NVS_TYPE_I8:
- if (entry["value"].is()) {
- int8_t v = entry["value"];
- EMSESP::nvs_.putChar(key.c_str(), v);
- LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v);
- }
- break;
- case NVS_TYPE_U8:
- if (entry["value"].is()) {
- uint8_t v = entry["value"];
- EMSESP::nvs_.putUChar(key.c_str(), v);
- LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v);
- }
- break;
- case NVS_TYPE_I32:
- if (entry["value"].is()) {
- int32_t v = entry["value"];
- EMSESP::nvs_.putInt(key.c_str(), v);
- LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v);
- }
- break;
- case NVS_TYPE_U32:
- if (entry["value"].is()) {
- uint32_t v = entry["value"];
- EMSESP::nvs_.putUInt(key.c_str(), v);
- LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v);
- }
- break;
- case NVS_TYPE_I64:
- if (entry["value"].is()) {
- int64_t v = entry["value"];
- EMSESP::nvs_.putLong64(key.c_str(), v);
- LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v);
- }
- break;
- case NVS_TYPE_U64:
- if (entry["value"].is()) {
- uint64_t v = entry["value"];
- EMSESP::nvs_.putULong64(key.c_str(), v);
- LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v);
- }
- break;
- case NVS_TYPE_BLOB:
- // used for double values
- if (entry["value"].is()) {
- double v = entry["value"];
- EMSESP::nvs_.putDouble(key.c_str(), v);
- LOG_DEBUG("Restored NVS value: %s = %d", key.c_str(), v);
- }
- break;
- case NVS_TYPE_STR:
- case NVS_TYPE_ANY:
- default:
- if (entry["value"].is()) {
- std::string v = entry["value"];
- EMSESP::nvs_.putString(key.c_str(), v.c_str());
- LOG_DEBUG("Restored NVS value: %s = %s", key.c_str(), v.c_str());
- }
- break;
- }
- }
- }
- }
- }
-
- // It's a single settings file. Parse each section separately. If it's system related it will require a reboot
- else if (settings_type == "settings") {
- reboot_required = saveSettings(NETWORK_SETTINGS_FILE, input);
- reboot_required |= saveSettings(AP_SETTINGS_FILE, input);
- reboot_required |= saveSettings(MQTT_SETTINGS_FILE, input);
- reboot_required |= saveSettings(NTP_SETTINGS_FILE, input);
- reboot_required |= saveSettings(SECURITY_SETTINGS_FILE, input);
- reboot_required |= saveSettings(EMSESP_SETTINGS_FILE, input);
- } else if (settings_type == "customizations") {
- saveSettings(EMSESP_CUSTOMIZATION_FILE, input);
- } else if (settings_type == "schedule") {
- saveSettings(EMSESP_SCHEDULER_FILE, input);
- } else if (settings_type == "entities") {
- saveSettings(EMSESP_CUSTOMENTITY_FILE, input);
- } else if (settings_type == "customSupport") {
- // it's a custom support file - save it to /config
- new_file.close();
- if (LittleFS.rename(TEMP_FILENAME_PATH, EMSESP_CUSTOMSUPPORT_FILE)) {
- LOG_INFO("Custom support file stored");
- return false; // no need to reboot
- } else {
- LOG_ERROR("Failed to save custom support file");
- }
- } else {
- LOG_ERROR("Unrecognized file uploaded");
- }
- } else {
- LOG_ERROR("Unrecognized file uploaded, not json.");
- }
-
- // close (just in case) and remove the temp file
- new_file.close();
- LittleFS.remove(TEMP_FILENAME_PATH);
- }
-#endif
-
- return reboot_required;
-}
-
-// handle upgrades from previous versions
-// this function will not be called on a clean install, with no settings files yet created
-// returns true if we need a reboot
-bool System::check_upgrade() {
- bool missing_version = true;
- std::string settingsVersion;
-
- // fetch current version from settings file
- EMSESP::webSettingsService.read([&](WebSettings const & settings) { settingsVersion = settings.version.c_str(); });
-
- // see if we're missing a version, will be < 3.5.0b13 from Dec 23 2022
- missing_version = (settingsVersion.empty() || (settingsVersion.length() < 5));
- if (missing_version) {
- LOG_WARNING("No version information found. Assuming version 3.5.0");
- settingsVersion = "3.5.0"; // this was the last stable version without version info
- }
-
- version::Semver200_version settings_version(settingsVersion);
- version::Semver200_version this_version(EMSESP_APP_VERSION);
-
- std::string settings_version_type = settings_version.prerelease().empty() ? "" : ("-" + settings_version.prerelease());
- std::string this_version_type = this_version.prerelease().empty() ? "" : ("-" + this_version.prerelease());
- bool save_version = true;
- bool reboot_required = false;
-
- LOG_DEBUG("Checking for version upgrades from v%d.%d.%d%s",
- settings_version.major(),
- settings_version.minor(),
- settings_version.patch(),
- settings_version_type.c_str());
-
- // compare versions
- if (this_version > settings_version) {
- // we need to do an upgrade
- if (missing_version) {
- LOG_NOTICE("Upgrading to version %d.%d.%d%s", this_version.major(), this_version.minor(), this_version.patch(), this_version_type.c_str());
- } else {
- LOG_NOTICE("Upgrading from version %d.%d.%d%s to %d.%d.%d%s",
- settings_version.major(),
- settings_version.minor(),
- settings_version.patch(),
- settings_version_type.c_str(),
- this_version.major(),
- this_version.minor(),
- this_version.patch(),
- this_version_type.c_str());
- }
-
- // if we're coming from 3.4.4 or 3.5.0b14 which had no version stored then we need to apply new settings
- if (missing_version) {
- LOG_INFO("Upgrade: Setting MQTT Entity ID format to older v3.4 format (0)");
- EMSESP::esp32React.getMqttSettingsService()->update([&](MqttSettings & mqttSettings) {
- mqttSettings.entity_format = Mqtt::entityFormat::SINGLE_LONG; // use old Entity ID format from v3.4
- return StateUpdateResult::CHANGED;
- });
- } else if (settings_version.major() == 3 && settings_version.minor() <= 6) {
- EMSESP::esp32React.getMqttSettingsService()->update([&](MqttSettings & mqttSettings) {
- if (mqttSettings.entity_format == 1) {
- mqttSettings.entity_format = Mqtt::entityFormat::SINGLE_OLD; // use old Entity ID format from v3.6
- LOG_INFO("Upgrade: Setting MQTT Entity ID format to v3.6 format (3)");
- return StateUpdateResult::CHANGED;
- } else if (mqttSettings.entity_format == 2) {
- mqttSettings.entity_format = Mqtt::entityFormat::MULTI_OLD; // use old Entity ID format from v3.6
- LOG_INFO("Upgrade: Setting MQTT Entity ID format to v3.6 format (4)");
- return StateUpdateResult::CHANGED;
- }
- return StateUpdateResult::UNCHANGED;
- });
- }
-
- // changes pre < v3.7.0
- if (settings_version.major() == 3 && settings_version.minor() < 7) {
- // network changes
- // 1) WiFi Tx Power is now using the value * 4 (was 20)
- // 2) WiFi sleep is now off by default (was on)
- EMSESP::esp32React.getNetworkSettingsService()->update([&](NetworkSettings & networkSettings) {
- auto changed = StateUpdateResult::UNCHANGED;
- if (networkSettings.tx_power == 20) {
- networkSettings.tx_power = WIFI_POWER_19_5dBm; // use 19.5 as we don't have 20 anymore
- LOG_INFO("Upgrade: Setting WiFi TX Power to Auto");
- changed = StateUpdateResult::CHANGED;
- }
- if (networkSettings.nosleep != true) {
- networkSettings.nosleep = true;
- LOG_INFO("Upgrade: Disabling WiFi nosleep");
- changed = StateUpdateResult::CHANGED;
- }
- return changed;
- });
- }
-
- // changes to application settings
- EMSESP::webSettingsService.update([&](WebSettings & settings) {
- // force web buffer to 25 for those boards without psram
- if ((EMSESP::system_.PSram() == 0) && (settings.weblog_buffer != 25)) {
- settings.weblog_buffer = 25;
- return StateUpdateResult::CHANGED;
- }
- return StateUpdateResult::UNCHANGED;
- });
- } else if (this_version < settings_version) {
- // downgrading
- LOG_NOTICE("Downgrading from version %d.%d.%d%s to version %d.%d.%d%s",
- settings_version.major(),
- settings_version.minor(),
- settings_version.patch(),
- settings_version_type.c_str(),
- this_version.major(),
- this_version.minor(),
- this_version.patch(),
- this_version_type.c_str());
- } else {
- save_version = false; // same version, do nothing
- }
-
- // if we did a change, set the new version and save it, no need to reboot
- if (save_version) {
- EMSESP::webSettingsService.update([&](WebSettings & settings) {
- settings.version = EMSESP_APP_VERSION;
- LOG_DEBUG("Upgrade: Setting version to %s", EMSESP_APP_VERSION);
- return StateUpdateResult::CHANGED;
- });
- }
-
- if (reboot_required) {
- LOG_INFO("Upgrade: Rebooting to apply changes");
- return true; // need reboot
- }
-
- return false; // no reboot required
-}
-
// map each config filename to its human-readable section key
static const std::pair SECTION_MAP[] = {
{NETWORK_SETTINGS_FILE, "Network"},
@@ -1740,1739 +1447,1741 @@ static const std::pair SECTION_MAP[] = {
{EMSESP_CUSTOMENTITY_FILE, "Entities"},
{EMSESP_MODULES_FILE, "Modules"},
};
-
-// convert a single config file into a section of the output json object
-void System::exportSettings(const std::string & type, const char * filename, JsonObject output) {
- if (type != "settings") {
- output["type"] = type; // add the type to the output, not for settings as it's already added because its grouped
- }
-
-#ifndef EMSESP_STANDALONE
- const char * section = nullptr;
- for (const auto & [f, label] : SECTION_MAP) {
- if (strcmp(f, filename) == 0) {
- section = label;
- break;
- }
- }
-
- if (!section) {
- return;
- }
-
- File settingsFile = LittleFS.open(filename);
- if (settingsFile) {
- JsonDocument jsonDocument;
- DeserializationError error = deserializeJson(jsonDocument, settingsFile);
- if (error == DeserializationError::Ok && jsonDocument.is()) {
- JsonObject node = output[section].to();
- for (JsonPair kvp : jsonDocument.as()) {
- node[kvp.key()] = kvp.value();
- }
- } else {
- LOG_ERROR("Failed to deserialize settings file %s", filename);
- }
- LOG_DEBUG("Exported %s settings from file %s", section, filename);
- settingsFile.close();
- } else {
- LOG_ERROR("No settings file for %s found", filename);
- }
#endif
-}
-
-// full system backup of all settings files
-void System::exportSystemBackup(JsonObject output) {
- output["type"] = "systembackup"; // add the type to the output
- output["version"] = EMSESP_APP_VERSION; // add the version to the output
-
-#ifndef EMSESP_STANDALONE
- // add date/time if NTP enabled and active
- if ((esp_sntp_enabled()) && (EMSESP::system_.ntp_connected())) {
- time_t now = time(nullptr);
- if (now > 1500000000L) {
- char t[25];
- strftime(t, sizeof(t), "%FT%T", localtime(&now));
- output["date"] = t;
- }
- }
-#endif
-
- // create an array of objects for each settings file
- JsonArray nodes = output["systembackup"].to();
-
- // start with settings by grouping them together
- JsonObject node = nodes.add();
- node["type"] = "settings"; // add type once for this group
- exportSettings("settings", NETWORK_SETTINGS_FILE, node);
- exportSettings("settings", AP_SETTINGS_FILE, node);
- exportSettings("settings", MQTT_SETTINGS_FILE, node);
- exportSettings("settings", NTP_SETTINGS_FILE, node);
- exportSettings("settings", SECURITY_SETTINGS_FILE, node);
- exportSettings("settings", EMSESP_SETTINGS_FILE, node);
-
- node = nodes.add();
- exportSettings("schedule", EMSESP_SCHEDULER_FILE, node);
- node = nodes.add();
- exportSettings("customizations", EMSESP_CUSTOMIZATION_FILE, node);
- node = nodes.add();
- exportSettings("entities", EMSESP_CUSTOMENTITY_FILE, node);
- node = nodes.add();
- exportSettings("modules", EMSESP_MODULES_FILE, node);
-
-#ifndef EMSESP_STANDALONE
- // special case for custom support
- File file = LittleFS.open(EMSESP_CUSTOMSUPPORT_FILE, "r");
- if (file) {
- JsonDocument jsonDocument;
- DeserializationError error = deserializeJson(jsonDocument, file);
- if (error == DeserializationError::Ok && jsonDocument.is()) {
- JsonObject node = nodes.add();
- node["type"] = "customSupport";
- node["data"] = jsonDocument.as();
- }
- file.close();
- LOG_DEBUG("Exported custom support file %s", EMSESP_CUSTOMSUPPORT_FILE);
- }
-
- // Backup NVS values
- node = nodes.add();
- node["type"] = "nvs";
-
- const char * nvs_part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_NVS, "nvs1") ? "nvs1" : "nvs"; // nvs1 is on 16MBs
- nvs_iterator_t it = nullptr;
- esp_err_t err = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY, &it);
- if (err != ESP_OK) {
- LOG_ERROR("Failed to find NVS entry for %s", nvs_part);
- return;
- }
-
- JsonArray entries = node["nvs"].to();
- while (err == ESP_OK) {
- nvs_entry_info_t info;
- nvs_entry_info(it, &info);
- JsonObject entry = entries.add();
- entry["type"] = info.type; // e.g. NVS_TYPE_U32 or NVS_TYPE_STR etc
- entry["key"] = info.key;
-
- LOG_DEBUG("Exporting NVS value: %s = %d", info.key, info.type);
-
- // serialize based on the type. We use putString, putChar, putUChar, putDouble, putBool, putULong only
- switch (info.type) {
- case NVS_TYPE_I8:
- entry["value"] = EMSESP::nvs_.getChar(info.key);
- break;
- case NVS_TYPE_U8:
- // also used for bool
- entry["value"] = EMSESP::nvs_.getUChar(info.key);
- break;
- case NVS_TYPE_I32:
- entry["value"] = EMSESP::nvs_.getInt(info.key);
- break;
- case NVS_TYPE_U32:
- entry["value"] = EMSESP::nvs_.getUInt(info.key);
- break;
- case NVS_TYPE_I64:
- entry["value"] = EMSESP::nvs_.getLong64(info.key);
- break;
- case NVS_TYPE_U64:
- entry["value"] = EMSESP::nvs_.getULong64(info.key);
- break;
- case NVS_TYPE_BLOB:
- // used for double (e.g. sensor values, nrgheat, nrgww), and stored as bytes in NVS
- entry["value"] = EMSESP::nvs_.getDouble(info.key);
- break;
- case NVS_TYPE_STR:
- case NVS_TYPE_ANY:
- default:
- // any other value we store as a string
- entry["value"] = EMSESP::nvs_.getString(info.key);
- break;
- }
-
- err = nvs_entry_next(&it);
- }
-
- if (it != nullptr) {
- nvs_release_iterator(it);
- }
-#endif
-}
-
-// write a settings file using input from a json object, called from upload/restore
-bool System::saveSettings(const char * filename, JsonObject input) {
-#ifndef EMSESP_STANDALONE
- const char * section = nullptr;
- for (const auto & [f, label] : SECTION_MAP) {
- if (strcmp(f, filename) == 0) {
- section = label;
- break;
- }
- }
-
- if (!section) {
- return false;
- }
-
- JsonObject section_json = input[section];
- if (section_json) {
- File section_file = LittleFS.open(filename, "w");
- if (section_file) {
- LOG_DEBUG("Applying new uploaded %s data", section);
- serializeJson(section_json, section_file);
- section_file.close();
- return true; // reboot required
- }
- }
-#endif
-
- return false; // not found
-}
-
-// set a entity of services 'network', 'settings', 'mqtt', etc.
-bool System::command_service(const char * cmd, const char * value) {
- bool ok = false;
- bool b;
- if (Helpers::value2bool(value, b)) {
- if (!strcmp(cmd, "settings/showertimer")) {
- EMSESP::webSettingsService.update([&](WebSettings & settings) {
- settings.shower_timer = b;
- return StateUpdateResult::CHANGED;
- });
- EMSESP::shower_.shower_timer(b);
- ok = true;
- } else if (!strcmp(cmd, "settings/showeralert")) {
- EMSESP::webSettingsService.update([&](WebSettings & settings) {
- settings.shower_alert = b;
- return StateUpdateResult::CHANGED;
- });
- EMSESP::shower_.shower_alert(b);
- ok = true;
- } else if (!strcmp(cmd, "settings/hideled")) {
- EMSESP::webSettingsService.update([&](WebSettings & settings) {
- settings.hide_led = b;
- return StateUpdateResult::CHANGED;
- });
- EMSESP::system_.hide_led(b);
- ok = true;
- } else if (!strcmp(cmd, "settings/analogenabled")) {
- EMSESP::webSettingsService.update([&](WebSettings & settings) {
- settings.analog_enabled = b;
- return StateUpdateResult::CHANGED;
- });
- EMSESP::system_.analog_enabled(b);
- ok = true;
- } else if (!strcmp(cmd, "mqtt/enabled")) {
- EMSESP::esp32React.getMqttSettingsService()->update([&](MqttSettings & Settings) {
- Settings.enabled = b;
- return StateUpdateResult::CHANGED;
- });
- ok = true;
- } else if (!strcmp(cmd, "ap/enabled")) {
- EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & Settings) {
- Settings.provisionMode = b ? 0 : 2;
- return StateUpdateResult::CHANGED;
- });
- ok = true;
- } else if (!strcmp(cmd, "ntp/enabled")) {
- EMSESP::esp32React.getNTPSettingsService()->update([&](NTPSettings & Settings) {
- Settings.enabled = b;
- return StateUpdateResult::CHANGED;
- });
- ok = true;
- } else if (!strcmp(cmd, "syslog/enabled")) {
- EMSESP::webSettingsService.update([&](WebSettings & settings) {
- settings.syslog_enabled = b;
- return StateUpdateResult::CHANGED;
- });
- EMSESP::system_.syslog_enabled_ = b;
- EMSESP::system_.syslog_init();
- ok = true;
- }
- }
-
- int n;
- if (!ok && Helpers::value2number(value, n)) {
-#ifndef EMSESP_STANDALONE
- if (!strcmp(cmd, "fuse/mfg")) {
- ok = esp_efuse_write_reg(EFUSE_BLK3, 0, (uint32_t)n) == ESP_OK;
- ok ? LOG_INFO("fuse programed with value '%X': successful", n) : LOG_ERROR("fuse programed with value '%X': failed", n);
- }
- if (!strcmp(cmd, "fuse/mfgadd")) {
- uint8_t reg = 0;
- while (esp_efuse_read_reg(EFUSE_BLK3, reg) != 0 && reg < 7)
- reg++;
- ok = esp_efuse_write_reg(EFUSE_BLK3, reg, (uint32_t)n) == ESP_OK;
- ok ? LOG_INFO("fuse %d programed with value '%X': successful", reg, n) : LOG_ERROR("fuse %d programed with value '%X': failed", reg, n);
- return true;
- }
-#endif
- }
-
- if (ok) {
- LOG_INFO("System command '%s' with value '%s'", cmd, value);
- }
- return ok;
-}
-
-// return back a system value
-bool System::get_value_info(JsonObject output, const char * cmd) {
- if (cmd == nullptr || strlen(cmd) == 0) {
- LOG_ERROR("empty system command");
- return false;
- }
-
- // check for hardcoded "info"/"value"
- if (!strcmp(cmd, F_(info)) || !strcmp(cmd, F_(values))) {
- return command_info("", 0, output);
- }
-
- // check for metrics
- if (!strcmp(cmd, F_(metrics))) {
- std::string metrics = get_metrics_prometheus();
- if (!metrics.empty()) {
- output["api_data"] = metrics;
- return true;
- }
- return false;
- }
-
- // fetch all the data from the system in a different json
- JsonDocument doc;
- JsonObject root = doc.to();
- (void)command_info("", 0, root);
-
- // list all entities
- if (!strcmp(cmd, F_(entities))) {
- for (JsonPair p : root) {
- if (p.value().is()) {
- const char * p_key = p.key().c_str(); // Cache the key
- for (JsonPair p1 : p.value().as()) {
- const char * p1_key = p1.key().c_str(); // Cache the key
- JsonObject entity = output[std::string(p_key) + "." + p1_key].to();
- get_value_json(entity, p_key, p1_key, p1.value());
- }
- }
- }
- return true;
- }
-
- char * val = (char *)strstr(cmd, "/value");
- if (val) {
- *val = '\0';
- }
-
- char * slash = (char *)strchr(cmd, '/');
- if (slash) {
- *slash = '\0';
- slash++;
- }
-
- // list values for a jsonObject in system, e.g. /api/system/network
- if (!slash || !strcmp(slash, F_(info)) || !strcmp(slash, F_(values))) {
- for (JsonPair p : root) {
- if (Helpers::toLower(p.key().c_str()) == cmd && p.value().is()) {
- for (JsonPair p1 : p.value().as()) {
- output[p1.key().c_str()] = p1.value().as();
- }
- return true;
- }
- }
- return false;
- }
-
- // value info or api_data for a single value
- // Loop through all the key-value pairs in root to find the key, case independent
- if (slash) { // search the top level first
- for (JsonPair p : root) {
- const char * p_key = p.key().c_str(); // Cache the key
- if (p.value().is() && Helpers::toLower(p_key) == cmd) {
- for (JsonPair p1 : p.value().as()) {
- const char * p1_key = p1.key().c_str(); // Cache the key
- if (Helpers::toLower(p1_key) == slash && !p1.value().is()) {
- if (val) {
- output["api_data"] = p1.value().as();
- return true;
- }
- get_value_json(output, p_key, p1_key, p1.value());
- return true;
- }
- }
- } // else skip, but we don't have value pairs in system root
- }
- }
- return false;
-}
-
-void System::get_value_json(JsonObject output, const std::string & circuit, const std::string & name, JsonVariant val) {
- output["name"] = name;
- if (circuit.length()) {
- output["circuit"] = circuit;
- }
- output["readable"] = true;
- output["writeable"] =
- (name == "txpause" || name == "showerTimer" || name == "showerAlert" || name == "enabled" || name == "hideLed" || name == "analogEnabled");
- output["visible"] = true;
- if (val.is()) {
- output["value"] = val.as();
- output["type"] = "boolean";
- } else if (val.is() || val.is()) {
- output["value"] = val.as();
- output["type"] = "number";
- } else {
- output["value"] = val.as();
- output["type"] = "string";
- }
-}
-
-// generate Prometheus metrics format from system values
-std::string System::get_metrics_prometheus() {
- std::string result;
- std::map seen_metrics;
-
- result.reserve(16000);
-
- // get system data
- JsonDocument doc;
- JsonObject root = doc.to();
- (void)command_info("", 0, root);
-
- // helper function to escape Prometheus label values
- auto escape_label = [](const std::string & str) -> std::string {
- std::string escaped;
- for (char c : str) {
- if (c == '\\') {
- escaped += "\\\\";
- } else if (c == '"') {
- escaped += "\\\"";
- } else if (c == '\n') {
- escaped += "\\n";
- } else {
- escaped += c;
- }
- }
- return escaped;
- };
-
- // helper function to sanitize metric name (convert to lowercase and replace dots with underscores)
- auto sanitize_name = [](const std::string & name) -> std::string {
- std::string sanitized = name;
- for (char & c : sanitized) {
- if (c == '.') {
- c = '_';
- } else if (isupper(c)) {
- c = tolower(c);
- } else if (!isalnum(c) && c != '_') {
- c = '_';
- }
- }
- return sanitized;
- };
-
- // helper function to convert label name to lowercase
- auto to_lowercase = [](const std::string & str) -> std::string {
- std::string result = str;
- for (char & c : result) {
- if (isupper(c)) {
- c = tolower(c);
- }
- }
- return result;
- };
-
- // helper function to check if a field should be ignored
- auto should_ignore = [](const std::string & path, const std::string & key) -> bool {
- if (path == "system" && key == "uptime") {
- return true;
- }
- if (path == "ntp" && key == "timestamp") {
- return true;
- }
- if (path.find("devices[") != std::string::npos) {
- if (key == "handlersReceived" || key == "handlersFetched" || key == "handlersPending" || key == "handlersIgnored") {
- return true;
- }
- }
- return false;
- };
-
- // helper function to process a JSON object recursively
- std::function process_object = [&](const JsonObject obj, const std::string & prefix) {
- std::vector> local_info_labels;
- bool has_nested_objects = false;
-
- for (JsonPair p : obj) {
- std::string key = p.key().c_str();
- std::string metric_name = prefix.empty() ? key : prefix + "_" + key;
-
- if (should_ignore(prefix, key)) {
- continue;
- }
-
- if (p.value().is()) {
- // recursive call for nested objects
- has_nested_objects = true;
- process_object(p.value().as(), metric_name);
- } else if (p.value().is()) {
- // handle arrays (devices)
- if (key == "devices") {
- JsonArray devices = p.value().as();
- for (JsonObject device : devices) {
- std::vector> device_labels;
-
- // collect labels from device object
- for (JsonPair dp : device) {
- std::string dkey = dp.key().c_str();
- if (dkey == "type" || dkey == "name" || dkey == "deviceID" || dkey == "brand" || dkey == "version") {
- if (dp.value().is()) {
- std::string val = dp.value().as();
- if (!val.empty()) {
- device_labels.push_back({to_lowercase(dkey), val});
- }
- }
- }
- }
-
- // create productID metric
- if (device["productID"].is()) {
- std::string metric = "emsesp_device_productid";
- if (seen_metrics.find(metric) == seen_metrics.end()) {
- result += "# HELP emsesp_device_productid productID\n";
- result += "# TYPE emsesp_device_productid gauge\n";
- seen_metrics[metric] = true;
- }
-
- result += metric;
- if (!device_labels.empty()) {
- result += "{";
- bool first = true;
- for (const auto & label : device_labels) {
- if (!first) {
- result += ", ";
- }
- result += label.first + "=\"" + escape_label(label.second) + "\"";
- first = false;
- }
- result += "}";
- }
- result += " " + std::to_string(device["productID"].as()) + "\n";
- }
-
- // create entities metric
- if (device["entities"].is()) {
- std::string metric = "emsesp_device_entities";
- if (seen_metrics.find(metric) == seen_metrics.end()) {
- result += "# HELP emsesp_device_entities entities\n";
- result += "# TYPE emsesp_device_entities gauge\n";
- seen_metrics[metric] = true;
- }
-
- result += metric;
- if (!device_labels.empty()) {
- result += "{";
- bool first = true;
- for (const auto & label : device_labels) {
- if (!first) {
- result += ", ";
- }
- result += label.first + "=\"" + escape_label(label.second) + "\"";
- first = false;
- }
- result += "}";
- }
- result += " " + std::to_string(device["entities"].as()) + "\n";
- }
- }
- }
- } else {
- // handle primitive values
- bool is_number = p.value().is() || p.value().is();
- bool is_bool = p.value().is();
- bool is_string = p.value().is();
-
- if (is_number || is_bool) {
- // add metric
- std::string full_metric_name = "emsesp_" + sanitize_name(metric_name);
- if (seen_metrics.find(full_metric_name) == seen_metrics.end()) {
- result += "# HELP emsesp_" + sanitize_name(metric_name) + " " + key + "\n";
- result += "# TYPE emsesp_" + sanitize_name(metric_name) + " gauge\n";
- seen_metrics[full_metric_name] = true;
- }
-
- result += full_metric_name + " ";
- if (is_bool) {
- result += p.value().as() ? "1" : "0";
- } else if (p.value().is()) {
- result += std::to_string(p.value().as());
- } else {
- char val_str[30];
- snprintf(val_str, sizeof(val_str), "%.2f", p.value().as());
- result += val_str;
- }
- result += "\n";
- } else if (is_string) {
- // collect string for info metric (skip dynamic strings like uptime and timestamp)
- std::string val = p.value().as();
- if (!val.empty() && key != "uptime" && key != "timestamp") {
- std::string lower_key = to_lowercase(key);
- // check if key already exists in local_info_labels
- bool key_exists = false;
- for (const auto & label : local_info_labels) {
- if (label.first == lower_key) {
- key_exists = true;
- break;
- }
- }
- if (!key_exists) {
- local_info_labels.push_back({lower_key, val});
- }
- }
- }
- }
- }
-
- // create _info metric for this object level if we have labels and this is a leaf node (no nested objects)
- if (!local_info_labels.empty() && !prefix.empty() && !has_nested_objects) {
- std::string info_metric = "emsesp_" + sanitize_name(prefix) + "_info";
- if (seen_metrics.find(info_metric) == seen_metrics.end()) {
- result += "# HELP " + info_metric + " info\n";
- result += "# TYPE " + info_metric + " gauge\n";
- seen_metrics[info_metric] = true;
- }
-
- result += info_metric;
- // TODO fix, as local_info_labels is always empty here
- if (!local_info_labels.empty()) {
- result += "{";
- bool first = true;
- for (const auto & label : local_info_labels) {
- if (!first) {
- result += ", ";
- }
- result += label.first + "=\"" + escape_label(label.second) + "\"";
- first = false;
- }
- result += "}";
- }
- result += " 1\n";
- }
- };
-
- // process root object
- process_object(root, "");
-
- result.shrink_to_fit();
-
- return result;
-}
-
-// return IP or hostname of the EMS-ESP device
-String System::get_ip_or_hostname() {
- String result = "ems-esp";
-#ifndef EMSESP_STANDALONE
- EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & settings) {
- if (settings.enableMDNS) {
- if (EMSESP::system_.ethernet_connected()) {
- result = ETH.getHostname();
- } else if (WiFi.status() == WL_CONNECTED) {
- result = WiFi.getHostname();
- }
- } else {
- // no DNS, use the IP
- if (EMSESP::system_.ethernet_connected()) {
- result = ETH.localIP().toString();
- } else if (WiFi.status() == WL_CONNECTED) {
- result = WiFi.localIP().toString();
- }
- }
- });
-#endif
- return result;
-}
-
-// export status information including the device information
-// http://ems-esp/api/system/info
-bool System::command_info(const char * value, const int8_t id, JsonObject output) {
- JsonObject node;
-
- // System
- node = output["system"].to();
-// prevent false-negatives in Unity tests every time the version changes
-#if defined(EMSESP_UNITY)
- node["version"] = "dev";
-#else
- node["version"] = EMSESP_APP_VERSION;
-#endif
- node["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
- node["uptimeSec"] = uuid::get_uptime_sec();
- node["resetReason"] = EMSESP::system_.reset_reason(0) + " / " + EMSESP::system_.reset_reason(1);
-#ifndef EMSESP_STANDALONE
- node["platform"] = EMSESP_PLATFORM;
- node["cpuType"] = ESP.getChipModel();
- node["arduino"] = ARDUINO_VERSION;
- node["sdk"] = ESP.getSdkVersion();
- node["freeMem"] = getHeapMem();
- node["maxAlloc"] = getMaxAllocMem();
- node["freeCaps"] = heap_caps_get_free_size(MALLOC_CAP_8BIT) / 1024; // includes heap and psram
- node["usedApp"] = EMSESP::system_.appUsed(); // kilobytes
- node["freeApp"] = EMSESP::system_.appFree(); // kilobytes
- node["partition"] = (const char *)esp_ota_get_running_partition()->label; // active partition
- node["flash_chip_size"] = ESP.getFlashChipSize() / 1024; // kilobytes
- node["psram"] = (EMSESP::system_.PSram() > 0); // make boolean
- if (EMSESP::system_.PSram()) {
- node["psramSize"] = EMSESP::system_.PSram();
- node["freePsram"] = ESP.getFreePsram() / 1024;
- }
- node["model"] = EMSESP::system_.getBBQKeesGatewayDetails();
-#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
- node["temperature"] = EMSESP::system_.temperature();
-#endif
-#endif
- node["txpause"] = EMSbus::tx_mode() == EMS_TXMODE_OFF;
-
- // GPIO information
- std::string gpios_allowed_str;
- for (const auto & gpio : valid_system_gpios_) {
- if (!gpios_allowed_str.empty()) {
- gpios_allowed_str += ", ";
- }
- gpios_allowed_str += Helpers::itoa(gpio);
- }
- node["gpios_allowed"] = gpios_allowed_str;
-
- std::string gpios_in_use_str;
- auto sorted_gpios = used_gpios_;
- std::sort(sorted_gpios.begin(), sorted_gpios.end(), [](const GpioUsage & a, const GpioUsage & b) { return a.pin < b.pin; });
- for (const auto & gpio : sorted_gpios) {
- if (!gpios_in_use_str.empty()) {
- gpios_in_use_str += ", ";
- }
- gpios_in_use_str += Helpers::itoa(gpio.pin);
- }
- node["gpios_in_use"] = gpios_in_use_str;
-
- std::string gpios_available_str;
- for (const auto & gpio : available_gpios()) {
- if (!gpios_available_str.empty()) {
- gpios_available_str += ", ";
- }
- gpios_available_str += Helpers::itoa(gpio);
- }
- node["gpios_available"] = gpios_available_str;
-
- // Network Status
- node = output["network"].to();
-#ifndef EMSESP_STANDALONE
- if (EMSESP::system_.ethernet_connected()) {
- node["network"] = "Ethernet";
- node["hostname"] = ETH.getHostname();
- // node["MAC"] = ETH.macAddress();
- // node["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask());
- // node["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP());
- // node["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP());
- // if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
- // node["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6());
- // }
- } else if (WiFi.status() == WL_CONNECTED) {
- node["network"] = "WiFi";
- node["hostname"] = WiFi.getHostname();
- node["RSSI"] = WiFi.RSSI();
- node["WIFIReconnects"] = EMSESP::esp32React.getWifiReconnects();
- // node["MAC"] = WiFi.macAddress();
- // node["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask());
- // node["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP());
- // node["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP());
- // if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
- // node["IPv6 address"] = uuid::printable_to_string(WiFi.localIPv6());
- // }
- }
-#else
- // for testing
- node["network"] = "WiFi";
- node["hostname"] = "ems-esp";
- node["RSSI"] = -23;
-#endif
- EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & settings) {
- if (WiFi.status() == WL_CONNECTED && !settings.bssid.isEmpty()) {
- node["BSSID"] = "set"; // we don't disclose the name
- }
- node["TxPowerSetting"] = settings.tx_power;
- node["staticIP"] = settings.staticIPConfig;
- node["lowBandwidth"] = settings.bandwidth20;
- node["disableSleep"] = settings.nosleep;
- node["enableMDNS"] = settings.enableMDNS;
- node["enableCORS"] = settings.enableCORS;
- if (settings.enableCORS) {
- node["CORSOrigin"] = settings.CORSOrigin;
- }
- });
-
- // NTP status
- node = output["ntp"].to();
- node["NTPstatus"] = EMSESP::system_.ntp_connected() ? "connected" : "disconnected";
- EMSESP::esp32React.getNTPSettingsService()->read([&](const NTPSettings & settings) {
-#ifndef EMSESP_STANDALONE
- node["enabled"] = settings.enabled;
-#else
- node["enabled"] = true;
-#endif
- node["server"] = settings.server;
- node["tzLabel"] = settings.tzLabel;
- });
-#ifndef EMSESP_STANDALONE
- node["timestamp"] = time(nullptr);
-#endif
- node["NTPStatus"] = EMSESP::system_.ntp_connected() ? "connected" : "disconnected";
-
- // AP Status
- node = output["ap"].to();
- EMSESP::esp32React.getAPSettingsService()->read([&](const APSettings & settings) {
- const char * pM[] = {"always", "disconnected", "never"};
- node["provisionMode"] = pM[settings.provisionMode];
- node["ssid"] = settings.ssid;
-#ifndef EMSESP_STANDALONE
- node["security"] = settings.password.length() ? "wpa2" : "open";
- node["channel"] = settings.channel;
- node["ssidHidden"] = settings.ssidHidden;
- node["maxClients"] = settings.maxClients;
- node["localIP"] = settings.localIP.toString();
- node["gatewayIP"] = settings.gatewayIP.toString();
- node["subnetMask"] = settings.subnetMask.toString();
-#endif
- });
-
- // MQTT Status
- node = output["mqtt"].to();
- node["MQTTStatus"] = Mqtt::connected() ? F_(connected) : F_(disconnected);
- if (Mqtt::enabled()) {
- node["MQTTPublishes"] = Mqtt::publish_count();
- node["MQTTQueued"] = Mqtt::publish_queued();
- node["MQTTPublishFails"] = Mqtt::publish_fails();
- node["MQTTReconnects"] = Mqtt::connect_count();
- }
- EMSESP::esp32React.getMqttSettingsService()->read([&](const MqttSettings & settings) {
- node["enabled"] = settings.enabled;
- node["clientID"] = settings.clientId;
- node["keepAlive"] = settings.keepAlive;
- node["cleanSession"] = settings.cleanSession;
- node["entityFormat"] = settings.entity_format;
- node["base"] = settings.base;
- node["discoveryPrefix"] = settings.discovery_prefix;
- node["discoveryType"] = settings.discovery_type;
- node["nestedFormat"] = settings.nested_format;
- node["haEnabled"] = settings.ha_enabled;
- node["mqttQos"] = settings.mqtt_qos;
- node["mqttRetain"] = settings.mqtt_retain;
- node["publishTimeHeartbeat"] = settings.publish_time_heartbeat;
- node["publishTimeBoiler"] = settings.publish_time_boiler;
- node["publishTimeThermostat"] = settings.publish_time_thermostat;
- node["publishTimeSolar"] = settings.publish_time_solar;
- node["publishTimeMixer"] = settings.publish_time_mixer;
- node["publishTimeWater"] = settings.publish_time_water;
- node["publishTimeOther"] = settings.publish_time_other;
- node["publishTimeSensor"] = settings.publish_time_sensor;
- node["publishSingle"] = settings.publish_single;
- node["publish2command"] = settings.publish_single2cmd;
- node["sendResponse"] = settings.send_response;
- });
-
- // Syslog Status
- node = output["syslog"].to();
- node["enabled"] = EMSESP::system_.syslog_enabled_;
-#ifndef EMSESP_STANDALONE
- if (EMSESP::system_.syslog_enabled_) {
- node["syslogStarted"] = syslog_.started();
- node["syslogLevel"] = FL_(list_syslog_level)[syslog_.log_level() + 1];
- node["syslogIP"] = syslog_.ip();
- node["syslogQueue"] = syslog_.queued();
- }
-#endif
-
- // Modbus Status
- node = output["modbus"].to();
- node["enabled"] = EMSESP::system_.modbus_enabled_;
- if (EMSESP::system_.modbus_enabled_) {
- node["maxClients"] = EMSESP::system_.modbus_max_clients_;
- node["port"] = EMSESP::system_.modbus_port_;
- node["timeout"] = EMSESP::system_.modbus_timeout_;
- }
-
- // Sensor Status
- node = output["sensor"].to();
- if (EMSESP::sensor_enabled()) {
- node["temperatureSensors"] = EMSESP::temperaturesensor_.count_entities();
- node["temperatureSensorReads"] = EMSESP::temperaturesensor_.reads();
- node["temperatureSensorFails"] = EMSESP::temperaturesensor_.fails();
- }
-
- // Analog Status
- node = output["analog"].to();
- node["enabled"] = EMSESP::analog_enabled();
- if (EMSESP::analog_enabled()) {
- node["analogSensors"] = EMSESP::analogsensor_.count_entities();
- node["analogSensorReads"] = EMSESP::analogsensor_.reads();
- node["analogSensorFails"] = EMSESP::analogsensor_.fails();
- }
-
- // API Status
- node = output["api"].to();
-
-// if we're generating test data for Unit Tests we dont want to count these API calls as it will pollute the data response
-#if defined(EMSESP_UNITY)
- node["APICalls"] = 0;
- node["APIFails"] = 0;
-#else
- node["APICalls"] = WebAPIService::api_count();
- node["APIFails"] = WebAPIService::api_fails();
-#endif
-
- // EMS Bus Status
- node = output["bus"].to();
- switch (EMSESP::bus_status()) {
- case EMSESP::BUS_STATUS_OFFLINE:
- node["busStatus"] = "disconnected";
- break;
- case EMSESP::BUS_STATUS_TX_ERRORS:
- node["busStatus"] = "connected, tx issues - try a different Tx Mode";
- break;
- case EMSESP::BUS_STATUS_CONNECTED:
- node["busStatus"] = "connected";
- break;
- default:
- node["busStatus"] = "unknown";
- break;
- }
- node["busProtocol"] = EMSbus::is_ht3() ? "HT3" : "Buderus";
- node["busTelegramsReceived"] = EMSESP::rxservice_.telegram_count();
- node["busReads"] = EMSESP::txservice_.telegram_read_count();
- node["busWrites"] = EMSESP::txservice_.telegram_write_count();
- node["busIncompleteTelegrams"] = EMSESP::rxservice_.telegram_error_count();
- node["busReadsFailed"] = EMSESP::txservice_.telegram_read_fail_count();
- node["busWritesFailed"] = EMSESP::txservice_.telegram_write_fail_count();
- node["busRxLineQuality"] = EMSESP::rxservice_.quality();
- node["busTxLineQuality"] = (EMSESP::txservice_.read_quality() + EMSESP::txservice_.write_quality()) / 2;
-
- // Settings
- node = output["settings"].to();
- EMSESP::webSettingsService.read([&](const WebSettings & settings) {
- node["boardProfile"] = settings.board_profile;
- node["locale"] = settings.locale;
- node["txMode"] = settings.tx_mode;
- node["emsBusID"] = settings.ems_bus_id;
- node["showerTimer"] = settings.shower_timer;
- node["showerMinDuration"] = settings.shower_min_duration; // seconds
- node["showerAlert"] = settings.shower_alert;
- if (settings.shower_alert) {
- node["showerAlertColdshot"] = settings.shower_alert_coldshot; // seconds
- node["showerAlertTrigger"] = settings.shower_alert_trigger; // minutes
- }
- if (settings.board_profile == "CUSTOM") {
- node["phyType"] = settings.phy_type;
- if (settings.phy_type != PHY_type::PHY_TYPE_NONE) {
- node["ethPower"] = settings.eth_power;
- node["ethPhyAddr"] = settings.eth_phy_addr;
- node["ethClockMmode"] = settings.eth_clock_mode;
- }
- node["rxGPIO"] = EMSESP::system_.rx_gpio_;
- node["txGPIO"] = EMSESP::system_.tx_gpio_;
- node["dallasGPIO"] = EMSESP::system_.dallas_gpio_;
- node["pbuttonGPIO"] = EMSESP::system_.pbutton_gpio_;
- node["ledGPIO"] = EMSESP::system_.led_gpio_;
- node["ledType"] = settings.led_type;
- }
- node["hideLed"] = settings.hide_led;
- node["noTokenApi"] = settings.notoken_api;
- node["readonlyMode"] = settings.readonly_mode;
- node["fahrenheit"] = settings.fahrenheit;
- node["dallasParasite"] = settings.dallas_parasite;
- node["boolFormat"] = settings.bool_format;
- node["boolDashboard"] = settings.bool_dashboard;
- node["enumFormat"] = settings.enum_format;
- node["analogEnabled"] = settings.analog_enabled;
- node["telnetEnabled"] = settings.telnet_enabled;
- node["maxWebLogBuffer"] = settings.weblog_buffer;
-
- /*
-#if defined(EMSESP_UNITY)
- node["webLogBuffer"] = 0;
-#else
- node["webLogBuffer"] = EMSESP::webLogService.num_log_messages();
-#endif
-*/
- node["modbusEnabled"] = settings.modbus_enabled;
- node["forceHeatingOff"] = settings.boiler_heatingoff;
- node["developerMode"] = settings.developer_mode;
- });
-
- // Devices - show EMS devices if we have any
- JsonArray devices = output["devices"].to();
- if (!EMSESP::emsdevices.empty()) {
- for (const auto & device_class : EMSFactory::device_handlers()) {
- for (const auto & emsdevice : EMSESP::emsdevices) {
- if (emsdevice && (emsdevice->device_type() == device_class.first)) {
- JsonObject obj = devices.add();
- obj["type"] = emsdevice->device_type_name(); // non translated name
- obj["name"] = emsdevice->name(); // custom name
- obj["deviceID"] = Helpers::hextoa(emsdevice->device_id());
- obj["productID"] = emsdevice->product_id();
- obj["brand"] = emsdevice->brand_to_char();
- obj["version"] = emsdevice->version();
- obj["entities"] = emsdevice->count_entities();
- char result[1000];
- (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::RECEIVED);
- if (result[0] != '\0') {
- obj["handlersReceived"] = result; // don't show handlers if there aren't any
- }
- (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::FETCHED);
- if (result[0] != '\0') {
- obj["handlersFetched"] = result;
- }
- (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::PENDING);
- if (result[0] != '\0') {
- obj["handlersPending"] = result;
- }
- (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::IGNORED);
- if (result[0] != '\0') {
- obj["handlersIgnored"] = result;
- }
- }
- }
- }
- }
-
- // Also show EMSESP devices if we have any
- if (EMSESP::temperaturesensor_.count_entities()) {
- JsonObject obj = devices.add();
- obj["type"] = F_(temperaturesensor);
- obj["name"] = F_(temperaturesensor);
- obj["entities"] = EMSESP::temperaturesensor_.count_entities();
- }
- if (EMSESP::analogsensor_.count_entities()) {
- JsonObject obj = devices.add();
- obj["type"] = F_(analogsensor);
- obj["name"] = F_(analogsensor);
- obj["entities"] = EMSESP::analogsensor_.count_entities();
- }
- if (EMSESP::webSchedulerService.count_entities()) {
- JsonObject obj = devices.add();
- obj["type"] = F_(scheduler);
- obj["name"] = F_(scheduler);
- obj["entities"] = EMSESP::webSchedulerService.count_entities();
- }
- if (EMSESP::webCustomEntityService.count_entities()) {
- JsonObject obj = devices.add();
- obj["type"] = F_(custom);
- obj["name"] = F_(custom);
- obj["entities"] = EMSESP::webCustomEntityService.count_entities();
- }
-
- return true; // this function always returns true!
-}
-
-#if defined(EMSESP_TEST)
-// run a test, e.g. http://ems-esp/api?device=system&cmd=test&data=boiler
-bool System::command_test(const char * value, const int8_t id) {
- if (value) {
- return Test::test(value, id);
- } else {
- return false;
- }
-}
-#endif
-
-// takes a board profile and populates a data array with GPIO configurations
-// returns false if profile is unknown
-//
-// 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
-//
-bool System::load_board_profile(std::vector & data, const std::string & board_profile) {
- if (board_profile == "default") {
- return false; // unknown, return false
- } else if (board_profile == "S32") {
- data = {2, 18, 23, 5, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // BBQKees Gateway S32
- valid_system_gpios_ = {0, 2, 5, 18, 23};
- } else if (board_profile == "E32") {
- data = {2, 4, 5, 17, 33, PHY_type::PHY_TYPE_LAN8720, 16, 1, 0, 0}; // BBQKees Gateway E32
- valid_system_gpios_ = {0, 2, 4, 5, 16, 17, 33};
- } else if (board_profile == "E32V2") {
- data = {2, 14, 4, 5, 34, PHY_type::PHY_TYPE_LAN8720, 15, 0, 1, 0}; // BBQKees Gateway E32 V2
- valid_system_gpios_ = {0, 2, 4, 5, 14, 15, 34};
- } else if (board_profile == "E32V2_2") {
- data = {32, 14, 4, 5, 34, PHY_type::PHY_TYPE_LAN8720, 15, 0, 1, 1}; // BBQKees Gateway E32 V2.2, rgb led
- valid_system_gpios_ = {0, 2, 4, 5, 14, 15, 32, 34, 36, 39}; // system analogs 36, 39, led 2
- } else if (board_profile == "MH-ET") {
- data = {2, 18, 23, 5, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // MH-ET Live D1 Mini
- // allow only pins that are marked as `can always be used`
- valid_system_gpios_ = {0, 2, 5, 18, 23, 12, 13, 14, 15, 16, 17, 26, 27, 33};
- // can always be used: 12, 13 ,14, 15, 16, 17, 26, 27, 33
- // can be used if no other function 2, 4, 5, 9, 10, 18, 19, 21, 22, 23, 25, 34, 35, 36, 39
- } else if (board_profile == "NODEMCU") {
- data = {2, 18, 23, 5, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // NodeMCU 32S
- // https://blog.berrybase.de/esp32-node-mcu-module-anfaenger-guide/
- // all available pins, exclude uart0
- valid_system_gpios_ = {0, 2, 5, 18, 23, 4, 12, 13, 14, 15, 16, 17, 21, 22, 25, 26, 27, 32, 33, 34, 35, 36, 39};
- } else if (board_profile == "LOLIN") {
- data = {2, 18, 17, 16, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // Lolin D32
- // https://www.wemos.cc/en/latest/d32/d32.html
- valid_system_gpios_ = {2, 18, 17, 16, 0, 4, 5, 12, 13, 14, 15, 21, 22, 25, 26, 27, 32, 33, 34, 35, 36, 39};
- } else if (board_profile == "OLIMEX") {
- data = {0, 0, 36, 4, 34, PHY_type::PHY_TYPE_LAN8720, -1, 0, 0, 0}; // Olimex ESP32-EVB (uses U1TXD/U1RXD/BUTTON, no LED or Temperature sensor)
- // https://github.com/OLIMEX/ESP32-EVB/blob/master/HARDWARE/REV-K1/ESP32-EVB_Rev_K1.pdf
- // uart0 = 1, 3; CAN = 5, 35; relais = 32, 33; ir = 12(tx), 39(rx); SD-card = 2, 14, 15, button = 34
- // relais and ir can be configured as analog sensor
- valid_system_gpios_ = {4, 34, 36, 12, 13, 21, 22, 25, 26, 27, 32, 33, 39};
- } else if (board_profile == "OLIMEXPOE") {
- data = {0, 0, 36, 4, 34, PHY_type::PHY_TYPE_LAN8720, 12, 0, 3, 0}; // Olimex ESP32-POE
- // https://github.com/OLIMEX/ESP32-POE/blob/master/HARDWARE/ESP32-PoE-hardware-revision-L1/ESP32-PoE_Rev_L1.pdf
- // uart0 = 1, 3; SD-card = 2, 14, 15; button = 34;
- valid_system_gpios_ = {4, 34, 36, 12, 13, 21, 22, 25, 26, 27, 32, 33, 39};
- } else if (board_profile == "C3MINI") {
-#if defined(BOARD_C3_MINI_V1)
- data = {7, 1, 4, 5, 9, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // Lolin C3 Mini V1
-#else
- data = {7, 1, 4, 5, 9, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 1}; // Lolin C3 Mini with RGB Led
-#endif
- // https://www.wemos.cc/en/latest/c3/c3_mini.html
- valid_system_gpios_ = {0, 1, 3, 4, 5, 6, 7, 9, 10, 20, 21};
- } else if (board_profile == "S2MINI") {
- data = {15, 7, 11, 12, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // Lolin S2 Mini
- // https://www.wemos.cc/en/latest/s2/s2_mini.html
- set_valid_system_gpios();
- } else if (board_profile == "S3MINI") {
- data = {17, 18, 8, 5, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // Liligo S3
- // https://lilygo.cc/products/t7-s3
- set_valid_system_gpios();
- } else if (board_profile == "S32S3") {
- data = {2, 18, 5, 17, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // BBQKees Gateway S3
- valid_system_gpios_ = {0, 2, 5, 17, 18};
- } else {
- return false; // unknown, return false
- }
-
- return true;
-}
-
-// txpause command - temporarily pause the TX, by setting Txmode to 0 (disabled)
-bool System::command_txpause(const char * value, const int8_t id) {
- bool arg;
- if (!Helpers::value2bool(value, arg)) {
- return false; // argument not recognized
- }
-
- if (!arg) {
- // arg = false: Tx mode to 0 (disabled) to pause
- if (EMSbus::tx_mode() == EMS_TXMODE_OFF) {
- EMSESP::webSettingsService.read([&](WebSettings & settings) {
- EMSbus::tx_mode(settings.tx_mode);
-#ifdef EMSESP_DEBUG
- LOG_INFO("TX mode restored (value %d)", settings.tx_mode);
-#else
- LOG_INFO("TX active");
-#endif
- });
- }
- } else {
- // pause = true: Tx mode to 0 (disabled) to pause
- if (EMSbus::tx_mode() != EMS_TXMODE_OFF) {
- EMSbus::tx_mode(EMS_TXMODE_OFF);
-#ifdef EMSESP_DEBUG
- LOG_INFO("TX mode set to OFF (value %d)", EMS_TXMODE_OFF);
-#else
- LOG_INFO("TX paused");
-#endif
- }
- }
- return true;
-}
-
-// format command - factory reset, removing all config files
-bool System::command_format(const char * value, const int8_t id) {
-#if !defined(EMSESP_STANDALONE) && !defined(EMSESP_TEST)
- // don't really format the filesystem in test or standalone mode
- if (LittleFS.format()) {
- LOG_INFO("Filesystem formatted successfully. All config files removed.");
- } else {
- LOG_ERROR("Format failed");
- }
-#else
- LOG_ERROR("Format command not available in standalone or test mode");
-#endif
-
- // restart will be handled by the main loop
- EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED);
- return true;
-}
-
-// restart command - perform a hard reset (system reboot)
-bool System::command_restart(const char * value, const int8_t id) {
- if (id == 0) {
- // if it has an id then it's a web call and we need to queue the restart
- // default id is -1 when calling /api/system/restart directly for example
- LOG_INFO("Preparing to restart system");
- EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);
- return true;
- }
-
- LOG_INFO("Restarting system immediately");
- // restart will be handled by the main loop
- EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED);
- return true;
-}
-
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wswitch"
-
-std::string System::reset_reason(uint8_t cpu) const {
-#ifndef EMSESP_STANDALONE
- switch (esp_rom_get_reset_reason(cpu)) {
- case RESET_REASON_CHIP_POWER_ON:
- return ("Power on reset");
- case 2: // not on esp32
- return ("reset pin");
- case RESET_REASON_CORE_SW:
- return ("Software reset");
- case 4: // not on S2, C3
- return ("Legacy watch dog reset");
- case RESET_REASON_CORE_DEEP_SLEEP:
- return ("Deep sleep reset");
- case 6: // RESET_REASON_CORE_SDIO: // not on S2, S3, C3
- return ("Reset by SDIO");
- case RESET_REASON_CORE_MWDT0:
- return ("Timer group0 watch dog reset");
- case RESET_REASON_CORE_MWDT1:
- return ("Timer group1 watch dog reset");
- case RESET_REASON_CORE_RTC_WDT:
- return ("RTC watch dog reset");
- case 10:
- return ("Intrusion reset CPU");
- case RESET_REASON_CPU0_MWDT0:
- return ("Timer group reset CPU");
- case RESET_REASON_CPU0_SW:
- return ("Software reset CPU");
- case RESET_REASON_CPU0_RTC_WDT:
- return ("RTC watch dog reset: CPU");
- case 14: // RESET_REASON_CPU1_CPU0: // not on S2, S3, C3
- return ("APP CPU reset by PRO CPU");
- case RESET_REASON_SYS_BROWN_OUT:
- return ("Brownout reset");
- case RESET_REASON_SYS_RTC_WDT:
- return ("RTC watch dog reset: CPU+RTC");
- default:
- break;
- }
-#endif
- return "Unknown";
-}
-#pragma GCC diagnostic pop
-
-// set NTP status
-void System::ntp_connected(bool b) {
- if (b != ntp_connected_) {
- if (b) {
- LOG_INFO("NTP connected");
- set_partition_install_date();
- } else {
- LOG_WARNING("NTP disconnected"); // if turned off report it
- }
- }
-
- ntp_connected_ = b;
- ntp_last_check_ = b ? uuid::get_uptime_sec() : 0;
-}
-
-// get NTP status
-bool System::ntp_connected() {
- // timeout 2 hours, ntp sync is normally every hour.
- if ((uuid::get_uptime_sec() - ntp_last_check_ > 7201) && ntp_connected_) {
- ntp_connected(false);
- }
-
- return ntp_connected_;
-}
-
-// see if its a BBQKees Gateway by checking the eFuse values
-String System::getBBQKeesGatewayDetails(uint8_t detail) {
-#ifndef EMSESP_STANDALONE
- union {
- struct {
- uint32_t no : 4;
- uint32_t month : 4;
- uint32_t year : 8;
- uint32_t rev_minor : 4;
- uint32_t rev_major : 4;
- uint32_t model : 4;
- uint32_t mfg : 4;
- };
- uint32_t reg;
- } gw;
-
- for (uint8_t reg = 0; reg < 8; reg++) {
- gw.reg = esp_efuse_read_reg(EFUSE_BLK3, reg);
- if (reg == 7 || esp_efuse_read_reg(EFUSE_BLK3, reg + 1) == 0)
- break;
- }
-
- const char * mfg[] = {"unknown", "BBQKees Electronics", "", "", "", "", "", ""};
- const char * model[] = {"unknown", "S3", "E32V2", "E32V2.2", "S32", "E32", "", "", ""};
- const char * board[] = {"CUSTOM", "S32S3", "E32V2", "E32V2_2", "S32", "E32", "", "", ""};
-
- switch (detail) {
- case FUSE_VALUE::MFG:
- return gw.mfg < 2 ? String(mfg[gw.mfg]) : "unknown";
- case FUSE_VALUE::MODEL:
- return gw.model < 6 ? String(model[gw.model]) : "unknown";
- case FUSE_VALUE::BOARD:
- return gw.model < 6 ? String(board[gw.model]) : board_profile_;
- case FUSE_VALUE::REV:
- return String(gw.rev_major) + "." + String(gw.rev_minor);
- case FUSE_VALUE::BATCH:
- return String(2000 + gw.year) + (gw.month < 10 ? "0" : "") + String(gw.month) + String(gw.no);
- case FUSE_VALUE::FUSE:
- return "0x" + String(gw.reg, 16);
- case FUSE_VALUE::ALL:
- default:
- break;
- }
-
- if (!gw.reg || gw.mfg > 1 || gw.model > 5) {
- return "";
- }
-
- return String(mfg[gw.mfg]) + " " + String(model[gw.model]) + " rev." + String(gw.rev_major) + "." + String(gw.rev_minor) + "/" + String(2000 + gw.year)
- + (gw.month < 10 ? "0" : "") + String(gw.month) + String(gw.no);
-#else
- return "";
-#endif
-}
-
-// Stream from an URL and send straight to OTA uploader service.
-//
-// This function needs to be called twice, 1st pass once with a url to persist it, 2nd pass with no arguments to start the upload
-// This is to avoid timeouts in callback functions, like calling from a web hook.
-bool System::uploadFirmwareURL(const char * url) {
-#ifndef EMSESP_STANDALONE
- static String saved_url;
-
- if (url && strlen(url) > 0) {
- // if the passed URL is "reset" abort the current upload. This is called when an error happens during OTA
- if (strncmp(url, "reset", 5) == 0) {
- LOG_DEBUG("Firmware upload - resetting");
- saved_url.clear();
- return true;
- }
-
- // given a URL to download from, save it ready for the 2nd pass
- saved_url = url;
- LOG_INFO("Firmware location: %s", saved_url.c_str());
- EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_UPLOAD); // we're ready to start the upload
- return true;
- }
-
- // check we have a valid URL from the 1st pass
- if (saved_url.isEmpty()) {
- LOG_ERROR("Firmware upload failed - invalid URL");
- return false; // error
- }
-
- Shell::loop_all(); // flush log buffers so latest messages are shown in console
-
- // Configure temporary client
- HTTPClient http;
- http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); // important for GitHub 302's
- http.setTimeout(8000);
- http.useHTTP10(true); // use HTTP/1.0 for update since the update handler does not support any transfer Encoding
- http.begin(saved_url);
-
- // start a connection, returns -1 if fails
- int httpCode = http.GET();
- if (httpCode != HTTP_CODE_OK) {
- LOG_ERROR("Firmware upload failed - HTTP code %d", httpCode);
- http.end();
- return false; // error
- }
-
- int firmware_size = http.getSize();
-
- // check we have a valid size
- if (firmware_size < 2097152) { // 2MB or greater is required
- LOG_ERROR("Firmware upload failed - invalid size");
- http.end();
- return false; // error
- }
-
- // check we have enough space for the upload in the ota partition
- if (!Update.begin(firmware_size)) {
- LOG_ERROR("Firmware upload failed - no space");
- http.end();
- return false; // error
- }
-
- LOG_INFO("Firmware uploading (size: %d KB). Please wait...", firmware_size / 1024);
-
- Shell::loop_all(); // flush log buffers so latest messages are shown in console
-
- // we're about to start the upload, set the status so the Web System Monitor spots it
- EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING);
-
- // set a callback so we can monitor progress in the WebUI
- Update.onProgress([](size_t progress, size_t total) { EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING + (progress * 100 / total)); });
-
- // get tcp stream and send it to Updater
- WiFiClient * stream = http.getStreamPtr();
- if (Update.writeStream(*stream) != firmware_size) {
- LOG_ERROR("Firmware upload failed - size differences");
- http.end();
- EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
- return false; // error
- }
-
- if (!Update.end(true)) {
- LOG_ERROR("Firmware upload failed - general error");
- http.end();
- EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
- return false; // error
- }
-
- // finished with upload
- http.end();
- saved_url.clear(); // prevent from downloading again
- LOG_INFO("Firmware uploaded successfully. Restarting...");
- EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);
-#endif
-
- return true; // OK
-}
-
-// read command, e.g. read [offset] [length] from console or API
-// from Console use quotes so: call system read " [offset] [length]"
-bool System::readCommand(const char * data) {
- if (!data) {
- return false;
- }
-
- // extract [offset] [length] from string
- char * p;
- char value[11];
-
- // make a copy so we can iterate, max 15 chars (XX XXXX XX XX)
- char data_args[15];
- strlcpy(data_args, data, sizeof(data_args));
-
- uint8_t device_id = 0; // is in hex
- uint16_t type_id = 0; // is in hex
- uint8_t length = 0;
- uint8_t offset = 0;
-
- // first check deviceID
- if ((p = strtok(data_args, " ,"))) { // delimiter comma or space
- strlcpy(value, p, sizeof(value)); // get string
- device_id = (uint8_t)Helpers::hextoint(value); // convert hex to int
- if (!EMSESP::valid_device(device_id)) {
- LOG_ERROR("Invalid device ID (0x%02X) in read command", device_id);
- return false; // invalid device
- }
- }
-
- // iterate until end
- uint8_t num_args = 0;
- while (p != 0) {
- if ((p = strtok(nullptr, " ,"))) { // delimiter comma or space
- strlcpy(value, p, sizeof(value)); // get string
- if (num_args == 0) {
- type_id = (uint16_t)Helpers::hextoint(value); // convert hex to int
- } else if (num_args == 1) {
- offset = Helpers::atoint(value); // decimal
- } else if (num_args == 2) {
- length = Helpers::atoint(value); // decimal
- }
- num_args++;
- }
- }
-
- if (num_args == 0) {
- return false; // invalid number of arguments
- }
-
- EMSESP::send_read_request(type_id, device_id, offset, length, true);
- EMSESP::set_read_id(type_id);
-
- return true;
-}
-
-// system read command
-bool System::command_read(const char * value, const int8_t id) {
- return readCommand(value);
-}
-
-// set the system status code - SYSTEM_STATUS in system.h
-// this is also used in the SystemMonitor.tsx WebUI to show the progress of the firmware upload, start at 100
-void System::systemStatus(uint8_t status_code) {
- if (systemStatus_ != status_code) {
- systemStatus_ = status_code;
-#ifdef EMSESP_DEBUG
- if (status_code < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) {
- LOG_DEBUG("Setting System status code %d", status_code);
- }
-#endif
- }
-}
-
-uint8_t System::systemStatus() {
- return systemStatus_;
-}
-
-// takes two arguments:
-// the first is the full range of pins to consider
-// the second is a string range of GPIOs to exclude, like "6-11, 1, 23, 24-48"
-// returns a vector array of GPIOs that are valid for use
-std::vector> System::string_range_to_vector(const std::string & range, const std::string & exclude) {
- std::vector> gpios;
- std::string::size_type pos = 0;
- std::string::size_type prev = 0;
-
- auto process_part = [&gpios](std::string part) {
- // trim whitespace
- part.erase(0, part.find_first_not_of(" \t"));
- part.erase(part.find_last_not_of(" \t") + 1);
-
- // check if it's a range (contains '-')
- std::string::size_type dash_pos = part.find('-');
- if (dash_pos != std::string::npos) {
- // it's a range like "6-11"
- int start = std::stoi(part.substr(0, dash_pos));
- int end = std::stoi(part.substr(dash_pos + 1));
- for (int i = start; i <= end; i++) {
- gpios.push_back(static_cast(i));
- }
- } else {
- gpios.push_back(static_cast(std::stoi(part)));
- }
- };
-
- while ((pos = range.find(',', prev)) != std::string::npos) {
- process_part(range.substr(prev, pos - prev));
- prev = pos + 1;
- }
-
- // handle the last part
- process_part(range.substr(prev));
-
- // if exclude list is provided, parse it and remove excluded GPIOs
- if (!exclude.empty()) {
- std::vector> exclude_gpios;
- pos = 0;
- prev = 0;
-
- auto process_exclude = [&exclude_gpios](std::string part) {
- // trim whitespace
- part.erase(0, part.find_first_not_of(" \t"));
- part.erase(part.find_last_not_of(" \t") + 1);
-
- // check if it's a range (contains '-')
- std::string::size_type dash_pos = part.find('-');
- if (dash_pos != std::string::npos) {
- // it's a range like "6-11"
- int start = std::stoi(part.substr(0, dash_pos));
- int end = std::stoi(part.substr(dash_pos + 1));
- for (int i = start; i <= end; i++) {
- exclude_gpios.push_back(static_cast(i));
- }
- } else {
- exclude_gpios.push_back(static_cast(std::stoi(part)));
- }
- };
-
- while ((pos = exclude.find(',', prev)) != std::string::npos) {
- process_exclude(exclude.substr(prev, pos - prev));
- prev = pos + 1;
- }
-
- // handle the last part
- process_exclude(exclude.substr(prev));
-
- // remove excluded GPIOs from the main list
- gpios.erase(std::remove_if(gpios.begin(),
- gpios.end(),
- [&exclude_gpios](uint8_t gpio) { return std::find(exclude_gpios.begin(), exclude_gpios.end(), gpio) != exclude_gpios.end(); }),
- gpios.end());
- }
-
- return gpios;
-}
-
-// initialize a list of valid GPIOs based on the ESP32 board
-// string_to_vector() take two strings, the first is the range of GPIOs to use, the second is a list of GPIOs to exclude
-// notes:
-// we always allow 0 (which is usually a strapping pin), because it's used to indicate whether EMS-ESP Dallas or the LED is disabled
-// we allow UART0, 1 and 2 as they are configurable
-// strapping pins are disabled as they can affect boot behaviour
-// we accept GPIOs that are fixed on BBQKees boards
-//
-void System::set_valid_system_gpios() {
- valid_system_gpios_.clear(); // reset system list
- used_gpios_.clear(); // reset used list
-
- // get free gpios based on board/platform type
-#if CONFIG_IDF_TARGET_ESP32C3
- // https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/peripherals/gpio.html
- // excluded:
- // GPIO2, GPIO8 - GPIO9 = strapping pins
- // GPIO12 - GPIO17 = used for SPI flash and PSRAM
- // GPIO18 - GPIO19 = USB-JTAG
- //
- // notes on what is allowed:
- // GPIO09 = button on BOARD_C3_MINI_V1
- // GPIO20 - GPIO21 = UART0, , no chip connected because native USB
- valid_system_gpios_ = string_range_to_vector("0-21", "2, 8, 12-17, 18-19");
-
-#elif CONFIG_IDF_TARGET_ESP32S2
- // https://docs.espressif.com/projects/esp-idf/en/stable/esp32s2/api-reference/peripherals/gpio.html
- // excluded:
- // GPIO26 - GPIO32 = SPI flash and PSRAM
- // GPIO45 - GPIO46 = strapping pins
- // GPIO39 - GPIO42 = USB-JTAG
- // GPIO22 - GPIO25 = don't exist
- // GPIO19 - GPIO20 = USB
- //
- // notes on what is allowed:
- // GPIO43, GPIO44 = UART0, no chip connected because native USB
- valid_system_gpios_ = string_range_to_vector("0-46", "19, 20, 26-32, 45-46, 39-42, 22-25");
-
-#elif CONFIG_IDF_TARGET_ESP32S3
- // https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/gpio.html
- // excluded:
- // GPIO3, GPIO45 - GPIO46 = strapping pins
- // GPIO26 - GPIO32 = SPI flash and PSRAM and not recommended
- // GPIO19 - GPIO20 = USB-JTAG
- // GPIO22 - GPIO25 = don't exist
- //
- // notes on what is allowed:
- // GPIO11 - GPIO19 = ADC analog input only pins
- // GPIO47 - GPIO48 = valid on a Wemos S3
- // GPIO8 = used by Liligo S3 board profile for Rx
- if (ESP.getPsramSize() > 0) {
- // GPIO33 - GPIO37 = Octal flash/PSRAM
- valid_system_gpios_ = string_range_to_vector("0-48", "3, 45-46, 26-32, 33-37, 19-20, 22-25");
- } else {
- valid_system_gpios_ = string_range_to_vector("0-48", "3, 45-46, 26-32, 19-20, 22-25");
- }
-
-#elif CONFIG_IDF_TARGET_ESP32
- // https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/gpio.html
- // excluded:
- // GPIO6 - GPIO11, GPIO16 - GPIO17 = used for SPI flash and PSRAM (dio mode only GPIO06-GPIO08, GPIO11)
- // GPIO20, GPIO24, GPIO28 - GPIO31 = don't exist
- // GPIO01, GPIO03 = UART0, normal connected to UART/USB chip
- //
- // notes on known boards:
- // boards have valid gpios depending on pinout and internal used gpios, see: `load_board_profile()`
- //
- // notes on BBQKees boards:
- // *** We block all free GPIOS in load_board_profile() ***
- // GPIO2, GPIO4, GPIO5, GPIO14 = used on BBQKees boards for either LED, Dallas or Rx
- // GPIO12, GPIO13, GPIO35 = BBQKees E32V2_2 internal system pins
- // GPIO33 = BBQKees E32V_2 unused internal NTC system sensor
- // GPIO36 = used on BBQKees boards for supply_voltage (E32V2.2)
- // GPIO39 = used on BBQKees boards for core_voltage (E32V2.2)
- //
- // notes on what is allowed with special functions:
- // GPIO01, GPIO03 = UART0, not always connected to uart/usb chip
- // GPIO12 - GPIO15 = JTAG, normally not used
- // GPIO00, GPIO05, GPIO12, GPIO15 = strapping pins, can be used with care
- // GPIO34, GPIO35, GPIO37 = input only
- // GPIO23 and GPIO18 are used by Ethernet, excluded later by eth config
- // GPIO00, GPIO02, GPIO04, GPIO12 - GPIO15, GPIO25 - GPIO27 = ADC2 (10 ch), used by WiFI-driver
- // GPIO32 - GPIO39 = ADC1 (8 ch), can always be used
- if (ESP.getPsramSize() > 0) {
- // remove SPI0/1 PSRAM pins GPIO16 (CS) and GPIO17 (CLK) from the list
- valid_system_gpios_ = string_range_to_vector("0-39", "6-11, 16, 17, 20, 24, 28-31");
- } else {
- valid_system_gpios_ = string_range_to_vector("0-39", "6-11, 20, 24, 28-31");
- }
-#elif CONFIG_IDF_TARGET_ESP32C6
- // https://docs.espressif.com/projects/esp-idf/en/v5.5.3/esp32c6/api-reference/peripherals/gpio.html
- // 24-30 used for flash, 12-13 USB, 16-17 uart0
- valid_system_gpios_ = string_range_to_vector("0-30", "12-13, 16-17, 24-30");
-#elif defined(EMSESP_STANDALONE)
- valid_system_gpios_ = string_range_to_vector("0-39");
-#endif
- valid_system_gpios_.shrink_to_fit();
-}
-
-// check if a pin is valid ESP32 pin and if not already used, add to the used gpio list
-// return false if not allowed or already used
-bool System::add_gpio(uint8_t pin, const char * source_name) {
- // check if this is a valid user GPIO
- if (std::find(valid_system_gpios_.begin(), valid_system_gpios_.end(), pin) != valid_system_gpios_.end()) {
- // It's valid now check if it's already in the used list
- auto it = std::find_if(used_gpios_.begin(), used_gpios_.end(), [pin](const GpioUsage & usage) { return usage.pin == pin; });
- if (it != used_gpios_.end()) {
- LOG_WARNING("GPIO %d for %s is already in use by %s", pin, source_name, it->source.c_str());
- return false; // Pin is already used
- }
- } else {
- // not valid
- LOG_WARNING("GPIO %d for %s is not valid", pin, source_name);
- return false;
- }
-
- // remove the old pin, if exists from used list
- remove_gpio(pin);
-
- LOG_DEBUG("Adding GPIO %d for %s to used gpio list", pin, source_name);
- used_gpios_.push_back({pin, source_name}); // add to used list
-
- return true;
-}
-
-// remove a gpio from both valid and used lists
-void System::remove_gpio(uint8_t pin, bool also_system) {
- auto it = std::find_if(used_gpios_.begin(), used_gpios_.end(), [pin](const GpioUsage & usage) { return usage.pin == pin; });
- if (it != used_gpios_.end()) {
- LOG_DEBUG("GPIO %d removed from used gpio list", pin);
- used_gpios_.erase(it);
- }
-
- if (also_system) {
- auto it_sys = std::find(valid_system_gpios_.begin(), valid_system_gpios_.end(), pin);
- if (it_sys != valid_system_gpios_.end()) {
- LOG_DEBUG("GPIO %d removed from valid gpio list", pin);
- valid_system_gpios_.erase(it_sys);
- }
- }
-}
-
-// return a list of GPIO's available for use
-std::vector System::available_gpios() {
- std::vector gpios;
- for (const auto & gpio : valid_system_gpios_) {
- if (std::find_if(used_gpios_.begin(), used_gpios_.end(), [gpio](const GpioUsage & usage) { return usage.pin == gpio; }) == used_gpios_.end()) {
- gpios.push_back(gpio); // didn't find it in used_gpios_, so it's available
- }
- }
- return gpios;
-}
-
-// make a snapshot of the current GPIOs
-void System::make_snapshot_gpios(std::vector & u_gpios, std::vector & s_gpios) {
- for (const auto & usage : used_gpios_) {
- u_gpios.push_back(usage.pin);
- }
- for (const auto & gpio : valid_system_gpios_) {
- s_gpios.push_back(gpio);
- }
-}
-
-// restore the GPIOs from the snapshot
-void System::restore_snapshot_gpios(std::vector & u_gpios, std::vector & s_gpios) {
- used_gpios_.clear();
- for (const auto & gpio : u_gpios) {
- used_gpios_.push_back({static_cast(gpio), "restored"});
- }
-
- valid_system_gpios_.clear();
- for (const auto & gpio : s_gpios) {
- valid_system_gpios_.push_back(gpio);
- }
-}
-
-} // namespace emsesp
+
+ // convert a single config file into a section of the output json object
+ void System::exportSettings(const std::string & type, const char * filename, JsonObject output) {
+ if (type != "settings") {
+ output["type"] = type; // add the type to the output, not for settings as it's already added because its grouped
+ }
+
+ #ifndef EMSESP_STANDALONE
+ const char * section = nullptr;
+ for (const auto & [f, label] : SECTION_MAP) {
+ if (strcmp(f, filename) == 0) {
+ section = label;
+ break;
+ }
+ }
+
+ if (!section) {
+ return;
+ }
+
+ File settingsFile = LittleFS.open(filename);
+ if (settingsFile) {
+ JsonDocument jsonDocument;
+ DeserializationError error = deserializeJson(jsonDocument, settingsFile);
+ if (error == DeserializationError::Ok && jsonDocument.is()) {
+ JsonObject node = output[section].to();
+ for (JsonPair kvp : jsonDocument.as()) {
+ node[kvp.key()] = kvp.value();
+ }
+ } else {
+ LOG_ERROR("Failed to deserialize settings file %s", filename);
+ }
+ LOG_DEBUG("Exported %s settings from file %s", section, filename);
+ settingsFile.close();
+ } else {
+ LOG_ERROR("No settings file for %s found", filename);
+ }
+ #endif
+ }
+
+ // full system backup of all settings files
+ void System::exportSystemBackup(JsonObject output) {
+ output["type"] = "systembackup"; // add the type to the output
+ output["version"] = EMSESP_APP_VERSION; // add the version to the output
+
+ #ifndef EMSESP_STANDALONE
+ // add date/time if NTP enabled and active
+ if ((esp_sntp_enabled()) && (EMSESP::system_.ntp_connected())) {
+ time_t now = time(nullptr);
+ if (now > 1500000000L) {
+ char t[25];
+ strftime(t, sizeof(t), "%FT%T", localtime(&now));
+ output["date"] = t;
+ }
+ }
+ #endif
+
+ // create an array of objects for each settings file
+ JsonArray nodes = output["systembackup"].to();
+
+ // start with settings by grouping them together
+ JsonObject node = nodes.add();
+ node["type"] = "settings"; // add type once for this group
+ exportSettings("settings", NETWORK_SETTINGS_FILE, node);
+ exportSettings("settings", AP_SETTINGS_FILE, node);
+ exportSettings("settings", MQTT_SETTINGS_FILE, node);
+ exportSettings("settings", NTP_SETTINGS_FILE, node);
+ exportSettings("settings", SECURITY_SETTINGS_FILE, node);
+ exportSettings("settings", EMSESP_SETTINGS_FILE, node);
+
+ node = nodes.add();
+ exportSettings("schedule", EMSESP_SCHEDULER_FILE, node);
+ node = nodes.add();
+ exportSettings("customizations", EMSESP_CUSTOMIZATION_FILE, node);
+ node = nodes.add();
+ exportSettings("entities", EMSESP_CUSTOMENTITY_FILE, node);
+ node = nodes.add();
+ exportSettings("modules", EMSESP_MODULES_FILE, node);
+
+ #ifndef EMSESP_STANDALONE
+ // special case for custom support
+ File file = LittleFS.open(EMSESP_CUSTOMSUPPORT_FILE, "r");
+ if (file) {
+ JsonDocument jsonDocument;
+ DeserializationError error = deserializeJson(jsonDocument, file);
+ if (error == DeserializationError::Ok && jsonDocument.is()) {
+ JsonObject node = nodes.add();
+ node["type"] = "customSupport";
+ node["data"] = jsonDocument.as();
+ }
+ file.close();
+ LOG_DEBUG("Exported custom support file %s", EMSESP_CUSTOMSUPPORT_FILE);
+ }
+
+ // Backup NVS values
+ node = nodes.add();
+ node["type"] = "nvs";
+
+ const char * nvs_part = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_NVS, "nvs1") ? "nvs1" : "nvs"; // nvs1 is on 16MBs
+ nvs_iterator_t it = nullptr;
+ esp_err_t err = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY, &it);
+ if (err != ESP_OK) {
+ LOG_ERROR("Failed to find NVS entry for %s", nvs_part);
+ return;
+ }
+
+ JsonArray entries = node["nvs"].to();
+ while (err == ESP_OK) {
+ nvs_entry_info_t info;
+ nvs_entry_info(it, &info);
+ JsonObject entry = entries.add();
+ entry["type"] = info.type; // e.g. NVS_TYPE_U32 or NVS_TYPE_STR etc
+ entry["key"] = info.key;
+
+ LOG_DEBUG("Exporting NVS value: %s = %d", info.key, info.type);
+
+ // serialize based on the type. We use putString, putChar, putUChar, putDouble, putBool, putULong only
+ switch (info.type) {
+ case NVS_TYPE_I8:
+ entry["value"] = EMSESP::nvs_.getChar(info.key);
+ break;
+ case NVS_TYPE_U8:
+ // also used for bool
+ entry["value"] = EMSESP::nvs_.getUChar(info.key);
+ break;
+ case NVS_TYPE_I32:
+ entry["value"] = EMSESP::nvs_.getInt(info.key);
+ break;
+ case NVS_TYPE_U32:
+ entry["value"] = EMSESP::nvs_.getUInt(info.key);
+ break;
+ case NVS_TYPE_I64:
+ entry["value"] = EMSESP::nvs_.getLong64(info.key);
+ break;
+ case NVS_TYPE_U64:
+ entry["value"] = EMSESP::nvs_.getULong64(info.key);
+ break;
+ case NVS_TYPE_BLOB:
+ // used for double (e.g. sensor values, nrgheat, nrgww), and stored as bytes in NVS
+ entry["value"] = EMSESP::nvs_.getDouble(info.key);
+ break;
+ case NVS_TYPE_STR:
+ case NVS_TYPE_ANY:
+ default:
+ // any other value we store as a string
+ entry["value"] = EMSESP::nvs_.getString(info.key);
+ break;
+ }
+
+ err = nvs_entry_next(&it);
+ }
+
+ if (it != nullptr) {
+ nvs_release_iterator(it);
+ }
+ #endif
+ }
+
+ // write a settings file using input from a json object, called from upload/restore
+ bool System::saveSettings(const char * filename, JsonObject input) {
+ #ifndef EMSESP_STANDALONE
+ const char * section = nullptr;
+ for (const auto & [f, label] : SECTION_MAP) {
+ if (strcmp(f, filename) == 0) {
+ section = label;
+ break;
+ }
+ }
+
+ if (!section) {
+ return false;
+ }
+
+ JsonObject section_json = input[section];
+ if (section_json) {
+ File section_file = LittleFS.open(filename, "w");
+ if (section_file) {
+ LOG_DEBUG("Applying new uploaded %s data", section);
+ serializeJson(section_json, section_file);
+ section_file.close();
+ return true; // reboot required
+ }
+ }
+ #endif
+
+ return false; // not found
+ }
+
+ // set a entity of services 'network', 'settings', 'mqtt', etc.
+ bool System::command_service(const char * cmd, const char * value) {
+ bool ok = false;
+ bool b;
+ if (Helpers::value2bool(value, b)) {
+ if (!strcmp(cmd, "settings/showertimer")) {
+ EMSESP::webSettingsService.update([&](WebSettings & settings) {
+ settings.shower_timer = b;
+ return StateUpdateResult::CHANGED;
+ });
+ EMSESP::shower_.shower_timer(b);
+ ok = true;
+ } else if (!strcmp(cmd, "settings/showeralert")) {
+ EMSESP::webSettingsService.update([&](WebSettings & settings) {
+ settings.shower_alert = b;
+ return StateUpdateResult::CHANGED;
+ });
+ EMSESP::shower_.shower_alert(b);
+ ok = true;
+ } else if (!strcmp(cmd, "settings/hideled")) {
+ EMSESP::webSettingsService.update([&](WebSettings & settings) {
+ settings.hide_led = b;
+ return StateUpdateResult::CHANGED;
+ });
+ EMSESP::system_.hide_led(b);
+ ok = true;
+ } else if (!strcmp(cmd, "settings/analogenabled")) {
+ EMSESP::webSettingsService.update([&](WebSettings & settings) {
+ settings.analog_enabled = b;
+ return StateUpdateResult::CHANGED;
+ });
+ EMSESP::system_.analog_enabled(b);
+ ok = true;
+ } else if (!strcmp(cmd, "mqtt/enabled")) {
+ EMSESP::esp32React.getMqttSettingsService()->update([&](MqttSettings & Settings) {
+ Settings.enabled = b;
+ return StateUpdateResult::CHANGED;
+ });
+ ok = true;
+ } else if (!strcmp(cmd, "ap/enabled")) {
+ EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & Settings) {
+ Settings.provisionMode = b ? 0 : 2;
+ return StateUpdateResult::CHANGED;
+ });
+ ok = true;
+ } else if (!strcmp(cmd, "ntp/enabled")) {
+ EMSESP::esp32React.getNTPSettingsService()->update([&](NTPSettings & Settings) {
+ Settings.enabled = b;
+ return StateUpdateResult::CHANGED;
+ });
+ ok = true;
+ } else if (!strcmp(cmd, "syslog/enabled")) {
+ EMSESP::webSettingsService.update([&](WebSettings & settings) {
+ settings.syslog_enabled = b;
+ return StateUpdateResult::CHANGED;
+ });
+ EMSESP::system_.syslog_enabled_ = b;
+ EMSESP::system_.syslog_init();
+ ok = true;
+ }
+ }
+
+ int n;
+ if (!ok && Helpers::value2number(value, n)) {
+ #ifndef EMSESP_STANDALONE
+ if (!strcmp(cmd, "fuse/mfg")) {
+ ok = esp_efuse_write_reg(EFUSE_BLK3, 0, (uint32_t)n) == ESP_OK;
+ ok ? LOG_INFO("fuse programed with value '%X': successful", n) : LOG_ERROR("fuse programed with value '%X': failed", n);
+ }
+ if (!strcmp(cmd, "fuse/mfgadd")) {
+ uint8_t reg = 0;
+ while (esp_efuse_read_reg(EFUSE_BLK3, reg) != 0 && reg < 7)
+ reg++;
+ ok = esp_efuse_write_reg(EFUSE_BLK3, reg, (uint32_t)n) == ESP_OK;
+ ok ? LOG_INFO("fuse %d programed with value '%X': successful", reg, n) : LOG_ERROR("fuse %d programed with value '%X': failed", reg, n);
+ return true;
+ }
+ #endif
+ }
+
+ if (ok) {
+ LOG_INFO("System command '%s' with value '%s'", cmd, value);
+ }
+ return ok;
+ }
+
+ // return back a system value
+ bool System::get_value_info(JsonObject output, const char * cmd) {
+ if (cmd == nullptr || strlen(cmd) == 0) {
+ LOG_ERROR("empty system command");
+ return false;
+ }
+
+ // check for hardcoded "info"/"value"
+ if (!strcmp(cmd, F_(info)) || !strcmp(cmd, F_(values))) {
+ return command_info("", 0, output);
+ }
+
+ // check for metrics
+ if (!strcmp(cmd, F_(metrics))) {
+ std::string metrics = get_metrics_prometheus();
+ if (!metrics.empty()) {
+ output["api_data"] = metrics;
+ return true;
+ }
+ return false;
+ }
+
+ // fetch all the data from the system in a different json
+ JsonDocument doc;
+ JsonObject root = doc.to();
+ (void)command_info("", 0, root);
+
+ // list all entities
+ if (!strcmp(cmd, F_(entities))) {
+ for (JsonPair p : root) {
+ if (p.value().is()) {
+ const char * p_key = p.key().c_str(); // Cache the key
+ for (JsonPair p1 : p.value().as()) {
+ const char * p1_key = p1.key().c_str(); // Cache the key
+ JsonObject entity = output[std::string(p_key) + "." + p1_key].to();
+ get_value_json(entity, p_key, p1_key, p1.value());
+ }
+ }
+ }
+ return true;
+ }
+
+ char * val = (char *)strstr(cmd, "/value");
+ if (val) {
+ *val = '\0';
+ }
+
+ char * slash = (char *)strchr(cmd, '/');
+ if (slash) {
+ *slash = '\0';
+ slash++;
+ }
+
+ // list values for a jsonObject in system, e.g. /api/system/network
+ if (!slash || !strcmp(slash, F_(info)) || !strcmp(slash, F_(values))) {
+ for (JsonPair p : root) {
+ if (Helpers::toLower(p.key().c_str()) == cmd && p.value().is()) {
+ for (JsonPair p1 : p.value().as()) {
+ output[p1.key().c_str()] = p1.value().as();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // value info or api_data for a single value
+ // Loop through all the key-value pairs in root to find the key, case independent
+ if (slash) { // search the top level first
+ for (JsonPair p : root) {
+ const char * p_key = p.key().c_str(); // Cache the key
+ if (p.value().is() && Helpers::toLower(p_key) == cmd) {
+ for (JsonPair p1 : p.value().as()) {
+ const char * p1_key = p1.key().c_str(); // Cache the key
+ if (Helpers::toLower(p1_key) == slash && !p1.value().is()) {
+ if (val) {
+ output["api_data"] = p1.value().as();
+ return true;
+ }
+ get_value_json(output, p_key, p1_key, p1.value());
+ return true;
+ }
+ }
+ } // else skip, but we don't have value pairs in system root
+ }
+ }
+ return false;
+ }
+
+ void System::get_value_json(JsonObject output, const std::string & circuit, const std::string & name, JsonVariant val) {
+ output["name"] = name;
+ if (circuit.length()) {
+ output["circuit"] = circuit;
+ }
+ output["readable"] = true;
+ output["writeable"] =
+ (name == "txpause" || name == "showerTimer" || name == "showerAlert" || name == "enabled" || name == "hideLed" || name == "analogEnabled");
+ output["visible"] = true;
+ if (val.is()) {
+ output["value"] = val.as();
+ output["type"] = "boolean";
+ } else if (val.is() || val.is()) {
+ output["value"] = val.as();
+ output["type"] = "number";
+ } else {
+ output["value"] = val.as();
+ output["type"] = "string";
+ }
+ }
+
+ // generate Prometheus metrics format from system values
+ std::string System::get_metrics_prometheus() {
+ std::string result;
+ std::map seen_metrics;
+
+ result.reserve(16000);
+
+ // get system data
+ JsonDocument doc;
+ JsonObject root = doc.to();
+ (void)command_info("", 0, root);
+
+ // helper function to escape Prometheus label values
+ auto escape_label = [](const std::string & str) -> std::string {
+ std::string escaped;
+ for (char c : str) {
+ if (c == '\\') {
+ escaped += "\\\\";
+ } else if (c == '"') {
+ escaped += "\\\"";
+ } else if (c == '\n') {
+ escaped += "\\n";
+ } else {
+ escaped += c;
+ }
+ }
+ return escaped;
+ };
+
+ // helper function to sanitize metric name (convert to lowercase and replace dots with underscores)
+ auto sanitize_name = [](const std::string & name) -> std::string {
+ std::string sanitized = name;
+ for (char & c : sanitized) {
+ if (c == '.') {
+ c = '_';
+ } else if (isupper(c)) {
+ c = tolower(c);
+ } else if (!isalnum(c) && c != '_') {
+ c = '_';
+ }
+ }
+ return sanitized;
+ };
+
+ // helper function to convert label name to lowercase
+ auto to_lowercase = [](const std::string & str) -> std::string {
+ std::string result = str;
+ for (char & c : result) {
+ if (isupper(c)) {
+ c = tolower(c);
+ }
+ }
+ return result;
+ };
+
+ // helper function to check if a field should be ignored
+ auto should_ignore = [](const std::string & path, const std::string & key) -> bool {
+ if (path == "system" && key == "uptime") {
+ return true;
+ }
+ if (path == "ntp" && key == "timestamp") {
+ return true;
+ }
+ if (path.find("devices[") != std::string::npos) {
+ if (key == "handlersReceived" || key == "handlersFetched" || key == "handlersPending" || key == "handlersIgnored") {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ // helper function to process a JSON object recursively
+ std::function process_object = [&](const JsonObject obj, const std::string & prefix) {
+ std::vector> local_info_labels;
+ bool has_nested_objects = false;
+
+ for (JsonPair p : obj) {
+ std::string key = p.key().c_str();
+ std::string metric_name = prefix.empty() ? key : prefix + "_" + key;
+
+ if (should_ignore(prefix, key)) {
+ continue;
+ }
+
+ if (p.value().is()) {
+ // recursive call for nested objects
+ has_nested_objects = true;
+ process_object(p.value().as(), metric_name);
+ } else if (p.value().is()) {
+ // handle arrays (devices)
+ if (key == "devices") {
+ JsonArray devices = p.value().as();
+ for (JsonObject device : devices) {
+ std::vector> device_labels;
+
+ // collect labels from device object
+ for (JsonPair dp : device) {
+ std::string dkey = dp.key().c_str();
+ if (dkey == "type" || dkey == "name" || dkey == "deviceID" || dkey == "brand" || dkey == "version") {
+ if (dp.value().is()) {
+ std::string val = dp.value().as();
+ if (!val.empty()) {
+ device_labels.push_back({to_lowercase(dkey), val});
+ }
+ }
+ }
+ }
+
+ // create productID metric
+ if (device["productID"].is()) {
+ std::string metric = "emsesp_device_productid";
+ if (seen_metrics.find(metric) == seen_metrics.end()) {
+ result += "# HELP emsesp_device_productid productID\n";
+ result += "# TYPE emsesp_device_productid gauge\n";
+ seen_metrics[metric] = true;
+ }
+
+ result += metric;
+ if (!device_labels.empty()) {
+ result += "{";
+ bool first = true;
+ for (const auto & label : device_labels) {
+ if (!first) {
+ result += ", ";
+ }
+ result += label.first + "=\"" + escape_label(label.second) + "\"";
+ first = false;
+ }
+ result += "}";
+ }
+ result += " " + std::to_string(device["productID"].as()) + "\n";
+ }
+
+ // create entities metric
+ if (device["entities"].is()) {
+ std::string metric = "emsesp_device_entities";
+ if (seen_metrics.find(metric) == seen_metrics.end()) {
+ result += "# HELP emsesp_device_entities entities\n";
+ result += "# TYPE emsesp_device_entities gauge\n";
+ seen_metrics[metric] = true;
+ }
+
+ result += metric;
+ if (!device_labels.empty()) {
+ result += "{";
+ bool first = true;
+ for (const auto & label : device_labels) {
+ if (!first) {
+ result += ", ";
+ }
+ result += label.first + "=\"" + escape_label(label.second) + "\"";
+ first = false;
+ }
+ result += "}";
+ }
+ result += " " + std::to_string(device["entities"].as()) + "\n";
+ }
+ }
+ }
+ } else {
+ // handle primitive values
+ bool is_number = p.value().is() || p.value().is();
+ bool is_bool = p.value().is();
+ bool is_string = p.value().is();
+
+ if (is_number || is_bool) {
+ // add metric
+ std::string full_metric_name = "emsesp_" + sanitize_name(metric_name);
+ if (seen_metrics.find(full_metric_name) == seen_metrics.end()) {
+ result += "# HELP emsesp_" + sanitize_name(metric_name) + " " + key + "\n";
+ result += "# TYPE emsesp_" + sanitize_name(metric_name) + " gauge\n";
+ seen_metrics[full_metric_name] = true;
+ }
+
+ result += full_metric_name + " ";
+ if (is_bool) {
+ result += p.value().as() ? "1" : "0";
+ } else if (p.value().is()) {
+ result += std::to_string(p.value().as());
+ } else {
+ char val_str[30];
+ snprintf(val_str, sizeof(val_str), "%.2f", p.value().as());
+ result += val_str;
+ }
+ result += "\n";
+ } else if (is_string) {
+ // collect string for info metric (skip dynamic strings like uptime and timestamp)
+ std::string val = p.value().as();
+ if (!val.empty() && key != "uptime" && key != "timestamp") {
+ std::string lower_key = to_lowercase(key);
+ // check if key already exists in local_info_labels
+ bool key_exists = false;
+ for (const auto & label : local_info_labels) {
+ if (label.first == lower_key) {
+ key_exists = true;
+ break;
+ }
+ }
+ if (!key_exists) {
+ local_info_labels.push_back({lower_key, val});
+ }
+ }
+ }
+ }
+ }
+
+ // create _info metric for this object level if we have labels and this is a leaf node (no nested objects)
+ if (!local_info_labels.empty() && !prefix.empty() && !has_nested_objects) {
+ std::string info_metric = "emsesp_" + sanitize_name(prefix) + "_info";
+ if (seen_metrics.find(info_metric) == seen_metrics.end()) {
+ result += "# HELP " + info_metric + " info\n";
+ result += "# TYPE " + info_metric + " gauge\n";
+ seen_metrics[info_metric] = true;
+ }
+
+ result += info_metric;
+ // TODO fix, as local_info_labels is always empty here
+ if (!local_info_labels.empty()) {
+ result += "{";
+ bool first = true;
+ for (const auto & label : local_info_labels) {
+ if (!first) {
+ result += ", ";
+ }
+ result += label.first + "=\"" + escape_label(label.second) + "\"";
+ first = false;
+ }
+ result += "}";
+ }
+ result += " 1\n";
+ }
+ };
+
+ // process root object
+ process_object(root, "");
+
+ result.shrink_to_fit();
+
+ return result;
+ }
+
+ // return IP or hostname of the EMS-ESP device
+ String System::get_ip_or_hostname() {
+ String result = "ems-esp";
+ #ifndef EMSESP_STANDALONE
+ EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & settings) {
+ if (settings.enableMDNS) {
+ if (EMSESP::system_.ethernet_connected()) {
+ result = ETH.getHostname();
+ } else if (WiFi.status() == WL_CONNECTED) {
+ result = WiFi.getHostname();
+ }
+ } else {
+ // no DNS, use the IP
+ if (EMSESP::system_.ethernet_connected()) {
+ result = ETH.localIP().toString();
+ } else if (WiFi.status() == WL_CONNECTED) {
+ result = WiFi.localIP().toString();
+ }
+ }
+ });
+ #endif
+ return result;
+ }
+
+ // export status information including the device information
+ // http://ems-esp/api/system/info
+ bool System::command_info(const char * value, const int8_t id, JsonObject output) {
+ JsonObject node;
+
+ // System
+ node = output["system"].to();
+ // prevent false-negatives in Unity tests every time the version changes
+ #if defined(EMSESP_UNITY)
+ node["version"] = "dev";
+ #else
+ node["version"] = EMSESP_APP_VERSION;
+ #endif
+ node["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
+ node["uptimeSec"] = uuid::get_uptime_sec();
+ node["resetReason"] = EMSESP::system_.reset_reason(0) + " / " + EMSESP::system_.reset_reason(1);
+ #ifndef EMSESP_STANDALONE
+ node["platform"] = EMSESP_PLATFORM;
+ node["cpuType"] = ESP.getChipModel();
+ node["arduino"] = ARDUINO_VERSION;
+ node["sdk"] = ESP.getSdkVersion();
+ node["freeMem"] = getHeapMem();
+ node["maxAlloc"] = getMaxAllocMem();
+ node["freeCaps"] = heap_caps_get_free_size(MALLOC_CAP_8BIT) / 1024; // includes heap and psram
+ node["usedApp"] = EMSESP::system_.appUsed(); // kilobytes
+ node["freeApp"] = EMSESP::system_.appFree(); // kilobytes
+ node["partition"] = (const char *)esp_ota_get_running_partition()->label; // active partition
+ node["flash_chip_size"] = ESP.getFlashChipSize() / 1024; // kilobytes
+ node["psram"] = (EMSESP::system_.PSram() > 0); // make boolean
+ if (EMSESP::system_.PSram()) {
+ node["psramSize"] = EMSESP::system_.PSram();
+ node["freePsram"] = ESP.getFreePsram() / 1024;
+ }
+ node["model"] = EMSESP::system_.getBBQKeesGatewayDetails();
+ #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
+ node["temperature"] = EMSESP::system_.temperature();
+ #endif
+ #endif
+ node["txpause"] = EMSbus::tx_mode() == EMS_TXMODE_OFF;
+
+ // GPIO information
+ std::string gpios_allowed_str;
+ for (const auto & gpio : valid_system_gpios_) {
+ if (!gpios_allowed_str.empty()) {
+ gpios_allowed_str += ", ";
+ }
+ gpios_allowed_str += Helpers::itoa(gpio);
+ }
+ node["gpios_allowed"] = gpios_allowed_str;
+
+ std::string gpios_in_use_str;
+ auto sorted_gpios = used_gpios_;
+ std::sort(sorted_gpios.begin(), sorted_gpios.end(), [](const GpioUsage & a, const GpioUsage & b) { return a.pin < b.pin; });
+ for (const auto & gpio : sorted_gpios) {
+ if (!gpios_in_use_str.empty()) {
+ gpios_in_use_str += ", ";
+ }
+ gpios_in_use_str += Helpers::itoa(gpio.pin);
+ }
+ node["gpios_in_use"] = gpios_in_use_str;
+
+ std::string gpios_available_str;
+ for (const auto & gpio : available_gpios()) {
+ if (!gpios_available_str.empty()) {
+ gpios_available_str += ", ";
+ }
+ gpios_available_str += Helpers::itoa(gpio);
+ }
+ node["gpios_available"] = gpios_available_str;
+
+ // Network Status
+ node = output["network"].to();
+ #ifndef EMSESP_STANDALONE
+ if (EMSESP::system_.ethernet_connected()) {
+ node["network"] = "Ethernet";
+ node["hostname"] = ETH.getHostname();
+ // node["MAC"] = ETH.macAddress();
+ // node["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask());
+ // node["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP());
+ // node["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP());
+ // if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
+ // node["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6());
+ // }
+ } else if (WiFi.status() == WL_CONNECTED) {
+ node["network"] = "WiFi";
+ node["hostname"] = WiFi.getHostname();
+ node["RSSI"] = WiFi.RSSI();
+ node["WIFIReconnects"] = EMSESP::esp32React.getWifiReconnects();
+ // node["MAC"] = WiFi.macAddress();
+ // node["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask());
+ // node["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP());
+ // node["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP());
+ // if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000") {
+ // node["IPv6 address"] = uuid::printable_to_string(WiFi.localIPv6());
+ // }
+ }
+ #else
+ // for testing
+ node["network"] = "WiFi";
+ node["hostname"] = "ems-esp";
+ node["RSSI"] = -23;
+ #endif
+ EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & settings) {
+ if (WiFi.status() == WL_CONNECTED && !settings.bssid.isEmpty()) {
+ node["BSSID"] = "set"; // we don't disclose the name
+ }
+ node["TxPowerSetting"] = settings.tx_power;
+ node["staticIP"] = settings.staticIPConfig;
+ node["lowBandwidth"] = settings.bandwidth20;
+ node["disableSleep"] = settings.nosleep;
+ node["enableMDNS"] = settings.enableMDNS;
+ node["enableCORS"] = settings.enableCORS;
+ if (settings.enableCORS) {
+ node["CORSOrigin"] = settings.CORSOrigin;
+ }
+ });
+
+ // NTP status
+ node = output["ntp"].to();
+ node["NTPstatus"] = EMSESP::system_.ntp_connected() ? "connected" : "disconnected";
+ EMSESP::esp32React.getNTPSettingsService()->read([&](const NTPSettings & settings) {
+ #ifndef EMSESP_STANDALONE
+ node["enabled"] = settings.enabled;
+ #else
+ node["enabled"] = true;
+ #endif
+ node["server"] = settings.server;
+ node["tzLabel"] = settings.tzLabel;
+ });
+ #ifndef EMSESP_STANDALONE
+ node["timestamp"] = time(nullptr);
+ #endif
+ node["NTPStatus"] = EMSESP::system_.ntp_connected() ? "connected" : "disconnected";
+
+ // AP Status
+ node = output["ap"].to();
+ EMSESP::esp32React.getAPSettingsService()->read([&](const APSettings & settings) {
+ const char * pM[] = {"always", "disconnected", "never"};
+ node["provisionMode"] = pM[settings.provisionMode];
+ node["ssid"] = settings.ssid;
+ #ifndef EMSESP_STANDALONE
+ node["security"] = settings.password.length() ? "wpa2" : "open";
+ node["channel"] = settings.channel;
+ node["ssidHidden"] = settings.ssidHidden;
+ node["maxClients"] = settings.maxClients;
+ node["localIP"] = settings.localIP.toString();
+ node["gatewayIP"] = settings.gatewayIP.toString();
+ node["subnetMask"] = settings.subnetMask.toString();
+ #endif
+ });
+
+ // MQTT Status
+ node = output["mqtt"].to();
+ node["MQTTStatus"] = Mqtt::connected() ? F_(connected) : F_(disconnected);
+ if (Mqtt::enabled()) {
+ node["MQTTPublishes"] = Mqtt::publish_count();
+ node["MQTTQueued"] = Mqtt::publish_queued();
+ node["MQTTPublishFails"] = Mqtt::publish_fails();
+ node["MQTTReconnects"] = Mqtt::connect_count();
+ }
+ EMSESP::esp32React.getMqttSettingsService()->read([&](const MqttSettings & settings) {
+ node["enabled"] = settings.enabled;
+ node["clientID"] = settings.clientId;
+ node["keepAlive"] = settings.keepAlive;
+ node["cleanSession"] = settings.cleanSession;
+ node["entityFormat"] = settings.entity_format;
+ node["base"] = settings.base;
+ node["discoveryPrefix"] = settings.discovery_prefix;
+ node["discoveryType"] = settings.discovery_type;
+ node["nestedFormat"] = settings.nested_format;
+ node["haEnabled"] = settings.ha_enabled;
+ node["mqttQos"] = settings.mqtt_qos;
+ node["mqttRetain"] = settings.mqtt_retain;
+ node["publishTimeHeartbeat"] = settings.publish_time_heartbeat;
+ node["publishTimeBoiler"] = settings.publish_time_boiler;
+ node["publishTimeThermostat"] = settings.publish_time_thermostat;
+ node["publishTimeSolar"] = settings.publish_time_solar;
+ node["publishTimeMixer"] = settings.publish_time_mixer;
+ node["publishTimeWater"] = settings.publish_time_water;
+ node["publishTimeOther"] = settings.publish_time_other;
+ node["publishTimeSensor"] = settings.publish_time_sensor;
+ node["publishSingle"] = settings.publish_single;
+ node["publish2command"] = settings.publish_single2cmd;
+ node["sendResponse"] = settings.send_response;
+ });
+
+ // Syslog Status
+ node = output["syslog"].to();
+ node["enabled"] = EMSESP::system_.syslog_enabled_;
+ #ifndef EMSESP_STANDALONE
+ if (EMSESP::system_.syslog_enabled_) {
+ node["syslogStarted"] = syslog_.started();
+ node["syslogLevel"] = FL_(list_syslog_level)[syslog_.log_level() + 1];
+ node["syslogIP"] = syslog_.ip();
+ node["syslogQueue"] = syslog_.queued();
+ }
+ #endif
+
+ // Modbus Status
+ node = output["modbus"].to();
+ node["enabled"] = EMSESP::system_.modbus_enabled_;
+ if (EMSESP::system_.modbus_enabled_) {
+ node["maxClients"] = EMSESP::system_.modbus_max_clients_;
+ node["port"] = EMSESP::system_.modbus_port_;
+ node["timeout"] = EMSESP::system_.modbus_timeout_;
+ }
+
+ // Sensor Status
+ node = output["sensor"].to();
+ if (EMSESP::sensor_enabled()) {
+ node["temperatureSensors"] = EMSESP::temperaturesensor_.count_entities();
+ node["temperatureSensorReads"] = EMSESP::temperaturesensor_.reads();
+ node["temperatureSensorFails"] = EMSESP::temperaturesensor_.fails();
+ }
+
+ // Analog Status
+ node = output["analog"].to();
+ node["enabled"] = EMSESP::analog_enabled();
+ if (EMSESP::analog_enabled()) {
+ node["analogSensors"] = EMSESP::analogsensor_.count_entities();
+ node["analogSensorReads"] = EMSESP::analogsensor_.reads();
+ node["analogSensorFails"] = EMSESP::analogsensor_.fails();
+ }
+
+ // API Status
+ node = output["api"].to();
+
+ // if we're generating test data for Unit Tests we dont want to count these API calls as it will pollute the data response
+ #if defined(EMSESP_UNITY)
+ node["APICalls"] = 0;
+ node["APIFails"] = 0;
+ #else
+ node["APICalls"] = WebAPIService::api_count();
+ node["APIFails"] = WebAPIService::api_fails();
+ #endif
+
+ // EMS Bus Status
+ node = output["bus"].to();
+ switch (EMSESP::bus_status()) {
+ case EMSESP::BUS_STATUS_OFFLINE:
+ node["busStatus"] = "disconnected";
+ break;
+ case EMSESP::BUS_STATUS_TX_ERRORS:
+ node["busStatus"] = "connected, tx issues - try a different Tx Mode";
+ break;
+ case EMSESP::BUS_STATUS_CONNECTED:
+ node["busStatus"] = "connected";
+ break;
+ default:
+ node["busStatus"] = "unknown";
+ break;
+ }
+ node["busProtocol"] = EMSbus::is_ht3() ? "HT3" : "Buderus";
+ node["busTelegramsReceived"] = EMSESP::rxservice_.telegram_count();
+ node["busReads"] = EMSESP::txservice_.telegram_read_count();
+ node["busWrites"] = EMSESP::txservice_.telegram_write_count();
+ node["busIncompleteTelegrams"] = EMSESP::rxservice_.telegram_error_count();
+ node["busReadsFailed"] = EMSESP::txservice_.telegram_read_fail_count();
+ node["busWritesFailed"] = EMSESP::txservice_.telegram_write_fail_count();
+ node["busRxLineQuality"] = EMSESP::rxservice_.quality();
+ node["busTxLineQuality"] = (EMSESP::txservice_.read_quality() + EMSESP::txservice_.write_quality()) / 2;
+
+ // Settings
+ node = output["settings"].to();
+ EMSESP::webSettingsService.read([&](const WebSettings & settings) {
+ node["boardProfile"] = settings.board_profile;
+ node["locale"] = settings.locale;
+ node["txMode"] = settings.tx_mode;
+ node["emsBusID"] = settings.ems_bus_id;
+ node["showerTimer"] = settings.shower_timer;
+ node["showerMinDuration"] = settings.shower_min_duration; // seconds
+ node["showerAlert"] = settings.shower_alert;
+ if (settings.shower_alert) {
+ node["showerAlertColdshot"] = settings.shower_alert_coldshot; // seconds
+ node["showerAlertTrigger"] = settings.shower_alert_trigger; // minutes
+ }
+ if (settings.board_profile == "CUSTOM") {
+ node["phyType"] = settings.phy_type;
+ if (settings.phy_type != PHY_type::PHY_TYPE_NONE) {
+ node["ethPower"] = settings.eth_power;
+ node["ethPhyAddr"] = settings.eth_phy_addr;
+ node["ethClockMmode"] = settings.eth_clock_mode;
+ }
+ node["rxGPIO"] = EMSESP::system_.rx_gpio_;
+ node["txGPIO"] = EMSESP::system_.tx_gpio_;
+ node["dallasGPIO"] = EMSESP::system_.dallas_gpio_;
+ node["pbuttonGPIO"] = EMSESP::system_.pbutton_gpio_;
+ node["ledGPIO"] = EMSESP::system_.led_gpio_;
+ node["ledType"] = settings.led_type;
+ }
+ node["hideLed"] = settings.hide_led;
+ node["noTokenApi"] = settings.notoken_api;
+ node["readonlyMode"] = settings.readonly_mode;
+ node["fahrenheit"] = settings.fahrenheit;
+ node["dallasParasite"] = settings.dallas_parasite;
+ node["boolFormat"] = settings.bool_format;
+ node["boolDashboard"] = settings.bool_dashboard;
+ node["enumFormat"] = settings.enum_format;
+ node["analogEnabled"] = settings.analog_enabled;
+ node["telnetEnabled"] = settings.telnet_enabled;
+ node["maxWebLogBuffer"] = settings.weblog_buffer;
+
+ /*
+ #if defined(EMSESP_UNITY)
+ node["webLogBuffer"] = 0;
+ #else
+ node["webLogBuffer"] = EMSESP::webLogService.num_log_messages();
+ #endif
+ */
+ node["modbusEnabled"] = settings.modbus_enabled;
+ node["forceHeatingOff"] = settings.boiler_heatingoff;
+ node["developerMode"] = settings.developer_mode;
+ });
+
+ // Devices - show EMS devices if we have any
+ JsonArray devices = output["devices"].to();
+ if (!EMSESP::emsdevices.empty()) {
+ for (const auto & device_class : EMSFactory::device_handlers()) {
+ for (const auto & emsdevice : EMSESP::emsdevices) {
+ if (emsdevice && (emsdevice->device_type() == device_class.first)) {
+ JsonObject obj = devices.add();
+ obj["type"] = emsdevice->device_type_name(); // non translated name
+ obj["name"] = emsdevice->name(); // custom name
+ obj["deviceID"] = Helpers::hextoa(emsdevice->device_id());
+ obj["productID"] = emsdevice->product_id();
+ obj["brand"] = emsdevice->brand_to_char();
+ obj["version"] = emsdevice->version();
+ obj["entities"] = emsdevice->count_entities();
+ char result[1000];
+ (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::RECEIVED);
+ if (result[0] != '\0') {
+ obj["handlersReceived"] = result; // don't show handlers if there aren't any
+ }
+ (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::FETCHED);
+ if (result[0] != '\0') {
+ obj["handlersFetched"] = result;
+ }
+ (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::PENDING);
+ if (result[0] != '\0') {
+ obj["handlersPending"] = result;
+ }
+ (void)emsdevice->show_telegram_handlers(result, sizeof(result), EMSdevice::Handlers::IGNORED);
+ if (result[0] != '\0') {
+ obj["handlersIgnored"] = result;
+ }
+ }
+ }
+ }
+ }
+
+ // Also show EMSESP devices if we have any
+ if (EMSESP::temperaturesensor_.count_entities()) {
+ JsonObject obj = devices.add();
+ obj["type"] = F_(temperaturesensor);
+ obj["name"] = F_(temperaturesensor);
+ obj["entities"] = EMSESP::temperaturesensor_.count_entities();
+ }
+ if (EMSESP::analogsensor_.count_entities()) {
+ JsonObject obj = devices.add();
+ obj["type"] = F_(analogsensor);
+ obj["name"] = F_(analogsensor);
+ obj["entities"] = EMSESP::analogsensor_.count_entities();
+ }
+ if (EMSESP::webSchedulerService.count_entities()) {
+ JsonObject obj = devices.add();
+ obj["type"] = F_(scheduler);
+ obj["name"] = F_(scheduler);
+ obj["entities"] = EMSESP::webSchedulerService.count_entities();
+ }
+ if (EMSESP::webCustomEntityService.count_entities()) {
+ JsonObject obj = devices.add();
+ obj["type"] = F_(custom);
+ obj["name"] = F_(custom);
+ obj["entities"] = EMSESP::webCustomEntityService.count_entities();
+ }
+
+ return true; // this function always returns true!
+ }
+
+ #if defined(EMSESP_TEST)
+ // run a test, e.g. http://ems-esp/api?device=system&cmd=test&data=boiler
+ bool System::command_test(const char * value, const int8_t id) {
+ if (value) {
+ return Test::test(value, id);
+ } else {
+ return false;
+ }
+ }
+ #endif
+
+ // takes a board profile and populates a data array with GPIO configurations
+ // returns false if profile is unknown
+ //
+ // 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
+ //
+ bool System::load_board_profile(std::vector & data, const std::string & board_profile) {
+ if (board_profile == "default") {
+ return false; // unknown, return false
+ } else if (board_profile == "S32") {
+ data = {2, 18, 23, 5, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // BBQKees Gateway S32
+ valid_system_gpios_ = {0, 2, 5, 18, 23};
+ } else if (board_profile == "E32") {
+ data = {2, 4, 5, 17, 33, PHY_type::PHY_TYPE_LAN8720, 16, 1, 0, 0}; // BBQKees Gateway E32
+ valid_system_gpios_ = {0, 2, 4, 5, 16, 17, 33};
+ } else if (board_profile == "E32V2") {
+ data = {2, 14, 4, 5, 34, PHY_type::PHY_TYPE_LAN8720, 15, 0, 1, 0}; // BBQKees Gateway E32 V2
+ valid_system_gpios_ = {0, 2, 4, 5, 14, 15, 34};
+ } else if (board_profile == "E32V2_2") {
+ data = {32, 14, 4, 5, 34, PHY_type::PHY_TYPE_LAN8720, 15, 0, 1, 1}; // BBQKees Gateway E32 V2.2, rgb led
+ valid_system_gpios_ = {0, 2, 4, 5, 14, 15, 32, 34, 36, 39}; // system analogs 36, 39, led 2
+ } else if (board_profile == "MH-ET") {
+ data = {2, 18, 23, 5, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // MH-ET Live D1 Mini
+ // allow only pins that are marked as `can always be used`
+ valid_system_gpios_ = {0, 2, 5, 18, 23, 12, 13, 14, 15, 16, 17, 26, 27, 33};
+ // can always be used: 12, 13 ,14, 15, 16, 17, 26, 27, 33
+ // can be used if no other function 2, 4, 5, 9, 10, 18, 19, 21, 22, 23, 25, 34, 35, 36, 39
+ } else if (board_profile == "NODEMCU") {
+ data = {2, 18, 23, 5, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // NodeMCU 32S
+ // https://blog.berrybase.de/esp32-node-mcu-module-anfaenger-guide/
+ // all available pins, exclude uart0
+ valid_system_gpios_ = {0, 2, 5, 18, 23, 4, 12, 13, 14, 15, 16, 17, 21, 22, 25, 26, 27, 32, 33, 34, 35, 36, 39};
+ } else if (board_profile == "LOLIN") {
+ data = {2, 18, 17, 16, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // Lolin D32
+ // https://www.wemos.cc/en/latest/d32/d32.html
+ valid_system_gpios_ = {2, 18, 17, 16, 0, 4, 5, 12, 13, 14, 15, 21, 22, 25, 26, 27, 32, 33, 34, 35, 36, 39};
+ } else if (board_profile == "OLIMEX") {
+ data = {0, 0, 36, 4, 34, PHY_type::PHY_TYPE_LAN8720, -1, 0, 0, 0}; // Olimex ESP32-EVB (uses U1TXD/U1RXD/BUTTON, no LED or Temperature sensor)
+ // https://github.com/OLIMEX/ESP32-EVB/blob/master/HARDWARE/REV-K1/ESP32-EVB_Rev_K1.pdf
+ // uart0 = 1, 3; CAN = 5, 35; relais = 32, 33; ir = 12(tx), 39(rx); SD-card = 2, 14, 15, button = 34
+ // relais and ir can be configured as analog sensor
+ valid_system_gpios_ = {4, 34, 36, 12, 13, 21, 22, 25, 26, 27, 32, 33, 39};
+ } else if (board_profile == "OLIMEXPOE") {
+ data = {0, 0, 36, 4, 34, PHY_type::PHY_TYPE_LAN8720, 12, 0, 3, 0}; // Olimex ESP32-POE
+ // https://github.com/OLIMEX/ESP32-POE/blob/master/HARDWARE/ESP32-PoE-hardware-revision-L1/ESP32-PoE_Rev_L1.pdf
+ // uart0 = 1, 3; SD-card = 2, 14, 15; button = 34;
+ valid_system_gpios_ = {4, 34, 36, 12, 13, 21, 22, 25, 26, 27, 32, 33, 39};
+ } else if (board_profile == "C3MINI") {
+ #if defined(BOARD_C3_MINI_V1)
+ data = {7, 1, 4, 5, 9, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // Lolin C3 Mini V1
+ #else
+ data = {7, 1, 4, 5, 9, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 1}; // Lolin C3 Mini with RGB Led
+ #endif
+ // https://www.wemos.cc/en/latest/c3/c3_mini.html
+ valid_system_gpios_ = {0, 1, 3, 4, 5, 6, 7, 9, 10, 20, 21};
+ } else if (board_profile == "S2MINI") {
+ data = {15, 7, 11, 12, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // Lolin S2 Mini
+ // https://www.wemos.cc/en/latest/s2/s2_mini.html
+ set_valid_system_gpios();
+ } else if (board_profile == "S3MINI") {
+ data = {17, 18, 8, 5, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // Liligo S3
+ // https://lilygo.cc/products/t7-s3
+ set_valid_system_gpios();
+ } else if (board_profile == "S32S3") {
+ data = {2, 18, 5, 17, 0, PHY_type::PHY_TYPE_NONE, 0, 0, 0, 0}; // BBQKees Gateway S3
+ valid_system_gpios_ = {0, 2, 5, 17, 18};
+ } else {
+ return false; // unknown, return false
+ }
+
+ return true;
+ }
+
+ // txpause command - temporarily pause the TX, by setting Txmode to 0 (disabled)
+ bool System::command_txpause(const char * value, const int8_t id) {
+ bool arg;
+ if (!Helpers::value2bool(value, arg)) {
+ return false; // argument not recognized
+ }
+
+ if (!arg) {
+ // arg = false: Tx mode to 0 (disabled) to pause
+ if (EMSbus::tx_mode() == EMS_TXMODE_OFF) {
+ EMSESP::webSettingsService.read([&](WebSettings & settings) {
+ EMSbus::tx_mode(settings.tx_mode);
+ #ifdef EMSESP_DEBUG
+ LOG_INFO("TX mode restored (value %d)", settings.tx_mode);
+ #else
+ LOG_INFO("TX active");
+ #endif
+ });
+ }
+ } else {
+ // pause = true: Tx mode to 0 (disabled) to pause
+ if (EMSbus::tx_mode() != EMS_TXMODE_OFF) {
+ EMSbus::tx_mode(EMS_TXMODE_OFF);
+ #ifdef EMSESP_DEBUG
+ LOG_INFO("TX mode set to OFF (value %d)", EMS_TXMODE_OFF);
+ #else
+ LOG_INFO("TX paused");
+ #endif
+ }
+ }
+ return true;
+ }
+
+ // format command - factory reset, removing all config files
+ bool System::command_format(const char * value, const int8_t id) {
+ #if !defined(EMSESP_STANDALONE) && !defined(EMSESP_TEST)
+ // don't really format the filesystem in test or standalone mode
+ if (LittleFS.format()) {
+ LOG_INFO("Filesystem formatted successfully. All config files removed.");
+ } else {
+ LOG_ERROR("Format failed");
+ }
+ #else
+ LOG_ERROR("Format command not available in standalone or test mode");
+ #endif
+
+ // restart will be handled by the main loop
+ EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED);
+ return true;
+ }
+
+ // restart command - perform a hard reset (system reboot)
+ bool System::command_restart(const char * value, const int8_t id) {
+ if (id == 0) {
+ // if it has an id then it's a web call and we need to queue the restart
+ // default id is -1 when calling /api/system/restart directly for example
+ LOG_INFO("Preparing to restart system");
+ EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);
+ return true;
+ }
+
+ LOG_INFO("Restarting system immediately");
+ // restart will be handled by the main loop
+ EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED);
+ return true;
+ }
+
+ #pragma GCC diagnostic push
+ #pragma GCC diagnostic ignored "-Wswitch"
+
+ std::string System::reset_reason(uint8_t cpu) const {
+ #ifndef EMSESP_STANDALONE
+ switch (esp_rom_get_reset_reason(cpu)) {
+ case RESET_REASON_CHIP_POWER_ON:
+ return ("Power on reset");
+ case 2: // not on esp32
+ return ("reset pin");
+ case RESET_REASON_CORE_SW:
+ return ("Software reset");
+ case 4: // not on S2, C3
+ return ("Legacy watch dog reset");
+ case RESET_REASON_CORE_DEEP_SLEEP:
+ return ("Deep sleep reset");
+ case 6: // RESET_REASON_CORE_SDIO: // not on S2, S3, C3
+ return ("Reset by SDIO");
+ case RESET_REASON_CORE_MWDT0:
+ return ("Timer group0 watch dog reset");
+ case RESET_REASON_CORE_MWDT1:
+ return ("Timer group1 watch dog reset");
+ case RESET_REASON_CORE_RTC_WDT:
+ return ("RTC watch dog reset");
+ case 10:
+ return ("Intrusion reset CPU");
+ case RESET_REASON_CPU0_MWDT0:
+ return ("Timer group reset CPU");
+ case RESET_REASON_CPU0_SW:
+ return ("Software reset CPU");
+ case RESET_REASON_CPU0_RTC_WDT:
+ return ("RTC watch dog reset: CPU");
+ case 14: // RESET_REASON_CPU1_CPU0: // not on S2, S3, C3
+ return ("APP CPU reset by PRO CPU");
+ case RESET_REASON_SYS_BROWN_OUT:
+ return ("Brownout reset");
+ case RESET_REASON_SYS_RTC_WDT:
+ return ("RTC watch dog reset: CPU+RTC");
+ default:
+ break;
+ }
+ #endif
+ return "Unknown";
+ }
+ #pragma GCC diagnostic pop
+
+ // set NTP status
+ void System::ntp_connected(bool b) {
+ if (b != ntp_connected_) {
+ if (b) {
+ LOG_INFO("NTP connected");
+ set_partition_install_date();
+ } else {
+ LOG_WARNING("NTP disconnected"); // if turned off report it
+ }
+ }
+
+ ntp_connected_ = b;
+ ntp_last_check_ = b ? uuid::get_uptime_sec() : 0;
+ }
+
+ // get NTP status
+ bool System::ntp_connected() {
+ // timeout 2 hours, ntp sync is normally every hour.
+ if ((uuid::get_uptime_sec() - ntp_last_check_ > 7201) && ntp_connected_) {
+ ntp_connected(false);
+ }
+
+ return ntp_connected_;
+ }
+
+ // see if its a BBQKees Gateway by checking the eFuse values
+ String System::getBBQKeesGatewayDetails(uint8_t detail) {
+ #ifndef EMSESP_STANDALONE
+ union {
+ struct {
+ uint32_t no : 4;
+ uint32_t month : 4;
+ uint32_t year : 8;
+ uint32_t rev_minor : 4;
+ uint32_t rev_major : 4;
+ uint32_t model : 4;
+ uint32_t mfg : 4;
+ };
+ uint32_t reg;
+ } gw;
+
+ for (uint8_t reg = 0; reg < 8; reg++) {
+ gw.reg = esp_efuse_read_reg(EFUSE_BLK3, reg);
+ if (reg == 7 || esp_efuse_read_reg(EFUSE_BLK3, reg + 1) == 0)
+ break;
+ }
+
+ const char * mfg[] = {"unknown", "BBQKees Electronics", "", "", "", "", "", ""};
+ const char * model[] = {"unknown", "S3", "E32V2", "E32V2.2", "S32", "E32", "", "", ""};
+ const char * board[] = {"CUSTOM", "S32S3", "E32V2", "E32V2_2", "S32", "E32", "", "", ""};
+
+ switch (detail) {
+ case FUSE_VALUE::MFG:
+ return gw.mfg < 2 ? String(mfg[gw.mfg]) : "unknown";
+ case FUSE_VALUE::MODEL:
+ return gw.model < 6 ? String(model[gw.model]) : "unknown";
+ case FUSE_VALUE::BOARD:
+ return gw.model < 6 ? String(board[gw.model]) : board_profile_;
+ case FUSE_VALUE::REV:
+ return String(gw.rev_major) + "." + String(gw.rev_minor);
+ case FUSE_VALUE::BATCH:
+ return String(2000 + gw.year) + (gw.month < 10 ? "0" : "") + String(gw.month) + String(gw.no);
+ case FUSE_VALUE::FUSE:
+ return "0x" + String(gw.reg, 16);
+ case FUSE_VALUE::ALL:
+ default:
+ break;
+ }
+
+ if (!gw.reg || gw.mfg > 1 || gw.model > 5) {
+ return "";
+ }
+
+ return String(mfg[gw.mfg]) + " " + String(model[gw.model]) + " rev." + String(gw.rev_major) + "." + String(gw.rev_minor) + "/" + String(2000 + gw.year)
+ + (gw.month < 10 ? "0" : "") + String(gw.month) + String(gw.no);
+ #else
+ return "";
+ #endif
+ }
+
+ // Stream from an URL and send straight to OTA uploader service.
+ //
+ // This function needs to be called twice, 1st pass once with a url to persist it, 2nd pass with no arguments to start the upload
+ // This is to avoid timeouts in callback functions, like calling from a web hook.
+ bool System::uploadFirmwareURL(const char * url) {
+ #ifndef EMSESP_STANDALONE
+ static String saved_url;
+
+ if (url && strlen(url) > 0) {
+ // if the passed URL is "reset" abort the current upload. This is called when an error happens during OTA
+ if (strncmp(url, "reset", 5) == 0) {
+ LOG_DEBUG("Firmware upload - resetting");
+ saved_url.clear();
+ return true;
+ }
+
+ // given a URL to download from, save it ready for the 2nd pass
+ saved_url = url;
+ LOG_INFO("Firmware location: %s", saved_url.c_str());
+ EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_UPLOAD); // we're ready to start the upload
+ return true;
+ }
+
+ // check we have a valid URL from the 1st pass
+ if (saved_url.isEmpty()) {
+ LOG_ERROR("Firmware upload failed - invalid URL");
+ return false; // error
+ }
+
+ Shell::loop_all(); // flush log buffers so latest messages are shown in console
+
+ // Configure temporary client
+ HTTPClient http;
+ http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); // important for GitHub 302's
+ http.setTimeout(8000);
+ http.useHTTP10(true); // use HTTP/1.0 for update since the update handler does not support any transfer Encoding
+ http.begin(saved_url);
+
+ // start a connection, returns -1 if fails
+ int httpCode = http.GET();
+ if (httpCode != HTTP_CODE_OK) {
+ LOG_ERROR("Firmware upload failed - HTTP code %d", httpCode);
+ http.end();
+ return false; // error
+ }
+
+ int firmware_size = http.getSize();
+
+ // check we have a valid size
+ if (firmware_size < 2097152) { // 2MB or greater is required
+ LOG_ERROR("Firmware upload failed - invalid size");
+ http.end();
+ return false; // error
+ }
+
+ // check we have enough space for the upload in the ota partition
+ if (!Update.begin(firmware_size)) {
+ LOG_ERROR("Firmware upload failed - no space");
+ http.end();
+ return false; // error
+ }
+
+ LOG_INFO("Firmware uploading (size: %d KB). Please wait...", firmware_size / 1024);
+
+ Shell::loop_all(); // flush log buffers so latest messages are shown in console
+
+ // we're about to start the upload, set the status so the Web System Monitor spots it
+ EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING);
+
+ // set a callback so we can monitor progress in the WebUI
+ Update.onProgress([](size_t progress, size_t total) { EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING + (progress * 100 / total)); });
+
+ // get tcp stream and send it to Updater
+ WiFiClient * stream = http.getStreamPtr();
+ if (Update.writeStream(*stream) != firmware_size) {
+ LOG_ERROR("Firmware upload failed - size differences");
+ http.end();
+ EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
+ return false; // error
+ }
+
+ if (!Update.end(true)) {
+ LOG_ERROR("Firmware upload failed - general error");
+ http.end();
+ EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
+ return false; // error
+ }
+
+ // finished with upload
+ http.end();
+ saved_url.clear(); // prevent from downloading again
+ LOG_INFO("Firmware uploaded successfully. Restarting...");
+ EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);
+ #endif
+
+ return true; // OK
+ }
+
+ // read command, e.g. read [offset] [length] from console or API
+ // from Console use quotes so: call system read " [offset] [length]"
+ bool System::readCommand(const char * data) {
+ if (!data) {
+ return false;
+ }
+
+ // extract [offset] [length] from string
+ char * p;
+ char value[11];
+
+ // make a copy so we can iterate, max 15 chars (XX XXXX XX XX)
+ char data_args[15];
+ strlcpy(data_args, data, sizeof(data_args));
+
+ uint8_t device_id = 0; // is in hex
+ uint16_t type_id = 0; // is in hex
+ uint8_t length = 0;
+ uint8_t offset = 0;
+
+ // first check deviceID
+ if ((p = strtok(data_args, " ,"))) { // delimiter comma or space
+ strlcpy(value, p, sizeof(value)); // get string
+ device_id = (uint8_t)Helpers::hextoint(value); // convert hex to int
+ if (!EMSESP::valid_device(device_id)) {
+ LOG_ERROR("Invalid device ID (0x%02X) in read command", device_id);
+ return false; // invalid device
+ }
+ }
+
+ // iterate until end
+ uint8_t num_args = 0;
+ while (p != 0) {
+ if ((p = strtok(nullptr, " ,"))) { // delimiter comma or space
+ strlcpy(value, p, sizeof(value)); // get string
+ if (num_args == 0) {
+ type_id = (uint16_t)Helpers::hextoint(value); // convert hex to int
+ } else if (num_args == 1) {
+ offset = Helpers::atoint(value); // decimal
+ } else if (num_args == 2) {
+ length = Helpers::atoint(value); // decimal
+ }
+ num_args++;
+ }
+ }
+
+ if (num_args == 0) {
+ return false; // invalid number of arguments
+ }
+
+ EMSESP::send_read_request(type_id, device_id, offset, length, true);
+ EMSESP::set_read_id(type_id);
+
+ return true;
+ }
+
+ // system read command
+ bool System::command_read(const char * value, const int8_t id) {
+ return readCommand(value);
+ }
+
+ // set the system status code - SYSTEM_STATUS in system.h
+ // this is also used in the SystemMonitor.tsx WebUI to show the progress of the firmware upload, start at 100
+ void System::systemStatus(uint8_t status_code) {
+ if (systemStatus_ != status_code) {
+ systemStatus_ = status_code;
+ #ifdef EMSESP_DEBUG
+ if (status_code < SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING) {
+ LOG_DEBUG("Setting System status code %d", status_code);
+ }
+ #endif
+ }
+ }
+
+ uint8_t System::systemStatus() {
+ return systemStatus_;
+ }
+
+ // takes two arguments:
+ // the first is the full range of pins to consider
+ // the second is a string range of GPIOs to exclude, like "6-11, 1, 23, 24-48"
+ // returns a vector array of GPIOs that are valid for use
+ std::vector> System::string_range_to_vector(const std::string & range, const std::string & exclude) {
+ std::vector> gpios;
+ std::string::size_type pos = 0;
+ std::string::size_type prev = 0;
+
+ auto process_part = [&gpios](std::string part) {
+ // trim whitespace
+ part.erase(0, part.find_first_not_of(" \t"));
+ part.erase(part.find_last_not_of(" \t") + 1);
+
+ // check if it's a range (contains '-')
+ std::string::size_type dash_pos = part.find('-');
+ if (dash_pos != std::string::npos) {
+ // it's a range like "6-11"
+ int start = std::stoi(part.substr(0, dash_pos));
+ int end = std::stoi(part.substr(dash_pos + 1));
+ for (int i = start; i <= end; i++) {
+ gpios.push_back(static_cast(i));
+ }
+ } else {
+ gpios.push_back(static_cast(std::stoi(part)));
+ }
+ };
+
+ while ((pos = range.find(',', prev)) != std::string::npos) {
+ process_part(range.substr(prev, pos - prev));
+ prev = pos + 1;
+ }
+
+ // handle the last part
+ process_part(range.substr(prev));
+
+ // if exclude list is provided, parse it and remove excluded GPIOs
+ if (!exclude.empty()) {
+ std::vector> exclude_gpios;
+ pos = 0;
+ prev = 0;
+
+ auto process_exclude = [&exclude_gpios](std::string part) {
+ // trim whitespace
+ part.erase(0, part.find_first_not_of(" \t"));
+ part.erase(part.find_last_not_of(" \t") + 1);
+
+ // check if it's a range (contains '-')
+ std::string::size_type dash_pos = part.find('-');
+ if (dash_pos != std::string::npos) {
+ // it's a range like "6-11"
+ int start = std::stoi(part.substr(0, dash_pos));
+ int end = std::stoi(part.substr(dash_pos + 1));
+ for (int i = start; i <= end; i++) {
+ exclude_gpios.push_back(static_cast(i));
+ }
+ } else {
+ exclude_gpios.push_back(static_cast(std::stoi(part)));
+ }
+ };
+
+ while ((pos = exclude.find(',', prev)) != std::string::npos) {
+ process_exclude(exclude.substr(prev, pos - prev));
+ prev = pos + 1;
+ }
+
+ // handle the last part
+ process_exclude(exclude.substr(prev));
+
+ // remove excluded GPIOs from the main list
+ gpios.erase(std::remove_if(gpios.begin(),
+ gpios.end(),
+ [&exclude_gpios](uint8_t gpio) { return std::find(exclude_gpios.begin(), exclude_gpios.end(), gpio) != exclude_gpios.end(); }),
+ gpios.end());
+ }
+
+ return gpios;
+ }
+
+ // initialize a list of valid GPIOs based on the ESP32 board
+ // string_to_vector() take two strings, the first is the range of GPIOs to use, the second is a list of GPIOs to exclude
+ // notes:
+ // we always allow 0 (which is usually a strapping pin), because it's used to indicate whether EMS-ESP Dallas or the LED is disabled
+ // we allow UART0, 1 and 2 as they are configurable
+ // strapping pins are disabled as they can affect boot behaviour
+ // we accept GPIOs that are fixed on BBQKees boards
+ //
+ void System::set_valid_system_gpios() {
+ valid_system_gpios_.clear(); // reset system list
+ used_gpios_.clear(); // reset used list
+
+ // get free gpios based on board/platform type
+ #if CONFIG_IDF_TARGET_ESP32C3
+ // https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/peripherals/gpio.html
+ // excluded:
+ // GPIO2, GPIO8 - GPIO9 = strapping pins
+ // GPIO12 - GPIO17 = used for SPI flash and PSRAM
+ // GPIO18 - GPIO19 = USB-JTAG
+ //
+ // notes on what is allowed:
+ // GPIO09 = button on BOARD_C3_MINI_V1
+ // GPIO20 - GPIO21 = UART0, , no chip connected because native USB
+ valid_system_gpios_ = string_range_to_vector("0-21", "2, 8, 12-17, 18-19");
+
+ #elif CONFIG_IDF_TARGET_ESP32S2
+ // https://docs.espressif.com/projects/esp-idf/en/stable/esp32s2/api-reference/peripherals/gpio.html
+ // excluded:
+ // GPIO26 - GPIO32 = SPI flash and PSRAM
+ // GPIO45 - GPIO46 = strapping pins
+ // GPIO39 - GPIO42 = USB-JTAG
+ // GPIO22 - GPIO25 = don't exist
+ // GPIO19 - GPIO20 = USB
+ //
+ // notes on what is allowed:
+ // GPIO43, GPIO44 = UART0, no chip connected because native USB
+ valid_system_gpios_ = string_range_to_vector("0-46", "19, 20, 26-32, 45-46, 39-42, 22-25");
+
+ #elif CONFIG_IDF_TARGET_ESP32S3
+ // https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/gpio.html
+ // excluded:
+ // GPIO3, GPIO45 - GPIO46 = strapping pins
+ // GPIO26 - GPIO32 = SPI flash and PSRAM and not recommended
+ // GPIO19 - GPIO20 = USB-JTAG
+ // GPIO22 - GPIO25 = don't exist
+ //
+ // notes on what is allowed:
+ // GPIO11 - GPIO19 = ADC analog input only pins
+ // GPIO47 - GPIO48 = valid on a Wemos S3
+ // GPIO8 = used by Liligo S3 board profile for Rx
+ if (ESP.getPsramSize() > 0) {
+ // GPIO33 - GPIO37 = Octal flash/PSRAM
+ valid_system_gpios_ = string_range_to_vector("0-48", "3, 45-46, 26-32, 33-37, 19-20, 22-25");
+ } else {
+ valid_system_gpios_ = string_range_to_vector("0-48", "3, 45-46, 26-32, 19-20, 22-25");
+ }
+
+ #elif CONFIG_IDF_TARGET_ESP32
+ // https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/gpio.html
+ // excluded:
+ // GPIO6 - GPIO11, GPIO16 - GPIO17 = used for SPI flash and PSRAM (dio mode only GPIO06-GPIO08, GPIO11)
+ // GPIO20, GPIO24, GPIO28 - GPIO31 = don't exist
+ // GPIO01, GPIO03 = UART0, normal connected to UART/USB chip
+ //
+ // notes on known boards:
+ // boards have valid gpios depending on pinout and internal used gpios, see: `load_board_profile()`
+ //
+ // notes on BBQKees boards:
+ // *** We block all free GPIOS in load_board_profile() ***
+ // GPIO2, GPIO4, GPIO5, GPIO14 = used on BBQKees boards for either LED, Dallas or Rx
+ // GPIO12, GPIO13, GPIO35 = BBQKees E32V2_2 internal system pins
+ // GPIO33 = BBQKees E32V_2 unused internal NTC system sensor
+ // GPIO36 = used on BBQKees boards for supply_voltage (E32V2.2)
+ // GPIO39 = used on BBQKees boards for core_voltage (E32V2.2)
+ //
+ // notes on what is allowed with special functions:
+ // GPIO01, GPIO03 = UART0, not always connected to uart/usb chip
+ // GPIO12 - GPIO15 = JTAG, normally not used
+ // GPIO00, GPIO05, GPIO12, GPIO15 = strapping pins, can be used with care
+ // GPIO34, GPIO35, GPIO37 = input only
+ // GPIO23 and GPIO18 are used by Ethernet, excluded later by eth config
+ // GPIO00, GPIO02, GPIO04, GPIO12 - GPIO15, GPIO25 - GPIO27 = ADC2 (10 ch), used by WiFI-driver
+ // GPIO32 - GPIO39 = ADC1 (8 ch), can always be used
+ if (ESP.getPsramSize() > 0) {
+ // remove SPI0/1 PSRAM pins GPIO16 (CS) and GPIO17 (CLK) from the list
+ valid_system_gpios_ = string_range_to_vector("0-39", "6-11, 16, 17, 20, 24, 28-31");
+ } else {
+ valid_system_gpios_ = string_range_to_vector("0-39", "6-11, 20, 24, 28-31");
+ }
+ #elif CONFIG_IDF_TARGET_ESP32C6
+ // https://docs.espressif.com/projects/esp-idf/en/v5.5.3/esp32c6/api-reference/peripherals/gpio.html
+ // 24-30 used for flash, 12-13 USB, 16-17 uart0
+ valid_system_gpios_ = string_range_to_vector("0-30", "12-13, 16-17, 24-30");
+ #elif defined(EMSESP_STANDALONE)
+ valid_system_gpios_ = string_range_to_vector("0-39");
+ #endif
+ valid_system_gpios_.shrink_to_fit();
+ }
+
+ // check if a pin is valid ESP32 pin and if not already used, add to the used gpio list
+ // return false if not allowed or already used
+ bool System::add_gpio(uint8_t pin, const char * source_name) {
+ // check if this is a valid user GPIO
+ if (std::find(valid_system_gpios_.begin(), valid_system_gpios_.end(), pin) != valid_system_gpios_.end()) {
+ // It's valid now check if it's already in the used list
+ auto it = std::find_if(used_gpios_.begin(), used_gpios_.end(), [pin](const GpioUsage & usage) { return usage.pin == pin; });
+ if (it != used_gpios_.end()) {
+ LOG_WARNING("GPIO %d for %s is already in use by %s", pin, source_name, it->source.c_str());
+ return false; // Pin is already used
+ }
+ } else {
+ // not valid
+ LOG_WARNING("GPIO %d for %s is not valid", pin, source_name);
+ return false;
+ }
+
+ // remove the old pin, if exists from used list
+ remove_gpio(pin);
+
+ LOG_DEBUG("Adding GPIO %d for %s to used gpio list", pin, source_name);
+ used_gpios_.push_back({pin, source_name}); // add to used list
+
+ return true;
+ }
+
+ // remove a gpio from both valid and used lists
+ void System::remove_gpio(uint8_t pin, bool also_system) {
+ auto it = std::find_if(used_gpios_.begin(), used_gpios_.end(), [pin](const GpioUsage & usage) { return usage.pin == pin; });
+ if (it != used_gpios_.end()) {
+ LOG_DEBUG("GPIO %d removed from used gpio list", pin);
+ used_gpios_.erase(it);
+ }
+
+ if (also_system) {
+ auto it_sys = std::find(valid_system_gpios_.begin(), valid_system_gpios_.end(), pin);
+ if (it_sys != valid_system_gpios_.end()) {
+ LOG_DEBUG("GPIO %d removed from valid gpio list", pin);
+ valid_system_gpios_.erase(it_sys);
+ }
+ }
+ }
+
+ // return a list of GPIO's available for use
+ std::vector System::available_gpios() {
+ std::vector gpios;
+ for (const auto & gpio : valid_system_gpios_) {
+ if (std::find_if(used_gpios_.begin(), used_gpios_.end(), [gpio](const GpioUsage & usage) { return usage.pin == gpio; }) == used_gpios_.end()) {
+ gpios.push_back(gpio); // didn't find it in used_gpios_, so it's available
+ }
+ }
+ return gpios;
+ }
+
+ // make a snapshot of the current GPIOs
+ void System::make_snapshot_gpios(std::vector & u_gpios, std::vector & s_gpios) {
+ for (const auto & usage : used_gpios_) {
+ u_gpios.push_back(usage.pin);
+ }
+ for (const auto & gpio : valid_system_gpios_) {
+ s_gpios.push_back(gpio);
+ }
+ }
+
+ // restore the GPIOs from the snapshot
+ void System::restore_snapshot_gpios(std::vector & u_gpios, std::vector & s_gpios) {
+ used_gpios_.clear();
+ for (const auto & gpio : u_gpios) {
+ used_gpios_.push_back({static_cast(gpio), "restored"});
+ }
+
+ valid_system_gpios_.clear();
+ for (const auto & gpio : s_gpios) {
+ valid_system_gpios_.push_back(gpio);
+ }
+ }
+
+ } // namespace emsesp
+
\ No newline at end of file
diff --git a/src/emsesp_version.h b/src/emsesp_version.h
index ffc3ab2c2..0aea80b67 100644
--- a/src/emsesp_version.h
+++ b/src/emsesp_version.h
@@ -1 +1 @@
-#define EMSESP_APP_VERSION "3.8.2-dev.C14"
+#define EMSESP_APP_VERSION "3.9.0-dev.0"
diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp
index 106e7c052..121b75d82 100644
--- a/src/web/WebSettingsService.cpp
+++ b/src/web/WebSettingsService.cpp
@@ -16,553 +16,554 @@
* along with this program. If not, see .
*/
-#include "emsesp.h"
-
-namespace emsesp {
-
-uint8_t WebSettings::flags_ = 0;
-
-WebSettingsService::WebSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
- : _httpEndpoint(WebSettings::read, WebSettings::update, this, server, EMSESP_SETTINGS_SERVICE_PATH, securityManager)
- , _fsPersistence(WebSettings::read, WebSettings::update, this, fs, EMSESP_SETTINGS_FILE) {
- securityManager->addEndpoint(server, EMSESP_BOARD_PROFILE_SERVICE_PATH, AuthenticationPredicates::IS_AUTHENTICATED, [this](AsyncWebServerRequest * request) {
- board_profile(request);
- });
-
- addUpdateHandler([this] { onUpdate(); }, false);
-}
-
-void WebSettings::read(WebSettings & settings, JsonObject root) {
- root["version"] = settings.version;
- root["board_profile"] = settings.board_profile;
- root["platform"] = EMSESP_PLATFORM;
- root["locale"] = settings.locale;
- root["tx_mode"] = settings.tx_mode;
- root["ems_bus_id"] = settings.ems_bus_id;
- root["syslog_enabled"] = settings.syslog_enabled;
- root["syslog_level"] = settings.syslog_level;
- root["trace_raw"] = settings.trace_raw;
- root["syslog_mark_interval"] = settings.syslog_mark_interval;
- root["syslog_host"] = settings.syslog_host;
- root["syslog_port"] = settings.syslog_port;
- root["boiler_heatingoff"] = settings.boiler_heatingoff;
- root["remote_timeout"] = settings.remote_timeout;
- root["remote_timeout_en"] = settings.remote_timeout_enabled;
- root["shower_timer"] = settings.shower_timer;
- root["shower_alert"] = settings.shower_alert;
- root["shower_alert_coldshot"] = settings.shower_alert_coldshot;
- root["shower_alert_trigger"] = settings.shower_alert_trigger;
- root["shower_min_duration"] = settings.shower_min_duration;
- root["rx_gpio"] = settings.rx_gpio;
- root["tx_gpio"] = settings.tx_gpio;
- root["dallas_gpio"] = settings.dallas_gpio;
- root["dallas_parasite"] = settings.dallas_parasite;
- root["led_gpio"] = settings.led_gpio;
- root["hide_led"] = settings.hide_led;
- root["led_type"] = settings.led_type;
- root["low_clock"] = settings.low_clock;
- root["telnet_enabled"] = settings.telnet_enabled;
- root["notoken_api"] = settings.notoken_api;
- root["readonly_mode"] = settings.readonly_mode;
- root["analog_enabled"] = settings.analog_enabled;
- root["pbutton_gpio"] = settings.pbutton_gpio;
- root["solar_maxflow"] = settings.solar_maxflow;
- root["fahrenheit"] = settings.fahrenheit;
- root["bool_format"] = settings.bool_format;
- root["bool_dashboard"] = settings.bool_dashboard;
- root["enum_format"] = settings.enum_format;
- root["weblog_level"] = settings.weblog_level;
- root["weblog_buffer"] = settings.weblog_buffer;
- root["weblog_compact"] = settings.weblog_compact;
- root["phy_type"] = settings.phy_type;
- root["eth_power"] = settings.eth_power;
- root["eth_phy_addr"] = settings.eth_phy_addr;
- root["eth_clock_mode"] = settings.eth_clock_mode;
- root["modbus_enabled"] = settings.modbus_enabled;
- root["modbus_port"] = settings.modbus_port;
- root["modbus_max_clients"] = settings.modbus_max_clients;
- root["modbus_timeout"] = settings.modbus_timeout;
- root["developer_mode"] = settings.developer_mode;
-#ifndef NO_TLS_SUPPORT
- root["email_enabled"] = settings.email_enabled;
-#else
- root["email_enabled"] = false;
-#endif
- root["email_ssl"] = settings.email_ssl;
- root["email_starttls"] = settings.email_starttls;
- root["email_server"] = settings.email_server;
- root["email_port"] = settings.email_port;
- root["email_login"] = settings.email_login;
- root["email_pass"] = settings.email_pass;
- root["email_sender"] = settings.email_sender;
- root["email_recp"] = settings.email_recp;
- root["email_subject"] = settings.email_subject;
-}
-
-// call on initialization and also when settings are updated/saved via web or console
-// note, settings is empty when the service starts
-StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
- // make a copy of the settings to compare to later
- const WebSettings original_settings(settings);
-
- // make a snapshot of the current GPIOs
- std::vector used_gpios;
- std::vector system_gpios;
- EMSESP::system_.make_snapshot_gpios(used_gpios, system_gpios);
-
- settings.version = root["version"] | EMSESP_APP_VERSION; // save the version, we use it later in System::check_upgrade()
- settings.board_profile = root["board_profile"] | EMSESP_DEFAULT_BOARD_PROFILE;
-
- // get current values that are related to the board profile
- settings.led_gpio = root["led_gpio"];
- settings.dallas_gpio = root["dallas_gpio"];
- settings.rx_gpio = root["rx_gpio"];
- settings.tx_gpio = root["tx_gpio"];
- settings.pbutton_gpio = root["pbutton_gpio"];
- settings.phy_type = root["phy_type"];
- settings.eth_power = root["eth_power"];
- settings.eth_phy_addr = root["eth_phy_addr"];
- settings.eth_clock_mode = root["eth_clock_mode"];
- settings.led_type = root["led_type"]; // 1 = RGB-LED
-
- reset_flags();
-
- // before loading new board profile free old gpios from used list to allow remapping
- EMSESP::system_.remove_gpio(original_settings.led_gpio);
- EMSESP::system_.remove_gpio(original_settings.dallas_gpio);
- EMSESP::system_.remove_gpio(original_settings.pbutton_gpio);
- EMSESP::system_.remove_gpio(original_settings.rx_gpio);
- EMSESP::system_.remove_gpio(original_settings.tx_gpio);
-
- // see if the user has changed the board profile
- // this will set: led_gpio, dallas_gpio, rx_gpio, tx_gpio, pbutton_gpio, phy_type, eth_power, eth_phy_addr, eth_clock_mode, led_type
- // this will always run when EMS-ESP starts since original_settings{} is empty
- if (original_settings.board_profile != settings.board_profile || original_settings.board_profile == "default"
- || original_settings.board_profile.length() == 0) {
- set_board_profile(settings);
- add_flags(ChangeFlags::RESTART);
- }
-
- check_flag(original_settings.phy_type, settings.phy_type, ChangeFlags::RESTART);
- // ETH has changed, so we need to check the ethernet pins. Only if ETH is being used.
- if (settings.phy_type != PHY_type::PHY_TYPE_NONE) {
- check_flag(original_settings.eth_power, settings.eth_power, ChangeFlags::RESTART);
- check_flag(original_settings.eth_clock_mode, settings.eth_clock_mode, ChangeFlags::RESTART);
- if (settings.eth_power != -1) { // Ethernet Power -1 means disabled
- EMSESP::system_.remove_gpio(settings.eth_power, true);
- }
- // remove the ethernet pins from valid list, regardless of whether the GPIOs are valid or not
- EMSESP::system_.remove_gpio(23, true); // MDC
- EMSESP::system_.remove_gpio(18, true); // MDIO
- EMSESP::system_.remove_gpio(19, true); // TXD0
- EMSESP::system_.remove_gpio(22, true); // TXD1
- EMSESP::system_.remove_gpio(21, true); // TXEN
- EMSESP::system_.remove_gpio(25, true); // RXD0
- EMSESP::system_.remove_gpio(26, true); // RXD1
- EMSESP::system_.remove_gpio(27, true); // CRS
-
- if (settings.eth_clock_mode < 2) {
- EMSESP::system_.remove_gpio(0, true); // ETH.clock input
- } else if (settings.eth_clock_mode == 2) {
- EMSESP::system_.remove_gpio(16, true); // ETH.clock output
- } else if (settings.eth_clock_mode == 3) {
- EMSESP::system_.remove_gpio(17, true); // ETH.clock output
- }
- }
-
- // if any of the GPIOs have changed and re-validate them
- bool have_valid_gpios = true;
-
- // Helper lambda for optional GPIOs (can be 0 to disable)
- auto add_optional_gpio = [&have_valid_gpios](uint8_t & gpio, const char * name) {
- if (gpio != 0 && !EMSESP::system_.add_gpio(gpio, name)) {
- gpio = 0; // 0 means disabled
- have_valid_gpios = false;
- }
- };
-
- // add new gpio assignment
- check_flag(original_settings.rx_gpio, settings.rx_gpio, ChangeFlags::UART);
- have_valid_gpios &= EMSESP::system_.add_gpio(settings.rx_gpio, "UART Rx");
-
- check_flag(original_settings.tx_gpio, settings.tx_gpio, ChangeFlags::UART);
- have_valid_gpios &= EMSESP::system_.add_gpio(settings.tx_gpio, "UART Tx");
-
- check_flag(original_settings.led_gpio, settings.led_gpio, ChangeFlags::LED);
- add_optional_gpio(settings.led_gpio, "LED");
-
- check_flag(original_settings.dallas_gpio, settings.dallas_gpio, ChangeFlags::TEMPERATURE_SENSOR);
- add_optional_gpio(settings.dallas_gpio, "Dallas");
-
- check_flag(original_settings.pbutton_gpio, settings.pbutton_gpio, ChangeFlags::BUTTON);
- have_valid_gpios &= EMSESP::system_.add_gpio(settings.pbutton_gpio, "Button");
-
- // check if the LED type, eth_phy_addr or eth_clock_mode have changed
- check_flag(original_settings.led_type, settings.led_type, ChangeFlags::LED);
- check_flag(original_settings.eth_phy_addr, settings.eth_phy_addr, ChangeFlags::RESTART);
- check_flag(original_settings.eth_clock_mode, settings.eth_clock_mode, ChangeFlags::RESTART);
-
- // tx_mode
- settings.tx_mode = root["tx_mode"] | EMSESP_DEFAULT_TX_MODE;
- check_flag(original_settings.tx_mode, settings.tx_mode, ChangeFlags::UART);
-
- // syslog
- settings.syslog_enabled = root["syslog_enabled"] | EMSESP_DEFAULT_SYSLOG_ENABLED;
- check_flag(original_settings.syslog_enabled, settings.syslog_enabled, ChangeFlags::SYSLOG);
- settings.syslog_level = root["syslog_level"] | EMSESP_DEFAULT_SYSLOG_LEVEL;
- check_flag(original_settings.syslog_level, settings.syslog_level, ChangeFlags::SYSLOG);
- settings.syslog_mark_interval = root["syslog_mark_interval"] | EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL;
- check_flag(original_settings.syslog_mark_interval, settings.syslog_mark_interval, ChangeFlags::SYSLOG);
- settings.syslog_port = root["syslog_port"] | EMSESP_DEFAULT_SYSLOG_PORT;
- check_flag(original_settings.syslog_port, settings.syslog_port, ChangeFlags::SYSLOG);
-
-#ifndef EMSESP_STANDALONE
- settings.syslog_host = root["syslog_host"] | EMSESP_DEFAULT_SYSLOG_HOST;
- if (original_settings.syslog_host != settings.syslog_host) {
- add_flags(ChangeFlags::SYSLOG);
- }
-#endif
-
- // temperature sensor
- settings.dallas_parasite = root["dallas_parasite"] | EMSESP_DEFAULT_DALLAS_PARASITE;
- check_flag(original_settings.dallas_parasite, settings.dallas_parasite, ChangeFlags::TEMPERATURE_SENSOR);
-
- // shower
- settings.shower_timer = root["shower_timer"] | EMSESP_DEFAULT_SHOWER_TIMER;
- check_flag(original_settings.shower_timer, settings.shower_timer, ChangeFlags::SHOWER);
- settings.shower_alert = root["shower_alert"] | EMSESP_DEFAULT_SHOWER_ALERT;
- check_flag(original_settings.shower_alert, settings.shower_alert, ChangeFlags::SHOWER);
- settings.shower_alert_trigger = root["shower_alert_trigger"] | EMSESP_DEFAULT_SHOWER_ALERT_TRIGGER;
- check_flag(original_settings.shower_alert_trigger, settings.shower_alert_trigger, ChangeFlags::SHOWER);
- settings.shower_min_duration = root["shower_min_duration"] | EMSESP_DEFAULT_SHOWER_MIN_DURATION;
- check_flag(original_settings.shower_min_duration, settings.shower_min_duration, ChangeFlags::SHOWER);
- settings.shower_alert_coldshot = root["shower_alert_coldshot"] | EMSESP_DEFAULT_SHOWER_ALERT_COLDSHOT;
- check_flag(original_settings.shower_alert_coldshot, settings.shower_alert_coldshot, ChangeFlags::SHOWER);
-
- // LED
- settings.hide_led = root["hide_led"] | EMSESP_DEFAULT_HIDE_LED;
- check_flag(original_settings.hide_led, settings.hide_led, ChangeFlags::LED);
-
- // adc
- settings.analog_enabled = root["analog_enabled"] | EMSESP_DEFAULT_ANALOG_ENABLED;
- check_flag(original_settings.analog_enabled, settings.analog_enabled, ChangeFlags::ANALOG_SENSOR);
-
- // telnet, ems bus id and low clock
- settings.telnet_enabled = root["telnet_enabled"] | EMSESP_DEFAULT_TELNET_ENABLED;
- check_flag(original_settings.telnet_enabled, settings.telnet_enabled, ChangeFlags::RESTART);
- settings.ems_bus_id = root["ems_bus_id"] | EMSESP_DEFAULT_EMS_BUS_ID;
- check_flag(original_settings.ems_bus_id, settings.ems_bus_id, ChangeFlags::RESTART);
- settings.low_clock = root["low_clock"];
- check_flag(original_settings.low_clock, settings.low_clock, ChangeFlags::RESTART);
-
- // Modbus settings
- settings.modbus_enabled = root["modbus_enabled"] | EMSESP_DEFAULT_MODBUS_ENABLED;
- settings.modbus_port = root["modbus_port"] | EMSESP_DEFAULT_MODBUS_PORT;
- settings.modbus_max_clients = root["modbus_max_clients"] | EMSESP_DEFAULT_MODBUS_MAX_CLIENTS;
- settings.modbus_timeout = root["modbus_timeout"] | EMSESP_DEFAULT_MODBUS_TIMEOUT;
-
- //
- // these may need mqtt restart to rebuild HA discovery topics
- //
- settings.bool_format = root["bool_format"] | EMSESP_DEFAULT_BOOL_FORMAT;
- EMSESP::system_.bool_format(settings.bool_format);
- if (Mqtt::ha_enabled()) {
- check_flag(original_settings.bool_format, settings.bool_format, ChangeFlags::MQTT);
- }
-
- settings.enum_format = root["enum_format"] | EMSESP_DEFAULT_ENUM_FORMAT;
- EMSESP::system_.enum_format(settings.enum_format);
- if (Mqtt::ha_enabled()) {
- check_flag(original_settings.enum_format, settings.enum_format, ChangeFlags::MQTT);
- }
-
- settings.locale = root["locale"] | EMSESP_DEFAULT_LOCALE;
- EMSESP::system_.locale(settings.locale);
- if (Mqtt::ha_enabled() && original_settings.locale != settings.locale) {
- add_flags(ChangeFlags::MQTT);
- }
-
- //
- // without checks or necessary restarts...
- //
- settings.trace_raw = root["trace_raw"] | EMSESP_DEFAULT_TRACELOG_RAW;
- EMSESP::trace_raw(settings.trace_raw);
-
- settings.notoken_api = root["notoken_api"] | EMSESP_DEFAULT_NOTOKEN_API;
- settings.solar_maxflow = root["solar_maxflow"] | EMSESP_DEFAULT_SOLAR_MAXFLOW;
- settings.boiler_heatingoff = root["boiler_heatingoff"] | EMSESP_DEFAULT_BOILER_HEATINGOFF;
- settings.remote_timeout = root["remote_timeout"] | EMSESP_DEFAULT_REMOTE_TIMEOUT;
- settings.remote_timeout_enabled = root["remote_timeout_en"] | EMSESP_DEFAULT_REMOTE_TIMEOUT_EN;
- Roomctrl::set_timeout(settings.remote_timeout_enabled ? settings.remote_timeout : 0);
-
- settings.fahrenheit = root["fahrenheit"];
- EMSESP::system_.fahrenheit(settings.fahrenheit);
-
- settings.readonly_mode = root["readonly_mode"];
- EMSESP::system_.readonly_mode(settings.readonly_mode);
-
- settings.developer_mode = root["developer_mode"];
- EMSESP::system_.developer_mode(settings.developer_mode);
-
- settings.bool_dashboard = root["bool_dashboard"] | EMSESP_DEFAULT_BOOL_FORMAT;
- EMSESP::system_.bool_dashboard(settings.bool_dashboard);
-
- settings.weblog_level = root["weblog_level"] | EMSESP_DEFAULT_WEBLOG_LEVEL;
- settings.weblog_compact = root["weblog_compact"] | EMSESP_DEFAULT_WEBLOG_COMPACT;
-
- settings.email_enabled = root["email_enabled"] | FACTORY_EMAIL_ENABLE;
- settings.email_ssl = root["email_ssl"] | FACTORY_EMAIL_SSL;
- settings.email_starttls = root["email_starttls"] | FACTORY_EMAIL_STARTTLS;
- settings.email_server = root["email_server"] | FACTORY_EMAIL_SERVER;
- settings.email_port = root["email_port"] | FACTORY_EMAIL_PORT;
- settings.email_login = root["email_login"] | FACTORY_EMAIL_LOGIN;
- settings.email_pass = root["email_pass"] | FACTORY_EMAIL_PASSWORD;
- settings.email_sender = root["email_sender"] | FACTORY_EMAIL_FROM;
- settings.email_recp = root["email_recp"] | FACTORY_EMAIL_TO;
- settings.email_subject = root["email_subject"] | FACTORY_EMAIL_SUBJECT;
-
- if (settings.email_ssl && settings.email_starttls) {
- settings.email_ssl = false;
- }
- // if no psram limit weblog buffer to 25 messages
- if (EMSESP::system_.PSram() > 0) {
- settings.weblog_buffer = root["weblog_buffer"] | EMSESP_DEFAULT_WEBLOG_BUFFER;
- } else {
- settings.weblog_buffer = root["weblog_buffer"] | 25; // limit to 25 messages if no psram
- }
-
- // save the settings if changed from the webUI
- // if we encountered an invalid GPIO, rollback changes and don't save settings,
- // and report the error to WebUI without a restart
- if (!have_valid_gpios) {
- // replace settings with original settings
- settings = original_settings;
- EMSESP::system_.restore_snapshot_gpios(used_gpios, system_gpios);
-
- // report the error to WebUI
- EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_INVALID_GPIO);
- return StateUpdateResult::ERROR; // don't save the settings if the GPIOs are invalid
- }
-
- // save the setting internally, for reference later
- EMSESP::system_.store_settings(settings);
-
- // and finally always write to the settings file
- if (has_flags(ChangeFlags::RESTART)) {
- return StateUpdateResult::CHANGED_RESTART;
- }
-
- return StateUpdateResult::CHANGED;
-}
-
-// this is called after any of the settings have been persisted to the filesystem
-// either via the Web UI or via the Console
-void WebSettingsService::onUpdate() {
- // skip if we're restarting anyway
-
- if (WebSettings::has_flags(WebSettings::ChangeFlags::RESTART)) {
- return;
- }
-
- if (WebSettings::has_flags(WebSettings::ChangeFlags::SHOWER)) {
- EMSESP::shower_.start();
- }
-
- if (WebSettings::has_flags(WebSettings::ChangeFlags::TEMPERATURE_SENSOR)) {
- EMSESP::temperaturesensor_.start();
- }
-
- if (WebSettings::has_flags(WebSettings::ChangeFlags::UART)) {
- EMSESP::system_.uart_init();
- }
-
- if (WebSettings::has_flags(WebSettings::ChangeFlags::SYSLOG)) {
- EMSESP::system_.syslog_init(); // re-start (or stop)
- }
-
- if (WebSettings::has_flags(WebSettings::ChangeFlags::ANALOG_SENSOR)) {
- EMSESP::analogsensor_.start();
- }
-
- if (WebSettings::has_flags(WebSettings::ChangeFlags::BUTTON)) {
- EMSESP::system_.button_init();
- }
-
- if (WebSettings::has_flags(WebSettings::ChangeFlags::LED)) {
- EMSESP::system_.led_init();
- }
-
- if (WebSettings::has_flags(WebSettings::ChangeFlags::MQTT)) {
- Mqtt::reset_mqtt(); // reload MQTT, init HA etc
- }
-
- WebSettings::reset_flags();
-}
-
-void WebSettingsService::begin() {
- _fsPersistence.readFromFS();
-}
-
-void WebSettingsService::save() {
- _fsPersistence.writeToFS();
-}
-
-// send the board profile as JSON
-void WebSettingsService::board_profile(AsyncWebServerRequest * request) {
- if (request->hasParam("boardProfile")) {
- std::string board_profile = request->getParam("boardProfile")->value().c_str();
-
- auto * response = new AsyncJsonResponse(false);
- JsonObject root = response->getRoot();
-
- // 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;
- (void)System::load_board_profile(data, board_profile);
- root["board_profile"] = board_profile;
- root["led_gpio"] = data[0];
- root["dallas_gpio"] = data[1];
- root["rx_gpio"] = data[2];
- root["tx_gpio"] = data[3];
- root["pbutton_gpio"] = data[4];
- root["phy_type"] = data[5];
- root["eth_power"] = data[6];
- root["eth_phy_addr"] = data[7];
- root["eth_clock_mode"] = data[8];
- root["led_type"] = data[9];
-
- response->setLength();
- request->send(response);
- return;
- }
-
- AsyncWebServerResponse * response = request->beginResponse(200);
- request->send(response);
-}
-
-// loads the board profile to set the gpios
-// if the board profile is not found, or default, it will try to autodetect the board profile
-void WebSettings::set_board_profile(WebSettings & settings) {
- // The optional NVS boot value has priority and overrides any board_profile setting.
- // This is only done for BBQKees boards
- // Note 1: we never set the NVS boot value in the code - this is done on initial pre-loading
- // Note 2: The board profile is dynamically changed for the session, but the value in the settings file on the FS remains untouched
- if (EMSESP::system_.getBBQKeesGatewayDetails(FUSE_VALUE::MFG).startsWith("BBQKees")) {
- String bbq_board = EMSESP::system_.getBBQKeesGatewayDetails(FUSE_VALUE::BOARD);
- if (!bbq_board.isEmpty() && settings.board_profile != "CUSTOM") {
-#if defined(EMSESP_DEBUG)
- EMSESP::logger().info("Overriding board profile with fuse value %s", bbq_board.c_str());
-#endif
- settings.board_profile = bbq_board;
- }
- }
-
- // if it's CUSTOM no need to load the board profile from the settings
- // as it's already set
- if (settings.board_profile == "CUSTOM") {
- EMSESP::logger().info("Using CUSTOM board profile");
- EMSESP::system_.set_valid_system_gpios();
- return;
- }
-
- // 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(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)
- EMSESP::logger().debug("Unable to identify board profile %s", settings.board_profile.c_str());
-#endif
- settings.board_profile = "default"; // can't find profile, fallback to "default"
- }
- }
-
- // we still don't have a valid board profile. Let's see if we can determine one from the build config or hardware
- if (settings.board_profile == "default") {
- EMSESP::logger().info("Autodetecting board profile");
-#if CONFIG_IDF_TARGET_ESP32
- // check for no PSRAM, could be a E32 or S32?
- if (!ESP.getPsramSize()) {
- if (ETH.begin(ETH_PHY_LAN8720, 1, 23, 18, 16, ETH_CLOCK_GPIO0_IN)) {
- settings.board_profile = "E32"; // Ethernet without PSRAM
- } else {
- settings.board_profile = "S32"; // ESP32 standard WiFi without PSRAM
- }
- } else {
- // check for boards with PSRAM, could be a E32V2 otherwise default back to the S32
- if (ETH.begin(ETH_PHY_LAN8720, 0, 23, 18, 15, ETH_CLOCK_GPIO0_OUT)) {
- if (analogReadMilliVolts(39) > 700) { // core voltage > 2.6V
- settings.board_profile = "E32V2_2"; // Ethernet, PSRAM, internal sensors
- } else {
- settings.board_profile = "E32V2"; // Ethernet and PSRAM
- }
- } else {
- settings.board_profile = "S32"; // ESP32 standard WiFi with PSRAM
- }
- }
-// override if we know the target from the build config like C3, S2, S3 etc..
-#elif CONFIG_IDF_TARGET_ESP32C3
- settings.board_profile = "C3MINI";
-#elif CONFIG_IDF_TARGET_ESP32S2
- settings.board_profile = "S2MINI";
-#elif CONFIG_IDF_TARGET_ESP32S3
- settings.board_profile = "S32S3"; // BBQKees Gateway S3
-#elif CONFIG_IDF_TARGET_ESP32C6
- settings.board_profile = "CUSTOM";
-#endif
- // apply the new board profile setting
- System::load_board_profile(data, settings.board_profile.c_str());
- }
-
-// log board profile and PSRAM info
-#ifndef EMSESP_STANDALONE
- uint32_t psram_size = ESP.getPsramSize() / 1024; // in KB
- if (psram_size > 0) {
- EMSESP::logger().info("Loaded board profile %s, PSRAM: %lu KB", settings.board_profile.c_str(), psram_size);
- } else {
- EMSESP::logger().info("Loaded board profile %s, PSRAM: not available", settings.board_profile.c_str());
- }
-#endif
-
- // apply the new board profile settings
- // 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
- settings.led_gpio = data[0]; // LED GPIO
- settings.dallas_gpio = data[1]; // Dallas GPIO
- settings.rx_gpio = data[2]; // UART Rx GPIO
- settings.tx_gpio = data[3]; // UART Tx GPIO
- settings.pbutton_gpio = data[4]; // Button GPIO
- settings.phy_type = data[5]; // PHY Type
- settings.eth_power = data[6]; // Ethernet Power GPIO
- settings.eth_phy_addr = data[7]; // Ethernet PHY Address
- settings.eth_clock_mode = data[8]; // Ethernet Clock Mode
- settings.led_type = data[9]; // LED Type
-}
-
-// returns true if the value was changed
-bool WebSettings::check_flag(int prev_v, int new_v, uint8_t flag) {
- if (prev_v != new_v) {
- add_flags(flag);
-#if defined(EMSESP_DEBUG)
- // EMSESP::logger().debug("check_flag: flag %d, prev_v=%d, new_v=%d", flag, prev_v, new_v);
-#endif
- return true;
- }
- return false;
-}
-
-void WebSettings::add_flags(uint8_t flags) {
- flags_ |= flags;
-}
-
-bool WebSettings::has_flags(uint8_t flags) {
- return (flags_ & flags) == flags;
-}
-
-void WebSettings::reset_flags() {
- flags_ = ChangeFlags::NONE;
-}
-
-uint8_t WebSettings::get_flags() {
- return flags_;
-}
-
-} // namespace emsesp
+ #include "emsesp.h"
+
+ namespace emsesp {
+
+ uint8_t WebSettings::flags_ = 0;
+
+ WebSettingsService::WebSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
+ : _httpEndpoint(WebSettings::read, WebSettings::update, this, server, EMSESP_SETTINGS_SERVICE_PATH, securityManager)
+ , _fsPersistence(WebSettings::read, WebSettings::update, this, fs, EMSESP_SETTINGS_FILE) {
+ securityManager->addEndpoint(server, EMSESP_BOARD_PROFILE_SERVICE_PATH, AuthenticationPredicates::IS_AUTHENTICATED, [this](AsyncWebServerRequest * request) {
+ board_profile(request);
+ });
+
+ addUpdateHandler([this] { onUpdate(); }, false);
+ }
+
+ void WebSettings::read(WebSettings & settings, JsonObject root) {
+ root["version"] = settings.version;
+ root["board_profile"] = settings.board_profile;
+ root["platform"] = EMSESP_PLATFORM;
+ root["locale"] = settings.locale;
+ root["tx_mode"] = settings.tx_mode;
+ root["ems_bus_id"] = settings.ems_bus_id;
+ root["syslog_enabled"] = settings.syslog_enabled;
+ root["syslog_level"] = settings.syslog_level;
+ root["trace_raw"] = settings.trace_raw;
+ root["syslog_mark_interval"] = settings.syslog_mark_interval;
+ root["syslog_host"] = settings.syslog_host;
+ root["syslog_port"] = settings.syslog_port;
+ root["boiler_heatingoff"] = settings.boiler_heatingoff;
+ root["remote_timeout"] = settings.remote_timeout;
+ root["remote_timeout_en"] = settings.remote_timeout_enabled;
+ root["shower_timer"] = settings.shower_timer;
+ root["shower_alert"] = settings.shower_alert;
+ root["shower_alert_coldshot"] = settings.shower_alert_coldshot;
+ root["shower_alert_trigger"] = settings.shower_alert_trigger;
+ root["shower_min_duration"] = settings.shower_min_duration;
+ root["rx_gpio"] = settings.rx_gpio;
+ root["tx_gpio"] = settings.tx_gpio;
+ root["dallas_gpio"] = settings.dallas_gpio;
+ root["dallas_parasite"] = settings.dallas_parasite;
+ root["led_gpio"] = settings.led_gpio;
+ root["hide_led"] = settings.hide_led;
+ root["led_type"] = settings.led_type;
+ root["low_clock"] = settings.low_clock;
+ root["telnet_enabled"] = settings.telnet_enabled;
+ root["notoken_api"] = settings.notoken_api;
+ root["readonly_mode"] = settings.readonly_mode;
+ root["analog_enabled"] = settings.analog_enabled;
+ root["pbutton_gpio"] = settings.pbutton_gpio;
+ root["solar_maxflow"] = settings.solar_maxflow;
+ root["fahrenheit"] = settings.fahrenheit;
+ root["bool_format"] = settings.bool_format;
+ root["bool_dashboard"] = settings.bool_dashboard;
+ root["enum_format"] = settings.enum_format;
+ root["weblog_level"] = settings.weblog_level;
+ root["weblog_buffer"] = settings.weblog_buffer;
+ root["weblog_compact"] = settings.weblog_compact;
+ root["phy_type"] = settings.phy_type;
+ root["eth_power"] = settings.eth_power;
+ root["eth_phy_addr"] = settings.eth_phy_addr;
+ root["eth_clock_mode"] = settings.eth_clock_mode;
+ root["modbus_enabled"] = settings.modbus_enabled;
+ root["modbus_port"] = settings.modbus_port;
+ root["modbus_max_clients"] = settings.modbus_max_clients;
+ root["modbus_timeout"] = settings.modbus_timeout;
+ root["developer_mode"] = settings.developer_mode;
+ #ifndef NO_TLS_SUPPORT
+ root["email_enabled"] = settings.email_enabled;
+ #else
+ root["email_enabled"] = false;
+ #endif
+ root["email_ssl"] = settings.email_ssl;
+ root["email_starttls"] = settings.email_starttls;
+ root["email_server"] = settings.email_server;
+ root["email_port"] = settings.email_port;
+ root["email_login"] = settings.email_login;
+ root["email_pass"] = settings.email_pass;
+ root["email_sender"] = settings.email_sender;
+ root["email_recp"] = settings.email_recp;
+ root["email_subject"] = settings.email_subject;
+ }
+
+ // call on initialization and also when settings are updated/saved via web or console
+ // note, settings is empty when the service starts
+ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
+ // make a copy of the settings to compare to later
+ const WebSettings original_settings(settings);
+
+ // make a snapshot of the current GPIOs
+ std::vector used_gpios;
+ std::vector system_gpios;
+ EMSESP::system_.make_snapshot_gpios(used_gpios, system_gpios);
+
+ settings.version = root["version"] | EMSESP_APP_VERSION; // save the version, we use it later in System::check_upgrade()
+ settings.board_profile = root["board_profile"] | EMSESP_DEFAULT_BOARD_PROFILE;
+
+ // get current values that are related to the board profile
+ settings.led_gpio = root["led_gpio"];
+ settings.dallas_gpio = root["dallas_gpio"];
+ settings.rx_gpio = root["rx_gpio"];
+ settings.tx_gpio = root["tx_gpio"];
+ settings.pbutton_gpio = root["pbutton_gpio"];
+ settings.phy_type = root["phy_type"];
+ settings.eth_power = root["eth_power"];
+ settings.eth_phy_addr = root["eth_phy_addr"];
+ settings.eth_clock_mode = root["eth_clock_mode"];
+ settings.led_type = root["led_type"]; // 1 = RGB-LED
+
+ reset_flags();
+
+ // before loading new board profile free old gpios from used list to allow remapping
+ EMSESP::system_.remove_gpio(original_settings.led_gpio);
+ EMSESP::system_.remove_gpio(original_settings.dallas_gpio);
+ EMSESP::system_.remove_gpio(original_settings.pbutton_gpio);
+ EMSESP::system_.remove_gpio(original_settings.rx_gpio);
+ EMSESP::system_.remove_gpio(original_settings.tx_gpio);
+
+ // see if the user has changed the board profile
+ // this will set: led_gpio, dallas_gpio, rx_gpio, tx_gpio, pbutton_gpio, phy_type, eth_power, eth_phy_addr, eth_clock_mode, led_type
+ // this will always run when EMS-ESP starts since original_settings{} is empty
+ if (original_settings.board_profile != settings.board_profile || original_settings.board_profile == "default"
+ || original_settings.board_profile.length() == 0) {
+ set_board_profile(settings);
+ add_flags(ChangeFlags::RESTART);
+ }
+
+ check_flag(original_settings.phy_type, settings.phy_type, ChangeFlags::RESTART);
+ // ETH has changed, so we need to check the ethernet pins. Only if ETH is being used.
+ if (settings.phy_type != PHY_type::PHY_TYPE_NONE) {
+ check_flag(original_settings.eth_power, settings.eth_power, ChangeFlags::RESTART);
+ check_flag(original_settings.eth_clock_mode, settings.eth_clock_mode, ChangeFlags::RESTART);
+ if (settings.eth_power != -1) { // Ethernet Power -1 means disabled
+ EMSESP::system_.remove_gpio(settings.eth_power, true);
+ }
+ // remove the ethernet pins from valid list, regardless of whether the GPIOs are valid or not
+ EMSESP::system_.remove_gpio(23, true); // MDC
+ EMSESP::system_.remove_gpio(18, true); // MDIO
+ EMSESP::system_.remove_gpio(19, true); // TXD0
+ EMSESP::system_.remove_gpio(22, true); // TXD1
+ EMSESP::system_.remove_gpio(21, true); // TXEN
+ EMSESP::system_.remove_gpio(25, true); // RXD0
+ EMSESP::system_.remove_gpio(26, true); // RXD1
+ EMSESP::system_.remove_gpio(27, true); // CRS
+
+ if (settings.eth_clock_mode < 2) {
+ EMSESP::system_.remove_gpio(0, true); // ETH.clock input
+ } else if (settings.eth_clock_mode == 2) {
+ EMSESP::system_.remove_gpio(16, true); // ETH.clock output
+ } else if (settings.eth_clock_mode == 3) {
+ EMSESP::system_.remove_gpio(17, true); // ETH.clock output
+ }
+ }
+
+ // if any of the GPIOs have changed and re-validate them
+ bool have_valid_gpios = true;
+
+ // Helper lambda for optional GPIOs (can be 0 to disable)
+ auto add_optional_gpio = [&have_valid_gpios](uint8_t & gpio, const char * name) {
+ if (gpio != 0 && !EMSESP::system_.add_gpio(gpio, name)) {
+ gpio = 0; // 0 means disabled
+ have_valid_gpios = false;
+ }
+ };
+
+ // add new gpio assignment
+ check_flag(original_settings.rx_gpio, settings.rx_gpio, ChangeFlags::UART);
+ have_valid_gpios &= EMSESP::system_.add_gpio(settings.rx_gpio, "UART Rx");
+
+ check_flag(original_settings.tx_gpio, settings.tx_gpio, ChangeFlags::UART);
+ have_valid_gpios &= EMSESP::system_.add_gpio(settings.tx_gpio, "UART Tx");
+
+ check_flag(original_settings.led_gpio, settings.led_gpio, ChangeFlags::LED);
+ add_optional_gpio(settings.led_gpio, "LED");
+
+ check_flag(original_settings.dallas_gpio, settings.dallas_gpio, ChangeFlags::TEMPERATURE_SENSOR);
+ add_optional_gpio(settings.dallas_gpio, "Dallas");
+
+ check_flag(original_settings.pbutton_gpio, settings.pbutton_gpio, ChangeFlags::BUTTON);
+ have_valid_gpios &= EMSESP::system_.add_gpio(settings.pbutton_gpio, "Button");
+
+ // check if the LED type, eth_phy_addr or eth_clock_mode have changed
+ check_flag(original_settings.led_type, settings.led_type, ChangeFlags::LED);
+ check_flag(original_settings.eth_phy_addr, settings.eth_phy_addr, ChangeFlags::RESTART);
+ check_flag(original_settings.eth_clock_mode, settings.eth_clock_mode, ChangeFlags::RESTART);
+
+ // tx_mode
+ settings.tx_mode = root["tx_mode"] | EMSESP_DEFAULT_TX_MODE;
+ check_flag(original_settings.tx_mode, settings.tx_mode, ChangeFlags::UART);
+
+ // syslog
+ settings.syslog_enabled = root["syslog_enabled"] | EMSESP_DEFAULT_SYSLOG_ENABLED;
+ check_flag(original_settings.syslog_enabled, settings.syslog_enabled, ChangeFlags::SYSLOG);
+ settings.syslog_level = root["syslog_level"] | EMSESP_DEFAULT_SYSLOG_LEVEL;
+ check_flag(original_settings.syslog_level, settings.syslog_level, ChangeFlags::SYSLOG);
+ settings.syslog_mark_interval = root["syslog_mark_interval"] | EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL;
+ check_flag(original_settings.syslog_mark_interval, settings.syslog_mark_interval, ChangeFlags::SYSLOG);
+ settings.syslog_port = root["syslog_port"] | EMSESP_DEFAULT_SYSLOG_PORT;
+ check_flag(original_settings.syslog_port, settings.syslog_port, ChangeFlags::SYSLOG);
+
+ #ifndef EMSESP_STANDALONE
+ settings.syslog_host = root["syslog_host"] | EMSESP_DEFAULT_SYSLOG_HOST;
+ if (original_settings.syslog_host != settings.syslog_host) {
+ add_flags(ChangeFlags::SYSLOG);
+ }
+ #endif
+
+ // temperature sensor
+ settings.dallas_parasite = root["dallas_parasite"] | EMSESP_DEFAULT_DALLAS_PARASITE;
+ check_flag(original_settings.dallas_parasite, settings.dallas_parasite, ChangeFlags::TEMPERATURE_SENSOR);
+
+ // shower
+ settings.shower_timer = root["shower_timer"] | EMSESP_DEFAULT_SHOWER_TIMER;
+ check_flag(original_settings.shower_timer, settings.shower_timer, ChangeFlags::SHOWER);
+ settings.shower_alert = root["shower_alert"] | EMSESP_DEFAULT_SHOWER_ALERT;
+ check_flag(original_settings.shower_alert, settings.shower_alert, ChangeFlags::SHOWER);
+ settings.shower_alert_trigger = root["shower_alert_trigger"] | EMSESP_DEFAULT_SHOWER_ALERT_TRIGGER;
+ check_flag(original_settings.shower_alert_trigger, settings.shower_alert_trigger, ChangeFlags::SHOWER);
+ settings.shower_min_duration = root["shower_min_duration"] | EMSESP_DEFAULT_SHOWER_MIN_DURATION;
+ check_flag(original_settings.shower_min_duration, settings.shower_min_duration, ChangeFlags::SHOWER);
+ settings.shower_alert_coldshot = root["shower_alert_coldshot"] | EMSESP_DEFAULT_SHOWER_ALERT_COLDSHOT;
+ check_flag(original_settings.shower_alert_coldshot, settings.shower_alert_coldshot, ChangeFlags::SHOWER);
+
+ // LED
+ settings.hide_led = root["hide_led"] | EMSESP_DEFAULT_HIDE_LED;
+ check_flag(original_settings.hide_led, settings.hide_led, ChangeFlags::LED);
+
+ // adc
+ settings.analog_enabled = root["analog_enabled"] | EMSESP_DEFAULT_ANALOG_ENABLED;
+ check_flag(original_settings.analog_enabled, settings.analog_enabled, ChangeFlags::ANALOG_SENSOR);
+
+ // telnet, ems bus id and low clock
+ settings.telnet_enabled = root["telnet_enabled"] | EMSESP_DEFAULT_TELNET_ENABLED;
+ check_flag(original_settings.telnet_enabled, settings.telnet_enabled, ChangeFlags::RESTART);
+ settings.ems_bus_id = root["ems_bus_id"] | EMSESP_DEFAULT_EMS_BUS_ID;
+ check_flag(original_settings.ems_bus_id, settings.ems_bus_id, ChangeFlags::RESTART);
+ settings.low_clock = root["low_clock"];
+ check_flag(original_settings.low_clock, settings.low_clock, ChangeFlags::RESTART);
+
+ // Modbus settings
+ settings.modbus_enabled = root["modbus_enabled"] | EMSESP_DEFAULT_MODBUS_ENABLED;
+ settings.modbus_port = root["modbus_port"] | EMSESP_DEFAULT_MODBUS_PORT;
+ settings.modbus_max_clients = root["modbus_max_clients"] | EMSESP_DEFAULT_MODBUS_MAX_CLIENTS;
+ settings.modbus_timeout = root["modbus_timeout"] | EMSESP_DEFAULT_MODBUS_TIMEOUT;
+
+ //
+ // these may need mqtt restart to rebuild HA discovery topics
+ //
+ settings.bool_format = root["bool_format"] | EMSESP_DEFAULT_BOOL_FORMAT;
+ EMSESP::system_.bool_format(settings.bool_format);
+ if (Mqtt::ha_enabled()) {
+ check_flag(original_settings.bool_format, settings.bool_format, ChangeFlags::MQTT);
+ }
+
+ settings.enum_format = root["enum_format"] | EMSESP_DEFAULT_ENUM_FORMAT;
+ EMSESP::system_.enum_format(settings.enum_format);
+ if (Mqtt::ha_enabled()) {
+ check_flag(original_settings.enum_format, settings.enum_format, ChangeFlags::MQTT);
+ }
+
+ settings.locale = root["locale"] | EMSESP_DEFAULT_LOCALE;
+ EMSESP::system_.locale(settings.locale);
+ if (Mqtt::ha_enabled() && original_settings.locale != settings.locale) {
+ add_flags(ChangeFlags::MQTT);
+ }
+
+ //
+ // without checks or necessary restarts...
+ //
+ settings.trace_raw = root["trace_raw"] | EMSESP_DEFAULT_TRACELOG_RAW;
+ EMSESP::trace_raw(settings.trace_raw);
+
+ settings.notoken_api = root["notoken_api"] | EMSESP_DEFAULT_NOTOKEN_API;
+ settings.solar_maxflow = root["solar_maxflow"] | EMSESP_DEFAULT_SOLAR_MAXFLOW;
+ settings.boiler_heatingoff = root["boiler_heatingoff"] | EMSESP_DEFAULT_BOILER_HEATINGOFF;
+ settings.remote_timeout = root["remote_timeout"] | EMSESP_DEFAULT_REMOTE_TIMEOUT;
+ settings.remote_timeout_enabled = root["remote_timeout_en"] | EMSESP_DEFAULT_REMOTE_TIMEOUT_EN;
+ Roomctrl::set_timeout(settings.remote_timeout_enabled ? settings.remote_timeout : 0);
+
+ settings.fahrenheit = root["fahrenheit"];
+ EMSESP::system_.fahrenheit(settings.fahrenheit);
+
+ settings.readonly_mode = root["readonly_mode"];
+ EMSESP::system_.readonly_mode(settings.readonly_mode);
+
+ settings.developer_mode = root["developer_mode"];
+ EMSESP::system_.developer_mode(settings.developer_mode);
+
+ settings.bool_dashboard = root["bool_dashboard"] | EMSESP_DEFAULT_BOOL_FORMAT;
+ EMSESP::system_.bool_dashboard(settings.bool_dashboard);
+
+ settings.weblog_level = root["weblog_level"] | EMSESP_DEFAULT_WEBLOG_LEVEL;
+ settings.weblog_compact = root["weblog_compact"] | EMSESP_DEFAULT_WEBLOG_COMPACT;
+
+ settings.email_enabled = root["email_enabled"] | FACTORY_EMAIL_ENABLE;
+ settings.email_ssl = root["email_ssl"] | FACTORY_EMAIL_SSL;
+ settings.email_starttls = root["email_starttls"] | FACTORY_EMAIL_STARTTLS;
+ settings.email_server = root["email_server"] | FACTORY_EMAIL_SERVER;
+ settings.email_port = root["email_port"] | FACTORY_EMAIL_PORT;
+ settings.email_login = root["email_login"] | FACTORY_EMAIL_LOGIN;
+ settings.email_pass = root["email_pass"] | FACTORY_EMAIL_PASSWORD;
+ settings.email_sender = root["email_sender"] | FACTORY_EMAIL_FROM;
+ settings.email_recp = root["email_recp"] | FACTORY_EMAIL_TO;
+ settings.email_subject = root["email_subject"] | FACTORY_EMAIL_SUBJECT;
+
+ if (settings.email_ssl && settings.email_starttls) {
+ settings.email_ssl = false;
+ }
+ // if no psram limit weblog buffer to 25 messages
+ if (EMSESP::system_.PSram() > 0) {
+ settings.weblog_buffer = root["weblog_buffer"] | EMSESP_DEFAULT_WEBLOG_BUFFER;
+ } else {
+ settings.weblog_buffer = root["weblog_buffer"] | 25; // limit to 25 messages if no psram
+ }
+
+ // save the settings if changed from the webUI
+ // if we encountered an invalid GPIO, rollback changes and don't save settings,
+ // and report the error to WebUI without a restart
+ if (!have_valid_gpios) {
+ // replace settings with original settings
+ settings = original_settings;
+ EMSESP::system_.restore_snapshot_gpios(used_gpios, system_gpios);
+
+ // report the error to WebUI
+ EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_INVALID_GPIO);
+ return StateUpdateResult::ERROR; // don't save the settings if the GPIOs are invalid
+ }
+
+ // save the setting internally, for reference later
+ EMSESP::system_.store_settings(settings);
+
+ // and finally always write to the settings file
+ if (has_flags(ChangeFlags::RESTART)) {
+ return StateUpdateResult::CHANGED_RESTART;
+ }
+
+ return StateUpdateResult::CHANGED;
+ }
+
+ // this is called after any of the settings have been persisted to the filesystem
+ // either via the Web UI or via the Console
+ void WebSettingsService::onUpdate() {
+ // skip if we're restarting anyway
+
+ if (WebSettings::has_flags(WebSettings::ChangeFlags::RESTART)) {
+ return;
+ }
+
+ if (WebSettings::has_flags(WebSettings::ChangeFlags::SHOWER)) {
+ EMSESP::shower_.start();
+ }
+
+ if (WebSettings::has_flags(WebSettings::ChangeFlags::TEMPERATURE_SENSOR)) {
+ EMSESP::temperaturesensor_.start();
+ }
+
+ if (WebSettings::has_flags(WebSettings::ChangeFlags::UART)) {
+ EMSESP::system_.uart_init();
+ }
+
+ if (WebSettings::has_flags(WebSettings::ChangeFlags::SYSLOG)) {
+ EMSESP::system_.syslog_init(); // re-start (or stop)
+ }
+
+ if (WebSettings::has_flags(WebSettings::ChangeFlags::ANALOG_SENSOR)) {
+ EMSESP::analogsensor_.start();
+ }
+
+ if (WebSettings::has_flags(WebSettings::ChangeFlags::BUTTON)) {
+ EMSESP::system_.button_init();
+ }
+
+ if (WebSettings::has_flags(WebSettings::ChangeFlags::LED)) {
+ EMSESP::system_.led_init();
+ }
+
+ if (WebSettings::has_flags(WebSettings::ChangeFlags::MQTT)) {
+ Mqtt::reset_mqtt(); // reload MQTT, init HA etc
+ }
+
+ WebSettings::reset_flags();
+ }
+
+ void WebSettingsService::begin() {
+ _fsPersistence.readFromFS();
+ }
+
+ void WebSettingsService::save() {
+ _fsPersistence.writeToFS();
+ }
+
+ // send the board profile as JSON
+ void WebSettingsService::board_profile(AsyncWebServerRequest * request) {
+ if (request->hasParam("boardProfile")) {
+ std::string board_profile = request->getParam("boardProfile")->value().c_str();
+
+ auto * response = new AsyncJsonResponse(false);
+ JsonObject root = response->getRoot();
+
+ // 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;
+ (void)System::load_board_profile(data, board_profile);
+ root["board_profile"] = board_profile;
+ root["led_gpio"] = data[0];
+ root["dallas_gpio"] = data[1];
+ root["rx_gpio"] = data[2];
+ root["tx_gpio"] = data[3];
+ root["pbutton_gpio"] = data[4];
+ root["phy_type"] = data[5];
+ root["eth_power"] = data[6];
+ root["eth_phy_addr"] = data[7];
+ root["eth_clock_mode"] = data[8];
+ root["led_type"] = data[9];
+
+ response->setLength();
+ request->send(response);
+ return;
+ }
+
+ AsyncWebServerResponse * response = request->beginResponse(200);
+ request->send(response);
+ }
+
+ // loads the board profile to set the gpios
+ // if the board profile is not found, or default, it will try to autodetect the board profile
+ void WebSettings::set_board_profile(WebSettings & settings) {
+ // The optional NVS boot value has priority and overrides any board_profile setting.
+ // This is only done for BBQKees boards
+ // Note 1: we never set the NVS boot value in the code - this is done on initial pre-loading
+ // Note 2: The board profile is dynamically changed for the session, but the value in the settings file on the FS remains untouched
+ if (EMSESP::system_.getBBQKeesGatewayDetails(FUSE_VALUE::MFG).startsWith("BBQKees")) {
+ String bbq_board = EMSESP::system_.getBBQKeesGatewayDetails(FUSE_VALUE::BOARD);
+ if (!bbq_board.isEmpty() && settings.board_profile != "CUSTOM") {
+ #if defined(EMSESP_DEBUG)
+ EMSESP::logger().info("Overriding board profile with fuse value %s", bbq_board.c_str());
+ #endif
+ settings.board_profile = bbq_board;
+ }
+ }
+
+ // if it's CUSTOM no need to load the board profile from the settings
+ // as it's already set
+ if (settings.board_profile == "CUSTOM") {
+ EMSESP::logger().info("Using CUSTOM board profile");
+ EMSESP::system_.set_valid_system_gpios();
+ return;
+ }
+
+ // 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(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)
+ EMSESP::logger().debug("Unable to identify board profile %s", settings.board_profile.c_str());
+ #endif
+ settings.board_profile = "default"; // can't find profile, fallback to "default"
+ }
+ }
+
+ // we still don't have a valid board profile. Let's see if we can determine one from the build config or hardware
+ if (settings.board_profile == "default") {
+ EMSESP::logger().info("Autodetecting board profile");
+ #if CONFIG_IDF_TARGET_ESP32
+ // check for no PSRAM, could be a E32 or S32?
+ if (!ESP.getPsramSize()) {
+ if (ETH.begin(ETH_PHY_LAN8720, 1, 23, 18, 16, ETH_CLOCK_GPIO0_IN)) {
+ settings.board_profile = "E32"; // Ethernet without PSRAM
+ } else {
+ settings.board_profile = "S32"; // ESP32 standard WiFi without PSRAM
+ }
+ } else {
+ // check for boards with PSRAM, could be a E32V2 otherwise default back to the S32
+ if (ETH.begin(ETH_PHY_LAN8720, 0, 23, 18, 15, ETH_CLOCK_GPIO0_OUT)) {
+ if (analogReadMilliVolts(39) > 700) { // core voltage > 2.6V
+ settings.board_profile = "E32V2_2"; // Ethernet, PSRAM, internal sensors
+ } else {
+ settings.board_profile = "E32V2"; // Ethernet and PSRAM
+ }
+ } else {
+ settings.board_profile = "S32"; // ESP32 standard WiFi with PSRAM
+ }
+ }
+ // override if we know the target from the build config like C3, S2, S3 etc..
+ #elif CONFIG_IDF_TARGET_ESP32C3
+ settings.board_profile = "C3MINI";
+ #elif CONFIG_IDF_TARGET_ESP32S2
+ settings.board_profile = "S2MINI";
+ #elif CONFIG_IDF_TARGET_ESP32S3
+ settings.board_profile = "S32S3"; // BBQKees Gateway S3
+ #elif CONFIG_IDF_TARGET_ESP32C6
+ settings.board_profile = "CUSTOM";
+ #endif
+ // apply the new board profile setting
+ System::load_board_profile(data, settings.board_profile.c_str());
+ }
+
+ // log board profile and PSRAM info
+ #ifndef EMSESP_STANDALONE
+ uint32_t psram_size = ESP.getPsramSize() / 1024; // in KB
+ if (psram_size > 0) {
+ EMSESP::logger().info("Loaded board profile %s (PSRAM: %lu KB)", settings.board_profile.c_str(), psram_size);
+ } else {
+ EMSESP::logger().info("Loaded board profile %s (PSRAM: not available)", settings.board_profile.c_str());
+ }
+ #endif
+
+ // apply the new board profile settings
+ // 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
+ settings.led_gpio = data[0]; // LED GPIO
+ settings.dallas_gpio = data[1]; // Dallas GPIO
+ settings.rx_gpio = data[2]; // UART Rx GPIO
+ settings.tx_gpio = data[3]; // UART Tx GPIO
+ settings.pbutton_gpio = data[4]; // Button GPIO
+ settings.phy_type = data[5]; // PHY Type
+ settings.eth_power = data[6]; // Ethernet Power GPIO
+ settings.eth_phy_addr = data[7]; // Ethernet PHY Address
+ settings.eth_clock_mode = data[8]; // Ethernet Clock Mode
+ settings.led_type = data[9]; // LED Type
+ }
+
+ // returns true if the value was changed
+ bool WebSettings::check_flag(int prev_v, int new_v, uint8_t flag) {
+ if (prev_v != new_v) {
+ add_flags(flag);
+ #if defined(EMSESP_DEBUG)
+ // EMSESP::logger().debug("check_flag: flag %d, prev_v=%d, new_v=%d", flag, prev_v, new_v);
+ #endif
+ return true;
+ }
+ return false;
+ }
+
+ void WebSettings::add_flags(uint8_t flags) {
+ flags_ |= flags;
+ }
+
+ bool WebSettings::has_flags(uint8_t flags) {
+ return (flags_ & flags) == flags;
+ }
+
+ void WebSettings::reset_flags() {
+ flags_ = ChangeFlags::NONE;
+ }
+
+ uint8_t WebSettings::get_flags() {
+ return flags_;
+ }
+
+ } // namespace emsesp
+
\ No newline at end of file
diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp
index e5e1225e1..23f4d3486 100644
--- a/src/web/WebStatusService.cpp
+++ b/src/web/WebStatusService.cpp
@@ -216,6 +216,9 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json)
} else if (action == "resetMQTT" && is_admin) {
EMSESP::mqtt_.reset_mqtt();
ok = true;
+ } else if (action == "upgradeImportantMessages") {
+ root["upgradeImportantMessageType"] = upgradeImportantMessages(param);
+ ok = true;
}
#if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY)
@@ -237,13 +240,75 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json)
request->send(response);
}
+// action = upgradeImportantMessages
+// returns the type of upgrade important message to display in the UI
+// 0 = no message (if just a minor version upgrade)
+// 1 = going from <= 3.8 to 3.9 (has new partition layout)
+// 2 = major version upgrade
+// version can be like 3.8.2 or a filename like EMS-ESP-3_8_2-dev_13-ESP32-16MB+.bin
+uint8_t WebStatusService::upgradeImportantMessages(std::string & version) {
+ if (version.empty()) {
+ return 0;
+ }
+
+ // it's a filename with a .bin or .md extension, try and extract the version from it
+ // e.g. EMS-ESP-3_8_2-dev_13-ESP32-16MB+.bin -> major=3 minor=8 patch=2
+ version::Semver200_version latest_version;
+ if ((version.find(".bin") != std::string::npos) || (version.find(".md") != std::string::npos)) {
+ std::string filename = version;
+ auto pos = filename.find("EMS-ESP-");
+ if (pos == std::string::npos) {
+ EMSESP::logger().err("Invalid version string: %s", version.c_str());
+ return 0;
+ }
+
+ pos += 8; // skip past "EMS-ESP-"
+ auto underscore1 = filename.find('_', pos);
+ auto underscore2 = filename.find('_', underscore1 + 1);
+ auto dash = filename.find('-', underscore2 + 1);
+ if (underscore1 == std::string::npos || underscore2 == std::string::npos || dash == std::string::npos) {
+ EMSESP::logger().err("Invalid version string: %s", version.c_str());
+ return 0;
+ }
+
+ std::string major_version = filename.substr(pos, underscore1 - pos);
+ std::string minor_version = filename.substr(underscore1 + 1, underscore2 - underscore1 - 1);
+ std::string patch_version = filename.substr(underscore2 + 1, dash - underscore2 - 1);
+ latest_version = version::Semver200_version(major_version + "." + minor_version + "." + patch_version);
+ } else {
+ // if it's .json file exit
+ if (version.find(".json") != std::string::npos) {
+ return 0;
+ } else {
+ // treat it like a version string like "3.9.0"
+ latest_version = version::Semver200_version(version);
+ }
+ }
+
+ version::Semver200_version current_version(current_version_s); // get current version
+
+ if (latest_version > current_version && current_version.minor() < latest_version.minor()) {
+ return 0; // if it's just a minor version upgrade return 0
+ }
+
+ if ((current_version.major() <= 3 && current_version.minor() <= 8) && (latest_version.major() == 3 && latest_version.minor() == 9)) {
+ return 1; // if moving from below 3.8.x to 3.9.x return 1
+ }
+
+ if (latest_version > current_version && current_version.major() < latest_version.major()) {
+ return 2; // if it's a major version upgrade return 2
+ }
+
+ return 0; // if it's not a valid version upgrade return 0
+}
+
// action = checkUpgrade
// versions holds the latest development version and stable version in one string, comma separated
-bool WebStatusService::checkUpgrade(JsonObject root, std::string & versions) {
- if (!versions.empty()) {
+bool WebStatusService::checkUpgrade(JsonObject root, std::string & version) {
+ if (!version.empty()) {
version::Semver200_version current_version(current_version_s);
- version::Semver200_version latest_dev_version(versions.substr(0, versions.find(',')));
- version::Semver200_version latest_stable_version(versions.substr(versions.find(',') + 1));
+ version::Semver200_version latest_dev_version(version.substr(0, version.find(',')));
+ version::Semver200_version latest_stable_version(version.substr(version.find(',') + 1));
bool dev_upgradeable = latest_dev_version > current_version;
bool stable_upgradeable = latest_stable_version > current_version;
@@ -342,7 +407,7 @@ bool WebStatusService::getCustomSupport(JsonObject root) {
#if defined(EMSESP_STANDALONE)
// dummy test data for "test api3"
- deserializeJson(doc, "{\"type\":\"customSupport\",\"Support\":{\"html\":[\"html code\",\"here\"], \"img_url\": \"https://emsesp.org/_media/images/designer.png\"}");
+ deserializeJson(doc, "{\"type\":\"customSupport\",\"Support\":{\"html\":[\"html code\",\"here\"], \"img_url\": \"https://emsesp.org/media/images/designer.png\"}");
#else
// check if we have custom support file uploaded
File file = LittleFS.open(EMSESP_CUSTOMSUPPORT_FILE, "r");
diff --git a/src/web/WebStatusService.h b/src/web/WebStatusService.h
index 0d38a5b73..8d2c69bfd 100644
--- a/src/web/WebStatusService.h
+++ b/src/web/WebStatusService.h
@@ -30,12 +30,13 @@ class WebStatusService {
SecurityManager * _securityManager;
// actions
- bool checkUpgrade(JsonObject root, std::string & latest_version);
- bool exportData(JsonObject root, std::string & type);
- bool getCustomSupport(JsonObject root);
- bool uploadURL(const char * url);
- bool setSystemStatus(const char * status);
- void allvalues(JsonObject output);
+ bool checkUpgrade(JsonObject root, std::string & latest_version);
+ bool exportData(JsonObject root, std::string & type);
+ bool getCustomSupport(JsonObject root);
+ bool uploadURL(const char * url);
+ bool setSystemStatus(const char * status);
+ void allvalues(JsonObject output);
+ uint8_t upgradeImportantMessages(std::string & version);
std::string current_version_s = EMSESP_APP_VERSION;
};
diff --git a/test/test_data/custom_support.json b/test/test_data/custom_support.json
index b528645a3..6d5b81502 100644
--- a/test/test_data/custom_support.json
+++ b/test/test_data/custom_support.json
@@ -13,6 +13,6 @@
"",
"For help and questions please contact your installer."
],
- "img_url": "https://emsesp.org/_media/images/designer.png"
+ "img_url": "https://emsesp.org/media/images/designer.png"
}
}
\ No newline at end of file
diff --git a/test/test_data/emsesp_allvalues.json b/test/test_data/emsesp_allvalues.json
new file mode 100644
index 000000000..6a8297eb1
--- /dev/null
+++ b/test/test_data/emsesp_allvalues.json
@@ -0,0 +1,103 @@
+{
+ "Boiler Nefit Trendline HRC30 (DeviceID:0x08, ProductID:123, Version:06.01)": {
+ "force heating off": "off",
+ "heating active": "off",
+ "tapwater active": "off",
+ "selected flow temperature": 5,
+ "heating pump modulation": 0,
+ "current flow temperature": 41.4,
+ "return temperature": 37.7,
+ "system pressure": 1.3,
+ "actual boiler temperature": 44.2,
+ "gas": "off",
+ "gas stage 2": "off",
+ "flame current": 0,
+ "fan": "off",
+ "ignition": "off",
+ "oil preheating": "off",
+ "burner min power": 0,
+ "burner max power": 50,
+ "burner min period": 10,
+ "hysteresis on temperature": -6,
+ "hysteresis off temperature": 6,
+ "heating activated": "on",
+ "heating temperature": 70,
+ "heating pump": "off",
+ "boiler pump max power": 70,
+ "boiler pump min power": 50,
+ "boiler pump mode": "proportional",
+ "pump delay": 2,
+ "burner selected max power": 0,
+ "burner current power": 0,
+ "burner starts": 394602,
+ "total burner operating time": "480 days 4 hours 23 minutes",
+ "burner stage 2 operating time": "0 days 0 hours 0 minutes",
+ "total heat operating time": "395 days 2 hours 14 minutes",
+ "burner starts heating": 46245,
+ "total UBA operating time": "3932 days 23 hours 58 minutes",
+ "last error code": "2E(207) 100.75.2000 65:00 (0 min)",
+ "service code": "0H",
+ "service code number": 203,
+ "maintenance message": "H00",
+ "maintenance scheduled": "manual",
+ "time to next maintenance": 6000,
+ "next maintenance date": "01.01.2012",
+ "dhw turn on/off": "on",
+ "dhw set temperature": 62,
+ "dhw selected temperature": 60,
+ "dhw type": "flow",
+ "dhw comfort": "hot",
+ "dhw flow temperature offset": 40,
+ "dhw max power": 100,
+ "dhw circulation pump available": "off",
+ "dhw charging type": "3-way valve",
+ "dhw hysteresis on temperature": -5,
+ "dhw hysteresis off temperature": 0,
+ "dhw disinfection temperature": 70,
+ "dhw circulation pump mode": "off",
+ "dhw circulation active": "off",
+ "dhw current intern temperature": 33.5,
+ "dhw current tap water flow": 0,
+ "dhw storage intern temperature": 33.5,
+ "dhw activated": "on",
+ "dhw one time charging": "off",
+ "dhw disinfecting": "off",
+ "dhw charging": "off",
+ "dhw recharging": "off",
+ "dhw temperature ok": "on",
+ "dhw active": "off",
+ "dhw 3-way valve active": "on",
+ "dhw set pump power": 0,
+ "dhw starts": 348357,
+ "dhw active time": "85 days 2 hours 9 minutes",
+ "nominal Power": 30,
+ "total energy": 3088.69,
+ "energy heating": 2532.94,
+ "dhw energy": 555.75
+ },
+ "Thermostat RC20 (DeviceID:0x17, ProductID:77, Version:03.03)": {
+ "date/time": "10.12.2023 13:49",
+ "hc1 how hot lounge should be": 19,
+ "hc1 current room temp": 19.5,
+ "hc1 mqtt discovery current room temperature": "roomTemp",
+ "hc1 mode": "auto",
+ "hc1 manual temperature": 21.5,
+ "hc1 temperature when mode is off": 7,
+ "hc1 day temperature T2": 20,
+ "hc1 day temperature T3": 20,
+ "hc1 day temperature T4": 20,
+ "hc1 night temperature T1": 15,
+ "hc1 program switchtime": "00 mo 00:00 T1"
+ },
+ "Controller Module BC10 (DeviceID:0x09, ProductID:190, Version:01.03)": {},
+ "Custom Entities": {
+ "boiler_flowtemp": 5,
+ "nominalpower": 30,
+ "minmodulation": 23,
+ "maxmodulation": 115
+ },
+ "Analog Sensors": {},
+ "Temperature Sensors": {
+ "zolder": 18.3
+ }
+}
\ No newline at end of file
diff --git a/test/test_data/emsesp_customizations.json b/test/test_data/emsesp_customizations.json
new file mode 100644
index 000000000..99ba91cef
--- /dev/null
+++ b/test/test_data/emsesp_customizations.json
@@ -0,0 +1,37 @@
+{
+ "type": "customizations",
+ "Customizations": {
+ "ts": [],
+ "as": [],
+ "masked_entities": [
+ {
+ "product_id": 77,
+ "device_id": 23,
+ "custom_name": "",
+ "custom_brand": "",
+ "entity_ids": [
+ "08datetime",
+ "08hc1/seltemp|<30",
+ "08hc1/currtemp",
+ "08hc1/mode"
+ ]
+ },
+ {
+ "product_id": 123,
+ "device_id": 8,
+ "custom_name": "",
+ "custom_brand": "",
+ "entity_ids": [
+ "08heatingactive",
+ "08burngas",
+ "08fanwork",
+ "08ignwork",
+ "08burnmaxpower|>23<121",
+ "08burnminperiod|<120",
+ "08lastcode",
+ "08servicecode"
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/test_data/emsesp_entities.json b/test/test_data/emsesp_entities.json
new file mode 100644
index 000000000..804cc81d4
--- /dev/null
+++ b/test/test_data/emsesp_entities.json
@@ -0,0 +1,6 @@
+{
+ "type": "entities",
+ "Entities": {
+ "entities": []
+ }
+}
\ No newline at end of file
diff --git a/test/test_data/emsesp_schedule.json b/test/test_data/emsesp_schedule.json
new file mode 100644
index 000000000..8674b3959
--- /dev/null
+++ b/test/test_data/emsesp_schedule.json
@@ -0,0 +1,6 @@
+{
+ "type": "schedule",
+ "Schedule": {
+ "schedule": []
+ }
+}
\ No newline at end of file
diff --git a/test/test_data/emsesp_settings.json b/test/test_data/emsesp_settings.json
new file mode 100644
index 000000000..80b8f74d4
--- /dev/null
+++ b/test/test_data/emsesp_settings.json
@@ -0,0 +1,145 @@
+{
+ "type": "settings",
+ "System": {
+ "version": "3.8.2"
+ },
+ "Network": {
+ "ssid": "",
+ "bssid": "",
+ "password": "",
+ "hostname": "ems-esp",
+ "static_ip_config": false,
+ "bandwidth20": false,
+ "nosleep": true,
+ "enableMDNS": true,
+ "enableCORS": false,
+ "CORSOrigin": "*",
+ "tx_power": 0
+ },
+ "AP": {
+ "provision_mode": 2,
+ "ssid": "ems-esp",
+ "password": "ems-esp-neo",
+ "channel": 1,
+ "ssid_hidden": false,
+ "max_clients": 4,
+ "local_ip": "192.168.4.1",
+ "gateway_ip": "192.168.4.1",
+ "subnet_mask": "255.255.255.0"
+ },
+ "MQTT": {
+ "enableTLS": false,
+ "rootCA": "",
+ "enabled": true,
+ "host": "192.168.X.X",
+ "port": 1883,
+ "base": "ems-esp",
+ "username": "xxxx",
+ "password": "xxxx",
+ "client_id": "esp32-395c7bcc",
+ "keep_alive": 60,
+ "clean_session": false,
+ "entity_format": 1,
+ "publish_time_boiler": 60,
+ "publish_time_thermostat": 60,
+ "publish_time_solar": 60,
+ "publish_time_mixer": 60,
+ "publish_time_water": 60,
+ "publish_time_other": 60,
+ "publish_time_sensor": 60,
+ "publish_time_heartbeat": 10,
+ "mqtt_qos": 0,
+ "mqtt_retain": false,
+ "ha_enabled": true,
+ "nested_format": 1,
+ "discovery_prefix": "homeassistant",
+ "discovery_type": 0,
+ "ha_number_mode": 1,
+ "publish_single": false,
+ "publish_single2cmd": false,
+ "send_response": false
+ },
+ "NTP": {
+ "enabled": true,
+ "server": "time.google.com",
+ "tz_label": "Europe/Amsterdam",
+ "tz_format": "CET-1CEST,M3.5.0,M10.5.0/3"
+ },
+ "Security": {
+ "jwt_secret": "ems-esp-neo",
+ "users": [
+ {
+ "username": "admin",
+ "password": "admin",
+ "admin": true
+ },
+ {
+ "username": "guest",
+ "password": "guest",
+ "admin": false
+ }
+ ]
+ },
+ "Settings": {
+ "version": "3.8.2",
+ "board_profile": "E32V2",
+ "platform": "ESP32",
+ "locale": "en",
+ "tx_mode": 1,
+ "ems_bus_id": 11,
+ "syslog_enabled": false,
+ "syslog_level": 3,
+ "trace_raw": false,
+ "syslog_mark_interval": 0,
+ "syslog_host": "",
+ "syslog_port": 514,
+ "boiler_heatingoff": false,
+ "remote_timeout": 24,
+ "remote_timeout_en": false,
+ "shower_timer": true,
+ "shower_alert": false,
+ "shower_alert_coldshot": 10,
+ "shower_alert_trigger": 7,
+ "shower_min_duration": 180,
+ "rx_gpio": 4,
+ "tx_gpio": 5,
+ "dallas_gpio": 14,
+ "dallas_parasite": false,
+ "led_gpio": 2,
+ "hide_led": true,
+ "led_type": 0,
+ "low_clock": false,
+ "telnet_enabled": true,
+ "notoken_api": false,
+ "readonly_mode": false,
+ "analog_enabled": true,
+ "pbutton_gpio": 34,
+ "solar_maxflow": 30,
+ "fahrenheit": false,
+ "bool_format": 1,
+ "bool_dashboard": 1,
+ "enum_format": 1,
+ "weblog_level": 6,
+ "weblog_buffer": 500,
+ "weblog_compact": true,
+ "phy_type": 1,
+ "eth_power": 15,
+ "eth_phy_addr": 0,
+ "eth_clock_mode": 1,
+ "modbus_enabled": false,
+ "modbus_port": 502,
+ "modbus_max_clients": 10,
+ "modbus_timeout": 300,
+ "developer_mode": true,
+ "email_enabled": false,
+ "email_ssl": false,
+ "email_starttls": true,
+ "email_server": "smtp.example.net",
+ "email_port": 587,
+ "email_login": "",
+ "email_pass": "",
+ "email_sender": "ems-esp@example.net",
+ "email_recp": "",
+ "email_subject": "ems-esp notification"
+ }
+}
\ No newline at end of file
diff --git a/test/test_data/emsesp_systembackup.json b/test/test_data/emsesp_systembackup.json
new file mode 100644
index 000000000..3f7b0816c
--- /dev/null
+++ b/test/test_data/emsesp_systembackup.json
@@ -0,0 +1,249 @@
+{
+ "type": "systembackup",
+ "version": "3.8.2",
+ "date": "2026-04-15T08:06:45",
+ "systembackup": [
+ {
+ "type": "settings",
+ "Network": {
+ "ssid": "",
+ "bssid": "",
+ "password": "",
+ "hostname": "ems-esp",
+ "static_ip_config": false,
+ "bandwidth20": false,
+ "nosleep": true,
+ "enableMDNS": true,
+ "enableCORS": false,
+ "CORSOrigin": "*",
+ "tx_power": 0
+ },
+ "AP": {
+ "provision_mode": 2,
+ "ssid": "ems-esp",
+ "password": "ems-esp-neo",
+ "channel": 1,
+ "ssid_hidden": false,
+ "max_clients": 4,
+ "local_ip": "192.168.X.X",
+ "gateway_ip": "192.168.X.X",
+ "subnet_mask": "255.255.255.0"
+ },
+ "MQTT": {
+ "enableTLS": false,
+ "rootCA": "",
+ "enabled": true,
+ "host": "192.168.X.X",
+ "port": 1883,
+ "base": "ems-esp",
+ "username": "xxxx",
+ "password": "xxxx",
+ "client_id": "esp32-395c7bcc",
+ "keep_alive": 60,
+ "clean_session": false,
+ "entity_format": 1,
+ "publish_time_boiler": 60,
+ "publish_time_thermostat": 60,
+ "publish_time_solar": 60,
+ "publish_time_mixer": 60,
+ "publish_time_water": 60,
+ "publish_time_other": 60,
+ "publish_time_sensor": 60,
+ "publish_time_heartbeat": 10,
+ "mqtt_qos": 0,
+ "mqtt_retain": false,
+ "ha_enabled": true,
+ "nested_format": 1,
+ "discovery_prefix": "homeassistant",
+ "discovery_type": 0,
+ "ha_number_mode": 1,
+ "publish_single": false,
+ "publish_single2cmd": false,
+ "send_response": false
+ },
+ "NTP": {
+ "enabled": true,
+ "server": "time.google.com",
+ "tz_label": "Europe/Amsterdam",
+ "tz_format": "CET-1CEST,M3.5.0,M10.5.0/3"
+ },
+ "Security": {
+ "jwt_secret": "ems-esp-neo",
+ "users": [
+ {
+ "username": "admin",
+ "password": "admin",
+ "admin": true
+ },
+ {
+ "username": "guest",
+ "password": "guest",
+ "admin": false
+ }
+ ]
+ },
+ "Settings": {
+ "version": "3.8.2",
+ "board_profile": "E32V2",
+ "platform": "ESP32",
+ "locale": "en",
+ "tx_mode": 1,
+ "ems_bus_id": 11,
+ "syslog_enabled": false,
+ "syslog_level": 3,
+ "trace_raw": false,
+ "syslog_mark_interval": 0,
+ "syslog_host": "",
+ "syslog_port": 514,
+ "boiler_heatingoff": false,
+ "remote_timeout": 24,
+ "remote_timeout_en": false,
+ "shower_timer": true,
+ "shower_alert": false,
+ "shower_alert_coldshot": 10,
+ "shower_alert_trigger": 7,
+ "shower_min_duration": 180,
+ "rx_gpio": 4,
+ "tx_gpio": 5,
+ "dallas_gpio": 14,
+ "dallas_parasite": false,
+ "led_gpio": 2,
+ "hide_led": true,
+ "led_type": 0,
+ "low_clock": false,
+ "telnet_enabled": true,
+ "notoken_api": false,
+ "readonly_mode": false,
+ "analog_enabled": true,
+ "pbutton_gpio": 34,
+ "solar_maxflow": 30,
+ "fahrenheit": false,
+ "bool_format": 1,
+ "bool_dashboard": 1,
+ "enum_format": 1,
+ "weblog_level": 6,
+ "weblog_buffer": 75,
+ "weblog_compact": true,
+ "phy_type": 1,
+ "eth_power": 15,
+ "eth_phy_addr": 0,
+ "eth_clock_mode": 1,
+ "modbus_enabled": false,
+ "modbus_port": 502,
+ "modbus_max_clients": 10,
+ "modbus_timeout": 300,
+ "developer_mode": true
+ }
+ },
+ {
+ "type": "schedule",
+ "Schedule": {
+ "schedule": []
+ }
+ },
+ {
+ "type": "customizations",
+ "Customizations": {
+ "ts": [],
+ "as": [],
+ "masked_entities": [
+ {
+ "product_id": 77,
+ "device_id": 23,
+ "custom_name": "",
+ "custom_brand": "",
+ "entity_ids": [
+ "08datetime",
+ "08hc1/seltemp|<30",
+ "08hc1/currtemp",
+ "08hc1/mode"
+ ]
+ },
+ {
+ "product_id": 123,
+ "device_id": 8,
+ "custom_name": "",
+ "custom_brand": "",
+ "entity_ids": [
+ "08heatingactive",
+ "08burngas",
+ "08fanwork",
+ "08ignwork",
+ "08burnmaxpower|>23<121",
+ "08burnminperiod|<120",
+ "08lastcode",
+ "08servicecode"
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "type": "entities",
+ "Entities": {
+ "entities": []
+ }
+ },
+ {
+ "type": "modules",
+ "Modules": {
+ "modules": []
+ }
+ },
+ {
+ "type": "nvs",
+ "nvs": [
+ {
+ "type": 1,
+ "key": "nompower",
+ "value": 30
+ },
+ {
+ "type": 33,
+ "key": "boot",
+ "value": "3.8.1-dev.4"
+ },
+ {
+ "type": 4,
+ "key": "d_boot",
+ "value": 1767525325
+ },
+ {
+ "type": 33,
+ "key": "app1",
+ "value": "3.8.2-dev.13"
+ },
+ {
+ "type": 33,
+ "key": "app0",
+ "value": "3.8.1"
+ },
+ {
+ "type": 4,
+ "key": "d_app0",
+ "value": 1774988066
+ },
+ {
+ "type": 1,
+ "key": "fresh_firmware",
+ "value": 0
+ },
+ {
+ "type": 4,
+ "key": "d_app1",
+ "value": 1776194060
+ },
+ {
+ "type": 66,
+ "key": "nrgheat",
+ "value": 485871.5481
+ },
+ {
+ "type": 66,
+ "key": "nrgww",
+ "value": 101649.2176
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file