From 4d3b31e5a188b071cb697696acbfdabe9aaae67e Mon Sep 17 00:00:00 2001 From: proddy Date: Wed, 15 Apr 2026 09:25:38 +0200 Subject: [PATCH 1/4] sync with dev --- interface/package.json | 24 +- interface/pnpm-lock.yaml | 862 +++++++++--------- interface/src/SignIn.tsx | 34 +- interface/src/app/main/CustomEntities.tsx | 12 +- .../src/app/main/CustomEntitiesDialog.tsx | 8 +- interface/src/app/main/Customizations.tsx | 41 +- .../src/app/main/CustomizationsDialog.tsx | 6 +- interface/src/app/main/Dashboard.tsx | 25 +- interface/src/app/main/DeviceIcon.tsx | 8 +- interface/src/app/main/Devices.tsx | 8 +- interface/src/app/main/DevicesDialog.tsx | 6 +- interface/src/app/main/Help.tsx | 18 +- interface/src/app/main/Modules.tsx | 10 +- interface/src/app/main/ModulesDialog.tsx | 2 +- interface/src/app/main/OptionIcon.tsx | 4 +- interface/src/app/main/Scheduler.tsx | 12 +- interface/src/app/main/SchedulerDialog.tsx | 16 +- interface/src/app/main/Sensors.tsx | 9 +- .../src/app/main/SensorsAnalogDialog.tsx | 10 +- .../src/app/main/SensorsTemperatureDialog.tsx | 11 +- interface/src/app/main/UserProfile.tsx | 8 +- interface/src/app/main/types.ts | 10 - .../src/app/settings/ApplicationSettings.tsx | 153 +--- interface/src/app/settings/DownloadUpload.tsx | 160 ++-- interface/src/app/settings/MqttSettings.tsx | 2 +- interface/src/app/settings/NTPSettings.tsx | 10 +- interface/src/app/settings/Settings.tsx | 12 +- .../app/settings/security/GenerateToken.tsx | 16 +- .../src/app/settings/security/ManageUsers.tsx | 12 +- interface/src/app/status/SystemLog.tsx | 2 +- interface/src/app/status/SystemMonitor.tsx | 13 +- interface/src/app/status/Version.tsx | 95 +- interface/src/components/MessageBox.tsx | 14 +- .../src/components/layout/LayoutDrawer.tsx | 2 +- .../src/components/layout/LayoutMenu.tsx | 4 +- .../src/components/loading/FormLoader.tsx | 12 +- .../src/components/loading/LoadingSpinner.tsx | 14 +- interface/src/components/upload/DragNdrop.tsx | 97 +- .../src/components/upload/SingleUpload.tsx | 9 +- interface/src/i18n/cz/index.ts | 7 +- interface/src/i18n/de/index.ts | 7 +- interface/src/i18n/en/index.ts | 7 +- interface/src/i18n/fr/index.ts | 7 +- interface/src/i18n/it/index.ts | 7 +- interface/src/i18n/nl/index.ts | 7 +- interface/src/i18n/no/index.ts | 7 +- interface/src/i18n/pl/index.ts | 7 +- interface/src/i18n/sk/index.ts | 15 +- interface/src/i18n/sv/index.ts | 7 +- interface/src/i18n/tr/index.ts | 7 +- mock-api/package.json | 2 +- mock-api/pnpm-lock.yaml | 32 +- mock-api/restServer.ts | 28 +- platformio.ini | 6 +- src/ESP32React/UploadFileService.cpp | 2 +- src/core/system.cpp | 261 ++---- src/emsesp_version.h | 2 +- src/web/WebSettingsService.cpp | 47 +- src/web/WebStatusService.cpp | 75 +- src/web/WebStatusService.h | 13 +- test/test_data/custom_support.json | 2 +- test/test_data/emsesp_allvalues.json | 103 +++ test/test_data/emsesp_customizations.json | 37 + test/test_data/emsesp_entities.json | 6 + test/test_data/emsesp_schedule.json | 6 + test/test_data/emsesp_settings.json | 145 +++ test/test_data/emsesp_systembackup.json | 249 +++++ 67 files changed, 1747 insertions(+), 1125 deletions(-) create mode 100644 test/test_data/emsesp_allvalues.json create mode 100644 test/test_data/emsesp_customizations.json create mode 100644 test/test_data/emsesp_entities.json create mode 100644 test/test_data/emsesp_schedule.json create mode 100644 test/test_data/emsesp_settings.json create mode 100644 test/test_data/emsesp_systembackup.json diff --git a/interface/package.json b/interface/package.json index 0234290c6..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", @@ -37,11 +37,11 @@ "jwt-decode": "^4.0.0", "magic-string": "^0.30.21", "mime-types": "^3.0.2", - "preact": "^10.29.0", - "react": "^19.2.4", - "react-dom": "^19.2.4", + "preact": "^10.29.1", + "react": "^19.2.5", + "react-dom": "^19.2.5", "react-icons": "^5.6.0", - "react-router": "^7.13.2", + "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.0", + "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "axe-core": "^4.11.1", + "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 a40414691..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.0) + 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 @@ -51,23 +51,23 @@ importers: specifier: ^3.0.2 version: 3.0.2 preact: - specifier: ^10.29.0 - version: 10.29.0 + 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.13.2 - version: 7.13.2(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.0)(rollup@4.59.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(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.0 - version: 25.5.0 + 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.1 - version: 4.11.1 + 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.0)(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.0)(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.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@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.1: - resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + 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.12: - resolution: {integrity: sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==} + 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==} @@ -1226,8 +1226,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -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.30001782: - resolution: {integrity: sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==} + 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.328: - resolution: {integrity: sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==} + 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: @@ -1792,8 +1792,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-xml-parser@4.5.5: - resolution: {integrity: sha512-cK9c5I/DwIOI7/Q7AlGN3DuTdwN61gwSfL8rvuVPK+0mcCNHHGxRrpiFtaZZRfRMJL3Gl8B2AFlBG6qXf03w9A==} + fast-xml-parser@4.5.6: + resolution: {integrity: sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==} hasBin: true fastq@1.20.1: @@ -2377,8 +2377,8 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} logalot@2.1.0: resolution: {integrity: sha512-Ah4CgdSRfeCJagxQhcVNMi9BfGYyEKLa6d7OA6xSbld/Hg3Cf2QiOa1mDpmG7Ve8LOH6DN3mdttzjQAvWTyVkw==} @@ -2507,8 +2507,8 @@ packages: node-html-parser@6.1.13: resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} - node-releases@2.0.36: - resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -2711,16 +2711,16 @@ 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: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} - preact@10.29.0: - resolution: {integrity: sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==} + preact@10.29.1: + resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -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.13.2: - resolution: {integrity: sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==} + 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 @@ -3426,7 +3426,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -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: @@ -3878,23 +3878,23 @@ snapshots: '@popperjs/core@2.11.8': {} - '@preact/compat@18.3.2(preact@10.29.0)': + '@preact/compat@18.3.2(preact@10.29.1)': dependencies: - preact: 10.29.0 + preact: 10.29.1 - '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.59.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(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.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(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.0)(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.0)(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 @@ -3903,75 +3903,74 @@ snapshots: '@prefresh/babel-plugin@0.5.3': {} - '@prefresh/core@1.5.9(preact@10.29.0)': + '@prefresh/core@1.5.9(preact@10.29.1)': dependencies: - preact: 10.29.0 + preact: 10.29.1 '@prefresh/utils@1.2.1': {} - '@prefresh/vite@2.4.12(preact@10.29.0)(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(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 - '@prefresh/core': 1.5.9(preact@10.29.0) + '@prefresh/core': 1.5.9(preact@10.29.1) '@prefresh/utils': 1.2.1 '@rollup/pluginutils': 4.2.1 - preact: 10.29.0 - vite: 8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(terser@5.46.1) + preact: 10.29.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,26 +4062,26 @@ 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 '@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 @@ -4098,7 +4097,7 @@ snapshots: '@types/glob@7.2.0': dependencies: '@types/minimatch': 6.0.0 - '@types/node': 25.5.0 + '@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.0 + '@types/node': 25.6.0 '@types/json-schema@7.0.15': {} '@types/keyv@3.1.4': dependencies: - '@types/node': 25.5.0 + '@types/node': 25.6.0 '@types/minimatch@6.0.0': dependencies: minimatch: 10.2.5 - '@types/node@25.5.0': + '@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.0 + '@types/node': 25.6.0 '@types/svgo@2.6.4': dependencies: - '@types/node': 25.5.0 + '@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.1: {} + 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.12: {} + 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 @@ -4385,13 +4384,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.1: + browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.12 - caniuse-lite: 1.0.30001782 - electron-to-chromium: 1.5.328 - node-releases: 2.0.36 - update-browserslist-db: 1.2.3(browserslist@4.28.1) + 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) buffer-alloc-unsafe@1.1.0: {} @@ -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.30001782: {} + caniuse-lite@1.0.30001788: {} caw@2.0.1: dependencies: @@ -4753,7 +4752,7 @@ snapshots: duplexer3@0.1.5: {} - electron-to-chromium@1.5.328: {} + 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 @@ -5050,7 +5049,7 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-xml-parser@4.5.5: + fast-xml-parser@4.5.6: dependencies: strnum: 1.1.2 @@ -5461,7 +5460,7 @@ snapshots: is-svg@4.4.0: dependencies: - fast-xml-parser: 4.5.5 + fast-xml-parser: 4.5.6 is-typed-array@1.1.15: dependencies: @@ -5601,7 +5600,7 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.23: {} + lodash-es@4.18.1: {} logalot@2.1.0: dependencies: @@ -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: {} @@ -5723,12 +5722,12 @@ snapshots: css-select: 5.2.2 he: 1.2.0 - node-releases@2.0.36: {} + node-releases@2.0.37: {} 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 @@ -5917,7 +5916,7 @@ snapshots: powershell-utils@0.1.0: {} - preact@10.29.0: {} + preact@10.29.1: {} prelude-ls@1.2.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.13.2(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,13 +6408,13 @@ snapshots: buffer: 5.7.1 through: 2.3.8 - undici-types@7.18.2: {} + undici-types@7.19.2: {} universalify@2.0.1: {} - update-browserslist-db@1.2.3(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.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.0)(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.0)(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.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)): 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.0)(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.0)(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.0 + '@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 && ( ) : ( - - + + {numChanges !== 0 && ( - + - + ©  { return ( <> - - {LL.MODULES_DESCRIPTION()}. - + + {LL.MODULES_DESCRIPTION()}. + { )}
- - + + {numChanges !== 0 && ( + + + + ), + [confirmBackup, handleCloseBackupDialog, LL] ); const handleDownload = useCallback( @@ -117,58 +128,57 @@ const DownloadUpload = () => { ); } - const gridButtons = downloadButtons.filter((btn) => btn.isGridButton); - const standaloneButton = downloadButtons.find((btn) => !btn.isGridButton); - return ( + {renderBackupDialog} + {LL.DOWNLOAD(0)} - - {LL.DOWNLOAD_SETTINGS_TEXT()}. - - - - {gridButtons.map((button) => ( - - - - ))} - - - - {LL.DOWNLOAD_SETTINGS_TEXT2()}. - - - {standaloneButton && ( + + + {LL.DOWNLOAD_SETTINGS_TEXT()}: + - )} + + + + + {LL.DOWNLOAD_SETTINGS_TEXT2()}: + + + {LL.UPLOAD()} - - {LL.UPLOAD_TEXT()}. - + + {LL.UPLOAD_TEXT()}: + - + ); }; diff --git a/interface/src/app/settings/MqttSettings.tsx b/interface/src/app/settings/MqttSettings.tsx index f910cadcd..a2ae1c236 100644 --- a/interface/src/app/settings/MqttSettings.tsx +++ b/interface/src/app/settings/MqttSettings.tsx @@ -129,7 +129,7 @@ const MqttSettings = () => { {blocker ? : null} <> - + { {timeZoneItems}
- + {!data.enabled && !dirtyFlags.length && ( - + - + {!downloadOnly && ( + + )} ); @@ -367,7 +385,9 @@ const InstallPartitionDialog = memo( {LL.INSTALL()} {LL.STORED_VERSIONS()} - {LL.INSTALL_VERSION(LL.INSTALL(), version)} + + {LL.INSTALL_VERSION(LL.INSTALL(), version)} + + {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()} + + + + + + + + + )} )} 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/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 e9bbe3927..1f6f95797 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -38,15 +38,6 @@ #include "../test/test.h" #endif -#ifndef NO_TLS_SUPPORT -#define ENABLE_SMTP -#define USE_ESP_SSLCLIENT -#define READYCLIENT_SSL_CLIENT ESP_SSLClient -#define READYCLIENT_TYPE_1 // TYPE 1 when using ESP_SSLClient -#include -#include -#endif - namespace emsesp { // Languages supported. Note: the order is important @@ -115,110 +106,6 @@ bool System::command_send(const char * value, const int8_t id) { return EMSESP::txservice_.send_raw(value); // ignore id } -bool System::command_sendmail(const char * value, const int8_t id) { - bool enabled = false; - bool ssl, starttls; - uint16_t port; - String server, login, pass, sender, recp, subject; - EMSESP::webSettingsService.read([&](WebSettings & settings) { - enabled = settings.email_enabled; - ssl = settings.email_ssl; - starttls = settings.email_starttls; - server = settings.email_server; - port = settings.email_port; - login = settings.email_login; - pass = settings.email_pass; - sender = settings.email_sender; - recp = settings.email_recp; - subject = settings.email_subject; - }); - if (!enabled) { - return false; - } - LOG_DEBUG("Command sendmail port %d%s called with '%s'", port, ssl ? " (SSL)" : starttls ? " (STARTTLS)" : "", value); - // LOG_DEBUG("Command sendmail port %d called with '%s'", port, value); - bool success = false; - -#ifndef NO_TLS_SUPPORT - WiFiClient * basic_client; - ESP_SSLClient * ssl_client; - ReadyClient * r_client; // rClient(ssl_client); - SMTPClient * smtp; // smtp(rClient); - basic_client = new WiFiClient; - ssl_client = new ESP_SSLClient; - r_client = new ReadyClient(*ssl_client); - smtp = new SMTPClient(*r_client); - - ssl_client->setClient(basic_client); - ssl_client->setInsecure(); - ssl_client->setBufferSizes(1024, 1024); - r_client->addPort(port, starttls ? readymail_protocol_tls : ssl ? readymail_protocol_ssl : readymail_protocol_plain_text); - - // smtp->connect(server, port, sendmailCallback); - smtp->connect(server, port); - if (!smtp->isConnected()) { - LOG_ERROR("Sendmail connection error"); - delete smtp; - delete r_client; - delete ssl_client; - delete basic_client; - return false; - } - - // LOG_INFO("authenticate %s:%s", login.c_str(), pass.c_str()); - smtp->authenticate(login, pass, readymail_auth_password); - if (!smtp->isAuthenticated()) { - LOG_ERROR("Sendmail authenticate error"); - delete smtp; - delete r_client; - delete ssl_client; - delete basic_client; - return false; - } - JsonDocument doc; - String body = value; - if (body.length()) { - auto error = deserializeJson(doc, (const char *)value); - if (!error && doc.as().size() >= 0) { - subject = doc["subject"] | subject; - recp = doc["to"] | recp; - sender = doc["from"] | sender; - body = doc["body"] | body; - } - } - - SMTPMessage & msg = smtp->getMessage(); - msg.headers.add(rfc822_subject, subject); - msg.headers.add(rfc822_from, sender); - msg.headers.add(rfc822_to, recp); - - // Use addCustom to add custom header e.g. Importance and Priority. - // msg.headers.addCustom("Importance", PRIORITY); - // msg.headers.addCustom("X-MSMail-Priority", PRIORITY); - // msg.headers.addCustom("X-Priority", PRIORITY_NUM); - - msg.text.body(body); - - // bodyText.replace("\r\n", "
\r\n"); - // msg.html.body("
" + bodyText + "
"); - // msg.html.transferEncoding("base64"); - - // With embedFile function, the html message will send as attachment. - // if (EMBED_MESSAGE) - // msg.html.embedFile(true, "msg.html", embed_message_type_attachment); - - msg.timestamp = time(nullptr); - - success = smtp->send(msg); - - delete smtp; - delete r_client; - delete ssl_client; - delete basic_client; -#endif - return success; -} - // return string of languages and count std::string System::languages_string() { std::string languages_string = std::to_string(NUM_LANGUAGES) + " languages ("; @@ -309,26 +196,26 @@ bool System::command_publish(const char * value, const int8_t id) { // syslog level // commenting this out - don't see the point on having an API service to change the syslog level /* -bool System::command_syslog_level(const char * value, const int8_t id) { - uint8_t s = 0xff; - if (Helpers::value2enum(value, s, FL_(list_syslog_level))) { - bool changed = false; - EMSESP::webSettingsService.update( - [&](WebSettings & settings) { - if (settings.syslog_level != (int8_t)s - 1) { - settings.syslog_level = (int8_t)s - 1; - changed = true; - } - return StateUpdateResult::CHANGED; - }); - if (changed) { - EMSESP::system_.syslog_init(); - } - return true; - } - return false; -} -*/ + bool System::command_syslog_level(const char * value, const int8_t id) { + uint8_t s = 0xff; + if (Helpers::value2enum(value, s, FL_(list_syslog_level))) { + bool changed = false; + EMSESP::webSettingsService.update( + [&](WebSettings & settings) { + if (settings.syslog_level != (int8_t)s - 1) { + settings.syslog_level = (int8_t)s - 1; + changed = true; + } + return StateUpdateResult::CHANGED; + }); + if (changed) { + EMSESP::system_.syslog_init(); + } + return true; + } + return false; + } + */ // send message - to system log and MQTT bool System::command_message(const char * value, const int8_t id, JsonObject output) { @@ -576,6 +463,7 @@ void System::system_restart(const char * partitionname) { Mqtt::disconnect(); // gracefully disconnect MQTT, needed for QOS1 EMSuart::stop(); // stop UART so there is no interference + #ifndef EMSESP_STANDALONE delay(1000); // wait 1 second ESP.restart(); // ka-boom! - this is the only place where the ESP32 restart is called @@ -677,6 +565,7 @@ void System::store_settings(WebSettings & settings) { locale_ = settings.locale; developer_mode_ = settings.developer_mode; + // start services if (settings.modbus_enabled) { if (EMSESP::modbus_ == nullptr) { @@ -718,11 +607,20 @@ void System::start() { appfree_ = esp_ota_get_running_partition()->size / 1024 - appused_; refreshHeapMem(); // refresh free heap and max alloc heap #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 +#if ESP_IDF_VERSION_MAJOR < 5 + temp_sensor_config_t temp_sensor = TSENS_CONFIG_DEFAULT(); + temp_sensor_get_config(&temp_sensor); + temp_sensor.dac_offset = TSENS_DAC_DEFAULT; // DEFAULT: range:-10℃ ~ 80℃, error < 1℃. + temp_sensor_set_config(temp_sensor); + temp_sensor_start(); + temp_sensor_read_celsius(&temperature_); +#else temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); temperature_sensor_install(&temp_sensor_config, &temperature_handle_); temperature_sensor_enable(temperature_handle_); temperature_sensor_get_celsius(temperature_handle_, &temperature_); #endif +#endif #endif EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { @@ -924,14 +822,14 @@ void System::send_info_mqtt() { doc["network"] = "ethernet"; doc["hostname"] = ETH.getHostname(); /* - doc["MAC"] = ETH.macAddress(); - doc["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask()); - doc["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP()); - doc["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP()); - if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.localIPv6().toString() != "::") { - doc["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6()); - } - */ + doc["MAC"] = ETH.macAddress(); + doc["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask()); + doc["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP()); + doc["IPv4 nameserver"] = uuid::printable_to_string(ETH.dnsIP()); + if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.localIPv6().toString() != "::") { + doc["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6()); + } + */ } else if (WiFi.status() == WL_CONNECTED) { doc["network"] = "wifi"; @@ -943,9 +841,16 @@ void System::send_info_mqtt() { doc["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP()); doc["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP()); +#if ESP_IDF_VERSION_MAJOR < 5 + if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.localIPv6().toString() != "::") { + doc["IPv6 address"] = uuid::printable_to_string(WiFi.localIPv6()); + } +#else if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { doc["IPv6 address"] = uuid::printable_to_string(WiFi.linkLocalIPv6()); } + +#endif } #endif Mqtt::queue_publish_retain(F_(info), doc.as()); // topic called "info" and it's Retained @@ -1054,8 +959,13 @@ void System::network_init() { delay(500); digitalWrite(eth_power_, HIGH); } + +#if ESP_IDF_VERSION_MAJOR < 5 + eth_present_ = ETH.begin(phy_addr, power, mdc, mdio, type, clock_mode); +#else eth_present_ = ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode); #endif +#endif } // check health of system, done every 5 seconds @@ -1066,9 +976,13 @@ void System::system_check() { #ifndef EMSESP_STANDALONE #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 +#if ESP_IDF_VERSION_MAJOR < 5 + temp_sensor_read_celsius(&temperature_); +#else temperature_sensor_get_celsius(temperature_handle_, &temperature_); #endif #endif +#endif #ifdef EMSESP_PINGTEST static uint64_t ping_count = 0; @@ -1120,7 +1034,6 @@ void System::commands_init() { Command::add(EMSdevice::DeviceType::SYSTEM, F_(read), System::command_read, FL_(read_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, FL_(send_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, FL_(fetch_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(sendmail), System::command_sendmail, FL_(sendmail_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(format), System::command_format, FL_(format_cmd), CommandFlag::ADMIN_ONLY); Command::add(EMSdevice::DeviceType::SYSTEM, F_(txpause), System::command_txpause, FL_(txpause_cmd), CommandFlag::ADMIN_ONLY); @@ -1362,9 +1275,16 @@ void System::show_system(uuid::console::Shell & shell) { shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(WiFi.localIP()).c_str(), uuid::printable_to_string(WiFi.subnetMask()).c_str()); shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(WiFi.gatewayIP()).c_str()); shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(WiFi.dnsIP()).c_str()); +#if ESP_IDF_VERSION_MAJOR < 5 + if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.localIPv6().toString() != "::") { + shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.localIPv6()).c_str()); + } +#else if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.linkLocalIPv6()).c_str()); } +#endif + break; case WL_CONNECT_FAILED: @@ -1395,9 +1315,15 @@ void System::show_system(uuid::console::Shell & shell) { shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(ETH.localIP()).c_str(), uuid::printable_to_string(ETH.subnetMask()).c_str()); shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(ETH.gatewayIP()).c_str()); shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(ETH.dnsIP()).c_str()); +#if ESP_IDF_VERSION_MAJOR < 5 + if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.localIPv6().toString() != "::") { + shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.localIPv6()).c_str()); + } +#else if (ETH.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.linkLocalIPv6().toString() != "::") { shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.linkLocalIPv6()).c_str()); } +#endif } shell.println(); @@ -1419,6 +1345,7 @@ void System::show_system(uuid::console::Shell & shell) { } shell.println(); + #endif } @@ -1722,6 +1649,7 @@ bool System::check_upgrade() { } // map each config filename to its human-readable section key +#ifndef EMSESP_STANDALONE static const std::pair SECTION_MAP[] = { {NETWORK_SETTINGS_FILE, "Network"}, {AP_SETTINGS_FILE, "AP"}, @@ -1734,6 +1662,7 @@ static const std::pair SECTION_MAP[] = { {EMSESP_CUSTOMENTITY_FILE, "Entities"}, {EMSESP_MODULES_FILE, "Modules"}, }; +#endif // 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) { @@ -1834,29 +1763,38 @@ void System::exportSystemBackup(JsonObject output) { 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 ESP_IDF_VERSION_MAJOR < 5 + it = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY); + if (it == nullptr) { +#else + esp_err_t err = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY, &it); if (err != ESP_OK) { +#endif LOG_ERROR("Failed to find NVS entry for %s", nvs_part); return; } JsonArray entries = node["nvs"].to(); +#if ESP_IDF_VERSION_MAJOR < 5 + while (it != nullptr) { + nvs_entry_info_t info; + nvs_entry_info(it, &info); +#else while (err == ESP_OK) { nvs_entry_info_t info; nvs_entry_info(it, &info); +#endif JsonObject entry = entries.add(); - entry["type"] = info.type; // e.g. NVS_TYPE_U32 or NVS_TYPE_STR etc + entry["type"] = info.type; 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: @@ -1872,19 +1810,22 @@ void System::exportSystemBackup(JsonObject output) { 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); + entry["value"] = EMSESP::nvs_.getDouble(info.key); // bytes used for double values 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; } +#if ESP_IDF_VERSION_MAJOR < 5 + it = nvs_entry_next(it); + } +#else err = nvs_entry_next(&it); } +#endif if (it != nullptr) { nvs_release_iterator(it); @@ -2686,12 +2627,12 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output node["maxWebLogBuffer"] = settings.weblog_buffer; /* -#if defined(EMSESP_UNITY) - node["webLogBuffer"] = 0; -#else - node["webLogBuffer"] = EMSESP::webLogService.num_log_messages(); -#endif -*/ + #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; @@ -3319,7 +3260,7 @@ void System::set_valid_system_gpios() { 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 + // 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 @@ -3332,7 +3273,7 @@ void System::set_valid_system_gpios() { 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 + // 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 @@ -3351,7 +3292,7 @@ void System::set_valid_system_gpios() { } #elif CONFIG_IDF_TARGET_ESP32 - // https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/gpio.html + // 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 @@ -3382,10 +3323,6 @@ void System::set_valid_system_gpios() { } 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 diff --git a/src/emsesp_version.h b/src/emsesp_version.h index ffc3ab2c2..29cd72cc1 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.8.2-dev.C13" diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index 106e7c052..c0b0b8d04 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -83,20 +83,6 @@ void WebSettings::read(WebSettings & settings, JsonObject root) { 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 @@ -257,9 +243,13 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) { // Modbus settings settings.modbus_enabled = root["modbus_enabled"] | EMSESP_DEFAULT_MODBUS_ENABLED; + check_flag(original_settings.modbus_enabled, settings.modbus_enabled, ChangeFlags::RESTART); settings.modbus_port = root["modbus_port"] | EMSESP_DEFAULT_MODBUS_PORT; + check_flag(original_settings.modbus_port, settings.modbus_port, ChangeFlags::RESTART); settings.modbus_max_clients = root["modbus_max_clients"] | EMSESP_DEFAULT_MODBUS_MAX_CLIENTS; + check_flag(original_settings.modbus_max_clients, settings.modbus_max_clients, ChangeFlags::RESTART); settings.modbus_timeout = root["modbus_timeout"] | EMSESP_DEFAULT_MODBUS_TIMEOUT; + check_flag(original_settings.modbus_timeout, settings.modbus_timeout, ChangeFlags::RESTART); // // these may need mqtt restart to rebuild HA discovery topics @@ -310,20 +300,6 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) { 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; @@ -482,14 +458,23 @@ void WebSettings::set_board_profile(WebSettings & settings) { #if CONFIG_IDF_TARGET_ESP32 // check for no PSRAM, could be a E32 or S32? if (!ESP.getPsramSize()) { +#if ESP_ARDUINO_VERSION_MAJOR < 3 + if (ETH.begin(1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN)) { +#else if (ETH.begin(ETH_PHY_LAN8720, 1, 23, 18, 16, ETH_CLOCK_GPIO0_IN)) { +#endif 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 +// check for boards with PSRAM, could be a E32V2 otherwise default back to the S32 +#if ESP_ARDUINO_VERSION_MAJOR < 3 + if (ETH.begin(0, 15, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_OUT)) { +#else if (ETH.begin(ETH_PHY_LAN8720, 0, 23, 18, 15, ETH_CLOCK_GPIO0_OUT)) { +#endif + if (analogReadMilliVolts(39) > 700) { // core voltage > 2.6V settings.board_profile = "E32V2_2"; // Ethernet, PSRAM, internal sensors } else { @@ -517,9 +502,9 @@ void WebSettings::set_board_profile(WebSettings & settings) { #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); + 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()); + EMSESP::logger().info("Loaded board profile %s (PSRAM: not available)", settings.board_profile.c_str()); } #endif 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 From 6b31fef1af0f24991e54908d91faa029ec0b9005 Mon Sep 17 00:00:00 2001 From: proddy Date: Wed, 15 Apr 2026 20:37:27 +0200 Subject: [PATCH 2/4] build on mac osx --- lib/espMqttClient/src/Transport/ClientPosix.cpp | 2 +- lib/espMqttClient/src/Transport/ClientPosix.h | 2 +- lib/espMqttClient/src/espMqttClient.cpp | 2 +- lib/espMqttClient/src/espMqttClient.h | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) 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(); From def5173692663cf68f1a71cc9ea6972535733c1b Mon Sep 17 00:00:00 2001 From: proddy Date: Wed, 15 Apr 2026 20:37:33 +0200 Subject: [PATCH 3/4] fix merge issues --- src/core/system.cpp | 6750 ++++++++++++++++---------------- src/web/WebSettingsService.cpp | 1086 ++--- 2 files changed, 3959 insertions(+), 3877 deletions(-) diff --git a/src/core/system.cpp b/src/core/system.cpp index 1f6f95797..1dcc9465a 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -16,186 +16,299 @@ * along with this program. If not, see . */ -#include "system.h" -#include "emsesp.h" // for send_raw_telegram() command - -#ifndef EMSESP_STANDALONE -#include "esp_image_format.h" -#include "esp_ota_ops.h" -#include "esp_partition.h" -#include -#include "esp_efuse.h" -#include -#include -#endif - -#include -#include - -#include - -#if defined(EMSESP_TEST) -#include "../test/test.h" -#endif - -namespace emsesp { - -// Languages supported. Note: the order is important -// and must match locale_translations.h and common.h -#if defined(EMSESP_TEST) -// in Test mode use two languages (en & de) to save flash memory needed for the tests -const char * const languages[] = {EMSESP_LOCALE_EN, EMSESP_LOCALE_DE}; -#elif defined(EMSESP_EN_ONLY) -// EN only -const char * const languages[] = {EMSESP_LOCALE_EN}; -#elif defined(EMSESP_DE_ONLY) -// EN + DE -const char * const languages[] = {EMSESP_LOCALE_EN, EMSESP_LOCALE_DE}; -#else -const char * const languages[] = {EMSESP_LOCALE_EN, - EMSESP_LOCALE_DE, - EMSESP_LOCALE_NL, - EMSESP_LOCALE_SV, - EMSESP_LOCALE_PL, - EMSESP_LOCALE_NO, - EMSESP_LOCALE_FR, - EMSESP_LOCALE_TR, - EMSESP_LOCALE_IT, - EMSESP_LOCALE_SK, - EMSESP_LOCALE_CZ}; -#endif - -static constexpr uint8_t NUM_LANGUAGES = sizeof(languages) / sizeof(const char *); - -#ifndef EMSESP_STANDALONE -uuid::syslog::SyslogService System::syslog_; -#endif - -uuid::log::Logger System::logger_{F_(system), uuid::log::Facility::KERN}; - -// init statics -PButton System::myPButton_; -bool System::test_set_all_active_ = false; -uint32_t System::max_alloc_mem_; -uint32_t System::heap_mem_; - -// LED flash timer -uint8_t System::led_flash_gpio_ = 0; -uint8_t System::led_flash_type_ = 0; -uint32_t System::led_flash_start_time_ = 0; -uint32_t System::led_flash_duration_ = 0; -bool System::led_flash_timer_ = false; - -// GPIOs -std::vector> System::valid_system_gpios_; -std::vector> System::used_gpios_; - -// find the index of the language -// 0 = EN, 1 = DE, etc... -uint8_t System::language_index() { - for (uint8_t i = 0; i < NUM_LANGUAGES; i++) { - if (languages[i] == locale()) { - return i; - } - } - return 0; // EN only -} - -// send raw to ems -bool System::command_send(const char * value, const int8_t id) { - return EMSESP::txservice_.send_raw(value); // ignore id -} - -// return string of languages and count -std::string System::languages_string() { - std::string languages_string = std::to_string(NUM_LANGUAGES) + " languages ("; - for (uint8_t i = 0; i < NUM_LANGUAGES; i++) { - languages_string += languages[i]; - if (i != NUM_LANGUAGES - 1) { - languages_string += ","; - } - } - languages_string += ")"; - return languages_string; -} - -// returns last response from MQTT -bool System::command_response(const char * value, const int8_t id, JsonObject output) { - JsonDocument doc; - if (DeserializationError::Ok == deserializeJson(doc, Mqtt::get_response())) { - for (JsonPair p : doc.as()) { - output[p.key()] = p.value(); - } - } else { - output["response"] = Mqtt::get_response(); - } - return true; -} - -// fetch device values -bool System::command_fetch(const char * value, const int8_t id) { - std::string value_s; - if (Helpers::value2string(value, value_s)) { - if (value_s == "all") { - LOG_INFO("Requesting data from EMS devices"); - EMSESP::fetch_device_values(); - } else if (value_s == F_(boiler)) { - EMSESP::fetch_device_values_type(EMSdevice::DeviceType::BOILER); - } else if (value_s == F_(thermostat)) { - EMSESP::fetch_device_values_type(EMSdevice::DeviceType::THERMOSTAT); - } else if (value_s == F_(solar)) { - EMSESP::fetch_device_values_type(EMSdevice::DeviceType::SOLAR); - } else if (value_s == F_(mixer)) { - EMSESP::fetch_device_values_type(EMSdevice::DeviceType::MIXER); - } - } else { - EMSESP::fetch_device_values(); // default if no name or id is given - } - - return true; // always true -} - -// mqtt publish -bool System::command_publish(const char * value, const int8_t id) { - std::string value_s; - if (Helpers::value2string(value, value_s)) { - if (value_s == "ha") { - EMSESP::publish_all(true); // includes HA - LOG_INFO("Publishing all data to MQTT, including HA configs"); - return true; - } else if (value_s == (F_(boiler))) { - EMSESP::publish_device_values(EMSdevice::DeviceType::BOILER); - return true; - } else if (value_s == (F_(thermostat))) { - EMSESP::publish_device_values(EMSdevice::DeviceType::THERMOSTAT); - return true; - } else if (value_s == (F_(solar))) { - EMSESP::publish_device_values(EMSdevice::DeviceType::SOLAR); - return true; - } else if (value_s == (F_(mixer))) { - EMSESP::publish_device_values(EMSdevice::DeviceType::MIXER); - return true; - } else if (value_s == (F_(water))) { - EMSESP::publish_device_values(EMSdevice::DeviceType::WATER); - return true; - } else if (value_s == "other") { - EMSESP::publish_other_values(); // switch and heat pump - return true; - } else if ((value_s == (F_(temperaturesensor))) || (value_s == (F_(analogsensor)))) { - EMSESP::publish_sensor_values(true); - return true; - } - } - - LOG_INFO("Publishing all data to MQTT"); - EMSESP::publish_all(); - - return true; -} - -// syslog level -// commenting this out - don't see the point on having an API service to change the syslog level -/* + #include "system.h" + #include "emsesp.h" // for send_raw_telegram() command + + #ifndef EMSESP_STANDALONE + #include "esp_image_format.h" + #include "esp_ota_ops.h" + #include "esp_partition.h" + #include + #include "esp_efuse.h" + #include + #include + #endif + + #include + #include + + #include + + #if defined(EMSESP_TEST) + #include "../test/test.h" + #endif + + #ifndef NO_TLS_SUPPORT + #define ENABLE_SMTP + #define USE_ESP_SSLCLIENT + #define READYCLIENT_SSL_CLIENT ESP_SSLClient + #define READYCLIENT_TYPE_1 // TYPE 1 when using ESP_SSLClient + #include + #include + #endif + + namespace emsesp { + + // Languages supported. Note: the order is important + // and must match locale_translations.h and common.h + #if defined(EMSESP_TEST) + // in Test mode use two languages (en & de) to save flash memory needed for the tests + const char * const languages[] = {EMSESP_LOCALE_EN, EMSESP_LOCALE_DE}; + #elif defined(EMSESP_EN_ONLY) + // EN only + const char * const languages[] = {EMSESP_LOCALE_EN}; + #elif defined(EMSESP_DE_ONLY) + // EN + DE + const char * const languages[] = {EMSESP_LOCALE_EN, EMSESP_LOCALE_DE}; + #else + const char * const languages[] = {EMSESP_LOCALE_EN, + EMSESP_LOCALE_DE, + EMSESP_LOCALE_NL, + EMSESP_LOCALE_SV, + EMSESP_LOCALE_PL, + EMSESP_LOCALE_NO, + EMSESP_LOCALE_FR, + EMSESP_LOCALE_TR, + EMSESP_LOCALE_IT, + EMSESP_LOCALE_SK, + EMSESP_LOCALE_CZ}; + #endif + + static constexpr uint8_t NUM_LANGUAGES = sizeof(languages) / sizeof(const char *); + + #ifndef EMSESP_STANDALONE + uuid::syslog::SyslogService System::syslog_; + #endif + + uuid::log::Logger System::logger_{F_(system), uuid::log::Facility::KERN}; + + // init statics + PButton System::myPButton_; + bool System::test_set_all_active_ = false; + uint32_t System::max_alloc_mem_; + uint32_t System::heap_mem_; + + // LED flash timer + uint8_t System::led_flash_gpio_ = 0; + uint8_t System::led_flash_type_ = 0; + uint32_t System::led_flash_start_time_ = 0; + uint32_t System::led_flash_duration_ = 0; + bool System::led_flash_timer_ = false; + + // GPIOs + std::vector> System::valid_system_gpios_; + std::vector> System::used_gpios_; + + // find the index of the language + // 0 = EN, 1 = DE, etc... + uint8_t System::language_index() { + for (uint8_t i = 0; i < NUM_LANGUAGES; i++) { + if (languages[i] == locale()) { + return i; + } + } + return 0; // EN only + } + + // send raw to ems + bool System::command_send(const char * value, const int8_t id) { + return EMSESP::txservice_.send_raw(value); // ignore id + } + + bool System::command_sendmail(const char * value, const int8_t id) { + bool enabled = false; + bool ssl, starttls; + uint16_t port; + String server, login, pass, sender, recp, subject; + EMSESP::webSettingsService.read([&](WebSettings & settings) { + enabled = settings.email_enabled; + ssl = settings.email_ssl; + starttls = settings.email_starttls; + server = settings.email_server; + port = settings.email_port; + login = settings.email_login; + pass = settings.email_pass; + sender = settings.email_sender; + recp = settings.email_recp; + subject = settings.email_subject; + }); + if (!enabled) { + return false; + } + LOG_DEBUG("Command sendmail port %d%s called with '%s'", port, ssl ? " (SSL)" : starttls ? " (STARTTLS)" : "", value); + // LOG_DEBUG("Command sendmail port %d called with '%s'", port, value); + bool success = false; + + #ifndef NO_TLS_SUPPORT + WiFiClient * basic_client; + ESP_SSLClient * ssl_client; + ReadyClient * r_client; // rClient(ssl_client); + SMTPClient * smtp; // smtp(rClient); + basic_client = new WiFiClient; + ssl_client = new ESP_SSLClient; + r_client = new ReadyClient(*ssl_client); + smtp = new SMTPClient(*r_client); + + ssl_client->setClient(basic_client); + ssl_client->setInsecure(); + ssl_client->setBufferSizes(1024, 1024); + r_client->addPort(port, starttls ? readymail_protocol_tls : ssl ? readymail_protocol_ssl : readymail_protocol_plain_text); + + // smtp->connect(server, port, sendmailCallback); + smtp->connect(server, port); + if (!smtp->isConnected()) { + LOG_ERROR("Sendmail connection error"); + delete smtp; + delete r_client; + delete ssl_client; + delete basic_client; + return false; + } + + // LOG_INFO("authenticate %s:%s", login.c_str(), pass.c_str()); + smtp->authenticate(login, pass, readymail_auth_password); + if (!smtp->isAuthenticated()) { + LOG_ERROR("Sendmail authenticate error"); + delete smtp; + delete r_client; + delete ssl_client; + delete basic_client; + return false; + } + JsonDocument doc; + String body = value; + if (body.length()) { + auto error = deserializeJson(doc, (const char *)value); + if (!error && doc.as().size() >= 0) { + subject = doc["subject"] | subject; + recp = doc["to"] | recp; + sender = doc["from"] | sender; + body = doc["body"] | body; + } + } + + SMTPMessage & msg = smtp->getMessage(); + msg.headers.add(rfc822_subject, subject); + msg.headers.add(rfc822_from, sender); + msg.headers.add(rfc822_to, recp); + + // Use addCustom to add custom header e.g. Importance and Priority. + // msg.headers.addCustom("Importance", PRIORITY); + // msg.headers.addCustom("X-MSMail-Priority", PRIORITY); + // msg.headers.addCustom("X-Priority", PRIORITY_NUM); + + msg.text.body(body); + + // bodyText.replace("\r\n", "
\r\n"); + // msg.html.body("
" + bodyText + "
"); + // msg.html.transferEncoding("base64"); + + // With embedFile function, the html message will send as attachment. + // if (EMBED_MESSAGE) + // msg.html.embedFile(true, "msg.html", embed_message_type_attachment); + + msg.timestamp = time(nullptr); + + success = smtp->send(msg); + + delete smtp; + delete r_client; + delete ssl_client; + delete basic_client; + #endif + return success; + } + + // return string of languages and count + std::string System::languages_string() { + std::string languages_string = std::to_string(NUM_LANGUAGES) + " languages ("; + for (uint8_t i = 0; i < NUM_LANGUAGES; i++) { + languages_string += languages[i]; + if (i != NUM_LANGUAGES - 1) { + languages_string += ","; + } + } + languages_string += ")"; + return languages_string; + } + + // returns last response from MQTT + bool System::command_response(const char * value, const int8_t id, JsonObject output) { + JsonDocument doc; + if (DeserializationError::Ok == deserializeJson(doc, Mqtt::get_response())) { + for (JsonPair p : doc.as()) { + output[p.key()] = p.value(); + } + } else { + output["response"] = Mqtt::get_response(); + } + return true; + } + + // fetch device values + bool System::command_fetch(const char * value, const int8_t id) { + std::string value_s; + if (Helpers::value2string(value, value_s)) { + if (value_s == "all") { + LOG_INFO("Requesting data from EMS devices"); + EMSESP::fetch_device_values(); + } else if (value_s == F_(boiler)) { + EMSESP::fetch_device_values_type(EMSdevice::DeviceType::BOILER); + } else if (value_s == F_(thermostat)) { + EMSESP::fetch_device_values_type(EMSdevice::DeviceType::THERMOSTAT); + } else if (value_s == F_(solar)) { + EMSESP::fetch_device_values_type(EMSdevice::DeviceType::SOLAR); + } else if (value_s == F_(mixer)) { + EMSESP::fetch_device_values_type(EMSdevice::DeviceType::MIXER); + } + } else { + EMSESP::fetch_device_values(); // default if no name or id is given + } + + return true; // always true + } + + // mqtt publish + bool System::command_publish(const char * value, const int8_t id) { + std::string value_s; + if (Helpers::value2string(value, value_s)) { + if (value_s == "ha") { + EMSESP::publish_all(true); // includes HA + LOG_INFO("Publishing all data to MQTT, including HA configs"); + return true; + } else if (value_s == (F_(boiler))) { + EMSESP::publish_device_values(EMSdevice::DeviceType::BOILER); + return true; + } else if (value_s == (F_(thermostat))) { + EMSESP::publish_device_values(EMSdevice::DeviceType::THERMOSTAT); + return true; + } else if (value_s == (F_(solar))) { + EMSESP::publish_device_values(EMSdevice::DeviceType::SOLAR); + return true; + } else if (value_s == (F_(mixer))) { + EMSESP::publish_device_values(EMSdevice::DeviceType::MIXER); + return true; + } else if (value_s == (F_(water))) { + EMSESP::publish_device_values(EMSdevice::DeviceType::WATER); + return true; + } else if (value_s == "other") { + EMSESP::publish_other_values(); // switch and heat pump + return true; + } else if ((value_s == (F_(temperaturesensor))) || (value_s == (F_(analogsensor)))) { + EMSESP::publish_sensor_values(true); + return true; + } + } + + LOG_INFO("Publishing all data to MQTT"); + EMSESP::publish_all(); + + return true; + } + + // syslog level + // commenting this out - don't see the point on having an API service to change the syslog level + /* bool System::command_syslog_level(const char * value, const int8_t id) { uint8_t s = 0xff; if (Helpers::value2enum(value, s, FL_(list_syslog_level))) { @@ -216,612 +329,601 @@ bool System::command_publish(const char * value, const int8_t id) { return false; } */ - -// send message - to system log and MQTT -bool System::command_message(const char * value, const int8_t id, JsonObject output) { - if (value == nullptr || value[0] == '\0') { - LOG_WARNING("Message is empty"); - return false; // must have a string value - } - - EMSESP::webSchedulerService.computed_value.clear(); - EMSESP::webSchedulerService.raw_value = value; - for (uint16_t wait = 0; wait < 2000 && !EMSESP::webSchedulerService.raw_value.empty(); wait++) { - delay(1); - } - - if (EMSESP::webSchedulerService.computed_value.empty()) { - LOG_WARNING("Message result is empty"); - return false; - } - - LOG_INFO("Message: %s", EMSESP::webSchedulerService.computed_value.c_str()); // send to log - Mqtt::queue_publish(F_(message), EMSESP::webSchedulerService.computed_value); // send to MQTT if enabled - output["api_data"] = EMSESP::webSchedulerService.computed_value; // send to API - - return true; -} - -// watch -bool System::command_watch(const char * value, const int8_t id) { - uint8_t w = 0xff; - uint16_t i = Helpers::hextoint(value); - if (Helpers::value2enum(value, w, FL_(list_watch))) { - if (w == 0 || EMSESP::watch() == EMSESP::Watch::WATCH_OFF) { - EMSESP::watch_id(0); - } - if (Mqtt::publish_single() && w != EMSESP::watch()) { - if (Mqtt::publish_single2cmd()) { - Mqtt::queue_publish("system/watch", EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(w) : (FL_(list_watch)[w])); - } else { - Mqtt::queue_publish("system_data/watch", EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(w) : (FL_(list_watch)[w])); - } - } - EMSESP::watch(w); - return true; - } else if (i) { - if (Mqtt::publish_single() && i != EMSESP::watch_id()) { - if (Mqtt::publish_single2cmd()) { - Mqtt::queue_publish("system/watch", Helpers::hextoa(i)); - } else { - Mqtt::queue_publish("system_data/watch", Helpers::hextoa(i)); - } - } - EMSESP::watch_id(i); - if (EMSESP::watch() == EMSESP::Watch::WATCH_OFF) { - EMSESP::watch(EMSESP::Watch::WATCH_ON); - } - return true; - } - return false; -} - -void System::store_nvs_values() { - if (Command::find_command(EMSdevice::DeviceType::BOILER, 0, "nompower", 0) != nullptr) { - Command::call(EMSdevice::DeviceType::BOILER, "nompower", "-1"); // trigger a write - } - EMSESP::analogsensor_.store_counters(); - EMSESP::nvs_.end(); -} - -// Build up a list of all partitions and their version info -void System::get_partition_info() { - partition_info_.clear(); // clear existing data - -#ifdef EMSESP_STANDALONE - // dummy data for standalone mode - version, size, install_date - partition_info_["app0"] = {EMSESP_APP_VERSION, 0, ""}; - partition_info_["app1"] = {"", 0, ""}; - partition_info_["factory"] = {"", 0, ""}; - partition_info_["boot"] = {"", 0, ""}; -#else - - auto current_partition = (const char *)esp_ota_get_running_partition()->label; - - // update the current version and partition name in NVS if not already set - if (EMSESP::nvs_.getString(current_partition) != EMSESP_APP_VERSION || emsesp::EMSESP::nvs_.getBool(emsesp::EMSESP_NVS_BOOT_NEW_FIRMWARE, true)) { - EMSESP::nvs_.putBool(emsesp::EMSESP_NVS_BOOT_NEW_FIRMWARE, false); - EMSESP::nvs_.putString(current_partition, EMSESP_APP_VERSION); - char c[20]; - snprintf(c, sizeof(c), "d_%s", current_partition); - auto t = time(nullptr); - // write timestamp always with new version, if clock is not set, this will be updated with ntp - EMSESP::nvs_.putULong(c, t); - } - - // Loop through all available partitions and update map with the version info pulled from NVS - // Partitions can be app0, app1, factory, boot - esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr); - uint64_t buffer; - - while (it != nullptr) { - bool is_valid = true; - const esp_partition_t * part = esp_partition_get(it); - - if (part->label != nullptr && part->label[0] != '\0') { - // check if partition is valid and not empty - esp_partition_read(part, 0, &buffer, 8); - if (buffer == 0xFFFFFFFFFFFFFFFF) { - is_valid = false; // skip this partition - } - } - - // get the version from the NVS store, and add to map - if (is_valid) { - PartitionInfo p_info; - // if there is an entry for this partition in NVS, get it's version from NVS - p_info.version = EMSESP::nvs_.getString(part->label, "").c_str(); - char c[20]; - snprintf(c, sizeof(c), "d_%s", (const char *)part->label); - time_t d = EMSESP::nvs_.getULong(c, 0); - char time_string[25]; - strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d)); - p_info.install_date = d > 1500000000L ? time_string : ""; - - esp_image_metadata_t meta = {}; - esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size}; - if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) { - p_info.size = meta.image_len / 1024; // actual firmware size in KB - } else { - p_info.size = 0; - } - - partition_info_[part->label] = p_info; - } - - it = esp_partition_next(it); // loop to next partition - } - esp_partition_iterator_release(it); -#endif -} - -// set NTP install time/date for the current partition -// assumes NTP is connected and working -void System::set_partition_install_date() { -#ifndef EMSESP_STANDALONE - auto current_partition = (const char *)esp_ota_get_running_partition()->label; - if (current_partition == nullptr) { - return; // fail-safe - } - - char c[20]; - snprintf(c, sizeof(c), "d_%s", current_partition); - time_t d = EMSESP::nvs_.getULong(c, 0); - if (d < 1500000000L) { - LOG_DEBUG("Setting the install date in partition %s", current_partition); - auto t = time(nullptr) - uuid::get_uptime_sec(); - EMSESP::nvs_.putULong(c, t); - } -#endif -} - -// sets the partition to use on the next restart -bool System::set_partition(const char * partitionname) { -#ifdef EMSESP_STANDALONE - return true; -#else - if (partitionname == nullptr) { - return false; - } - - // Find the partition by label - esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, partitionname); - if (it == nullptr) { - return false; // partition not found - } - - const esp_partition_t * partition = esp_partition_get(it); - esp_partition_iterator_release(it); - - if (partition == nullptr) { - return false; - } - - // Set the boot partition - esp_err_t err = esp_ota_set_boot_partition(partition); - if (err != ESP_OK) { - return false; - } - - // initiate the restart - EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED); - return true; -#endif -} - -// restart EMS-ESP -// app0 or app1, or boot/factory on 16MB boards -void System::system_restart(const char * partitionname) { - // see if we are forcing a partition to use - if (partitionname != nullptr) { -#ifndef EMSESP_STANDALONE - // Factory partition - label will be "factory" - const esp_partition_t * partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL); - if (partition && !strcmp(partition->label, partitionname)) { - esp_ota_set_boot_partition(partition); - } else - // try and find the partition by name - if (strcmp(esp_ota_get_running_partition()->label, partitionname)) { - // not found, get next one in cycle - partition = esp_ota_get_next_update_partition(nullptr); - if (!partition) { - LOG_ERROR("Partition '%s' not found", partitionname); - return; - } - if (strcmp(partition->label, partitionname) && strcmp(partitionname, "boot") != 0) { - partition = esp_ota_get_next_update_partition(partition); - if (!partition || strcmp(partition->label, partitionname)) { - LOG_ERROR("Partition '%s' not found", partitionname); - return; - } - } - // error if partition is empty - uint64_t buffer; - esp_partition_read(partition, 0, &buffer, 8); - if (buffer == 0xFFFFFFFFFFFFFFFF) { - LOG_ERROR("Partition '%s' is empty, not bootable", partition->label); - return; - } - // set the boot partition - esp_ota_set_boot_partition(partition); - } -#endif - LOG_INFO("Restarting EMS-ESP from %s partition", partitionname); - } else { - LOG_INFO("Restarting EMS-ESP..."); - } - - store_nvs_values(); // save any NVS values - - // flush all the log - EMSESP::webLogService.loop(); // dump all to web log - for (int i = 0; i < 10; i++) { - Shell::loop_all(); - delay(10); // give telnet TCP stack time to transmit - } - Serial.flush(); // wait for hardware TX buffer to drain - - Mqtt::disconnect(); // gracefully disconnect MQTT, needed for QOS1 - EMSuart::stop(); // stop UART so there is no interference - -#ifndef EMSESP_STANDALONE - delay(1000); // wait 1 second - ESP.restart(); // ka-boom! - this is the only place where the ESP32 restart is called -#endif -} - -// saves all settings -void System::wifi_reconnect() { - EMSESP::esp32React.getNetworkSettingsService()->read( - [](NetworkSettings & networkSettings) { LOG_INFO("WiFi reconnecting to SSID '%s'...", networkSettings.ssid.c_str()); }); - delay(500); // wait - EMSESP::webSettingsService.save(); // save local settings - EMSESP::esp32React.getNetworkSettingsService()->callUpdateHandlers(); // in case we've changed ssid or password -} - -void System::syslog_init() { - EMSESP::webSettingsService.read([&](WebSettings & settings) { - syslog_enabled_ = settings.syslog_enabled; - syslog_level_ = settings.syslog_level; - syslog_mark_interval_ = settings.syslog_mark_interval; - syslog_host_ = settings.syslog_host; - syslog_port_ = settings.syslog_port; - }); -#ifndef EMSESP_STANDALONE - if (syslog_enabled_) { - // start & configure syslog - syslog_.maximum_log_messages(10); - syslog_.log_level((uuid::log::Level)syslog_level_); - syslog_.mark_interval(syslog_mark_interval_); - syslog_.destination(syslog_host_.c_str(), syslog_port_); - syslog_.hostname(hostname()); - EMSESP::logger().info("Starting Syslog service"); - } else if (syslog_.started()) { - // in case service is still running, this flushes the queue - // https://github.com/emsesp/EMS-ESP/issues/496 - EMSESP::logger().info("Stopping Syslog"); - syslog_.loop(); - syslog_.log_level(uuid::log::Level::OFF); // stop server - syslog_.mark_interval(0); - // syslog_.destination(""); - } - if (Mqtt::publish_single()) { - if (Mqtt::publish_single2cmd()) { - Mqtt::queue_publish("system/syslog", syslog_enabled_ ? (FL_(list_syslog_level)[syslog_level_ + 1]) : "off"); - if (EMSESP::watch_id() == 0 || EMSESP::watch() == 0) { - Mqtt::queue_publish("system/watch", - EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(EMSESP::watch()) : (FL_(list_watch)[EMSESP::watch()])); - } else { - Mqtt::queue_publish("system/watch", Helpers::hextoa(EMSESP::watch_id())); - } - - } else { - Mqtt::queue_publish("system_data/syslog", syslog_enabled_ ? (FL_(list_syslog_level)[syslog_level_ + 1]) : "off"); - if (EMSESP::watch_id() == 0 || EMSESP::watch() == 0) { - Mqtt::queue_publish("system_data/watch", - EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(EMSESP::watch()) : (FL_(list_watch)[EMSESP::watch()])); - } else { - Mqtt::queue_publish("system_data/watch", Helpers::hextoa(EMSESP::watch_id())); - } - } - } -#endif -} - -// read specific major system settings to store locally for faster access -void System::store_settings(WebSettings & settings) { - version_ = settings.version; - - rx_gpio_ = settings.rx_gpio; - tx_gpio_ = settings.tx_gpio; - pbutton_gpio_ = settings.pbutton_gpio; - dallas_gpio_ = settings.dallas_gpio; - led_gpio_ = settings.led_gpio; - - analog_enabled_ = settings.analog_enabled; - low_clock_ = settings.low_clock; - hide_led_ = settings.hide_led; - led_type_ = settings.led_type; - board_profile_ = settings.board_profile; - telnet_enabled_ = settings.telnet_enabled; - - tx_mode_ = settings.tx_mode; - syslog_enabled_ = settings.syslog_enabled; - syslog_level_ = settings.syslog_level; - syslog_mark_interval_ = settings.syslog_mark_interval; - syslog_host_ = settings.syslog_host; - syslog_port_ = settings.syslog_port; - - fahrenheit_ = settings.fahrenheit; - bool_format_ = settings.bool_format; - bool_dashboard_ = settings.bool_dashboard; - enum_format_ = settings.enum_format; - readonly_mode_ = settings.readonly_mode; - - phy_type_ = settings.phy_type; - eth_power_ = settings.eth_power; - eth_phy_addr_ = settings.eth_phy_addr; - eth_clock_mode_ = settings.eth_clock_mode; - - locale_ = settings.locale; - developer_mode_ = settings.developer_mode; - - // start services - if (settings.modbus_enabled) { - if (EMSESP::modbus_ == nullptr) { - EMSESP::modbus_ = new Modbus; - EMSESP::modbus_->start(1, settings.modbus_port, settings.modbus_max_clients, settings.modbus_timeout * 1000); - } else if (settings.modbus_port != modbus_port_ || settings.modbus_max_clients != modbus_max_clients_ || settings.modbus_timeout != modbus_timeout_) { - EMSESP::modbus_->stop(); - EMSESP::modbus_->start(1, settings.modbus_port, settings.modbus_max_clients, settings.modbus_timeout * 1000); - } - } else if (EMSESP::modbus_ != nullptr) { - EMSESP::modbus_->stop(); - delete EMSESP::modbus_; - EMSESP::modbus_ = nullptr; - } - modbus_enabled_ = settings.modbus_enabled; - modbus_port_ = settings.modbus_port; - modbus_max_clients_ = settings.modbus_max_clients; - modbus_timeout_ = settings.modbus_timeout; -} - -// Starts up core services -void System::start() { - get_partition_info(); // get the partition info - -#ifndef EMSESP_STANDALONE - // disable bluetooth module - // periph_module_disable(PERIPH_BT_MODULE); - if (low_clock_) { -#if CONFIG_IDF_TARGET_ESP32C3 - setCpuFrequencyMhz(80); -#else - setCpuFrequencyMhz(160); -#endif - } - - // get current memory values - fstotal_ = LittleFS.totalBytes() / 1024; // read only once, it takes 500 ms to read - appused_ = ESP.getSketchSize() / 1024; - appfree_ = esp_ota_get_running_partition()->size / 1024 - appused_; - refreshHeapMem(); // refresh free heap and max alloc heap -#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 -#if ESP_IDF_VERSION_MAJOR < 5 - temp_sensor_config_t temp_sensor = TSENS_CONFIG_DEFAULT(); - temp_sensor_get_config(&temp_sensor); - temp_sensor.dac_offset = TSENS_DAC_DEFAULT; // DEFAULT: range:-10℃ ~ 80℃, error < 1℃. - temp_sensor_set_config(temp_sensor); - temp_sensor_start(); - temp_sensor_read_celsius(&temperature_); -#else - temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); - temperature_sensor_install(&temp_sensor_config, &temperature_handle_); - temperature_sensor_enable(temperature_handle_); - temperature_sensor_get_celsius(temperature_handle_, &temperature_); -#endif -#endif -#endif - - EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { - hostname(networkSettings.hostname.c_str()); // sets the hostname - }); - - commands_init(); // console & api commands - led_init(); // init LED - button_init(); // button - network_init(); // network - uart_init(); // start UART - syslog_init(); // start syslog -} - -// button single click -void System::button_OnClick(PButton & b) { - LOG_NOTICE("Button pressed - single click"); - -#if defined(EMSESP_TEST) -#ifndef EMSESP_STANDALONE - // show filesystem - Test::listDir(LittleFS, "/", 3); -#endif -#endif -} - -// button double click -void System::button_OnDblClick(PButton & b) { - LOG_NOTICE("Button pressed - double click - wifi reconnect to AP"); - // set AP mode to always so will join AP if wifi ssid fails to connect - EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & apSettings) { - apSettings.provisionMode = AP_MODE_ALWAYS; - return StateUpdateResult::CHANGED; - }); - // remove SSID from network settings - EMSESP::esp32React.getNetworkSettingsService()->update([&](NetworkSettings & networkSettings) { - networkSettings.ssid = ""; - return StateUpdateResult::CHANGED; - }); - EMSESP::esp32React.getNetworkSettingsService()->callUpdateHandlers(); // in case we've changed ssid or password -} - -// LED flash every 100ms -void System::led_flash() { - static bool led_flash_state_ = false; - static uint32_t last_toggle_time_ = 0; - uint32_t current_time = uuid::get_uptime(); - - if (current_time - last_toggle_time_ >= 100) { // every 100ms - led_flash_state_ = !led_flash_state_; - last_toggle_time_ = current_time; - - if (led_flash_type_) { - uint8_t intensity = led_flash_state_ ? RGB_LED_BRIGHTNESS : 0; - EMSESP_RGB_WRITE(led_flash_gpio_, intensity, intensity, 0); // RGB LED - Yellow - } else { - digitalWrite(led_flash_gpio_, led_flash_state_ ? LED_ON : !LED_ON); // Standard LED - } - } - - // after duration, turn off the LED - if (current_time - led_flash_start_time_ >= led_flash_duration_) { - if (led_flash_type_) { - EMSESP_RGB_WRITE(led_flash_gpio_, 0, 0, 0); - } else { - digitalWrite(led_flash_gpio_, !LED_ON); - } - led_flash_timer_ = false; - command_format(nullptr, 0); // Execute format operation - } -} - -// Start the LED flash timer - duration in seconds -void System::start_led_flash(uint8_t duration) { - // Don't start if already running - if (led_flash_timer_) { - return; - } - - // Get LED settings - EMSESP::webSettingsService.read([&](WebSettings & settings) { - led_flash_type_ = settings.led_type; - led_flash_gpio_ = settings.led_gpio; - }); - - // Reset counter and state - led_flash_start_time_ = uuid::get_uptime(); // current time - led_flash_duration_ = duration * 1000; // duration in milliseconds - led_flash_timer_ = true; // it's active -} - -// button long press -void System::button_OnLongPress(PButton & b) { - LOG_NOTICE("Button pressed - long press - restart EMS-ESP"); - EMSESP::system_.system_restart("boot"); -} - -// button indefinite press -void System::button_OnVLongPress(PButton & b) { - LOG_NOTICE("Button pressed - very long press - perform factory reset"); - start_led_flash(5); // Start LED flash timer for 5 seconds -} - -// push button -void System::button_init() { -#ifndef EMSESP_STANDALONE - if (!myPButton_.init(pbutton_gpio_, HIGH)) { - LOG_WARNING("Multi-functional button not detected"); - return; - } - LOG_DEBUG("Multi-functional button enabled"); - - myPButton_.onClick(BUTTON_Debounce, button_OnClick); - myPButton_.onDblClick(BUTTON_DblClickDelay, button_OnDblClick); - myPButton_.onLongPress(BUTTON_LongPressDelay, button_OnLongPress); - myPButton_.onVLongPress(BUTTON_VLongPressDelay, button_OnVLongPress); -#endif -} - -// set the LED to on or off when in normal operating mode -void System::led_init() { - // disabled old led port before setting new one - led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); - - if ((led_gpio_)) { // 0 means disabled - if (led_type_) { - // rgb LED WS2812B, use Neopixel - EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0); - } else { - pinMode(led_gpio_, OUTPUT); - digitalWrite(led_gpio_, !LED_ON); // start with LED off - } - } else { - LOG_INFO("LED disabled"); - } -} - -void System::uart_init() { - EMSuart::stop(); - EMSuart::start(tx_mode_, rx_gpio_, tx_gpio_); // start UART, GPIOs have already been checked - EMSESP::txservice_.start(); // reset counters and send devices request -} - -// checks system health and handles LED flashing wizardry -// returns true if the LED flash is active -bool System::loop() { - // check if we're supposed to do a reset/restart - if (systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED) { - system_restart(); - } - - // if LED flashing is active, run the LED flash - if (led_flash_timer_) { - led_flash(); - return true; // is active - } - - led_monitor(); // check status and report back using the LED - myPButton_.check(); // check button press - system_check(); // check system health - -// syslog -#ifndef EMSESP_STANDALONE - if (syslog_enabled_) { - syslog_.loop(); - } -#endif - - send_info_mqtt(); - - return false; // LED flashing is not active -} - -// send MQTT info topic appended with the version information as JSON, as a retained flag -// this is only done once when the connection is established -void System::send_info_mqtt() { - static uint8_t _connection = 0; - uint8_t connection = (ethernet_connected() ? 1 : 0) + ((WiFi.status() == WL_CONNECTED) ? 2 : 0) + (ntp_connected_ ? 4 : 0) + (has_ipv6_ ? 8 : 0); - // check if connection status has changed - if (!Mqtt::connected() || connection == _connection) { - return; - } - _connection = connection; - JsonDocument doc; - // doc["event"] = "connected"; - doc["version"] = EMSESP_APP_VERSION; - - // if NTP is enabled send the boot_time in local time in ISO 8601 format (eg: 2022-11-15 20:46:38) - // https://github.com/emsesp/EMS-ESP32/issues/751 - if (ntp_connected_) { - char time_string[25]; - time_t now = time(nullptr) - uuid::get_uptime_sec(); - strftime(time_string, 25, "%FT%T%z", localtime(&now)); - doc["bootTime"] = time_string; - } - -#ifndef EMSESP_STANDALONE - if (EMSESP::system_.ethernet_connected()) { - doc["network"] = "ethernet"; - doc["hostname"] = ETH.getHostname(); - /* + + // send message - to system log and MQTT + bool System::command_message(const char * value, const int8_t id, JsonObject output) { + if (value == nullptr || value[0] == '\0') { + LOG_WARNING("Message is empty"); + return false; // must have a string value + } + + EMSESP::webSchedulerService.computed_value.clear(); + EMSESP::webSchedulerService.raw_value = value; + for (uint16_t wait = 0; wait < 2000 && !EMSESP::webSchedulerService.raw_value.empty(); wait++) { + delay(1); + } + + if (EMSESP::webSchedulerService.computed_value.empty()) { + LOG_WARNING("Message result is empty"); + return false; + } + + LOG_INFO("Message: %s", EMSESP::webSchedulerService.computed_value.c_str()); // send to log + Mqtt::queue_publish(F_(message), EMSESP::webSchedulerService.computed_value); // send to MQTT if enabled + output["api_data"] = EMSESP::webSchedulerService.computed_value; // send to API + + return true; + } + + // watch + bool System::command_watch(const char * value, const int8_t id) { + uint8_t w = 0xff; + uint16_t i = Helpers::hextoint(value); + if (Helpers::value2enum(value, w, FL_(list_watch))) { + if (w == 0 || EMSESP::watch() == EMSESP::Watch::WATCH_OFF) { + EMSESP::watch_id(0); + } + if (Mqtt::publish_single() && w != EMSESP::watch()) { + if (Mqtt::publish_single2cmd()) { + Mqtt::queue_publish("system/watch", EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(w) : (FL_(list_watch)[w])); + } else { + Mqtt::queue_publish("system_data/watch", EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(w) : (FL_(list_watch)[w])); + } + } + EMSESP::watch(w); + return true; + } else if (i) { + if (Mqtt::publish_single() && i != EMSESP::watch_id()) { + if (Mqtt::publish_single2cmd()) { + Mqtt::queue_publish("system/watch", Helpers::hextoa(i)); + } else { + Mqtt::queue_publish("system_data/watch", Helpers::hextoa(i)); + } + } + EMSESP::watch_id(i); + if (EMSESP::watch() == EMSESP::Watch::WATCH_OFF) { + EMSESP::watch(EMSESP::Watch::WATCH_ON); + } + return true; + } + return false; + } + + void System::store_nvs_values() { + if (Command::find_command(EMSdevice::DeviceType::BOILER, 0, "nompower", 0) != nullptr) { + Command::call(EMSdevice::DeviceType::BOILER, "nompower", "-1"); // trigger a write + } + EMSESP::analogsensor_.store_counters(); + EMSESP::nvs_.end(); + } + + // Build up a list of all partitions and their version info + void System::get_partition_info() { + partition_info_.clear(); // clear existing data + + #ifdef EMSESP_STANDALONE + // dummy data for standalone mode - version, size, install_date + partition_info_["app0"] = {EMSESP_APP_VERSION, 0, ""}; + partition_info_["app1"] = {"", 0, ""}; + partition_info_["factory"] = {"", 0, ""}; + partition_info_["boot"] = {"", 0, ""}; + #else + + auto current_partition = (const char *)esp_ota_get_running_partition()->label; + + // update the current version and partition name in NVS if not already set + if (EMSESP::nvs_.getString(current_partition) != EMSESP_APP_VERSION || emsesp::EMSESP::nvs_.getBool(emsesp::EMSESP_NVS_BOOT_NEW_FIRMWARE, true)) { + EMSESP::nvs_.putBool(emsesp::EMSESP_NVS_BOOT_NEW_FIRMWARE, false); + EMSESP::nvs_.putString(current_partition, EMSESP_APP_VERSION); + char c[20]; + snprintf(c, sizeof(c), "d_%s", current_partition); + auto t = time(nullptr); + // write timestamp always with new version, if clock is not set, this will be updated with ntp + EMSESP::nvs_.putULong(c, t); + } + + // Loop through all available partitions and update map with the version info pulled from NVS + // Partitions can be app0, app1, factory, boot + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr); + uint64_t buffer; + + while (it != nullptr) { + bool is_valid = true; + const esp_partition_t * part = esp_partition_get(it); + + if (part->label != nullptr && part->label[0] != '\0') { + // check if partition is valid and not empty + esp_partition_read(part, 0, &buffer, 8); + if (buffer == 0xFFFFFFFFFFFFFFFF) { + is_valid = false; // skip this partition + } + } + + // get the version from the NVS store, and add to map + if (is_valid) { + PartitionInfo p_info; + // if there is an entry for this partition in NVS, get it's version from NVS + p_info.version = EMSESP::nvs_.getString(part->label, "").c_str(); + char c[20]; + snprintf(c, sizeof(c), "d_%s", (const char *)part->label); + time_t d = EMSESP::nvs_.getULong(c, 0); + char time_string[25]; + strftime(time_string, sizeof(time_string), "%FT%T", localtime(&d)); + p_info.install_date = d > 1500000000L ? time_string : ""; + + esp_image_metadata_t meta = {}; + esp_partition_pos_t part_pos = {.offset = part->address, .size = part->size}; + if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &meta) == ESP_OK) { + p_info.size = meta.image_len / 1024; // actual firmware size in KB + } else { + p_info.size = 0; + } + + partition_info_[part->label] = p_info; + } + + it = esp_partition_next(it); // loop to next partition + } + esp_partition_iterator_release(it); + #endif + } + + // set NTP install time/date for the current partition + // assumes NTP is connected and working + void System::set_partition_install_date() { + #ifndef EMSESP_STANDALONE + auto current_partition = (const char *)esp_ota_get_running_partition()->label; + if (current_partition == nullptr) { + return; // fail-safe + } + + char c[20]; + snprintf(c, sizeof(c), "d_%s", current_partition); + time_t d = EMSESP::nvs_.getULong(c, 0); + if (d < 1500000000L) { + LOG_DEBUG("Setting the install date in partition %s", current_partition); + auto t = time(nullptr) - uuid::get_uptime_sec(); + EMSESP::nvs_.putULong(c, t); + } + #endif + } + + // sets the partition to use on the next restart + bool System::set_partition(const char * partitionname) { + #ifdef EMSESP_STANDALONE + return true; + #else + if (partitionname == nullptr) { + return false; + } + + // Find the partition by label + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, partitionname); + if (it == nullptr) { + return false; // partition not found + } + + const esp_partition_t * partition = esp_partition_get(it); + esp_partition_iterator_release(it); + + if (partition == nullptr) { + return false; + } + + // Set the boot partition + esp_err_t err = esp_ota_set_boot_partition(partition); + if (err != ESP_OK) { + return false; + } + + // initiate the restart + EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED); + return true; + #endif + } + + // restart EMS-ESP + // app0 or app1, or boot/factory on 16MB boards + void System::system_restart(const char * partitionname) { + // see if we are forcing a partition to use + if (partitionname != nullptr) { + #ifndef EMSESP_STANDALONE + // Factory partition - label will be "factory" + const esp_partition_t * partition = esp_partition_find_first(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_FACTORY, NULL); + if (partition && !strcmp(partition->label, partitionname)) { + esp_ota_set_boot_partition(partition); + } else + // try and find the partition by name + if (strcmp(esp_ota_get_running_partition()->label, partitionname)) { + // not found, get next one in cycle + partition = esp_ota_get_next_update_partition(nullptr); + if (!partition) { + LOG_ERROR("Partition '%s' not found", partitionname); + return; + } + if (strcmp(partition->label, partitionname) && strcmp(partitionname, "boot") != 0) { + partition = esp_ota_get_next_update_partition(partition); + if (!partition || strcmp(partition->label, partitionname)) { + LOG_ERROR("Partition '%s' not found", partitionname); + return; + } + } + // error if partition is empty + uint64_t buffer; + esp_partition_read(partition, 0, &buffer, 8); + if (buffer == 0xFFFFFFFFFFFFFFFF) { + LOG_ERROR("Partition '%s' is empty, not bootable", partition->label); + return; + } + // set the boot partition + esp_ota_set_boot_partition(partition); + } + #endif + LOG_INFO("Restarting EMS-ESP from %s partition", partitionname); + } else { + LOG_INFO("Restarting EMS-ESP..."); + } + + store_nvs_values(); // save any NVS values + + // flush all the log + EMSESP::webLogService.loop(); // dump all to web log + for (int i = 0; i < 10; i++) { + Shell::loop_all(); + delay(10); // give telnet TCP stack time to transmit + } + Serial.flush(); // wait for hardware TX buffer to drain + + Mqtt::disconnect(); // gracefully disconnect MQTT, needed for QOS1 + EMSuart::stop(); // stop UART so there is no interference + #ifndef EMSESP_STANDALONE + delay(1000); // wait 1 second + ESP.restart(); // ka-boom! - this is the only place where the ESP32 restart is called + #endif + } + + // saves all settings + void System::wifi_reconnect() { + EMSESP::esp32React.getNetworkSettingsService()->read( + [](NetworkSettings & networkSettings) { LOG_INFO("WiFi reconnecting to SSID '%s'...", networkSettings.ssid.c_str()); }); + delay(500); // wait + EMSESP::webSettingsService.save(); // save local settings + EMSESP::esp32React.getNetworkSettingsService()->callUpdateHandlers(); // in case we've changed ssid or password + } + + void System::syslog_init() { + EMSESP::webSettingsService.read([&](WebSettings & settings) { + syslog_enabled_ = settings.syslog_enabled; + syslog_level_ = settings.syslog_level; + syslog_mark_interval_ = settings.syslog_mark_interval; + syslog_host_ = settings.syslog_host; + syslog_port_ = settings.syslog_port; + }); + #ifndef EMSESP_STANDALONE + if (syslog_enabled_) { + // start & configure syslog + syslog_.maximum_log_messages(10); + syslog_.log_level((uuid::log::Level)syslog_level_); + syslog_.mark_interval(syslog_mark_interval_); + syslog_.destination(syslog_host_.c_str(), syslog_port_); + syslog_.hostname(hostname()); + EMSESP::logger().info("Starting Syslog service"); + } else if (syslog_.started()) { + // in case service is still running, this flushes the queue + // https://github.com/emsesp/EMS-ESP/issues/496 + EMSESP::logger().info("Stopping Syslog"); + syslog_.loop(); + syslog_.log_level(uuid::log::Level::OFF); // stop server + syslog_.mark_interval(0); + // syslog_.destination(""); + } + if (Mqtt::publish_single()) { + if (Mqtt::publish_single2cmd()) { + Mqtt::queue_publish("system/syslog", syslog_enabled_ ? (FL_(list_syslog_level)[syslog_level_ + 1]) : "off"); + if (EMSESP::watch_id() == 0 || EMSESP::watch() == 0) { + Mqtt::queue_publish("system/watch", + EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(EMSESP::watch()) : (FL_(list_watch)[EMSESP::watch()])); + } else { + Mqtt::queue_publish("system/watch", Helpers::hextoa(EMSESP::watch_id())); + } + + } else { + Mqtt::queue_publish("system_data/syslog", syslog_enabled_ ? (FL_(list_syslog_level)[syslog_level_ + 1]) : "off"); + if (EMSESP::watch_id() == 0 || EMSESP::watch() == 0) { + Mqtt::queue_publish("system_data/watch", + EMSESP::system_.enum_format() == ENUM_FORMAT_INDEX ? Helpers::itoa(EMSESP::watch()) : (FL_(list_watch)[EMSESP::watch()])); + } else { + Mqtt::queue_publish("system_data/watch", Helpers::hextoa(EMSESP::watch_id())); + } + } + } + #endif + } + + // read specific major system settings to store locally for faster access + void System::store_settings(WebSettings & settings) { + version_ = settings.version; + + rx_gpio_ = settings.rx_gpio; + tx_gpio_ = settings.tx_gpio; + pbutton_gpio_ = settings.pbutton_gpio; + dallas_gpio_ = settings.dallas_gpio; + led_gpio_ = settings.led_gpio; + + analog_enabled_ = settings.analog_enabled; + low_clock_ = settings.low_clock; + hide_led_ = settings.hide_led; + led_type_ = settings.led_type; + board_profile_ = settings.board_profile; + telnet_enabled_ = settings.telnet_enabled; + + tx_mode_ = settings.tx_mode; + syslog_enabled_ = settings.syslog_enabled; + syslog_level_ = settings.syslog_level; + syslog_mark_interval_ = settings.syslog_mark_interval; + syslog_host_ = settings.syslog_host; + syslog_port_ = settings.syslog_port; + + fahrenheit_ = settings.fahrenheit; + bool_format_ = settings.bool_format; + bool_dashboard_ = settings.bool_dashboard; + enum_format_ = settings.enum_format; + readonly_mode_ = settings.readonly_mode; + + phy_type_ = settings.phy_type; + eth_power_ = settings.eth_power; + eth_phy_addr_ = settings.eth_phy_addr; + eth_clock_mode_ = settings.eth_clock_mode; + + locale_ = settings.locale; + developer_mode_ = settings.developer_mode; + // start services + if (settings.modbus_enabled) { + if (EMSESP::modbus_ == nullptr) { + EMSESP::modbus_ = new Modbus; + EMSESP::modbus_->start(1, settings.modbus_port, settings.modbus_max_clients, settings.modbus_timeout * 1000); + } else if (settings.modbus_port != modbus_port_ || settings.modbus_max_clients != modbus_max_clients_ || settings.modbus_timeout != modbus_timeout_) { + EMSESP::modbus_->stop(); + EMSESP::modbus_->start(1, settings.modbus_port, settings.modbus_max_clients, settings.modbus_timeout * 1000); + } + } else if (EMSESP::modbus_ != nullptr) { + EMSESP::modbus_->stop(); + delete EMSESP::modbus_; + EMSESP::modbus_ = nullptr; + } + modbus_enabled_ = settings.modbus_enabled; + modbus_port_ = settings.modbus_port; + modbus_max_clients_ = settings.modbus_max_clients; + modbus_timeout_ = settings.modbus_timeout; + } + + // Starts up core services + void System::start() { + get_partition_info(); // get the partition info + + #ifndef EMSESP_STANDALONE + // disable bluetooth module + // periph_module_disable(PERIPH_BT_MODULE); + if (low_clock_) { + #if CONFIG_IDF_TARGET_ESP32C3 + setCpuFrequencyMhz(80); + #else + setCpuFrequencyMhz(160); + #endif + } + + // get current memory values + fstotal_ = LittleFS.totalBytes() / 1024; // read only once, it takes 500 ms to read + appused_ = ESP.getSketchSize() / 1024; + appfree_ = esp_ota_get_running_partition()->size / 1024 - appused_; + refreshHeapMem(); // refresh free heap and max alloc heap + #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 + temperature_sensor_config_t temp_sensor_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); + temperature_sensor_install(&temp_sensor_config, &temperature_handle_); + temperature_sensor_enable(temperature_handle_); + temperature_sensor_get_celsius(temperature_handle_, &temperature_); + #endif + #endif + + EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) { + hostname(networkSettings.hostname.c_str()); // sets the hostname + }); + + commands_init(); // console & api commands + led_init(); // init LED + button_init(); // button + network_init(); // network + uart_init(); // start UART + syslog_init(); // start syslog + } + + // button single click + void System::button_OnClick(PButton & b) { + LOG_NOTICE("Button pressed - single click"); + + #if defined(EMSESP_TEST) + #ifndef EMSESP_STANDALONE + // show filesystem + Test::listDir(LittleFS, "/", 3); + #endif + #endif + } + + // button double click + void System::button_OnDblClick(PButton & b) { + LOG_NOTICE("Button pressed - double click - wifi reconnect to AP"); + // set AP mode to always so will join AP if wifi ssid fails to connect + EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & apSettings) { + apSettings.provisionMode = AP_MODE_ALWAYS; + return StateUpdateResult::CHANGED; + }); + // remove SSID from network settings + EMSESP::esp32React.getNetworkSettingsService()->update([&](NetworkSettings & networkSettings) { + networkSettings.ssid = ""; + return StateUpdateResult::CHANGED; + }); + EMSESP::esp32React.getNetworkSettingsService()->callUpdateHandlers(); // in case we've changed ssid or password + } + + // LED flash every 100ms + void System::led_flash() { + static bool led_flash_state_ = false; + static uint32_t last_toggle_time_ = 0; + uint32_t current_time = uuid::get_uptime(); + + if (current_time - last_toggle_time_ >= 100) { // every 100ms + led_flash_state_ = !led_flash_state_; + last_toggle_time_ = current_time; + + if (led_flash_type_) { + uint8_t intensity = led_flash_state_ ? RGB_LED_BRIGHTNESS : 0; + EMSESP_RGB_WRITE(led_flash_gpio_, intensity, intensity, 0); // RGB LED - Yellow + } else { + digitalWrite(led_flash_gpio_, led_flash_state_ ? LED_ON : !LED_ON); // Standard LED + } + } + + // after duration, turn off the LED + if (current_time - led_flash_start_time_ >= led_flash_duration_) { + if (led_flash_type_) { + EMSESP_RGB_WRITE(led_flash_gpio_, 0, 0, 0); + } else { + digitalWrite(led_flash_gpio_, !LED_ON); + } + led_flash_timer_ = false; + command_format(nullptr, 0); // Execute format operation + } + } + + // Start the LED flash timer - duration in seconds + void System::start_led_flash(uint8_t duration) { + // Don't start if already running + if (led_flash_timer_) { + return; + } + + // Get LED settings + EMSESP::webSettingsService.read([&](WebSettings & settings) { + led_flash_type_ = settings.led_type; + led_flash_gpio_ = settings.led_gpio; + }); + + // Reset counter and state + led_flash_start_time_ = uuid::get_uptime(); // current time + led_flash_duration_ = duration * 1000; // duration in milliseconds + led_flash_timer_ = true; // it's active + } + + // button long press + void System::button_OnLongPress(PButton & b) { + LOG_NOTICE("Button pressed - long press - restart EMS-ESP"); + EMSESP::system_.system_restart("boot"); + } + + // button indefinite press + void System::button_OnVLongPress(PButton & b) { + LOG_NOTICE("Button pressed - very long press - perform factory reset"); + start_led_flash(5); // Start LED flash timer for 5 seconds + } + + // push button + void System::button_init() { + #ifndef EMSESP_STANDALONE + if (!myPButton_.init(pbutton_gpio_, HIGH)) { + LOG_WARNING("Multi-functional button not detected"); + return; + } + LOG_DEBUG("Multi-functional button enabled"); + + myPButton_.onClick(BUTTON_Debounce, button_OnClick); + myPButton_.onDblClick(BUTTON_DblClickDelay, button_OnDblClick); + myPButton_.onLongPress(BUTTON_LongPressDelay, button_OnLongPress); + myPButton_.onVLongPress(BUTTON_VLongPressDelay, button_OnVLongPress); + #endif + } + + // set the LED to on or off when in normal operating mode + void System::led_init() { + // disabled old led port before setting new one + led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); + + if ((led_gpio_)) { // 0 means disabled + if (led_type_) { + // rgb LED WS2812B, use Neopixel + EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0); + } else { + pinMode(led_gpio_, OUTPUT); + digitalWrite(led_gpio_, !LED_ON); // start with LED off + } + } else { + LOG_INFO("LED disabled"); + } + } + + void System::uart_init() { + EMSuart::stop(); + EMSuart::start(tx_mode_, rx_gpio_, tx_gpio_); // start UART, GPIOs have already been checked + EMSESP::txservice_.start(); // reset counters and send devices request + } + + // checks system health and handles LED flashing wizardry + // returns true if the LED flash is active + bool System::loop() { + // check if we're supposed to do a reset/restart + if (systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED) { + system_restart(); + } + + // if LED flashing is active, run the LED flash + if (led_flash_timer_) { + led_flash(); + return true; // is active + } + + led_monitor(); // check status and report back using the LED + myPButton_.check(); // check button press + system_check(); // check system health + + // syslog + #ifndef EMSESP_STANDALONE + if (syslog_enabled_) { + syslog_.loop(); + } + #endif + + send_info_mqtt(); + + return false; // LED flashing is not active + } + + // send MQTT info topic appended with the version information as JSON, as a retained flag + // this is only done once when the connection is established + void System::send_info_mqtt() { + static uint8_t _connection = 0; + uint8_t connection = (ethernet_connected() ? 1 : 0) + ((WiFi.status() == WL_CONNECTED) ? 2 : 0) + (ntp_connected_ ? 4 : 0) + (has_ipv6_ ? 8 : 0); + // check if connection status has changed + if (!Mqtt::connected() || connection == _connection) { + return; + } + _connection = connection; + JsonDocument doc; + // doc["event"] = "connected"; + doc["version"] = EMSESP_APP_VERSION; + + // if NTP is enabled send the boot_time in local time in ISO 8601 format (eg: 2022-11-15 20:46:38) + // https://github.com/emsesp/EMS-ESP32/issues/751 + if (ntp_connected_) { + char time_string[25]; + time_t now = time(nullptr) - uuid::get_uptime_sec(); + strftime(time_string, 25, "%FT%T%z", localtime(&now)); + doc["bootTime"] = time_string; + } + + #ifndef EMSESP_STANDALONE + if (EMSESP::system_.ethernet_connected()) { + doc["network"] = "ethernet"; + doc["hostname"] = ETH.getHostname(); + /* doc["MAC"] = ETH.macAddress(); doc["IPv4 address"] = uuid::printable_to_string(ETH.localIP()) + "/" + uuid::printable_to_string(ETH.subnetMask()); doc["IPv4 gateway"] = uuid::printable_to_string(ETH.gatewayIP()); @@ -830,826 +932,797 @@ void System::send_info_mqtt() { doc["IPv6 address"] = uuid::printable_to_string(ETH.localIPv6()); } */ - - } else if (WiFi.status() == WL_CONNECTED) { - doc["network"] = "wifi"; - doc["hostname"] = WiFi.getHostname(); - doc["SSID"] = WiFi.SSID(); - doc["BSSID"] = WiFi.BSSIDstr(); - doc["MAC"] = WiFi.macAddress(); - doc["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask()); - doc["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP()); - doc["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP()); - -#if ESP_IDF_VERSION_MAJOR < 5 - if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.localIPv6().toString() != "::") { - doc["IPv6 address"] = uuid::printable_to_string(WiFi.localIPv6()); - } -#else - if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { - doc["IPv6 address"] = uuid::printable_to_string(WiFi.linkLocalIPv6()); - } - -#endif - } -#endif - Mqtt::queue_publish_retain(F_(info), doc.as()); // topic called "info" and it's Retained -} - -// create the json for heartbeat -void System::heartbeat_json(JsonObject output) { - switch (EMSESP::bus_status()) { - case EMSESP::BUS_STATUS_OFFLINE: - output["bus_status"] = "connecting"; // EMS-ESP is booting... - break; - case EMSESP::BUS_STATUS_TX_ERRORS: - output["bus_status"] = "txerror"; - break; - case EMSESP::BUS_STATUS_CONNECTED: - output["bus_status"] = "connected"; - break; - default: - output["bus_status"] = "disconnected"; - break; - } - - output["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3); - output["uptime_sec"] = uuid::get_uptime_sec(); - - output["rxreceived"] = EMSESP::rxservice_.telegram_count(); - output["rxfails"] = EMSESP::rxservice_.telegram_error_count(); - output["txreads"] = EMSESP::txservice_.telegram_read_count(); - output["txwrites"] = EMSESP::txservice_.telegram_write_count(); - output["txfails"] = EMSESP::txservice_.telegram_read_fail_count() + EMSESP::txservice_.telegram_write_fail_count(); - - if (Mqtt::enabled()) { - output["mqttcount"] = Mqtt::publish_count(); - output["mqttfails"] = Mqtt::publish_fails(); - output["mqttreconnects"] = Mqtt::connect_count(); - } - output["apicalls"] = WebAPIService::api_count(); // + WebAPIService::api_fails(); - output["apifails"] = WebAPIService::api_fails(); - - if (EMSESP::sensor_enabled() || EMSESP::analog_enabled()) { - output["sensorreads"] = EMSESP::temperaturesensor_.reads() + EMSESP::analogsensor_.reads(); - output["sensorfails"] = EMSESP::temperaturesensor_.fails() + EMSESP::analogsensor_.fails(); - } - + + } else if (WiFi.status() == WL_CONNECTED) { + doc["network"] = "wifi"; + doc["hostname"] = WiFi.getHostname(); + doc["SSID"] = WiFi.SSID(); + doc["BSSID"] = WiFi.BSSIDstr(); + doc["MAC"] = WiFi.macAddress(); + doc["IPv4 address"] = uuid::printable_to_string(WiFi.localIP()) + "/" + uuid::printable_to_string(WiFi.subnetMask()); + doc["IPv4 gateway"] = uuid::printable_to_string(WiFi.gatewayIP()); + doc["IPv4 nameserver"] = uuid::printable_to_string(WiFi.dnsIP()); + + if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { + doc["IPv6 address"] = uuid::printable_to_string(WiFi.linkLocalIPv6()); + } + } + #endif + Mqtt::queue_publish_retain(F_(info), doc.as()); // topic called "info" and it's Retained + } + + // create the json for heartbeat + void System::heartbeat_json(JsonObject output) { + switch (EMSESP::bus_status()) { + case EMSESP::BUS_STATUS_OFFLINE: + output["bus_status"] = "connecting"; // EMS-ESP is booting... + break; + case EMSESP::BUS_STATUS_TX_ERRORS: + output["bus_status"] = "txerror"; + break; + case EMSESP::BUS_STATUS_CONNECTED: + output["bus_status"] = "connected"; + break; + default: + output["bus_status"] = "disconnected"; + break; + } + + output["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3); + output["uptime_sec"] = uuid::get_uptime_sec(); + + output["rxreceived"] = EMSESP::rxservice_.telegram_count(); + output["rxfails"] = EMSESP::rxservice_.telegram_error_count(); + output["txreads"] = EMSESP::txservice_.telegram_read_count(); + output["txwrites"] = EMSESP::txservice_.telegram_write_count(); + output["txfails"] = EMSESP::txservice_.telegram_read_fail_count() + EMSESP::txservice_.telegram_write_fail_count(); + + if (Mqtt::enabled()) { + output["mqttcount"] = Mqtt::publish_count(); + output["mqttfails"] = Mqtt::publish_fails(); + output["mqttreconnects"] = Mqtt::connect_count(); + } + output["apicalls"] = WebAPIService::api_count(); // + WebAPIService::api_fails(); + output["apifails"] = WebAPIService::api_fails(); + + if (EMSESP::sensor_enabled() || EMSESP::analog_enabled()) { + output["sensorreads"] = EMSESP::temperaturesensor_.reads() + EMSESP::analogsensor_.reads(); + output["sensorfails"] = EMSESP::temperaturesensor_.fails() + EMSESP::analogsensor_.fails(); + } + + #ifndef EMSESP_STANDALONE + output["freemem"] = getHeapMem(); + output["max_alloc"] = getMaxAllocMem(); + #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 + output["temperature"] = (int)temperature_; + #endif + #endif + + #ifndef EMSESP_STANDALONE + if (!ethernet_connected_) { + int8_t rssi = WiFi.RSSI(); + output["rssi"] = rssi; + output["wifistrength"] = wifi_quality(rssi); + output["wifireconnects"] = EMSESP::esp32React.getWifiReconnects(); + } + #endif + } + + // send periodic MQTT message with system information + void System::send_heartbeat() { + refreshHeapMem(); // refresh free heap and max alloc heap + + JsonDocument doc; + JsonObject json = doc.to(); + + heartbeat_json(json); + Mqtt::queue_publish(F_(heartbeat), json); // send to MQTT with retain off. This will add to MQTT queue. + } + + // initializes network + void System::network_init() { + last_system_check_ = 0; // force the LED to go from fast flash to pulse + + #if CONFIG_IDF_TARGET_ESP32 + bool disableEth; + EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & settings) { disableEth = settings.ssid.length() > 0; }); + + // no ethernet present or disabled + if (phy_type_ == PHY_type::PHY_TYPE_NONE || disableEth) { + return; + } // no ethernet present + + // configure Ethernet + int mdc = 23; // Pin# of the I²C clock signal for the Ethernet PHY - hardcoded + int mdio = 18; // Pin# of the I²C IO signal for the Ethernet PHY - hardcoded + uint8_t phy_addr = eth_phy_addr_; // I²C-address of Ethernet PHY (0 or 1 for LAN8720, 31 for TLK110) + int8_t power = eth_power_; // Pin# of the enable signal for the external crystal oscillator (-1 to disable for internal APLL source) + eth_phy_type_t type = (phy_type_ == PHY_type::PHY_TYPE_LAN8720) ? ETH_PHY_LAN8720 + : (phy_type_ == PHY_type::PHY_TYPE_TLK110) ? ETH_PHY_TLK110 + : ETH_PHY_RTL8201; // Type of the Ethernet PHY (LAN8720 or TLK110) + // clock mode: + // ETH_CLOCK_GPIO0_IN = 0 RMII clock input to GPIO0 + // ETH_CLOCK_GPIO0_OUT = 1 RMII clock output from GPIO0 + // ETH_CLOCK_GPIO16_OUT = 2 RMII clock output from GPIO16 + // ETH_CLOCK_GPIO17_OUT = 3 RMII clock output from GPIO17, for 50hz inverted clock + auto clock_mode = (eth_clock_mode_t)eth_clock_mode_; + + // reset power and add a delay as ETH doesn't not always start up correctly after a warm boot + if (eth_power_ != -1) { + pinMode(eth_power_, OUTPUT); + digitalWrite(eth_power_, LOW); + delay(500); + digitalWrite(eth_power_, HIGH); + } + eth_present_ = ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode); + #endif + } + + // check health of system, done every 5 seconds + void System::system_check() { + uint32_t current_uptime = uuid::get_uptime(); + if (!last_system_check_ || ((uint32_t)(current_uptime - last_system_check_) >= SYSTEM_CHECK_FREQUENCY)) { + last_system_check_ = current_uptime; + + #ifndef EMSESP_STANDALONE + #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 + temperature_sensor_get_celsius(temperature_handle_, &temperature_); + #endif + #endif + + #ifdef EMSESP_PINGTEST + static uint64_t ping_count = 0; + LOG_NOTICE("Ping test, #%d", ping_count++); + #endif + + // check if we have a valid network connection + if (!ethernet_connected() && (WiFi.status() != WL_CONNECTED)) { + healthcheck_ |= HEALTHCHECK_NO_NETWORK; + } else { + healthcheck_ &= ~HEALTHCHECK_NO_NETWORK; + } + + // check if we have a bus connection + if (!EMSbus::bus_connected()) { + healthcheck_ |= HEALTHCHECK_NO_BUS; + } else { + healthcheck_ &= ~HEALTHCHECK_NO_BUS; + } + + // see if the healthcheck state has changed + static uint8_t last_healthcheck_ = 0; + if (healthcheck_ != last_healthcheck_) { + last_healthcheck_ = healthcheck_; + + EMSESP::system_.send_heartbeat(); // send MQTT heartbeat immediately when connected + + // see if we're better now + if (healthcheck_ == 0) { + // everything is healthy, show LED permanently on or off depending on setting + // Green on RGB LED, on/off on standard LED + if (led_gpio_) { + led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, hide_led_ ? 0 : RGB_LED_BRIGHTNESS, 0) + : digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // Green + } + } else { + // turn off LED so we're ready for the warning flashes + if (led_gpio_) { + led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); + } + } + } + } + } + + // commands - takes static function pointers + // can be called via Console using 'call system ' + void System::commands_init() { + Command::add(EMSdevice::DeviceType::SYSTEM, F_(read), System::command_read, FL_(read_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, FL_(send_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, FL_(fetch_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(sendmail), System::command_sendmail, FL_(sendmail_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(format), System::command_format, FL_(format_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(txpause), System::command_txpause, FL_(txpause_cmd), CommandFlag::ADMIN_ONLY); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, FL_(watch_cmd)); + Command::add(EMSdevice::DeviceType::SYSTEM, F_(message), System::command_message, FL_(message_cmd)); + #if defined(EMSESP_TEST) + Command::add(EMSdevice::DeviceType::SYSTEM, ("test"), System::command_test, FL_(test_cmd)); + #endif + + // these commands will return data in JSON format + Command::add(EMSdevice::DeviceType::SYSTEM, F("response"), System::command_response, FL_(commands_response)); + + // MQTT subscribe "ems-esp/system/#" + Mqtt::subscribe(EMSdevice::DeviceType::SYSTEM, "system/#", nullptr); // use empty function callback + } + + // uses LED to show system health + void System::led_monitor() { + // if button is pressed, show LED (yellow on RGB LED, on/off on standard LED) + static bool button_busy_ = false; + if (button_busy_ != myPButton_.button_busy()) { + button_busy_ = myPButton_.button_busy(); + if (led_type_) { + EMSESP_RGB_WRITE(led_gpio_, button_busy_ ? RGB_LED_BRIGHTNESS : 0, button_busy_ ? RGB_LED_BRIGHTNESS : 0, 0); // Yellow + } else { + digitalWrite(led_gpio_, button_busy_ ? LED_ON : !LED_ON); + } + } + + // we only need to run the LED healthcheck if there are errors + // skip if we're in the led_flash_timer or if a button has been pressed + if (!healthcheck_ || !led_gpio_ || button_busy_ || led_flash_timer_) { + return; // all good + } + + static uint32_t led_long_timer_ = 1; // 1 will kick it off immediately + static uint32_t led_short_timer_ = 0; + static uint8_t led_flash_step_ = 0; // 0 means we're not in the short flash timer + + auto current_time = uuid::get_uptime(); + + // first long pause before we start flashing + if (led_long_timer_ && (uint32_t)(current_time - led_long_timer_) >= HEALTHCHECK_LED_LONG_DUARATION) { + led_short_timer_ = current_time; // start the short timer + led_long_timer_ = 0; // stop long timer + led_flash_step_ = 1; // enable the short flash timer + } + + // the flash timer which starts after the long pause + if (led_flash_step_ && (uint32_t)(current_time - led_short_timer_) >= HEALTHCHECK_LED_FLASH_DUARATION) { + led_long_timer_ = 0; // stop the long timer + led_short_timer_ = current_time; + static bool led_on_ = false; + + if (++led_flash_step_ == 8) { + // reset the whole sequence + led_long_timer_ = uuid::get_uptime(); + led_flash_step_ = 0; + led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); // LED off + } else if (led_flash_step_ % 2) { + // handle the step events (on odd numbers 3,5,7,etc). see if we need to turn on a LED + // 1 flash (blue) is the EMS bus is not connected + // 2 flashes (red, red) if the network (wifi or ethernet) is not connected + // 3 flashes (red, red, blue) is both the bus and the network are not connected + bool no_network = (healthcheck_ & HEALTHCHECK_NO_NETWORK) == HEALTHCHECK_NO_NETWORK; + bool no_bus = (healthcheck_ & HEALTHCHECK_NO_BUS) == HEALTHCHECK_NO_BUS; + + if (led_type_) { + if (led_flash_step_ == 3) { + if (no_network) { + EMSESP_RGB_WRITE(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red + } else if (no_bus) { + EMSESP_RGB_WRITE(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue + } + } + if (led_flash_step_ == 5 && no_network) { + EMSESP_RGB_WRITE(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red + } + if ((led_flash_step_ == 7) && no_network && no_bus) { + EMSESP_RGB_WRITE(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue + } + } else { + if ((led_flash_step_ == 3) && (no_network || no_bus)) { + led_on_ = true; + } + + if ((led_flash_step_ == 5) && no_network) { + led_on_ = true; + } + + if ((led_flash_step_ == 7) && no_network && no_bus) { + led_on_ = true; + } + + if (led_on_) { + digitalWrite(led_gpio_, LED_ON); // LED on + } + } + } else { + // turn the led off after the flash, on even number count + if (led_on_) { + led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); + led_on_ = false; + } + } + } + } + + // Return the quality (Received Signal Strength Indicator) of the WiFi network as a % + // High quality: 90% ~= -55dBm + // Medium quality: 50% ~= -75dBm + // Low quality: 30% ~= -85dBm + // Unusable quality: 8% ~= -96dBm + int8_t System::wifi_quality(int8_t dBm) { + if (dBm <= -100) { + return 0; + } + + if (dBm >= -50) { + return 100; + } + return 2 * (dBm + 100); + } + + // print users to console + void System::show_users(uuid::console::Shell & shell) { + if (!shell.has_flags(CommandFlags::ADMIN)) { + shell.printfln("Unauthorized. You need to be an admin to view users."); + return; + } + + shell.printfln("Users:"); + + #ifndef EMSESP_STANDALONE + EMSESP::esp32React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { + for (const User & user : securitySettings.users) { + shell.printfln(" username: %s, password: %s, is_admin: %s", user.username.c_str(), user.password.c_str(), user.admin ? ("yes") : ("no")); + } + }); + #endif + + shell.println(); + } + + // shell command 'show system' + void System::show_system(uuid::console::Shell & shell) { + refreshHeapMem(); // refresh free heap and max alloc heap + + shell.println(); + shell.println("System:"); + shell.printfln(" Version: %s", EMSESP_APP_VERSION); + #ifndef EMSESP_STANDALONE + shell.printfln(" Platform: %s (%s)", EMSESP_PLATFORM, ESP.getChipModel()); + shell.printfln(" Model: %s", getBBQKeesGatewayDetails().c_str()); + #endif + shell.printfln(" Language: %s", locale().c_str()); + shell.printfln(" Board profile: %s", board_profile().c_str()); + shell.printfln(" Uptime: %s", uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3).c_str()); + #ifndef EMSESP_STANDALONE + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/misc_system_api.html + unsigned char mac_base[6] = {0}; + esp_efuse_mac_get_default(mac_base); + esp_read_mac(mac_base, ESP_MAC_WIFI_STA); + shell.printfln(" Base MAC Address: %02X:%02X:%02X:%02X:%02X:%02X", mac_base[0], mac_base[1], mac_base[2], mac_base[3], mac_base[4], mac_base[5]); + + shell.printfln(" SDK version: %s", ESP.getSdkVersion()); + shell.printfln(" CPU frequency: %lu MHz", ESP.getCpuFreqMHz()); + #if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 + shell.printfln(" CPU temperature: %d °C", (int)temperature()); + #endif + shell.printfln(" Free heap/Max alloc: %lu KB / %lu KB", getHeapMem(), getMaxAllocMem()); + shell.printfln(" App used/free: %lu KB / %lu KB", appUsed(), appFree()); + uint32_t FSused = LittleFS.usedBytes() / 1024; + shell.printfln(" FS used/free: %lu KB / %lu KB", FSused, FStotal() - FSused); + shell.printfln(" Flash size: %lu KB", ESP.getFlashChipSize() / 1024); + if (PSram()) { + shell.printfln(" PSRAM size/free: %lu KB / %lu KB", PSram(), ESP.getFreePsram() / 1024); + } else { + shell.printfln(" PSRAM: not available"); + } + // GPIOs + shell.println(" GPIOs:"); + shell.printf(" allowed:"); + for (const auto & gpio : valid_system_gpios_) { + shell.printf(" %d", gpio); + } + shell.printfln(" [total %d]", valid_system_gpios_.size()); + shell.printf(" in use:"); + 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) { + shell.printf(" %d(%s)", gpio.pin, gpio.source.c_str()); + } + shell.printfln(" [total %d]", used_gpios_.size()); + auto available = available_gpios(); + shell.printf(" available:"); + for (const auto & gpio : available) { + shell.printf(" %d", gpio); + } + shell.printfln(" [total %d]", available.size()); + // List all partitions and their version info + shell.println(" Partitions:"); + for (const auto & partition : partition_info_) { + if (partition.second.version.empty()) { + continue; // no version, empty string + } + shell.printfln(" %s: v%s (%d KB%s) %s", + partition.first.c_str(), + partition.second.version.c_str(), + partition.second.size, + partition.second.install_date.empty() ? "" : (std::string(", installed on ") + partition.second.install_date).c_str(), + (strcmp(esp_ota_get_running_partition()->label, partition.first.c_str()) == 0) ? "** active **" : ""); + } + + shell.println(); + shell.println("Network:"); + switch (WiFi.status()) { + case WL_IDLE_STATUS: + shell.printfln(" Status: Idle"); + break; + + case WL_NO_SSID_AVAIL: + shell.printfln(" Status: Network not found"); + break; + + case WL_SCAN_COMPLETED: + shell.printfln(" Status: Network scan complete"); + break; + + case WL_CONNECTED: + shell.printfln(" Status: WiFi connected"); + shell.printfln(" SSID: %s", WiFi.SSID().c_str()); + shell.printfln(" BSSID: %s", WiFi.BSSIDstr().c_str()); + shell.printfln(" RSSI: %d dBm (%d %%)", WiFi.RSSI(), wifi_quality(WiFi.RSSI())); + char result[10]; + shell.printfln(" TxPower: %s dBm", Helpers::render_value(result, (double)(WiFi.getTxPower() / 4), 1)); + shell.printfln(" MAC address: %s", WiFi.macAddress().c_str()); + shell.printfln(" Hostname: %s", WiFi.getHostname()); + shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(WiFi.localIP()).c_str(), uuid::printable_to_string(WiFi.subnetMask()).c_str()); + shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(WiFi.gatewayIP()).c_str()); + shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(WiFi.dnsIP()).c_str()); + if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { + shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.linkLocalIPv6()).c_str()); + } + break; + + case WL_CONNECT_FAILED: + shell.printfln(" WiFi Network: Connection failed"); + break; + + case WL_CONNECTION_LOST: + shell.printfln(" WiFi Network: Connection lost"); + break; + + case WL_DISCONNECTED: + shell.printfln(" WiFi Network: Disconnected"); + break; + + // case WL_NO_SHIELD: + default: + shell.printfln(" WiFi MAC address: %s", WiFi.macAddress().c_str()); + shell.printfln(" WiFi Network: not connected"); + break; + } + + // show Ethernet if connected + if (ethernet_connected_) { + shell.println(); + shell.printfln(" Ethernet Status: connected"); + shell.printfln(" Ethernet MAC address: %s", ETH.macAddress().c_str()); + shell.printfln(" Hostname: %s", ETH.getHostname()); + shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(ETH.localIP()).c_str(), uuid::printable_to_string(ETH.subnetMask()).c_str()); + shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(ETH.gatewayIP()).c_str()); + shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(ETH.dnsIP()).c_str()); + if (ETH.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.linkLocalIPv6().toString() != "::") { + shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.linkLocalIPv6()).c_str()); + } + } + shell.println(); + + shell.println("Syslog:"); + if (!syslog_enabled_) { + shell.printfln(" Syslog: disabled"); + } else { + shell.printfln(" Syslog: %s", syslog_.started() ? "started" : "stopped"); + shell.print(" "); + shell.printfln(F_(host_fmt), !syslog_host_.isEmpty() ? syslog_host_.c_str() : F_(unset)); + shell.printfln(" IP: %s", uuid::printable_to_string(syslog_.ip()).c_str()); + shell.print(" "); + shell.printfln(F_(port_fmt), syslog_port_); + shell.print(" "); + shell.printfln(F_(log_level_fmt), uuid::log::format_level_lowercase(static_cast(syslog_level_))); + shell.print(" "); + shell.printfln(F_(mark_interval_fmt), syslog_mark_interval_); + shell.printfln(" Queued: %d", syslog_.queued()); + } + + shell.println(); + #endif + } + + // see if there is a restore of an older settings file that needs to be applied + // note there can be only one file at a time + 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 + } + #ifndef EMSESP_STANDALONE - output["freemem"] = getHeapMem(); - output["max_alloc"] = getMaxAllocMem(); -#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 - output["temperature"] = (int)temperature_; -#endif -#endif - -#ifndef EMSESP_STANDALONE - if (!ethernet_connected_) { - int8_t rssi = WiFi.RSSI(); - output["rssi"] = rssi; - output["wifistrength"] = wifi_quality(rssi); - output["wifireconnects"] = EMSESP::esp32React.getWifiReconnects(); - } -#endif -} - -// send periodic MQTT message with system information -void System::send_heartbeat() { - refreshHeapMem(); // refresh free heap and max alloc heap - - JsonDocument doc; - JsonObject json = doc.to(); - - heartbeat_json(json); - Mqtt::queue_publish(F_(heartbeat), json); // send to MQTT with retain off. This will add to MQTT queue. -} - -// initializes network -void System::network_init() { - last_system_check_ = 0; // force the LED to go from fast flash to pulse - -#if CONFIG_IDF_TARGET_ESP32 - bool disableEth; - EMSESP::esp32React.getNetworkSettingsService()->read([&](NetworkSettings & settings) { disableEth = settings.ssid.length() > 0; }); - - // no ethernet present or disabled - if (phy_type_ == PHY_type::PHY_TYPE_NONE || disableEth) { - return; - } // no ethernet present - - // configure Ethernet - int mdc = 23; // Pin# of the I²C clock signal for the Ethernet PHY - hardcoded - int mdio = 18; // Pin# of the I²C IO signal for the Ethernet PHY - hardcoded - uint8_t phy_addr = eth_phy_addr_; // I²C-address of Ethernet PHY (0 or 1 for LAN8720, 31 for TLK110) - int8_t power = eth_power_; // Pin# of the enable signal for the external crystal oscillator (-1 to disable for internal APLL source) - eth_phy_type_t type = (phy_type_ == PHY_type::PHY_TYPE_LAN8720) ? ETH_PHY_LAN8720 - : (phy_type_ == PHY_type::PHY_TYPE_TLK110) ? ETH_PHY_TLK110 - : ETH_PHY_RTL8201; // Type of the Ethernet PHY (LAN8720 or TLK110) - // clock mode: - // ETH_CLOCK_GPIO0_IN = 0 RMII clock input to GPIO0 - // ETH_CLOCK_GPIO0_OUT = 1 RMII clock output from GPIO0 - // ETH_CLOCK_GPIO16_OUT = 2 RMII clock output from GPIO16 - // ETH_CLOCK_GPIO17_OUT = 3 RMII clock output from GPIO17, for 50hz inverted clock - auto clock_mode = (eth_clock_mode_t)eth_clock_mode_; - - // reset power and add a delay as ETH doesn't not always start up correctly after a warm boot - if (eth_power_ != -1) { - pinMode(eth_power_, OUTPUT); - digitalWrite(eth_power_, LOW); - delay(500); - digitalWrite(eth_power_, HIGH); - } - -#if ESP_IDF_VERSION_MAJOR < 5 - eth_present_ = ETH.begin(phy_addr, power, mdc, mdio, type, clock_mode); -#else - eth_present_ = ETH.begin(type, phy_addr, mdc, mdio, power, clock_mode); -#endif -#endif -} - -// check health of system, done every 5 seconds -void System::system_check() { - uint32_t current_uptime = uuid::get_uptime(); - if (!last_system_check_ || ((uint32_t)(current_uptime - last_system_check_) >= SYSTEM_CHECK_FREQUENCY)) { - last_system_check_ = current_uptime; - -#ifndef EMSESP_STANDALONE -#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 -#if ESP_IDF_VERSION_MAJOR < 5 - temp_sensor_read_celsius(&temperature_); -#else - temperature_sensor_get_celsius(temperature_handle_, &temperature_); -#endif -#endif -#endif - -#ifdef EMSESP_PINGTEST - static uint64_t ping_count = 0; - LOG_NOTICE("Ping test, #%d", ping_count++); -#endif - - // check if we have a valid network connection - if (!ethernet_connected() && (WiFi.status() != WL_CONNECTED)) { - healthcheck_ |= HEALTHCHECK_NO_NETWORK; - } else { - healthcheck_ &= ~HEALTHCHECK_NO_NETWORK; - } - - // check if we have a bus connection - if (!EMSbus::bus_connected()) { - healthcheck_ |= HEALTHCHECK_NO_BUS; - } else { - healthcheck_ &= ~HEALTHCHECK_NO_BUS; - } - - // see if the healthcheck state has changed - static uint8_t last_healthcheck_ = 0; - if (healthcheck_ != last_healthcheck_) { - last_healthcheck_ = healthcheck_; - - EMSESP::system_.send_heartbeat(); // send MQTT heartbeat immediately when connected - - // see if we're better now - if (healthcheck_ == 0) { - // everything is healthy, show LED permanently on or off depending on setting - // Green on RGB LED, on/off on standard LED - if (led_gpio_) { - led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, hide_led_ ? 0 : RGB_LED_BRIGHTNESS, 0) - : digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // Green - } - } else { - // turn off LED so we're ready for the warning flashes - if (led_gpio_) { - led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); - } - } - } - } -} - -// commands - takes static function pointers -// can be called via Console using 'call system ' -void System::commands_init() { - Command::add(EMSdevice::DeviceType::SYSTEM, F_(read), System::command_read, FL_(read_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send, FL_(send_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(fetch), System::command_fetch, FL_(fetch_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(restart), System::command_restart, FL_(restart_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(format), System::command_format, FL_(format_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(txpause), System::command_txpause, FL_(txpause_cmd), CommandFlag::ADMIN_ONLY); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(watch), System::command_watch, FL_(watch_cmd)); - Command::add(EMSdevice::DeviceType::SYSTEM, F_(message), System::command_message, FL_(message_cmd)); -#if defined(EMSESP_TEST) - Command::add(EMSdevice::DeviceType::SYSTEM, ("test"), System::command_test, FL_(test_cmd)); -#endif - - // these commands will return data in JSON format - Command::add(EMSdevice::DeviceType::SYSTEM, F("response"), System::command_response, FL_(commands_response)); - - // MQTT subscribe "ems-esp/system/#" - Mqtt::subscribe(EMSdevice::DeviceType::SYSTEM, "system/#", nullptr); // use empty function callback -} - -// uses LED to show system health -void System::led_monitor() { - // if button is pressed, show LED (yellow on RGB LED, on/off on standard LED) - static bool button_busy_ = false; - if (button_busy_ != myPButton_.button_busy()) { - button_busy_ = myPButton_.button_busy(); - if (led_type_) { - EMSESP_RGB_WRITE(led_gpio_, button_busy_ ? RGB_LED_BRIGHTNESS : 0, button_busy_ ? RGB_LED_BRIGHTNESS : 0, 0); // Yellow - } else { - digitalWrite(led_gpio_, button_busy_ ? LED_ON : !LED_ON); - } - } - - // we only need to run the LED healthcheck if there are errors - // skip if we're in the led_flash_timer or if a button has been pressed - if (!healthcheck_ || !led_gpio_ || button_busy_ || led_flash_timer_) { - return; // all good - } - - static uint32_t led_long_timer_ = 1; // 1 will kick it off immediately - static uint32_t led_short_timer_ = 0; - static uint8_t led_flash_step_ = 0; // 0 means we're not in the short flash timer - - auto current_time = uuid::get_uptime(); - - // first long pause before we start flashing - if (led_long_timer_ && (uint32_t)(current_time - led_long_timer_) >= HEALTHCHECK_LED_LONG_DUARATION) { - led_short_timer_ = current_time; // start the short timer - led_long_timer_ = 0; // stop long timer - led_flash_step_ = 1; // enable the short flash timer - } - - // the flash timer which starts after the long pause - if (led_flash_step_ && (uint32_t)(current_time - led_short_timer_) >= HEALTHCHECK_LED_FLASH_DUARATION) { - led_long_timer_ = 0; // stop the long timer - led_short_timer_ = current_time; - static bool led_on_ = false; - - if (++led_flash_step_ == 8) { - // reset the whole sequence - led_long_timer_ = uuid::get_uptime(); - led_flash_step_ = 0; - led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); // LED off - } else if (led_flash_step_ % 2) { - // handle the step events (on odd numbers 3,5,7,etc). see if we need to turn on a LED - // 1 flash (blue) is the EMS bus is not connected - // 2 flashes (red, red) if the network (wifi or ethernet) is not connected - // 3 flashes (red, red, blue) is both the bus and the network are not connected - bool no_network = (healthcheck_ & HEALTHCHECK_NO_NETWORK) == HEALTHCHECK_NO_NETWORK; - bool no_bus = (healthcheck_ & HEALTHCHECK_NO_BUS) == HEALTHCHECK_NO_BUS; - - if (led_type_) { - if (led_flash_step_ == 3) { - if (no_network) { - EMSESP_RGB_WRITE(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red - } else if (no_bus) { - EMSESP_RGB_WRITE(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue - } - } - if (led_flash_step_ == 5 && no_network) { - EMSESP_RGB_WRITE(led_gpio_, RGB_LED_BRIGHTNESS, 0, 0); // red - } - if ((led_flash_step_ == 7) && no_network && no_bus) { - EMSESP_RGB_WRITE(led_gpio_, 0, 0, RGB_LED_BRIGHTNESS); // blue - } - } else { - if ((led_flash_step_ == 3) && (no_network || no_bus)) { - led_on_ = true; - } - - if ((led_flash_step_ == 5) && no_network) { - led_on_ = true; - } - - if ((led_flash_step_ == 7) && no_network && no_bus) { - led_on_ = true; - } - - if (led_on_) { - digitalWrite(led_gpio_, LED_ON); // LED on - } - } - } else { - // turn the led off after the flash, on even number count - if (led_on_) { - led_type_ ? EMSESP_RGB_WRITE(led_gpio_, 0, 0, 0) : digitalWrite(led_gpio_, !LED_ON); - led_on_ = false; - } - } - } -} - -// Return the quality (Received Signal Strength Indicator) of the WiFi network as a % -// High quality: 90% ~= -55dBm -// Medium quality: 50% ~= -75dBm -// Low quality: 30% ~= -85dBm -// Unusable quality: 8% ~= -96dBm -int8_t System::wifi_quality(int8_t dBm) { - if (dBm <= -100) { - return 0; - } - - if (dBm >= -50) { - return 100; - } - return 2 * (dBm + 100); -} - -// print users to console -void System::show_users(uuid::console::Shell & shell) { - if (!shell.has_flags(CommandFlags::ADMIN)) { - shell.printfln("Unauthorized. You need to be an admin to view users."); - return; - } - - shell.printfln("Users:"); - -#ifndef EMSESP_STANDALONE - EMSESP::esp32React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) { - for (const User & user : securitySettings.users) { - shell.printfln(" username: %s, password: %s, is_admin: %s", user.username.c_str(), user.password.c_str(), user.admin ? ("yes") : ("no")); - } - }); -#endif - - shell.println(); -} - -// shell command 'show system' -void System::show_system(uuid::console::Shell & shell) { - refreshHeapMem(); // refresh free heap and max alloc heap - - shell.println(); - shell.println("System:"); - shell.printfln(" Version: %s", EMSESP_APP_VERSION); -#ifndef EMSESP_STANDALONE - shell.printfln(" Platform: %s (%s)", EMSESP_PLATFORM, ESP.getChipModel()); - shell.printfln(" Model: %s", getBBQKeesGatewayDetails().c_str()); -#endif - shell.printfln(" Language: %s", locale().c_str()); - shell.printfln(" Board profile: %s", board_profile().c_str()); - shell.printfln(" Uptime: %s", uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3).c_str()); -#ifndef EMSESP_STANDALONE - // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/misc_system_api.html - unsigned char mac_base[6] = {0}; - esp_efuse_mac_get_default(mac_base); - esp_read_mac(mac_base, ESP_MAC_WIFI_STA); - shell.printfln(" Base MAC Address: %02X:%02X:%02X:%02X:%02X:%02X", mac_base[0], mac_base[1], mac_base[2], mac_base[3], mac_base[4], mac_base[5]); - - shell.printfln(" SDK version: %s", ESP.getSdkVersion()); - shell.printfln(" CPU frequency: %lu MHz", ESP.getCpuFreqMHz()); -#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 - shell.printfln(" CPU temperature: %d °C", (int)temperature()); -#endif - shell.printfln(" Free heap/Max alloc: %lu KB / %lu KB", getHeapMem(), getMaxAllocMem()); - shell.printfln(" App used/free: %lu KB / %lu KB", appUsed(), appFree()); - uint32_t FSused = LittleFS.usedBytes() / 1024; - shell.printfln(" FS used/free: %lu KB / %lu KB", FSused, FStotal() - FSused); - shell.printfln(" Flash size: %lu KB", ESP.getFlashChipSize() / 1024); - if (PSram()) { - shell.printfln(" PSRAM size/free: %lu KB / %lu KB", PSram(), ESP.getFreePsram() / 1024); - } else { - shell.printfln(" PSRAM: not available"); - } - // GPIOs - shell.println(" GPIOs:"); - shell.printf(" allowed:"); - for (const auto & gpio : valid_system_gpios_) { - shell.printf(" %d", gpio); - } - shell.printfln(" [total %d]", valid_system_gpios_.size()); - shell.printf(" in use:"); - 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) { - shell.printf(" %d(%s)", gpio.pin, gpio.source.c_str()); - } - shell.printfln(" [total %d]", used_gpios_.size()); - auto available = available_gpios(); - shell.printf(" available:"); - for (const auto & gpio : available) { - shell.printf(" %d", gpio); - } - shell.printfln(" [total %d]", available.size()); - // List all partitions and their version info - shell.println(" Partitions:"); - for (const auto & partition : partition_info_) { - if (partition.second.version.empty()) { - continue; // no version, empty string - } - shell.printfln(" %s: v%s (%d KB%s) %s", - partition.first.c_str(), - partition.second.version.c_str(), - partition.second.size, - partition.second.install_date.empty() ? "" : (std::string(", installed on ") + partition.second.install_date).c_str(), - (strcmp(esp_ota_get_running_partition()->label, partition.first.c_str()) == 0) ? "** active **" : ""); - } - - shell.println(); - shell.println("Network:"); - switch (WiFi.status()) { - case WL_IDLE_STATUS: - shell.printfln(" Status: Idle"); - break; - - case WL_NO_SSID_AVAIL: - shell.printfln(" Status: Network not found"); - break; - - case WL_SCAN_COMPLETED: - shell.printfln(" Status: Network scan complete"); - break; - - case WL_CONNECTED: - shell.printfln(" Status: WiFi connected"); - shell.printfln(" SSID: %s", WiFi.SSID().c_str()); - shell.printfln(" BSSID: %s", WiFi.BSSIDstr().c_str()); - shell.printfln(" RSSI: %d dBm (%d %%)", WiFi.RSSI(), wifi_quality(WiFi.RSSI())); - char result[10]; - shell.printfln(" TxPower: %s dBm", Helpers::render_value(result, (double)(WiFi.getTxPower() / 4), 1)); - shell.printfln(" MAC address: %s", WiFi.macAddress().c_str()); - shell.printfln(" Hostname: %s", WiFi.getHostname()); - shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(WiFi.localIP()).c_str(), uuid::printable_to_string(WiFi.subnetMask()).c_str()); - shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(WiFi.gatewayIP()).c_str()); - shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(WiFi.dnsIP()).c_str()); -#if ESP_IDF_VERSION_MAJOR < 5 - if (WiFi.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.localIPv6().toString() != "::") { - shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.localIPv6()).c_str()); - } -#else - if (WiFi.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && WiFi.linkLocalIPv6().toString() != "::") { - shell.printfln(" IPv6 address: %s", uuid::printable_to_string(WiFi.linkLocalIPv6()).c_str()); - } -#endif - - break; - - case WL_CONNECT_FAILED: - shell.printfln(" WiFi Network: Connection failed"); - break; - - case WL_CONNECTION_LOST: - shell.printfln(" WiFi Network: Connection lost"); - break; - - case WL_DISCONNECTED: - shell.printfln(" WiFi Network: Disconnected"); - break; - - // case WL_NO_SHIELD: - default: - shell.printfln(" WiFi MAC address: %s", WiFi.macAddress().c_str()); - shell.printfln(" WiFi Network: not connected"); - break; - } - - // show Ethernet if connected - if (ethernet_connected_) { - shell.println(); - shell.printfln(" Ethernet Status: connected"); - shell.printfln(" Ethernet MAC address: %s", ETH.macAddress().c_str()); - shell.printfln(" Hostname: %s", ETH.getHostname()); - shell.printfln(" IPv4 address: %s/%s", uuid::printable_to_string(ETH.localIP()).c_str(), uuid::printable_to_string(ETH.subnetMask()).c_str()); - shell.printfln(" IPv4 gateway: %s", uuid::printable_to_string(ETH.gatewayIP()).c_str()); - shell.printfln(" IPv4 nameserver: %s", uuid::printable_to_string(ETH.dnsIP()).c_str()); -#if ESP_IDF_VERSION_MAJOR < 5 - if (ETH.localIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.localIPv6().toString() != "::") { - shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.localIPv6()).c_str()); - } -#else - if (ETH.linkLocalIPv6().toString() != "0000:0000:0000:0000:0000:0000:0000:0000" && ETH.linkLocalIPv6().toString() != "::") { - shell.printfln(" IPv6 address: %s", uuid::printable_to_string(ETH.linkLocalIPv6()).c_str()); - } -#endif - } - shell.println(); - - shell.println("Syslog:"); - if (!syslog_enabled_) { - shell.printfln(" Syslog: disabled"); - } else { - shell.printfln(" Syslog: %s", syslog_.started() ? "started" : "stopped"); - shell.print(" "); - shell.printfln(F_(host_fmt), !syslog_host_.isEmpty() ? syslog_host_.c_str() : F_(unset)); - shell.printfln(" IP: %s", uuid::printable_to_string(syslog_.ip()).c_str()); - shell.print(" "); - shell.printfln(F_(port_fmt), syslog_port_); - shell.print(" "); - shell.printfln(F_(log_level_fmt), uuid::log::format_level_lowercase(static_cast(syslog_level_))); - shell.print(" "); - shell.printfln(F_(mark_interval_fmt), syslog_mark_interval_); - shell.printfln(" Queued: %d", syslog_.queued()); - } - - shell.println(); - -#endif -} - -// see if there is a restore of an older settings file that needs to be applied -// note there can be only one file at a time -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 -#ifndef EMSESP_STANDALONE static const std::pair SECTION_MAP[] = { {NETWORK_SETTINGS_FILE, "Network"}, {AP_SETTINGS_FILE, "AP"}, @@ -1663,1747 +1736,1740 @@ static const std::pair SECTION_MAP[] = { {EMSESP_MODULES_FILE, "Modules"}, }; #endif - -// 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; -#if ESP_IDF_VERSION_MAJOR < 5 - it = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY); - if (it == nullptr) { -#else - esp_err_t err = nvs_entry_find(nvs_part, "ems-esp", NVS_TYPE_ANY, &it); - if (err != ESP_OK) { -#endif - LOG_ERROR("Failed to find NVS entry for %s", nvs_part); - return; - } - - JsonArray entries = node["nvs"].to(); -#if ESP_IDF_VERSION_MAJOR < 5 - while (it != nullptr) { - nvs_entry_info_t info; - nvs_entry_info(it, &info); -#else - while (err == ESP_OK) { - nvs_entry_info_t info; - nvs_entry_info(it, &info); -#endif - JsonObject entry = entries.add(); - entry["type"] = info.type; - entry["key"] = info.key; - - LOG_DEBUG("Exporting NVS value: %s = %d", info.key, info.type); - - switch (info.type) { - case NVS_TYPE_I8: - entry["value"] = EMSESP::nvs_.getChar(info.key); - break; - case NVS_TYPE_U8: - 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: - entry["value"] = EMSESP::nvs_.getDouble(info.key); // bytes used for double values - break; - case NVS_TYPE_STR: - case NVS_TYPE_ANY: - default: - entry["value"] = EMSESP::nvs_.getString(info.key); - break; - } - -#if ESP_IDF_VERSION_MAJOR < 5 - it = nvs_entry_next(it); - } -#else - err = nvs_entry_next(&it); - } -#endif - - 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; - - /* + + // 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 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 + 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/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index c0b0b8d04..121b75d82 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -16,538 +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; -} - -// 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; - check_flag(original_settings.modbus_enabled, settings.modbus_enabled, ChangeFlags::RESTART); - settings.modbus_port = root["modbus_port"] | EMSESP_DEFAULT_MODBUS_PORT; - check_flag(original_settings.modbus_port, settings.modbus_port, ChangeFlags::RESTART); - settings.modbus_max_clients = root["modbus_max_clients"] | EMSESP_DEFAULT_MODBUS_MAX_CLIENTS; - check_flag(original_settings.modbus_max_clients, settings.modbus_max_clients, ChangeFlags::RESTART); - settings.modbus_timeout = root["modbus_timeout"] | EMSESP_DEFAULT_MODBUS_TIMEOUT; - check_flag(original_settings.modbus_timeout, settings.modbus_timeout, ChangeFlags::RESTART); - - // - // 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; - - // 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 ESP_ARDUINO_VERSION_MAJOR < 3 - if (ETH.begin(1, 16, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_IN)) { -#else - if (ETH.begin(ETH_PHY_LAN8720, 1, 23, 18, 16, ETH_CLOCK_GPIO0_IN)) { -#endif - 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 ESP_ARDUINO_VERSION_MAJOR < 3 - if (ETH.begin(0, 15, 23, 18, ETH_PHY_LAN8720, ETH_CLOCK_GPIO0_OUT)) { -#else - if (ETH.begin(ETH_PHY_LAN8720, 0, 23, 18, 15, ETH_CLOCK_GPIO0_OUT)) { -#endif - - 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 From 84105acf5d8307b049e96da5ad9c02bd4afe4938 Mon Sep 17 00:00:00 2001 From: proddy Date: Wed, 15 Apr 2026 20:48:11 +0200 Subject: [PATCH 4/4] 3.9.0-dev.0 --- src/emsesp_version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emsesp_version.h b/src/emsesp_version.h index 29cd72cc1..0aea80b67 100644 --- a/src/emsesp_version.h +++ b/src/emsesp_version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.8.2-dev.C13" +#define EMSESP_APP_VERSION "3.9.0-dev.0"