Merge pull request #2744 from proddy/dev

webUI gpio improvements
This commit is contained in:
Proddy
2025-11-15 23:01:50 +01:00
committed by GitHub
33 changed files with 328 additions and 339 deletions

1
.gitignore vendored
View File

@@ -74,3 +74,4 @@ sdkconfig.*
sdkconfig_tasmota_esp32 sdkconfig_tasmota_esp32
pnpm-lock.yaml pnpm-lock.yaml
package.json package.json
.cache/

View File

@@ -67,3 +67,4 @@ For more details go to [docs.emsesp.org](https://docs.emsesp.org/).
- place system message command in side scheduler loop to reduce stack memory usage by 2KB - place system message command in side scheduler loop to reduce stack memory usage by 2KB
- syslog mark interval set to 1 hour - syslog mark interval set to 1 hour
- handle process_telegram in oneloop - handle process_telegram in oneloop
- improved GPIO validation in Analog Sensors and System settings

View File

@@ -17,7 +17,7 @@
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"pnpm:mock-rest\" \"vite preview\"", "preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"pnpm:mock-rest\" \"vite preview\"",
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite dev\"", "standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite dev\"",
"typesafe-i18n": "typesafe-i18n --no-watch", "typesafe-i18n": "typesafe-i18n --no-watch",
"webUI": "vite build && node progmem-generator.js", "build_webUI": "typesafe-i18n --no-watch && vite build && node progmem-generator.js",
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'", "format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
"lint": "eslint . --fix", "lint": "eslint . --fix",
"standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\"" "standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\""
@@ -53,7 +53,7 @@
"@preact/preset-vite": "^2.10.2", "@preact/preset-vite": "^2.10.2",
"@trivago/prettier-plugin-sort-imports": "^6.0.0", "@trivago/prettier-plugin-sort-imports": "^6.0.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.4", "@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"axe-core": "^4.11.0", "axe-core": "^4.11.0",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",

130
interface/pnpm-lock.yaml generated
View File

@@ -13,22 +13,22 @@ importers:
version: 2.2.1(alova@3.3.4) version: 2.2.1(alova@3.3.4)
'@emotion/react': '@emotion/react':
specifier: ^11.14.0 specifier: ^11.14.0
version: 11.14.0(@types/react@19.2.4)(react@19.2.0) version: 11.14.0(@types/react@19.2.5)(react@19.2.0)
'@emotion/styled': '@emotion/styled':
specifier: ^11.14.1 specifier: ^11.14.1
version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)
'@mui/icons-material': '@mui/icons-material':
specifier: ^7.3.5 specifier: ^7.3.5
version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) version: 7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)
'@mui/material': '@mui/material':
specifier: ^7.3.5 specifier: ^7.3.5
version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@preact/compat': '@preact/compat':
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1(preact@10.27.2) version: 18.3.1(preact@10.27.2)
'@table-library/react-table-library': '@table-library/react-table-library':
specifier: 4.1.15 specifier: 4.1.15
version: 4.1.15(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 4.1.15(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
alova: alova:
specifier: 3.3.4 specifier: 3.3.4
version: 3.3.4 version: 3.3.4
@@ -91,11 +91,11 @@ importers:
specifier: ^24.10.1 specifier: ^24.10.1
version: 24.10.1 version: 24.10.1
'@types/react': '@types/react':
specifier: ^19.2.4 specifier: ^19.2.5
version: 19.2.4 version: 19.2.5
'@types/react-dom': '@types/react-dom':
specifier: ^19.2.3 specifier: ^19.2.3
version: 19.2.3(@types/react@19.2.4) version: 19.2.3(@types/react@19.2.5)
axe-core: axe-core:
specifier: ^4.11.0 specifier: ^4.11.0
version: 4.11.0 version: 4.11.0
@@ -879,8 +879,8 @@ packages:
peerDependencies: peerDependencies:
'@types/react': '*' '@types/react': '*'
'@types/react@19.2.4': '@types/react@19.2.5':
resolution: {integrity: sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==} resolution: {integrity: sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==}
'@types/responselike@1.0.3': '@types/responselike@1.0.3':
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
@@ -1225,8 +1225,8 @@ packages:
resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
csstype@3.1.3: csstype@3.2.1:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} resolution: {integrity: sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g==}
currently-unhandled@0.4.1: currently-unhandled@0.4.1:
resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==}
@@ -1337,8 +1337,8 @@ packages:
duplexer3@0.1.5: duplexer3@0.1.5:
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
electron-to-chromium@1.5.250: electron-to-chromium@1.5.253:
resolution: {integrity: sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==} resolution: {integrity: sha512-O0tpQ/35rrgdiGQ0/OFWhy1itmd9A6TY9uQzlqj3hKSu/aYpe7UIn5d7CU2N9myH6biZiWF3VMZVuup8pw5U9w==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -3268,7 +3268,7 @@ snapshots:
'@emotion/memoize@0.9.0': {} '@emotion/memoize@0.9.0': {}
'@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0)': '@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
'@emotion/babel-plugin': 11.13.5 '@emotion/babel-plugin': 11.13.5
@@ -3280,7 +3280,7 @@ snapshots:
hoist-non-react-statics: 3.3.2 hoist-non-react-statics: 3.3.2
react: 19.2.0 react: 19.2.0
optionalDependencies: optionalDependencies:
'@types/react': 19.2.4 '@types/react': 19.2.5
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3290,22 +3290,22 @@ snapshots:
'@emotion/memoize': 0.9.0 '@emotion/memoize': 0.9.0
'@emotion/unitless': 0.10.0 '@emotion/unitless': 0.10.0
'@emotion/utils': 1.4.2 '@emotion/utils': 1.4.2
csstype: 3.1.3 csstype: 3.2.1
'@emotion/sheet@1.4.0': {} '@emotion/sheet@1.4.0': {}
'@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
'@emotion/babel-plugin': 11.13.5 '@emotion/babel-plugin': 11.13.5
'@emotion/is-prop-valid': 1.4.0 '@emotion/is-prop-valid': 1.4.0
'@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0)
'@emotion/serialize': 1.3.3 '@emotion/serialize': 1.3.3
'@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0)
'@emotion/utils': 1.4.2 '@emotion/utils': 1.4.2
react: 19.2.0 react: 19.2.0
optionalDependencies: optionalDependencies:
'@types/react': 19.2.4 '@types/react': 19.2.5
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3489,90 +3489,90 @@ snapshots:
'@mui/core-downloads-tracker@7.3.5': {} '@mui/core-downloads-tracker@7.3.5': {}
'@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': '@mui/icons-material@7.3.5(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
'@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0 react: 19.2.0
optionalDependencies: optionalDependencies:
'@types/react': 19.2.4 '@types/react': 19.2.5
'@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': '@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
'@mui/core-downloads-tracker': 7.3.5 '@mui/core-downloads-tracker': 7.3.5
'@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) '@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)
'@mui/types': 7.4.8(@types/react@19.2.4) '@mui/types': 7.4.8(@types/react@19.2.5)
'@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0)
'@popperjs/core': 2.11.8 '@popperjs/core': 2.11.8
'@types/react-transition-group': 4.4.12(@types/react@19.2.4) '@types/react-transition-group': 4.4.12(@types/react@19.2.5)
clsx: 2.1.1 clsx: 2.1.1
csstype: 3.1.3 csstype: 3.2.1
prop-types: 15.8.1 prop-types: 15.8.1
react: 19.2.0 react: 19.2.0
react-dom: 19.2.0(react@19.2.0) react-dom: 19.2.0(react@19.2.0)
react-is: 19.2.0 react-is: 19.2.0
react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-transition-group: 4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
optionalDependencies: optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)
'@types/react': 19.2.4 '@types/react': 19.2.5
'@mui/private-theming@7.3.5(@types/react@19.2.4)(react@19.2.0)': '@mui/private-theming@7.3.5(@types/react@19.2.5)(react@19.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
'@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0)
prop-types: 15.8.1 prop-types: 15.8.1
react: 19.2.0 react: 19.2.0
optionalDependencies: optionalDependencies:
'@types/react': 19.2.4 '@types/react': 19.2.5
'@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(react@19.2.0)': '@mui/styled-engine@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(react@19.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
'@emotion/cache': 11.14.0 '@emotion/cache': 11.14.0
'@emotion/serialize': 1.3.3 '@emotion/serialize': 1.3.3
'@emotion/sheet': 1.4.0 '@emotion/sheet': 1.4.0
csstype: 3.1.3 csstype: 3.2.1
prop-types: 15.8.1 prop-types: 15.8.1
react: 19.2.0 react: 19.2.0
optionalDependencies: optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)
'@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': '@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
'@mui/private-theming': 7.3.5(@types/react@19.2.4)(react@19.2.0) '@mui/private-theming': 7.3.5(@types/react@19.2.5)(react@19.2.0)
'@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0))(react@19.2.0) '@mui/styled-engine': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0))(react@19.2.0)
'@mui/types': 7.4.8(@types/react@19.2.4) '@mui/types': 7.4.8(@types/react@19.2.5)
'@mui/utils': 7.3.5(@types/react@19.2.4)(react@19.2.0) '@mui/utils': 7.3.5(@types/react@19.2.5)(react@19.2.0)
clsx: 2.1.1 clsx: 2.1.1
csstype: 3.1.3 csstype: 3.2.1
prop-types: 15.8.1 prop-types: 15.8.1
react: 19.2.0 react: 19.2.0
optionalDependencies: optionalDependencies:
'@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(@types/react@19.2.5)(react@19.2.0)
'@types/react': 19.2.4 '@types/react': 19.2.5
'@mui/types@7.4.8(@types/react@19.2.4)': '@mui/types@7.4.8(@types/react@19.2.5)':
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
optionalDependencies: optionalDependencies:
'@types/react': 19.2.4 '@types/react': 19.2.5
'@mui/utils@7.3.5(@types/react@19.2.4)(react@19.2.0)': '@mui/utils@7.3.5(@types/react@19.2.5)(react@19.2.0)':
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
'@mui/types': 7.4.8(@types/react@19.2.4) '@mui/types': 7.4.8(@types/react@19.2.5)
'@types/prop-types': 15.7.15 '@types/prop-types': 15.7.15
clsx: 2.1.1 clsx: 2.1.1
prop-types: 15.8.1 prop-types: 15.8.1
react: 19.2.0 react: 19.2.0
react-is: 19.2.0 react-is: 19.2.0
optionalDependencies: optionalDependencies:
'@types/react': 19.2.4 '@types/react': 19.2.5
'@noble/hashes@1.8.0': {} '@noble/hashes@1.8.0': {}
@@ -3707,9 +3707,9 @@ snapshots:
'@sindresorhus/is@0.7.0': {} '@sindresorhus/is@0.7.0': {}
'@table-library/react-table-library@4.1.15(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': '@table-library/react-table-library@4.1.15(@emotion/react@11.14.0(@types/react@19.2.5)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies: dependencies:
'@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) '@emotion/react': 11.14.0(@types/react@19.2.5)(react@19.2.0)
clsx: 1.1.1 clsx: 1.1.1
react: 19.2.0 react: 19.2.0
react-dom: 19.2.0(react@19.2.0) react-dom: 19.2.0(react@19.2.0)
@@ -3786,17 +3786,17 @@ snapshots:
'@types/prop-types@15.7.15': {} '@types/prop-types@15.7.15': {}
'@types/react-dom@19.2.3(@types/react@19.2.4)': '@types/react-dom@19.2.3(@types/react@19.2.5)':
dependencies: dependencies:
'@types/react': 19.2.4 '@types/react': 19.2.5
'@types/react-transition-group@4.4.12(@types/react@19.2.4)': '@types/react-transition-group@4.4.12(@types/react@19.2.5)':
dependencies: dependencies:
'@types/react': 19.2.4 '@types/react': 19.2.5
'@types/react@19.2.4': '@types/react@19.2.5':
dependencies: dependencies:
csstype: 3.1.3 csstype: 3.2.1
'@types/responselike@1.0.3': '@types/responselike@1.0.3':
dependencies: dependencies:
@@ -4022,7 +4022,7 @@ snapshots:
dependencies: dependencies:
baseline-browser-mapping: 2.8.28 baseline-browser-mapping: 2.8.28
caniuse-lite: 1.0.30001754 caniuse-lite: 1.0.30001754
electron-to-chromium: 1.5.250 electron-to-chromium: 1.5.253
node-releases: 2.0.27 node-releases: 2.0.27
update-browserslist-db: 1.1.4(browserslist@4.28.0) update-browserslist-db: 1.1.4(browserslist@4.28.0)
@@ -4211,7 +4211,7 @@ snapshots:
dependencies: dependencies:
css-tree: 1.1.3 css-tree: 1.1.3
csstype@3.1.3: {} csstype@3.2.1: {}
currently-unhandled@0.4.1: currently-unhandled@0.4.1:
dependencies: dependencies:
@@ -4294,7 +4294,7 @@ snapshots:
dom-helpers@5.2.1: dom-helpers@5.2.1:
dependencies: dependencies:
'@babel/runtime': 7.28.4 '@babel/runtime': 7.28.4
csstype: 3.1.3 csstype: 3.2.1
dom-serializer@1.4.1: dom-serializer@1.4.1:
dependencies: dependencies:
@@ -4367,7 +4367,7 @@ snapshots:
duplexer3@0.1.5: {} duplexer3@0.1.5: {}
electron-to-chromium@1.5.250: {} electron-to-chromium@1.5.253: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}

View File

@@ -54,7 +54,7 @@ const MS_PER_SECOND = 1000;
const MS_PER_MINUTE = 60 * MS_PER_SECOND; const MS_PER_MINUTE = 60 * MS_PER_SECOND;
const MS_PER_HOUR = 60 * MS_PER_MINUTE; const MS_PER_HOUR = 60 * MS_PER_MINUTE;
const MS_PER_DAY = 24 * MS_PER_HOUR; const MS_PER_DAY = 24 * MS_PER_HOUR;
const DEFAULT_GPIO = 21; // Safe GPIO for all platforms const DEFAULT_GPIO = 99; // not set
const MIN_TEMP_ID = -100; const MIN_TEMP_ID = -100;
const MAX_TEMP_ID = 100; const MAX_TEMP_ID = 100;
const GPIO_25 = 25; const GPIO_25 = 25;
@@ -134,6 +134,7 @@ const Sensors = () => {
ts: [], ts: [],
as: [], as: [],
analog_enabled: false, analog_enabled: false,
valid_gpio_list: [],
platform: 'ESP32' platform: 'ESP32'
} }
}); });
@@ -573,12 +574,8 @@ const Sensors = () => {
onSave={onAnalogDialogSave} onSave={onAnalogDialogSave}
creating={creating} creating={creating}
selectedItem={selectedAnalogSensor} selectedItem={selectedAnalogSensor}
validator={analogSensorItemValidation( analogGPIOList={sensorData.valid_gpio_list}
sensorData.as, validator={analogSensorItemValidation(sensorData.as, selectedAnalogSensor)}
selectedAnalogSensor,
creating,
sensorData.platform
)}
/> />
)} )}
{sensorData?.analog_enabled === true && me.admin && ( {sensorData?.analog_enabled === true && me.admin && (

View File

@@ -35,6 +35,7 @@ interface DashboardSensorsAnalogDialogProps {
onSave: (as: AnalogSensor) => void; onSave: (as: AnalogSensor) => void;
creating: boolean; creating: boolean;
selectedItem: AnalogSensor; selectedItem: AnalogSensor;
analogGPIOList: number[];
validator: Schema; validator: Schema;
} }
@@ -44,6 +45,7 @@ const SensorsAnalogDialog = ({
onSave, onSave,
creating, creating,
selectedItem, selectedItem,
analogGPIOList,
validator validator
}: DashboardSensorsAnalogDialogProps) => { }: DashboardSensorsAnalogDialogProps) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -151,33 +153,51 @@ const SensorsAnalogDialog = ({
[creating, LL] [creating, LL]
); );
// Ensure the current GPIO is in the list when no creating
// note GPIO 99 means not set
const availableGPIOs = useMemo(() => {
const filteredList = analogGPIOList.filter((gpio) => gpio !== 99);
if (
editItem.g !== undefined &&
editItem.g !== 99 &&
!filteredList.includes(editItem.g)
) {
return [...filteredList, editItem.g].sort((a, b) => a - b);
}
return filteredList;
}, [analogGPIOList, editItem.g]);
return ( return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}> <Dialog sx={dialogStyle} open={open} onClose={handleClose}>
<DialogTitle>{dialogTitle}</DialogTitle> <DialogTitle>{dialogTitle}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid> {editItem.s ? (
<ValidatedTextField <TextField
fieldErrors={fieldErrors || {}}
name="g" name="g"
label="GPIO" label="GPIO"
sx={{ width: '11ch' }} value={editItem.g}
value={numberValue(editItem.g)} sx={{ width: '8ch' }}
type="number" disabled={true}
variant="outlined" ></TextField>
) : (
<ValidatedTextField
name="g"
label="GPIO"
value={editItem.g}
sx={{ width: '8ch' }}
select
onChange={updateFormValue} onChange={updateFormValue}
/> >
</Grid> {availableGPIOs?.map((gpio: number) => (
{creating && ( <MenuItem key={gpio} value={gpio}>
<Grid> {gpio}
<Box color="warning.main" mt={2}> </MenuItem>
<Typography variant="body2">{LL.WARN_GPIO()}</Typography> ))}
</Box> </ValidatedTextField>
</Grid>
)} )}
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}}
name="n" name="n"
label={LL.NAME(0)} label={LL.NAME(0)}
value={editItem.n} value={editItem.n}
@@ -194,6 +214,7 @@ const SensorsAnalogDialog = ({
fullWidth fullWidth
select select
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
> >
{analogTypeMenuItems} {analogTypeMenuItems}
</TextField> </TextField>
@@ -207,6 +228,7 @@ const SensorsAnalogDialog = ({
sx={{ width: '15ch' }} sx={{ width: '15ch' }}
select select
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
> >
{uomMenuItems} {uomMenuItems}
</TextField> </TextField>
@@ -222,6 +244,7 @@ const SensorsAnalogDialog = ({
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
slotProps={{ slotProps={{
input: { input: {
startAdornment: ( startAdornment: (
@@ -243,6 +266,7 @@ const SensorsAnalogDialog = ({
type="number" type="number"
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
slotProps={{ slotProps={{
input: { input: {
startAdornment: ( startAdornment: (
@@ -264,6 +288,7 @@ const SensorsAnalogDialog = ({
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
slotProps={{ slotProps={{
htmlInput: { step: '0.001' } htmlInput: { step: '0.001' }
}} }}
@@ -280,6 +305,7 @@ const SensorsAnalogDialog = ({
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
/> />
</Grid> </Grid>
)} )}
@@ -293,6 +319,7 @@ const SensorsAnalogDialog = ({
type="number" type="number"
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
slotProps={{ slotProps={{
htmlInput: { step: '0.001' } htmlInput: { step: '0.001' }
}} }}
@@ -309,6 +336,7 @@ const SensorsAnalogDialog = ({
type="number" type="number"
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
slotProps={{ slotProps={{
htmlInput: { min: '0', max: '255', step: '1' } htmlInput: { min: '0', max: '255', step: '1' }
}} }}
@@ -325,6 +353,7 @@ const SensorsAnalogDialog = ({
select select
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
> >
<MenuItem value={0}>{LL.OFF()}</MenuItem> <MenuItem value={0}>{LL.OFF()}</MenuItem>
<MenuItem value={1}>{LL.ON()}</MenuItem> <MenuItem value={1}>{LL.ON()}</MenuItem>
@@ -338,6 +367,7 @@ const SensorsAnalogDialog = ({
sx={{ width: '15ch' }} sx={{ width: '15ch' }}
select select
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
> >
<MenuItem value={1}>{LL.ACTIVEHIGH()}</MenuItem> <MenuItem value={1}>{LL.ACTIVEHIGH()}</MenuItem>
<MenuItem value={-1}>{LL.ACTIVELOW()}</MenuItem> <MenuItem value={-1}>{LL.ACTIVELOW()}</MenuItem>
@@ -351,6 +381,7 @@ const SensorsAnalogDialog = ({
value={editItem.u} value={editItem.u}
select select
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
> >
<MenuItem value={0}>{LL.UNCHANGED()}</MenuItem> <MenuItem value={0}>{LL.UNCHANGED()}</MenuItem>
<MenuItem value={1}> <MenuItem value={1}>
@@ -374,6 +405,7 @@ const SensorsAnalogDialog = ({
variant="outlined" variant="outlined"
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
slotProps={{ slotProps={{
input: { input: {
startAdornment: ( startAdornment: (
@@ -393,6 +425,7 @@ const SensorsAnalogDialog = ({
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
slotProps={{ slotProps={{
input: { input: {
startAdornment: ( startAdornment: (
@@ -415,6 +448,7 @@ const SensorsAnalogDialog = ({
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
select select
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
> >
<MenuItem value={0}>{LL.ACTIVEHIGH()}</MenuItem> <MenuItem value={0}>{LL.ACTIVEHIGH()}</MenuItem>
<MenuItem value={1}>{LL.ACTIVELOW()}</MenuItem> <MenuItem value={1}>{LL.ACTIVELOW()}</MenuItem>
@@ -429,6 +463,7 @@ const SensorsAnalogDialog = ({
sx={{ width: '15ch' }} sx={{ width: '15ch' }}
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
disabled={editItem.s}
slotProps={{ slotProps={{
input: { input: {
startAdornment: ( startAdornment: (
@@ -442,6 +477,24 @@ const SensorsAnalogDialog = ({
</> </>
)} )}
</Grid> </Grid>
{fieldErrors && Object.keys(fieldErrors).length > 0 && (
<Box mt={1}>
{Object.values(fieldErrors).map((errArr, idx) =>
Array.isArray(errArr)
? errArr.map((err, j) => (
<Typography
key={`${idx}-${j}`}
color="error"
variant="caption"
display="block"
>
{err.message}
</Typography>
))
: null
)}
</Box>
)}
{editItem.s && ( {editItem.s && (
<Grid> <Grid>
<Typography mt={1} color="warning.main" variant="body2"> <Typography mt={1} color="warning.main" variant="body2">

View File

@@ -111,6 +111,7 @@ export interface SensorData {
ts: TemperatureSensor[]; ts: TemperatureSensor[];
as: AnalogSensor[]; as: AnalogSensor[];
analog_enabled: boolean; analog_enabled: boolean;
valid_gpio_list: number[];
platform: string; platform: string;
} }

View File

@@ -45,121 +45,15 @@ const VALIDATION_LIMITS = {
HEX_BASE: 16 HEX_BASE: 16
} as const; } as const;
// Helper to create GPIO validator from invalid ranges
const createGPIOValidator = (
invalidRanges: Array<number | [number, number]>,
maxValue: number
) => ({
validator(
_rule: InternalRuleItem,
value: number,
callback: (error?: string) => void
) {
if (!value) {
callback();
return;
}
if (value < 0 || value > maxValue) {
callback(ERROR_MESSAGES.GPIO_INVALID);
return;
}
for (const range of invalidRanges) {
if (typeof range === 'number') {
if (value === range) {
callback(ERROR_MESSAGES.GPIO_INVALID);
return;
}
} else {
const [start, end] = range;
if (value >= start && value <= end) {
callback(ERROR_MESSAGES.GPIO_INVALID);
return;
}
}
}
callback();
}
});
export const GPIO_VALIDATOR = createGPIOValidator(
[[6, 11], 1, 20, 24, [28, 31]],
40
);
export const GPIO_VALIDATORC3 = createGPIOValidator([[11, 19]], 21);
export const GPIO_VALIDATORS2 = createGPIOValidator(
[
[19, 20],
[22, 32]
],
40
);
export const GPIO_VALIDATORS3 = createGPIOValidator(
[
[19, 20],
[22, 37],
[39, 42]
],
48
);
const GPIO_FIELD_NAMES = [
'led_gpio',
'dallas_gpio',
'pbutton_gpio',
'tx_gpio',
'rx_gpio'
] as const;
type ValidationRules = Array<{ type ValidationRules = Array<{
required?: boolean; required?: boolean;
message?: string; message?: string;
[key: string]: unknown; [key: string]: unknown;
}>; }>;
const createGPIOValidations = (
validator: typeof GPIO_VALIDATOR
): Record<string, ValidationRules> =>
GPIO_FIELD_NAMES.reduce(
(acc, field) => {
const fieldName = field.replace('_gpio', '').toUpperCase();
acc[field] = [
{ required: true, message: `${fieldName} GPIO is required` },
validator
];
return acc;
},
{} as Record<string, ValidationRules>
);
const PLATFORM_VALIDATORS = {
ESP32: GPIO_VALIDATOR,
ESP32C3: GPIO_VALIDATORC3,
ESP32S2: GPIO_VALIDATORS2,
ESP32S3: GPIO_VALIDATORS3
} as const;
export const createSettingsValidator = (settings: Settings) => { export const createSettingsValidator = (settings: Settings) => {
const schema: Record<string, ValidationRules> = {}; const schema: Record<string, ValidationRules> = {};
// Add GPIO validations for CUSTOM board profiles
if (
settings.board_profile === 'CUSTOM' &&
settings.platform in PLATFORM_VALIDATORS
) {
Object.assign(
schema,
createGPIOValidations(
PLATFORM_VALIDATORS[settings.platform as keyof typeof PLATFORM_VALIDATORS]
)
);
}
// Syslog validations // Syslog validations
if (settings.syslog_enabled) { if (settings.syslog_enabled) {
schema.syslog_host = [ schema.syslog_host = [
@@ -401,52 +295,29 @@ export const temperatureSensorItemValidation = (
n: [NAME_PATTERN, uniqueTemperatureNameValidator(sensors, sensor.o_n)] n: [NAME_PATTERN, uniqueTemperatureNameValidator(sensors, sensor.o_n)]
}); });
export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({
validator(
_rule: InternalRuleItem,
gpio: number,
callback: (error?: string) => void
) {
if (sensors.some((as) => as.g === gpio)) {
callback(ERROR_MESSAGES.GPIO_DUPLICATE);
return;
}
callback();
}
});
export const uniqueAnalogNameValidator = ( export const uniqueAnalogNameValidator = (
sensors: AnalogSensor[], sensors: AnalogSensor[],
o_name?: string o_name?: string
) => createUniqueFieldNameValidator(sensors, (s) => s.n, o_name); ) => createUniqueFieldNameValidator(sensors, (s) => s.n, o_name);
const getPlatformGPIOValidator = (platform: string) => {
switch (platform) {
case 'ESP32S3':
return GPIO_VALIDATORS3;
case 'ESP32S2':
return GPIO_VALIDATORS2;
case 'ESP32C3':
return GPIO_VALIDATORC3;
default:
return GPIO_VALIDATOR;
}
};
export const analogSensorItemValidation = ( export const analogSensorItemValidation = (
sensors: AnalogSensor[], sensors: AnalogSensor[],
sensor: AnalogSensor, sensor: AnalogSensor
creating: boolean,
platform: string
) => { ) => {
const gpioValidator = getPlatformGPIOValidator(platform);
return new Schema({ return new Schema({
n: [NAME_PATTERN, uniqueAnalogNameValidator(sensors, sensor.o_n)], // name is required and must be unique
n: [
{ required: true, message: 'Name is required' },
NAME_PATTERN,
uniqueAnalogNameValidator(sensors, sensor.o_n)
],
g: [ g: [
{ required: true, message: 'GPIO is required' }, {
gpioValidator, required: true,
...(creating ? [isGPIOUniqueValidator(sensors)] : []) type: 'number',
min: 1,
message: 'GPIO is required'
}
] ]
}); });
}; };

View File

@@ -60,7 +60,6 @@ const cz: Translation = {
DUTY_CYCLE: 'Pracovní cyklus', DUTY_CYCLE: 'Pracovní cyklus',
UNIT: 'Jednotka', UNIT: 'Jednotka',
STARTVALUE: 'Počáteční hodnota', STARTVALUE: 'Počáteční hodnota',
WARN_GPIO: 'Upozornění: buďte opatrní při přiřazování GPIO!',
EDIT: 'Upravit', EDIT: 'Upravit',
SENSOR: 'Senzor', SENSOR: 'Senzor',
TEMP_SENSOR: 'Teplotní senzor', TEMP_SENSOR: 'Teplotní senzor',

View File

@@ -60,7 +60,6 @@ const de: Translation = {
DUTY_CYCLE: 'Arbeitszyklus', DUTY_CYCLE: 'Arbeitszyklus',
UNIT: 'Maßeinheit', UNIT: 'Maßeinheit',
STARTVALUE: 'Startwert', STARTVALUE: 'Startwert',
WARN_GPIO: 'Warnung: Vorsicht bei der korrekten Wahl des GPIO!',
EDIT: 'Editiere', EDIT: 'Editiere',
SENSOR: 'Sensor', SENSOR: 'Sensor',
TEMP_SENSOR: 'Temperatursensor', TEMP_SENSOR: 'Temperatursensor',

View File

@@ -60,7 +60,6 @@ const en: Translation = {
DUTY_CYCLE: 'Duty Cycle', DUTY_CYCLE: 'Duty Cycle',
UNIT: 'UoM', UNIT: 'UoM',
STARTVALUE: 'Start Value', STARTVALUE: 'Start Value',
WARN_GPIO: 'Warning: be careful when assigning a GPIO!',
EDIT: 'Edit', EDIT: 'Edit',
SENSOR: 'Sensor', SENSOR: 'Sensor',
TEMP_SENSOR: 'Temperature Sensor', TEMP_SENSOR: 'Temperature Sensor',

View File

@@ -60,7 +60,6 @@ const fr: Translation = {
DUTY_CYCLE: 'Cycle de fonctionnement', DUTY_CYCLE: 'Cycle de fonctionnement',
UNIT: 'Unité', UNIT: 'Unité',
STARTVALUE: 'Valeur de départ', STARTVALUE: 'Valeur de départ',
WARN_GPIO: 'Attention: soyez vigilant en choisissant un GPIO!',
EDIT: 'Éditer', EDIT: 'Éditer',
SENSOR: 'Capteur', SENSOR: 'Capteur',
TEMP_SENSOR: 'Capteur de température', TEMP_SENSOR: 'Capteur de température',

View File

@@ -60,7 +60,6 @@ const it: Translation = {
DUTY_CYCLE: 'Ciclo di lavoro', DUTY_CYCLE: 'Ciclo di lavoro',
UNIT: 'UoM', UNIT: 'UoM',
STARTVALUE: 'Valore di partenza', STARTVALUE: 'Valore di partenza',
WARN_GPIO: 'Avvertimento: prestare attenzione quando si assegna un GPIO!',
EDIT: 'Modifica', EDIT: 'Modifica',
SENSOR: 'Sensore', SENSOR: 'Sensore',
TEMP_SENSOR: 'Sensore Temperatura', TEMP_SENSOR: 'Sensore Temperatura',

View File

@@ -60,7 +60,6 @@ const nl: Translation = {
DUTY_CYCLE: 'Duty Cycle', DUTY_CYCLE: 'Duty Cycle',
UNIT: 'UoM', UNIT: 'UoM',
STARTVALUE: 'Startwaarde', STARTVALUE: 'Startwaarde',
WARN_GPIO: 'Waarschuwing: let op met het koppelen van de juiste GPIO pin!',
EDIT: 'Wijzigen', EDIT: 'Wijzigen',
SENSOR: 'Sensor', SENSOR: 'Sensor',
TEMP_SENSOR: 'Temperatuur sensor', TEMP_SENSOR: 'Temperatuur sensor',

View File

@@ -60,7 +60,6 @@ const no: Translation = {
DUTY_CYCLE: 'Duty Cycle', DUTY_CYCLE: 'Duty Cycle',
UNIT: 'UoM', UNIT: 'UoM',
STARTVALUE: 'Startverdi', STARTVALUE: 'Startverdi',
WARN_GPIO: 'Advarsel: vær forsiktig ved aktivering av GPIO!',
EDIT: 'Endre', EDIT: 'Endre',
SENSOR: 'Sensor', SENSOR: 'Sensor',
TEMP_SENSOR: 'Temperatursensor', TEMP_SENSOR: 'Temperatursensor',

View File

@@ -60,7 +60,6 @@ const pl: BaseTranslation = {
DUTY_CYCLE: 'Wypełnienie', DUTY_CYCLE: 'Wypełnienie',
UNIT: 'J.m.', UNIT: 'J.m.',
STARTVALUE: 'Wartość początkowa', STARTVALUE: 'Wartość początkowa',
WARN_GPIO: 'Uwaga! Zachowaj ostrożność przypisując GPIO do urządzenia!',
EDIT: 'Edycja', EDIT: 'Edycja',
SENSOR: '{{c|ustawienia c||ustawień c|}}zujnika', SENSOR: '{{c|ustawienia c||ustawień c|}}zujnika',
TEMP_SENSOR: 'czujnika temperatury', TEMP_SENSOR: 'czujnika temperatury',

View File

@@ -60,7 +60,6 @@ const sk: Translation = {
DUTY_CYCLE: 'Pracovný cyklus', DUTY_CYCLE: 'Pracovný cyklus',
UNIT: 'UoM', UNIT: 'UoM',
STARTVALUE: 'Počiatočná hodnota', STARTVALUE: 'Počiatočná hodnota',
WARN_GPIO: 'Upozornenie: Buďte opatrní pri priraďovaní GPIO!',
EDIT: 'Editovať', EDIT: 'Editovať',
SENSOR: 'Snímač', SENSOR: 'Snímač',
TEMP_SENSOR: 'Snímač teploty', TEMP_SENSOR: 'Snímač teploty',

View File

@@ -60,7 +60,6 @@ const sv: Translation = {
DUTY_CYCLE: 'Pulskvot', DUTY_CYCLE: 'Pulskvot',
UNIT: 'Måttenhet', UNIT: 'Måttenhet',
STARTVALUE: 'Startvärde', STARTVALUE: 'Startvärde',
WARN_GPIO: 'Varning: Var försiktig vid aktivering av GPIO!',
EDIT: 'Ändra', EDIT: 'Ändra',
SENSOR: 'Sensor', SENSOR: 'Sensor',
TEMP_SENSOR: 'Temperatursensor', TEMP_SENSOR: 'Temperatursensor',

View File

@@ -60,7 +60,6 @@ const tr: Translation = {
DUTY_CYCLE: 'Görev Çevrimi', DUTY_CYCLE: 'Görev Çevrimi',
UNIT: 'ÖB', UNIT: 'ÖB',
STARTVALUE: 'Başlangıç değeri', STARTVALUE: 'Başlangıç değeri',
WARN_GPIO: 'Uyarı: bir GPIO atarken dikkatli olun!',
EDIT: 'Değiştir', EDIT: 'Değiştir',
SENSOR: 'Sensör', SENSOR: 'Sensör',
TEMP_SENSOR: 'Sıcaklık Sensörü', TEMP_SENSOR: 'Sıcaklık Sensörü',

View File

@@ -46,15 +46,15 @@ type UpdateEntity<S> = (state: (prevState: Readonly<S>) => S) => void;
*/ */
export const updateValue = export const updateValue =
<S extends Record<string, unknown>>(updateEntity: UpdateEntity<S>) => <S extends Record<string, unknown>>(updateEntity: UpdateEntity<S>) =>
(event: React.ChangeEvent<HTMLInputElement>): void => { (event: React.ChangeEvent<HTMLInputElement>): void => {
const { name } = event.target; const { name } = event.target;
const value = extractEventValue(event); const value = extractEventValue(event);
updateEntity((prevState) => ({ updateEntity((prevState) => ({
...prevState, ...prevState,
[name]: value [name]: value
})); }));
}; };
/** /**
* Creates an event handler that tracks dirty flags for modified fields. * Creates an event handler that tracks dirty flags for modified fields.
@@ -67,22 +67,22 @@ export const updateValueDirty =
setDirtyFlags: React.Dispatch<React.SetStateAction<string[]>>, setDirtyFlags: React.Dispatch<React.SetStateAction<string[]>>,
updateDataValue: (updater: (prevState: T) => T) => void updateDataValue: (updater: (prevState: T) => T) => void
) => ) =>
(event: React.ChangeEvent<HTMLInputElement>): void => { (event: React.ChangeEvent<HTMLInputElement>): void => {
const { name } = event.target; const { name } = event.target;
const updatedValue = extractEventValue(event); const updatedValue = extractEventValue(event);
updateDataValue((prevState) => ({ updateDataValue((prevState) => ({
...prevState, ...prevState,
[name]: updatedValue [name]: updatedValue
})); }));
const isDirty = origData[name] !== updatedValue; const isDirty = origData[name] !== updatedValue;
const wasDirty = dirtyFlags.includes(name); const wasDirty = dirtyFlags.includes(name);
// Only update dirty flags if the state changed // Only update dirty flags if the state changed
if (isDirty !== wasDirty) { if (isDirty !== wasDirty) {
setDirtyFlags( setDirtyFlags(
isDirty ? [...dirtyFlags, name] : dirtyFlags.filter((f) => f !== name) isDirty ? [...dirtyFlags, name] : dirtyFlags.filter((f) => f !== name)
); );
} }
}; };

View File

@@ -988,7 +988,7 @@ const emsesp_sensordata = {
{ {
id: 2, id: 2,
g: 37, g: 37,
n: 'External switch', n: 'External_switch',
v: 13, v: 13,
u: 0, u: 0,
o: 17, o: 17,
@@ -999,8 +999,8 @@ const emsesp_sensordata = {
}, },
{ {
id: 3, id: 3,
g: 39, g: 37,
n: 'Pulse count', n: 'Pulse_count',
v: 144, v: 144,
u: 0, u: 0,
o: 0, o: 0,
@@ -1011,7 +1011,7 @@ const emsesp_sensordata = {
}, },
{ {
id: 4, id: 4,
g: 40, g: 23,
n: 'Pressure', n: 'Pressure',
v: 16, v: 16,
u: 17, u: 17,
@@ -1046,7 +1046,8 @@ const emsesp_sensordata = {
s: true s: true
} }
], ],
analog_enabled: true analog_enabled: true,
valid_gpio_list: [0, 2, 5, 12, 13, 15, 18, 19, 23, 25, 26, 27, 33, 37, 38]
}; };
const activity = { const activity = {

View File

@@ -1444,3 +1444,4 @@ proplow
chimneysweeper chimneysweeper
pumpopt pumpopt
intergral intergral
vchip

View File

@@ -74,9 +74,7 @@ def buildWeb():
# Run pnpm commands in the interface directory # Run pnpm commands in the interface directory
commands = [ commands = [
f"{pnpm_exe} install", f"{pnpm_exe} install",
f"{pnpm_exe} typesafe-i18n", f"{pnpm_exe} build_webUI"
f"{pnpm_exe} build",
f"{pnpm_exe} webUI"
] ]
for command in commands: for command in commands:
@@ -93,7 +91,8 @@ def buildWeb():
w.write(text) w.write(text)
print("Setting WebUI locale to 'en'") print("Setting WebUI locale to 'en'")
else: else:
print(f"Warning: {i18n_file} not found, skipping locale modification") print(
f"Warning: {i18n_file} not found, skipping locale modification")
print("Web interface build completed successfully!") print("Web interface build completed successfully!")
return True return True
@@ -110,6 +109,7 @@ def build_webUI(*args, **kwargs):
env.Exit(1) env.Exit(1)
env.Exit(0) env.Exit(0)
# Create custom target that only runs the script and then exits, without continuing with the pio workflow # Create custom target that only runs the script and then exits, without continuing with the pio workflow
env.AddCustomTarget( env.AddCustomTarget(
name="build", name="build",
@@ -119,4 +119,3 @@ env.AddCustomTarget(
description="installs pnpm packages, updates libraries and builds web UI", description="installs pnpm packages, updates libraries and builds web UI",
always_build=True always_build=True
) )

View File

@@ -20,7 +20,7 @@ pnpm format
cd .. cd ..
cd interface cd interface
pnpm webUI pnpm build_webUI
cd .. cd ..
npx cspell "**" npx cspell "**"

View File

@@ -16,7 +16,6 @@
#include <Arduino.h> #include <Arduino.h>
#include <AsyncJson.h> #include <AsyncJson.h>
#include <AsyncMessagePack.h>
#include <AsyncTCP.h> #include <AsyncTCP.h>
#include <WiFi.h> #include <WiFi.h>

View File

@@ -187,8 +187,8 @@ void AnalogSensor::reload(bool get_nvs) {
for (auto & sensor : sensors_) { for (auto & sensor : sensors_) {
sensor.ha_registered = false; // force HA configs to be re-created sensor.ha_registered = false; // force HA configs to be re-created
// first check if the GPIO is valid. If not, force set it to disabled // first check if the GPIO is valid. If not, force set the sensor to disabled, but don't remove it
if (!System::is_valid_gpio(sensor.gpio())) { if (!EMSESP::system_.is_valid_gpio(sensor.gpio())) {
LOG_WARNING("Bad GPIO %d for Sensor %s. Disabling.", sensor.gpio(), sensor.name().c_str()); LOG_WARNING("Bad GPIO %d for Sensor %s. Disabling.", sensor.gpio(), sensor.name().c_str());
sensor.set_type(AnalogType::NOTUSED); // set disabled sensor.set_type(AnalogType::NOTUSED); // set disabled
continue; // skip this loop pass continue; // skip this loop pass
@@ -542,7 +542,7 @@ bool AnalogSensor::update(uint8_t gpio, std::string & name, double offset, doubl
// return false if it's an invalid GPIO, an error will show in WebUI // return false if it's an invalid GPIO, an error will show in WebUI
// and reported as an error in the log // and reported as an error in the log
return System::is_valid_gpio(gpio); return EMSESP::system_.is_valid_gpio(gpio);
} }
// check to see if values have been updated // check to see if values have been updated

View File

@@ -255,7 +255,7 @@ void EMSESP::uart_init() {
EMSuart::stop(); EMSuart::stop();
// don't start UART if we have invalid GPIOs // don't start UART if we have invalid GPIOs
if (System::is_valid_gpio(rx_gpio) && System::is_valid_gpio(tx_gpio)) { if (EMSESP::system_.is_valid_gpio(rx_gpio) && EMSESP::system_.is_valid_gpio(tx_gpio)) {
EMSuart::start(tx_mode, rx_gpio, tx_gpio); // start UART EMSuart::start(tx_mode, rx_gpio, tx_gpio); // start UART
} else { } else {
LOG_WARNING("Invalid UART Rx/Tx GPIOs. Check config."); LOG_WARNING("Invalid UART Rx/Tx GPIOs. Check config.");

View File

@@ -19,12 +19,13 @@
#include "system.h" #include "system.h"
#include "emsesp.h" // for send_raw_telegram() command #include "emsesp.h" // for send_raw_telegram() command
#include "shuntingYard.h"
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
#include "esp_ota_ops.h" #include "esp_ota_ops.h"
#endif #endif
#include <HTTPClient.h>
#include <semver200.h> #include <semver200.h>
#if defined(EMSESP_TEST) #if defined(EMSESP_TEST)
@@ -448,31 +449,9 @@ void System::reload_settings() {
}); });
} }
// check for valid ESP32 pins. This is very dependent on which ESP32 board is being used. // check for valid ESP32 pins
// Typically you can't use 1, 6-11, 20, 24, 28-31 and 40+ bool System::is_valid_gpio(uint8_t pin) {
// we allow 0 as it has a special function on the NodeMCU apparently return std::find(valid_gpio_list().begin(), valid_gpio_list().end(), pin) != valid_gpio_list().end();
// See https://diyprojects.io/esp32-how-to-use-gpio-digital-io-arduino-code/#.YFpVEq9KhjG
// and https://nodemcu.readthedocs.io/en/dev-esp32/modules/gpio/
bool System::is_valid_gpio(uint8_t pin, bool has_psram) {
#if CONFIG_IDF_TARGET_ESP32 || defined(EMSESP_STANDALONE)
if ((pin == 1) || (pin >= 6 && pin <= 11) || (pin == 20) || (pin == 24) || (pin >= 28 && pin <= 31) || (pin > 40)
|| ((EMSESP::system_.PSram() > 0 || has_psram) && pin >= 16 && pin <= 17)) {
#elif CONFIG_IDF_TARGET_ESP32S2
if ((pin >= 19 && pin <= 20) || (pin >= 22 && pin <= 32) || (pin > 40)) {
#elif CONFIG_IDF_TARGET_ESP32C3
// https://www.wemos.cc/en/latest/c3/c3_mini.html
if ((pin >= 11 && pin <= 19) || (pin > 21)) {
#elif CONFIG_IDF_TARGET_ESP32S3
if ((pin >= 19 && pin <= 20) || (pin >= 22 && pin <= 37) || (pin >= 39 && pin <= 42) || (pin > 48)) {
#endif
return false; // bad pin
}
// extra check for pins 21 and 22 (I2C) when ethernet is onboard
if ((EMSESP::system_.ethernet_connected() || EMSESP::system_.phy_type_ != PHY_type::PHY_TYPE_NONE) && (pin >= 21 && pin <= 22)) {
return false; // bad pin
}
return true;
} }
// Starts up the UART Serial bridge // Starts up the UART Serial bridge
@@ -2320,4 +2299,97 @@ uint8_t System::systemStatus() {
return systemStatus_; return systemStatus_;
} }
// takes a string range like "6-11, 1, 23, 24-48" which has optional ranges and single values and converts to a vector of ints
std::vector<uint8_t> System::string_range_to_vector(const std::string & range) {
std::vector<uint8_t> valid_gpios;
std::string::size_type pos = 0;
std::string::size_type prev = 0;
auto process_part = [&valid_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++) {
valid_gpios.push_back(static_cast<uint8_t>(i));
}
} else {
valid_gpios.push_back(static_cast<uint8_t>(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));
return valid_gpios;
}
// return a list of valid GPIOs for the ESP32 board
// notes:
// - we allow 0, which is used sometimes to indicate a disabled pin
// - also allow input only pins are accepted (34-39) on some boards
// - and allow pins 33-38 for octal SPI for 32M vchip version on some boards
std::vector<uint8_t> System::valid_gpio_list() {
// get free gpios based on board/platform type
#if CONFIG_IDF_TARGET_ESP32C3
// https://www.wemos.cc/en/latest/c3/c3_mini.html
std::vector<uint8_t> valid_gpios = string_range_to_vector("0-10");
#elif CONFIG_IDF_TARGET_ESP32S2
std::vector<uint8_t> valid_gpios = string_range_to_vector("0-14, 19, 20, 21, 33-38, 45, 46");
#elif CONFIG_IDF_TARGET_ESP32S3
std::vector<uint8_t> valid_gpios = string_range_to_vector("2, 4-14, 17, 18, 21, 33-38, 45, 46");
#elif CONFIG_IDF_TARGET_ESP32 || defined(EMSESP_STANDALONE)
std::vector<uint8_t> valid_gpios = string_range_to_vector("0, 2, 4, 5, 12-15, 18, 19, 23, 25-27, 32-39");
#else
std::vector<uint8_t> valid_gpios = {};
#endif
#if CONFIG_IDF_TARGET_ESP32
// if psram is enabled remove pins 16 and 17 from the list
if (ESP.getPsramSize() > 0) {
valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), 16), valid_gpios.end());
valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), 17), valid_gpios.end());
}
#endif
// if ethernet is enabled, remove pins 21 and 22 (I2C)
if ((EMSESP::system_.ethernet_connected() || EMSESP::system_.phy_type_ != PHY_type::PHY_TYPE_NONE)) {
valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), 21), valid_gpios.end());
valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), 22), valid_gpios.end());
}
// filter out GPIOs already used in application settings
for (const auto & gpio : valid_gpios) {
if (gpio == EMSESP::system_.pbutton_gpio_ || gpio == EMSESP::system_.led_gpio_ || gpio == EMSESP::system_.dallas_gpio_
|| gpio == EMSESP::system_.rx_gpio_ || gpio == EMSESP::system_.tx_gpio_) {
valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), gpio), valid_gpios.end());
}
}
// filter out GPIOs already used in analog sensors, if enabled
if (EMSESP::system_.analog_enabled_) {
for (const auto & sensor : EMSESP::analogsensor_.sensors()) {
if (std::find(valid_gpios.begin(), valid_gpios.end(), sensor.gpio()) != valid_gpios.end()) {
valid_gpios.erase(std::find(valid_gpios.begin(), valid_gpios.end(), sensor.gpio()));
}
}
}
// sort the list of valid GPIOs
std::sort(valid_gpios.begin(), valid_gpios.end());
return valid_gpios;
}
} // namespace emsesp } // namespace emsesp

View File

@@ -140,7 +140,7 @@ class System {
static void extractSettings(const char * filename, const char * section, JsonObject output); static void extractSettings(const char * filename, const char * section, JsonObject output);
static bool saveSettings(const char * filename, const char * section, JsonObject input); static bool saveSettings(const char * filename, const char * section, JsonObject input);
static bool is_valid_gpio(uint8_t pin, bool has_psram = false); bool is_valid_gpio(uint8_t pin);
static bool load_board_profile(std::vector<int8_t> & data, const std::string & board_profile); static bool load_board_profile(std::vector<int8_t> & data, const std::string & board_profile);
static bool readCommand(const char * data); static bool readCommand(const char * data);
@@ -303,6 +303,7 @@ class System {
uint32_t PSram() { uint32_t PSram() {
return psram_; return psram_;
} }
uint32_t appFree() { uint32_t appFree() {
return appfree_; return appfree_;
} }
@@ -336,6 +337,8 @@ class System {
test_set_all_active_ = n; test_set_all_active_ = n;
} }
static std::vector<uint8_t> valid_gpio_list();
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
float temperature() { float temperature() {
return temperature_; return temperature_;
@@ -381,6 +384,8 @@ class System {
void led_monitor(); void led_monitor();
void system_check(); void system_check();
static std::vector<uint8_t> string_range_to_vector(const std::string & range);
int8_t wifi_quality(int8_t dBm); int8_t wifi_quality(int8_t dBm);
uint8_t healthcheck_ = HEALTHCHECK_NO_NETWORK | HEALTHCHECK_NO_BUS; // start with all flags set, no wifi and no ems bus connection uint8_t healthcheck_ = HEALTHCHECK_NO_NETWORK | HEALTHCHECK_NO_BUS; // start with all flags set, no wifi and no ems bus connection

View File

@@ -100,7 +100,7 @@ class Boiler : public EMSdevice {
uint8_t wwAlternatingOper_; // alternating operation on/off uint8_t wwAlternatingOper_; // alternating operation on/off
uint8_t wwAltOpPrioHeat_; // alternating operation, prioritize heat time uint8_t wwAltOpPrioHeat_; // alternating operation, prioritize heat time
uint8_t wwAltOpPrioWw_; // alternating operation, prioritize dhw time uint8_t wwAltOpPrioWw_; // alternating operation, prioritize dhw time
uint8_t wwPrio_; uint8_t wwPrio_;
// special function // special function
uint8_t forceHeatingOff_; uint8_t forceHeatingOff_;

View File

@@ -294,7 +294,7 @@ bool Mixer::set_wwprio(const char * value, const int8_t id) {
return false; return false;
} }
uint8_t hc = device_id() - 0x20; uint8_t hc = device_id() - 0x20;
write_command(0x2CD + hc, 3, b ? 0xFF: 0, 0x2CD + hc); write_command(0x2CD + hc, 3, b ? 0xFF : 0, 0x2CD + hc);
return true; return true;
} }

View File

@@ -157,6 +157,11 @@ void WebDataService::sensor_data(AsyncWebServerRequest * request) {
root["analog_enabled"] = EMSESP::analog_enabled(); root["analog_enabled"] = EMSESP::analog_enabled();
root["platform"] = EMSESP_PLATFORM; root["platform"] = EMSESP_PLATFORM;
JsonArray valid_gpio_list = root["valid_gpio_list"].to<JsonArray>();
for (const auto & gpio : EMSESP::system_.valid_gpio_list()) {
valid_gpio_list.add(gpio);
}
response->setLength(); response->setLength();
request->send(response); request->send(response);
} }

View File

@@ -90,12 +90,6 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
// load the version from the settings config. This can be blank and later used in System::check_upgrade() // load the version from the settings config. This can be blank and later used in System::check_upgrade()
settings.version = root["version"] | EMSESP_DEFAULT_VERSION; settings.version = root["version"] | EMSESP_DEFAULT_VERSION;
#ifndef EMSESP_STANDALONE
bool psram = ESP.getPsramSize() > 0; // System::PSram() is initialized later
#else
bool psram = false;
#endif
#if defined(EMSESP_DEBUG) #if defined(EMSESP_DEBUG)
EMSESP::logger().debug("NVS boot value=[%s], board profile=[%s], EMSESP_DEFAULT_BOARD_PROFILE=[%s]", EMSESP::logger().debug("NVS boot value=[%s], board profile=[%s], EMSESP_DEFAULT_BOARD_PROFILE=[%s]",
EMSESP::nvs_.getString("boot").c_str(), EMSESP::nvs_.getString("boot").c_str(),
@@ -143,8 +137,8 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
#endif #endif
} }
// check valid pins in this board profile // check valid pins in this board profile
if (!System::is_valid_gpio(data[0], psram) || !System::is_valid_gpio(data[1], psram) || !System::is_valid_gpio(data[2], psram) if (!EMSESP::system_.is_valid_gpio(data[0]) || !EMSESP::system_.is_valid_gpio(data[1]) || !EMSESP::system_.is_valid_gpio(data[2])
|| !System::is_valid_gpio(data[3], psram) || !System::is_valid_gpio(data[4], psram) || !System::is_valid_gpio(data[6], psram)) { || !EMSESP::system_.is_valid_gpio(data[3]) || !EMSESP::system_.is_valid_gpio(data[4]) || !EMSESP::system_.is_valid_gpio(data[6])) {
settings.board_profile = "default"; // reset to factory default settings.board_profile = "default"; // reset to factory default
} }
} else { } else {
@@ -161,8 +155,8 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) {
#if defined(EMSESP_STANDALONE) #if defined(EMSESP_STANDALONE)
settings.board_profile = "S32"; settings.board_profile = "S32";
#elif CONFIG_IDF_TARGET_ESP32 #elif CONFIG_IDF_TARGET_ESP32
// check for no PSRAM, could be a E32 or S32 // check for no PSRAM, could be a E32 or S32?
if (!psram) { if (!ESP.getPsramSize()) {
#if ESP_ARDUINO_VERSION_MAJOR < 3 #if ESP_ARDUINO_VERSION_MAJOR < 3
if (ETH.begin(1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN)) { if (ETH.begin(1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN)) {
#else #else