177 Commits

Author SHA1 Message Date
Proddy
da3ed6cd3a Merge pull request #3054 from proddy/core3
add back version check
2026-05-02 11:04:15 +02:00
proddy
23519a8a90 add back version check 2026-05-02 11:03:33 +02:00
Proddy
242708358e Merge pull request #3052 from proddy/core3
refactor networking into a single class
2026-05-02 10:51:37 +02:00
proddy
3cc3c74e5a update versions 2026-05-02 10:44:06 +02:00
proddy
cb4cb39396 update 2026-05-02 10:43:36 +02:00
proddy
363799c9c6 fix connect spelling 2026-05-02 09:49:34 +02:00
proddy
132f83aa79 update dictionary 2026-05-02 09:49:21 +02:00
proddy
f998714225 add missing #endif 2026-05-02 09:49:09 +02:00
proddy
323fc1bb99 remove comments 2026-05-02 09:48:55 +02:00
proddy
3062d3f0e3 remove Divider 2026-05-02 09:48:44 +02:00
proddy
8f37bb7623 package update 2026-05-02 09:48:31 +02:00
proddy
a57ed90756 use new network code 2026-05-02 09:48:19 +02:00
proddy
eaf8332d16 Merge remote-tracking branch 'origin/dev' into core3 2026-05-02 09:35:31 +02:00
proddy
522286ff74 remove double wifi lost message 2026-05-02 09:11:19 +02:00
proddy
747047556e fix lint warnings on osx 2026-05-01 17:58:22 +02:00
proddy
df3d75c702 ignore .vscode/settings.json
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-01 17:44:41 +02:00
proddy
e40beeadd4 performance updates 2026-05-01 17:04:46 +02:00
proddy
751f10603d upgrade if AP provision mode is AP_MODE_ALWAYS 2026-05-01 17:03:56 +02:00
proddy
751c540cb3 refactor network code 2026-05-01 08:07:05 +02:00
proddy
d7bbc329bb use 3.9.0 as dummy latest dev version 2026-04-29 08:58:04 +02:00
proddy
41cd49a61c remove comment 2026-04-29 08:57:53 +02:00
proddy
fd5a39702b update 2026-04-29 08:57:46 +02:00
proddy
4d3408254e chore: stop tracking .vscode/settings.json
Already listed in .gitignore but was tracked, so local edits kept
showing up as pending changes. Untrack it so the ignore rule applies.

Made-with: Cursor
2026-04-29 08:57:12 +02:00
proddy
2cbb5ec5f2 move restart button from Settings to Version page. only show Factory Reset when in developer mode 2026-04-28 20:09:22 +02:00
proddy
3b765b308e remove unused useMemo 2026-04-28 17:31:50 +02:00
proddy
53ac82520e DeserializationError is enum 2026-04-28 16:28:11 +02:00
proddy
381fcf4080 ESP32Async/ESPAsyncWebServer @ 3.11.0 2026-04-28 16:27:59 +02:00
proddy
a3f0faf022 package update 2026-04-28 16:27:46 +02:00
proddy
b3a8737a71 move Version from status to settings 2026-04-28 16:27:39 +02:00
proddy
6e76bcc9af show badge if there is an update available, which is cached 2026-04-27 18:12:05 +02:00
proddy
6473c55317 don't force an update on each request 2026-04-27 18:11:48 +02:00
proddy
1a880f14a0 Remove useMemo/useCallback across the web UI 2026-04-27 13:24:07 +02:00
proddy
e39af36589 fix lint errors 2026-04-27 13:23:39 +02:00
proddy
c5b262af8a dont update cloudflare KV for forks 2026-04-27 11:34:03 +02:00
proddy
43ec5c1925 move mockserver to standalone section only 2026-04-27 11:30:40 +02:00
proddy
5e260f0239 refactoring 2026-04-27 11:09:51 +02:00
proddy
ab67f97b40 3.8.2-dev.20 2026-04-27 11:09:34 +02:00
proddy
9ac35e2e14 fetch emsesp firmware versions after IP connected 2026-04-27 11:09:24 +02:00
proddy
7c6259dddd tidy up comments 2026-04-27 11:08:52 +02:00
proddy
1cff1abc33 package update 2026-04-27 11:08:22 +02:00
proddy
d834d46586 rename EMSESP_Version to firmwareVersion 2026-04-27 11:08:13 +02:00
proddy
1107e1bdf3 package update 2026-04-26 16:10:35 +02:00
proddy
3a11327e7e https://github.com/emsesp/EMS-ESP32/discussions/3044 2026-04-26 16:10:30 +02:00
proddy
74062bab57 update tests 2026-04-26 16:07:45 +02:00
proddy
6802336b6b remove old code 2026-04-26 16:07:29 +02:00
proddy
a9db134d3a version updates 2026-04-26 13:24:40 +02:00
proddy
ee7be1d907 add 2026-04-26 12:20:48 +02:00
proddy
5ecda88457 inlclude full date/time 2026-04-25 21:14:16 +02:00
proddy
7056c446fa use emsesp.org/versions.json 2026-04-25 20:55:10 +02:00
proddy
147c09ae64 automatically update versions in Cloudflare KV store 2026-04-25 11:42:26 +02:00
proddy
112adf9eb0 add vscode 2026-04-25 11:19:39 +02:00
Proddy
469d412951 Merge pull request #3045 from MichaelDvP/dev
fix legegram length, #2969
2026-04-24 17:14:58 +02:00
MichaelDvP
6edbac86e2 fix legegram length, #2969 2026-04-24 14:46:53 +02:00
Proddy
0e08334132 Merge pull request #3043 from MichaelDvP/core3
sync Core3
2026-04-22 21:44:19 +02:00
MichaelDvP
3d51acf9e7 Merge branch 'core3' of https://github.com/emsesp/EMS-ESP32 into core3 2026-04-22 16:51:25 +02:00
MichaelDvP
fd6ea5ed7e Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into core3 2026-04-22 16:48:39 +02:00
proddy
db2be70d66 chore: update generated files for v3.8.2-dev.18 2026-04-22 14:22:25 +00:00
Proddy
c36f231990 Merge pull request #3042 from proddy/dev
minor updates
2026-04-22 16:10:20 +02:00
Proddy
d18e5b1f14 Merge pull request #3041 from proddy/core3
Core3 sync
2026-04-22 16:09:58 +02:00
proddy
20327d817d async-validator fixes 2026-04-22 16:07:59 +02:00
proddy
26102121e1 async-validator fixes 2026-04-22 16:07:56 +02:00
proddy
8e64c6303e package update 2026-04-22 15:43:58 +02:00
proddy
051c332426 package update 2026-04-22 15:43:46 +02:00
proddy
a09258325e remove YIELD 2026-04-22 15:43:36 +02:00
proddy
74c76eb90b remove YIELD 2026-04-22 15:43:29 +02:00
proddy
daffdcf58e https://github.com/emsesp/EMS-ESP32/issues/2686 2026-04-22 15:43:20 +02:00
proddy
61dca0cbda https://github.com/emsesp/EMS-ESP32/issues/2686 2026-04-22 15:43:10 +02:00
Proddy
2bff299193 Merge pull request #3037 from MichaelDvP/core3
Core3 update
2026-04-22 15:26:26 +02:00
Proddy
4bc4fa903f Merge pull request #3040 from MichaelDvP/dev
version checks prelease
2026-04-22 15:11:02 +02:00
MichaelDvP
1329b13db3 Merge branch 'dev' into core3 2026-04-22 15:05:39 +02:00
MichaelDvP
29380f0303 version checks prelease 2026-04-22 14:59:46 +02:00
MichaelDvP
9dd894f0fe Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into core3 2026-04-22 10:14:34 +02:00
Proddy
6b2370b79d Merge pull request #3035 from mattreim/dev
Update German translation
2026-04-22 08:34:14 +02:00
Proddy
dbc636c9bf Merge pull request #3036 from MichaelDvP/dev
small fixes
2026-04-22 08:33:38 +02:00
MichaelDvP
30d1ae5642 update otadata when littlefs fails 2026-04-22 08:21:29 +02:00
MichaelDvP
79aceef382 Merge branch 'core3' of https://github.com/emsesp/EMS-ESP32 into core3 2026-04-22 07:51:58 +02:00
MichaelDvP
a28e52210a Merge branch 'dev' into core3 2026-04-22 07:50:43 +02:00
MichaelDvP
0c0660c04b Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2026-04-22 07:41:29 +02:00
MichaelDvP
08eb294213 update readymail 2026-04-22 07:26:17 +02:00
mattreim
c9fd076394 Update German translation 2026-04-22 01:05:05 +02:00
MichaelDvP
888baed81a pkg update 2026-04-21 21:39:10 +02:00
MichaelDvP
4de3955db2 set partition after update 2026-04-21 21:38:52 +02:00
MichaelDvP
25f08c7624 Merge branch 'dev' into core3 2026-04-21 20:44:02 +02:00
MichaelDvP
35550553be check fetch length for custom entities, dev17 2026-04-21 19:51:48 +02:00
MichaelDvP
06ff219385 version check order 2026-04-21 18:58:42 +02:00
MichaelDvP
e705a5629f fetch length of holiday to 18 2026-04-21 18:58:13 +02:00
proddy
1e8013100c rename build-webUI with build_webUI 2026-04-20 15:46:54 +02:00
proddy
62c8f55568 package update (vite fix) 2026-04-20 15:46:39 +02:00
Proddy
cb3c9653ce Merge pull request #3032 from proddy/dev
rename build_webUI for Python
2026-04-20 15:38:52 +02:00
proddy
0b5a83f6ae package update (vite fix) 2026-04-20 15:37:50 +02:00
MichaelDvP
a079169005 backup nvs1 if exist 2026-04-20 13:18:50 +02:00
proddy
845c51d5f9 rename build_webUI for Python 2026-04-19 21:23:59 +02:00
Proddy
c40d828749 Merge pull request #3031 from proddy/core3
build_webUI -> build-webUI
2026-04-19 19:15:28 +02:00
Proddy
d6d3a034ad Merge pull request #3030 from proddy/dev
build_webUI -> build-webUI
2026-04-19 19:14:31 +02:00
proddy
84ad08887a build_webUI -> build-webUI 2026-04-19 19:13:15 +02:00
proddy
ece08d96ee build_webUI -> build-webUI 2026-04-19 19:09:45 +02:00
MichaelDvP
ed0a678020 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into core3 2026-04-19 15:49:36 +02:00
MichaelDvP
854f4d559a Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2026-04-19 15:40:15 +02:00
Proddy
f186f2a8f2 Merge pull request #3028 from proddy/dev
heap memory optimizations
2026-04-19 15:15:45 +02:00
Proddy
37107d8500 Merge pull request #3029 from proddy/core3
sync memory optimizations from dev-16 to core3
2026-04-19 15:15:14 +02:00
proddy
6b68cb7c61 store UTC epoch time and convert to localtime when render (fixes bug as TZ not set) 2026-04-19 15:12:22 +02:00
proddy
a1e0288e09 close dialog after downloading 2026-04-19 15:11:41 +02:00
proddy
e6c173bdf9 don't show system backup as it's the same page! 2026-04-19 15:11:29 +02:00
proddy
dde6a8c5db close dialog after download 2026-04-19 15:10:51 +02:00
proddy
e2750b8572 don't show system backup as its the same page! 2026-04-19 15:10:41 +02:00
proddy
acd23925b5 download text changes 2026-04-19 15:10:20 +02:00
proddy
b0db054e11 fix firmware install date (was using UTC as TZ not initialised) 2026-04-19 14:17:31 +02:00
MichaelDvP
d9b6de0652 Merge branch 'dev' of https://github.com/proddy/EMS-ESP32 into dev 2026-04-19 13:41:01 +02:00
MichaelDvP
c54da18822 remove pr#3021 2026-04-19 13:40:51 +02:00
proddy
51cea8e757 no need to call ntp on begin as its in the loop 2026-04-19 13:15:11 +02:00
proddy
bbb086ea41 add back NTP 2026-04-19 13:14:54 +02:00
proddy
539e6ed080 remove lazy loading 2026-04-19 10:17:10 +02:00
proddy
555801dc5c remove lazy loading, optimize chunking 2026-04-19 10:05:47 +02:00
proddy
1d33a26318 fix dns name being set to "tasmota" 2026-04-18 19:42:45 +02:00
proddy
86a20fc97a sync with dev-16 2026-04-18 18:54:33 +02:00
proddy
d6e00c4534 UART_FIFO_LEN is deprecated 2026-04-18 18:50:08 +02:00
proddy
6f81945da6 typo 2026-04-18 18:43:03 +02:00
proddy
865c309475 remove c++17 2026-04-18 18:39:45 +02:00
proddy
77b8b21aea use C++ 20 (espressif32@6.13.0 still uses GCC 8 so only 2a supported) 2026-04-18 18:34:47 +02:00
proddy
2f5edffec6 update changelog 2026-04-18 18:29:30 +02:00
proddy
71de64502e include cstdint for uint8_t on new GCC 2026-04-18 17:52:05 +02:00
proddy
6994d3559a package update 2026-04-18 17:43:31 +02:00
proddy
a7d484d218 3.8.2-dev.16 2026-04-18 17:43:23 +02:00
proddy
a810c41acd exclude js 2026-04-18 17:43:14 +02:00
proddy
2fbfdf94ab minor optimizations, use EMSESP_Version, only call esp_image_verify() and store the entry for partitions that actually have a value 2026-04-18 17:40:21 +02:00
proddy
2d7c8f0863 remove semver 2026-04-18 17:36:08 +02:00
proddy
c3b734ab47 add back LTO, remove semver 2026-04-18 17:35:57 +02:00
proddy
644abf105d replace semver with home grown simplier alternative 2026-04-18 17:35:11 +02:00
proddy
5a8a451774 improve chunking 2026-04-18 17:34:48 +02:00
proddy
dae139aa01 single static-content handler serving all assets 2026-04-18 17:33:13 +02:00
proddy
b13fcd8939 single static-content handler serving all assets 2026-04-18 17:32:54 +02:00
Proddy
26b42b4eea Merge pull request #3027 from proddy/dev
update github actions
2026-04-18 10:22:39 +02:00
proddy
c9005e8aa9 upgrade github actions 2026-04-18 10:02:25 +02:00
proddy
6658b11adf use c++20 2026-04-17 18:01:51 +02:00
proddy
e542f5809f remove bogus file 2026-04-17 18:01:44 +02:00
proddy
ce1dd6233d update 2026-04-17 18:01:37 +02:00
proddy
fe488443da package update 2026-04-17 18:01:26 +02:00
Proddy
b264a39780 Merge pull request #3026 from MichaelDvP/dev 2026-04-17 16:41:59 +02:00
MichaelDvP
d2302eaa85 dev15, rollback mbedlt change for memory saving 2026-04-17 15:59:13 +02:00
Proddy
a813d38108 Merge pull request #3025 from MichaelDvP/core3
Core3
2026-04-17 15:44:51 +02:00
MichaelDvP
685a49c212 Merge branch 'dev' into core3, formatting, add back sendmail settings 2026-04-17 12:28:41 +02:00
Proddy
994706c9f2 Merge pull request #3023 from MichaelDvP/dev
fetch telegrams with length, dev14
2026-04-16 12:22:06 +02:00
MichaelDvP
8a72ab42cb dev 14, changelog 2026-04-16 08:18:17 +02:00
MichaelDvP
c4db8e3914 set length for more fetch telegrams 2026-04-16 08:10:43 +02:00
MichaelDvP
8d0225e595 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2026-04-16 08:09:58 +02:00
Proddy
966049d0c9 Merge pull request #3006 from proddy/dev
sync with core3 features
2026-04-15 21:29:24 +02:00
proddy
907a65a701 show link to backup page 2026-04-15 21:19:09 +02:00
proddy
f97b8e14e7 package update 2026-04-15 21:18:51 +02:00
proddy
e65f634b21 3.8.2 2026-04-15 21:18:38 +02:00
proddy
fc71ed2b9d 3.8.2 2026-04-15 21:18:27 +02:00
proddy
5a8195d430 auto-formatting 2026-04-15 08:12:22 +02:00
proddy
24a7a607f3 add test data 2026-04-15 08:08:49 +02:00
proddy
061f9ffc52 update prettier 2026-04-15 08:08:45 +02:00
proddy
9e17936bfc fix lint 2026-04-14 21:13:25 +02:00
Proddy
18bb2c4f39 Merge branch 'emsesp:dev' into dev 2026-04-14 21:09:31 +02:00
proddy
7c3782a43f upload warnings 2026-04-14 21:08:45 +02:00
proddy
3ac807bdd5 text change 2026-04-14 21:08:04 +02:00
proddy
1111458863 upgrade message warnings 2026-04-14 09:31:50 +02:00
proddy
99c5e2230c fix link 2026-04-14 09:31:35 +02:00
proddy
3317aa845a package update 2026-04-14 09:31:19 +02:00
proddy
97cd657336 fix links 2026-04-14 09:31:05 +02:00
MichaelDvP
3338f919bd Merge branch 'dev' of https://github.com/proddy/EMS-ESP32 into dev 2026-04-14 08:09:50 +02:00
proddy
7dd13bcab7 mui upgrade 2026-04-13 23:30:36 +02:00
MichaelDvP
f226cb359f Merge branch 'dev' of https://github.com/MichaelDvP/EMS-ESP32 into dev 2026-04-13 21:04:23 +02:00
MichaelDvP
abbba0aa42 telegram length for fetched telegrams 2026-04-13 20:57:20 +02:00
MichaelDvP
39b5a52b01 chore: update generated files for v3.8.2-dev.13 2026-04-13 12:12:42 +00:00
MichaelDvP
b6c3fc5bee read fragmented telegram 0x484, #3017 2026-04-13 14:00:12 +02:00
MichaelDvP
909bea00df update espressif32 6.13.0 2026-04-13 13:59:12 +02:00
MichaelDvP
9522945e06 uart buffer size 2026-04-13 13:58:30 +02:00
MichaelDvP
d6a9f2a731 prepare for translations #3015, update pkg 2026-04-13 13:58:10 +02:00
proddy
0f30c81554 fix compile on linux/osx 2026-04-12 20:32:36 +02:00
MichaelDvP
e514ba4bb5 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2026-04-12 09:46:43 +02:00
Proddy
38e63e3eaa Merge pull request #3016 from misa1515/patch-30
Update index.ts
2026-04-11 22:44:28 +02:00
misa1515
0058324edd Update index.ts 2026-04-10 21:10:36 +02:00
MichaelDvP
ac143d607a http-client to heap 2026-04-10 14:20:17 +02:00
MichaelDvP
e9e3759db3 add solar ts3 2026-04-10 14:19:39 +02:00
proddy
fb09e10f19 sync with core3 features 2026-03-30 23:26:04 +02:00
166 changed files with 6171 additions and 6911 deletions

View File

@@ -28,7 +28,7 @@ jobs:
node-version: 24
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Enable Corepack
run: corepack enable pnpm
@@ -47,7 +47,7 @@ jobs:
- name: Build webUI
run: |
platformio run -e build_webUI
platformio run -e build-webUI
- name: Build modbus
run: |
@@ -62,13 +62,13 @@ jobs:
platformio run
- name: Commit the generated files
uses: stefanzweifel/git-auto-commit-action@v5
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "chore: update generated files for v${{steps.build_info.outputs.VERSION}}"
- name: Create GitHub Release
id: 'automatic_releases'
uses: emsesp/action-automatic-releases@v1.0.0
uses: emsesp/action-automatic-releases@v1.0.1
with:
repo_token: '${{ secrets.GITHUB_TOKEN }}'
title: Development Build v${{steps.build_info.outputs.VERSION}}
@@ -77,3 +77,23 @@ jobs:
files: |
CHANGELOG_LATEST.md
./build/firmware/*.*
- name: Update version in Cloudflare KV store
if: github.repository == 'emsesp/EMS-ESP32'
env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_NAMESPACE_ID: ${{ secrets.CF_NAMESPACE_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
VERSION: ${{ steps.build_info.outputs.VERSION }}
run: |
JSON_DATA=$(jq -n \
--arg version "$VERSION" \
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{version: $version, date: $date}')
echo "JSON_DATA: $JSON_DATA"
curl -sS --fail-with-body \
-X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${CF_NAMESPACE_ID}/values/dev" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "$JSON_DATA"
echo

View File

@@ -11,7 +11,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: GitHub Releases To Discord
uses: SethCohen/github-releases-to-discord@v1.13.1

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install python 3.13
uses: actions/setup-python@v6

View File

@@ -19,7 +19,7 @@ jobs:
BUILD_WRAPPER_OUT_DIR: bw-output
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install Build Wrapper

View File

@@ -26,11 +26,18 @@ jobs:
node-version: 24
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Enable Corepack
run: corepack enable pnpm
- name: Get the EMS-ESP version
id: build_info
run: |
version=`grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}'`
echo "VERSION=$version" >> $GITHUB_OUTPUT
- name: Install PlatformIO
run: |
python -m pip install --upgrade pip
@@ -39,7 +46,7 @@ jobs:
- name: Build webUI
run: |
platformio run -e build_webUI
platformio run -e build-webUI
- name: Build modbus
run: |
@@ -54,10 +61,30 @@ jobs:
platformio run
- name: Create GitHub Release
uses: emsesp/action-automatic-releases@v1.0.0
uses: emsesp/action-automatic-releases@v1.0.1
with:
repo_token: '${{ secrets.GITHUB_TOKEN }}'
prerelease: false
files: |
CHANGELOG.md
./build/firmware/*.*
- name: Update version in Cloudflare KV store
if: github.repository == 'emsesp/EMS-ESP32'
env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_NAMESPACE_ID: ${{ secrets.CF_NAMESPACE_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
VERSION: ${{ steps.build_info.outputs.VERSION }}
run: |
JSON_DATA=$(jq -n \
--arg version "$VERSION" \
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{version: $version, date: $date}')
echo "JSON_DATA: $JSON_DATA"
curl -sS --fail-with-body \
-X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${CF_NAMESPACE_ID}/values/stable" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "$JSON_DATA"
echo

View File

@@ -28,7 +28,7 @@ jobs:
node-version: 24
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Enable Corepack
run: corepack enable pnpm
@@ -47,7 +47,7 @@ jobs:
- name: Build webUI
run: |
platformio run -e build_webUI
platformio run -e build-webUI
- name: Build modbus
run: |
@@ -63,7 +63,7 @@ jobs:
- name: Create GitHub Release
id: 'automatic_releases'
uses: emsesp/action-automatic-releases@v1.0.0
uses: emsesp/action-automatic-releases@v1.0.1
with:
repo_token: '${{ secrets.GITHUB_TOKEN }}'
title: Test Build v${{steps.build_info.outputs.VERSION}}

40
.github/workflows/update_versions.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: 'Update versions'
on:
workflow_dispatch:
permissions:
contents: write
jobs:
update-version:
name: 'Update versions in Cloudflare KV store'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Get and Send EMS-ESP version to Cloudflare
env:
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
CF_NAMESPACE_ID: ${{ secrets.CF_NAMESPACE_ID }}
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
run: |
version=$(grep -E '^#define EMSESP_APP_VERSION' ./src/emsesp_version.h | awk -F'"' '{print $2}')
if [ "$GITHUB_REF" = "refs/heads/main" ]; then
KV_ENV="stable"
else
KV_ENV="dev"
fi
JSON_DATA=$(jq -n \
--arg version "$version" \
--arg date "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{version: $version, date: $date}')
echo "KV_ENV: $KV_ENV"
echo "JSON_DATA: $JSON_DATA"
curl -sS --fail-with-body \
-X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/storage/kv/namespaces/${CF_NAMESPACE_ID}/values/${KV_ENV}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d "$JSON_DATA"
echo

4
.gitignore vendored
View File

@@ -2,6 +2,7 @@
.vscode/c_cpp_properties.json
.vscode/extensions.json
.vscode/launch.json
.vscode/settings.json
# c++ compiling
.clang_complete
@@ -63,7 +64,7 @@ words-found-verbose.txt
# sonarlint
compile_commands.json
# pioarduino + hybrid
# other files
managed_components
dependencies.lock
CMakeLists.txt
@@ -75,3 +76,4 @@ pnpm-lock.yaml
.cache/
interface/.tsbuildinfo
test/test_api/package-lock.json
.clangd

101
.vscode/settings.json vendored
View File

@@ -1,101 +0,0 @@
{
"search.exclude": {
"**/.yarn": true,
"**/.pnp.*": true
},
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"eslint.validate": [
"typescript"
],
"eslint.codeActionsOnSave.rules": null,
"eslint.nodePath": "interface/.yarn/sdks",
"eslint.workingDirectories": ["interface"],
"prettier.prettierPath": "",
"typescript.enablePromptUseWorkspaceTsdk": true,
"files.associations": {
"*.tsx": "typescriptreact",
"*.tcc": "cpp",
"optional": "cpp",
"istream": "cpp",
"ostream": "cpp",
"ratio": "cpp",
"system_error": "cpp",
"array": "cpp",
"functional": "cpp",
"regex": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"string": "cpp",
"string_view": "cpp",
"atomic": "cpp",
"bitset": "cpp",
"cctype": "cpp",
"chrono": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"condition_variable": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"list": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"iterator": "cpp",
"map": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"random": "cpp",
"set": "cpp",
"fstream": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"limits": "cpp",
"mutex": "cpp",
"new": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"thread": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp"
},
"todo-tree.filtering.excludeGlobs": [
"**/vendor/**",
"**/node_modules/**",
"**/dist/**",
"**/bower_components/**",
"**/build/**",
"**/.vscode/**",
"**/.github/**",
"**/_output/**",
"**/*.min.*",
"**/*.map",
"**/ArduinoJson/**"
],
"cSpell.enableFiletypes": [
"ini",
"makefile"
],
"typescript.preferences.preferTypeOnlyAutoImports": true,
"sonarlint.pathToCompileCommands": "${workspaceFolder}/compile_commands.json",
"sonarlint.connectedMode.project": {
"connectionId": "emsesp",
"projectKey": "emsesp_EMS-ESP32"
}
}

View File

@@ -2,7 +2,7 @@
For more details go to [emsesp.org](https://emsesp.org/).
## [3.8.2]
## [3.9.0]
## Added
@@ -13,11 +13,15 @@ For more details go to [emsesp.org](https://emsesp.org/).
- boiler pumpkick [#2965](https://github.com/emsesp/EMS-ESP32/discussions/2965)
- heatpump reset [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933)
- e-mail notification using ReadyMail Client
- 2.nd freshwater module (dhw4) [#2991](https://github.com/emsesp/EMS-ESP32/issues/2991)
- 2.nd freshwater module (dhw4, dhw5) [#2991](https://github.com/emsesp/EMS-ESP32/issues/2991)
- full system backup and restore
- updated version check [#3047](https://github.com/emsesp/EMS-ESP32/issues/3047)
## Fixed
- SRC climate creation [#2936](https://github.com/emsesp/EMS-ESP32/issues/2936) and [#2960](https://github.com/emsesp/EMS-ESP32/issues/2960)
- missing translations [#3015](https://github.com/emsesp/EMS-ESP32/issues/3015)
- custom entities check fetch length
## Changed
@@ -31,3 +35,7 @@ For more details go to [emsesp.org](https://emsesp.org/).
- device class for % values [#2980](https://github.com/emsesp/EMS-ESP32/issues/2980)
- use tasmota core 2026.03.30
- secure mqtt uses ESP_SSLClient
- fetch telegrams: set length to fetch [#3017](https://github.com/emsesp/EMS-ESP32/issues/3017)
- move http client from stack to heap
- heap optimizations [#3021](https://github.com/emsesp/EMS-ESP32/discussions/3021)
- refactored network code into a single class [#3052](https://github.com/emsesp/EMS-ESP32/pull/3052)

View File

@@ -47,8 +47,8 @@ MAKEFLAGS += -j$(JOBS) -l$(shell echo $$(($(JOBS) * 2)))
#----------------------------------------------------------------------
TARGET := emsesp
BUILD := build
SOURCES := src/core src/devices src/web src/test lib_standalone lib/semver lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton
INCLUDES := src/core src/devices src/web src/test lib_standalone lib/* lib/semver lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src
SOURCES := src/core src/devices src/web src/test lib_standalone lib/espMqttClient/src lib/espMqttClient/src/* lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/PButton
INCLUDES := src/core src/devices src/web src/test lib_standalone lib/* lib/espMqttClient/src lib/espMqttClient/src/Transport lib/ArduinoJson/src lib/uuid-common/src lib/uuid-console/src lib/uuid-log/src lib/uuid-telnet/src lib/uuid-syslog/src
LIBRARIES :=
CPPCHECK = cppcheck
@@ -113,7 +113,7 @@ CXX := /usr/bin/g++
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
CPPFLAGS += -ggdb -g3 -MMD
CPPFLAGS += -flto=auto
CPPFLAGS += -Wall -Wextra -Werror -Wswitch-enum
CPPFLAGS += -Wall -Wextra -Werror -Wno-switch-enum
CPPFLAGS += -Wno-unused-parameter -Wno-missing-braces -Wno-vla-cxx-extension
CPPFLAGS += -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics
CPPFLAGS += -Os -DNDEBUG

View File

@@ -38,6 +38,7 @@
"vite.config.ts",
"lib/esp32-psram/**",
"test/test_api/test_api.h",
"lib_standalone/**"
"lib_standalone/**",
"**/*.js"
]
}

View File

@@ -7654,7 +7654,7 @@ uint8
| cylmaxtemp | maximum cylinder temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 5 | 1 | 1 |
| collectorshutdown | collector shutdown | boolean | | false | DEVICE_DATA | 6 | 1 | 1 |
| cylheated | cyl heated | boolean | | false | DEVICE_DATA | 7 | 1 | 1 |
| cylmiddletemp | cylinder middle temperature (TS3) | int16 | C | false | DEVICE_DATA | 17 | 1 | 1/10 |
| cylmiddletemp | cylinder middle temperature (TS14) | int16 | C | false | DEVICE_DATA | 17 | 1 | 1/10 |
| retheatassist | return temperature heat assistance (TS4) | int16 | C | false | DEVICE_DATA | 18 | 1 | 1/10 |
| heatassistvalve | heat assistance valve (M1) | boolean | | false | DEVICE_DATA | 19 | 1 | 1 |
| energylasthour | energy last hour | uint24 | Wh | false | DEVICE_DATA | 13 | 2 | 1/10 |
@@ -7671,7 +7671,7 @@ uint8
| cylmaxtemp | maximum cylinder temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 5 | 1 | 1 |
| collectorshutdown | collector shutdown | boolean | | false | DEVICE_DATA | 6 | 1 | 1 |
| cylheated | cyl heated | boolean | | false | DEVICE_DATA | 7 | 1 | 1 |
| cylmiddletemp | cylinder middle temperature (TS3) | int16 | C | false | DEVICE_DATA | 17 | 1 | 1/10 |
| cylmiddletemp | cylinder middle temperature (TS14) | int16 | C | false | DEVICE_DATA | 17 | 1 | 1/10 |
| retheatassist | return temperature heat assistance (TS4) | int16 | C | false | DEVICE_DATA | 18 | 1 | 1/10 |
| heatassistvalve | heat assistance valve (M1) | boolean | | false | DEVICE_DATA | 19 | 1 | 1 |
| energylasthour | energy last hour | uint24 | Wh | false | DEVICE_DATA | 13 | 2 | 1/10 |
@@ -7696,67 +7696,68 @@ uint8
uint8
| turnoffdiff | pump turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 11 | 1 | 1/10 |
| collector2temp | collector 2 temperature (TS7) | int16 | C | false | DEVICE_DATA | 20 | 1 | 1/10 |
| cylmiddletemp | cylinder middle temperature (TS3) | int16 | C | false | DEVICE_DATA | 17 | 1 | 1/10 |
| cylmiddletemp | cylinder middle temperature (TS14) | int16 | C | false | DEVICE_DATA | 17 | 1 | 1/10 |
| ts3 | cylinder middle temperature (TS3) | int16 | C | false | DEVICE_DATA | 21 | 1 | 1/10 |
| retheatassist | return temperature heat assistance (TS4) | int16 | C | false | DEVICE_DATA | 18 | 1 | 1/10 |
| ts8 | (TS8) | int16 | C | false | DEVICE_DATA | 21 | 1 | 1/10 |
| ts16 | (TS16) | int16 | C | false | DEVICE_DATA | 22 | 1 | 1/10 |
| ts8 | (TS8) | int16 | C | false | DEVICE_DATA | 22 | 1 | 1/10 |
| ts16 | (TS16) | int16 | C | false | DEVICE_DATA | 23 | 1 | 1/10 |
| heatassistvalve | heat assistance valve (M1) | boolean | | false | DEVICE_DATA | 19 | 1 | 1 |
| heatassistpower | heat assistance valve power (M1) | uint8 | % | false | DEVICE_DATA | 23 | 1 | 1 |
| solarpump2 | pump 2 (PS4) | boolean | | false | DEVICE_DATA | 24 | 1 | 1 |
| solarpump2mod | pump 2 modulation (PS4) | uint8 | % | false | DEVICE_DATA | 25 | 1 | 1 |
| cyl2bottomtemp | second cylinder bottom temperature (TS5) | int16 | C | false | DEVICE_DATA | 26 | 1 | 1/10 |
| cyl3bottomtemp | third cylinder bottom temperature (TS11) | int16 | C | false | DEVICE_DATA | 27 | 1 | 1/10 |
| cyltoptemp | cylinder top temperature (TS10) | int16 | C | false | DEVICE_DATA | 28 | 1 | 1/10 |
| heatexchangertemp | heat exchanger temperature (TS6) | int16 | C | false | DEVICE_DATA | 29 | 1 | 1/10 |
| cylpumpmod | cylinder pump modulation (PS5) | uint8 | % | false | DEVICE_DATA | 30 | 1 | 1 |
| valvestatus | valve status | boolean | | false | DEVICE_DATA | 31 | 1 | 1 |
| vs1status | valve status VS1 | boolean | | false | DEVICE_DATA | 32 | 1 | 1 |
| vs3status | valve status VS3 | boolean | | false | DEVICE_DATA | 33 | 1 | 1 |
| transferpump | transfer pump | boolean | | false | DEVICE_DATA | 34 | 1 | 1 |
| transferpumpmod | transfer pump modulation | uint8 | % | false | DEVICE_DATA | 35 | 1 | 1 |
| heatassistpower | heat assistance valve power (M1) | uint8 | % | false | DEVICE_DATA | 24 | 1 | 1 |
| solarpump2 | pump 2 (PS4) | boolean | | false | DEVICE_DATA | 25 | 1 | 1 |
| solarpump2mod | pump 2 modulation (PS4) | uint8 | % | false | DEVICE_DATA | 26 | 1 | 1 |
| cyl2bottomtemp | second cylinder bottom temperature (TS5) | int16 | C | false | DEVICE_DATA | 27 | 1 | 1/10 |
| cyl3bottomtemp | third cylinder bottom temperature (TS11) | int16 | C | false | DEVICE_DATA | 28 | 1 | 1/10 |
| cyltoptemp | cylinder top temperature (TS10) | int16 | C | false | DEVICE_DATA | 29 | 1 | 1/10 |
| heatexchangertemp | heat exchanger temperature (TS6) | int16 | C | false | DEVICE_DATA | 30 | 1 | 1/10 |
| cylpumpmod | cylinder pump modulation (PS5) | uint8 | % | false | DEVICE_DATA | 31 | 1 | 1 |
| valvestatus | valve status | boolean | | false | DEVICE_DATA | 32 | 1 | 1 |
| vs1status | valve status VS1 | boolean | | false | DEVICE_DATA | 33 | 1 | 1 |
| vs3status | valve status VS3 | boolean | | false | DEVICE_DATA | 34 | 1 | 1 |
| transferpump | transfer pump | boolean | | false | DEVICE_DATA | 35 | 1 | 1 |
| transferpumpmod | transfer pump modulation | uint8 | % | false | DEVICE_DATA | 36 | 1 | 1 |
uint8
| collectormaxtemp | maximum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 36 | 1 | 1 |
| collectormaxtemp | maximum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 37 | 1 | 1 |
uint8
| collectormintemp | minimum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 37 | 1 | 1 |
| collectormintemp | minimum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 38 | 1 | 1 |
| energylasthour | energy last hour | uint24 | Wh | false | DEVICE_DATA | 13 | 2 | 1/10 |
| energytoday | total energy today | uint24 | Wh | false | DEVICE_DATA | 38 | 2 | 1 |
| energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 40 | 2 | 1/10 |
| pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 42 | 2 | 1 |
| m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 44 | 2 | 1 |
| heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 46 | 1 | 1 |
| externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 47 | 1 | 1 |
| thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 48 | 1 | 1 |
| heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 49 | 1 | 1 |
| activated | activated | boolean | | true | DEVICE_DATA | 50 | 1 | 1 |
| solarpumpmode | solar pump mode | enum | | true | DEVICE_DATA | 51 | 1 | 1 |
| solarpumpkick | solar pump kick | boolean | | true | DEVICE_DATA | 52 | 1 | 1 |
| plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 53 | 1 | 1 |
| doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 54 | 1 | 1 |
| energytoday | total energy today | uint24 | Wh | false | DEVICE_DATA | 39 | 2 | 1 |
| energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 41 | 2 | 1/10 |
| pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 43 | 2 | 1 |
| m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 45 | 2 | 1 |
| heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 47 | 1 | 1 |
| externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 48 | 1 | 1 |
| thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 49 | 1 | 1 |
| heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 50 | 1 | 1 |
| activated | activated | boolean | | true | DEVICE_DATA | 51 | 1 | 1 |
| solarpumpmode | solar pump mode | enum | | true | DEVICE_DATA | 52 | 1 | 1 |
| solarpumpkick | solar pump kick | boolean | | true | DEVICE_DATA | 53 | 1 | 1 |
| plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 54 | 1 | 1 |
| doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 55 | 1 | 1 |
uint8
| pump2minmod | minimum pump 2 modulation | uint8 (&gt;=0&lt;=0) | % | true | DEVICE_DATA | 55 | 1 | 1 |
| pump2minmod | minimum pump 2 modulation | uint8 (&gt;=0&lt;=0) | % | true | DEVICE_DATA | 56 | 1 | 1 |
uint8
| turnondiff2 | pump 2 turn on difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 56 | 1 | 1/10 |
| turnondiff2 | pump 2 turn on difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 57 | 1 | 1/10 |
uint8
| turnoffdiff2 | pump 2 turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 57 | 1 | 1/10 |
| pump2kick | pump kick 2 | boolean | | true | DEVICE_DATA | 58 | 1 | 1 |
| turnoffdiff2 | pump 2 turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 58 | 1 | 1/10 |
| pump2kick | pump kick 2 | boolean | | true | DEVICE_DATA | 59 | 1 | 1 |
uint8
| climatezone | climate zone | uint8 (&gt;=0&lt;=0) | | true | DEVICE_DATA | 59 | 1 | 1 |
| climatezone | climate zone | uint8 (&gt;=0&lt;=0) | | true | DEVICE_DATA | 60 | 1 | 1 |
uint16
| collector1area | collector 1 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 60 | 1 | 1/10 |
| collector1type | collector 1 type | enum | | true | DEVICE_DATA | 61 | 1 | 1 |
| collector1area | collector 1 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 61 | 1 | 1/10 |
| collector1type | collector 1 type | enum | | true | DEVICE_DATA | 62 | 1 | 1 |
uint16
| collector2area | collector 2 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 62 | 1 | 1/10 |
| collector2type | collector 2 type | enum | | true | DEVICE_DATA | 63 | 1 | 1 |
| cylpriority | cylinder priority | enum | | true | DEVICE_DATA | 64 | 1 | 1 |
| heatcntflowtemp | heat counter flow temperature | uint16 | C | false | DEVICE_DATA | 65 | 1 | 1/10 |
| heatcntrettemp | heat counter return temperature | uint16 | C | false | DEVICE_DATA | 66 | 1 | 1/10 |
| heatcnt | heat counter impulses | uint8 | | false | DEVICE_DATA | 67 | 1 | 1 |
| swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 68 | 1 | 1/10 |
| swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 |
| collector2area | collector 2 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 63 | 1 | 1/10 |
| collector2type | collector 2 type | enum | | true | DEVICE_DATA | 64 | 1 | 1 |
| cylpriority | cylinder priority | enum | | true | DEVICE_DATA | 65 | 1 | 1 |
| heatcntflowtemp | heat counter flow temperature | uint16 | C | false | DEVICE_DATA | 66 | 1 | 1/10 |
| heatcntrettemp | heat counter return temperature | uint16 | C | false | DEVICE_DATA | 67 | 1 | 1/10 |
| heatcnt | heat counter impulses | uint8 | | false | DEVICE_DATA | 68 | 1 | 1 |
| swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 |
| swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 70 | 1 | 1/10 |
int8
| heatassiston | heat assistance on | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 70 | 1 | 1/10 |
| heatassiston | heat assistance on | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 71 | 1 | 1/10 |
int8
| heatassistoff | heat assistance off | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 71 | 1 | 1/10 |
| heatassistoff | heat assistance off | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 72 | 1 | 1/10 |
### SM100, MS100
@@ -7778,67 +7779,68 @@ uint8
uint8
| turnoffdiff | pump turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 11 | 1 | 1/10 |
| collector2temp | collector 2 temperature (TS7) | int16 | C | false | DEVICE_DATA | 20 | 1 | 1/10 |
| cylmiddletemp | cylinder middle temperature (TS3) | int16 | C | false | DEVICE_DATA | 17 | 1 | 1/10 |
| cylmiddletemp | cylinder middle temperature (TS14) | int16 | C | false | DEVICE_DATA | 17 | 1 | 1/10 |
| ts3 | cylinder middle temperature (TS3) | int16 | C | false | DEVICE_DATA | 21 | 1 | 1/10 |
| retheatassist | return temperature heat assistance (TS4) | int16 | C | false | DEVICE_DATA | 18 | 1 | 1/10 |
| ts8 | (TS8) | int16 | C | false | DEVICE_DATA | 21 | 1 | 1/10 |
| ts16 | (TS16) | int16 | C | false | DEVICE_DATA | 22 | 1 | 1/10 |
| ts8 | (TS8) | int16 | C | false | DEVICE_DATA | 22 | 1 | 1/10 |
| ts16 | (TS16) | int16 | C | false | DEVICE_DATA | 23 | 1 | 1/10 |
| heatassistvalve | heat assistance valve (M1) | boolean | | false | DEVICE_DATA | 19 | 1 | 1 |
| heatassistpower | heat assistance valve power (M1) | uint8 | % | false | DEVICE_DATA | 23 | 1 | 1 |
| solarpump2 | pump 2 (PS4) | boolean | | false | DEVICE_DATA | 24 | 1 | 1 |
| solarpump2mod | pump 2 modulation (PS4) | uint8 | % | false | DEVICE_DATA | 25 | 1 | 1 |
| cyl2bottomtemp | second cylinder bottom temperature (TS5) | int16 | C | false | DEVICE_DATA | 26 | 1 | 1/10 |
| cyl3bottomtemp | third cylinder bottom temperature (TS11) | int16 | C | false | DEVICE_DATA | 27 | 1 | 1/10 |
| cyltoptemp | cylinder top temperature (TS10) | int16 | C | false | DEVICE_DATA | 28 | 1 | 1/10 |
| heatexchangertemp | heat exchanger temperature (TS6) | int16 | C | false | DEVICE_DATA | 29 | 1 | 1/10 |
| cylpumpmod | cylinder pump modulation (PS5) | uint8 | % | false | DEVICE_DATA | 30 | 1 | 1 |
| valvestatus | valve status | boolean | | false | DEVICE_DATA | 31 | 1 | 1 |
| vs1status | valve status VS1 | boolean | | false | DEVICE_DATA | 32 | 1 | 1 |
| vs3status | valve status VS3 | boolean | | false | DEVICE_DATA | 33 | 1 | 1 |
| transferpump | transfer pump | boolean | | false | DEVICE_DATA | 34 | 1 | 1 |
| transferpumpmod | transfer pump modulation | uint8 | % | false | DEVICE_DATA | 35 | 1 | 1 |
| heatassistpower | heat assistance valve power (M1) | uint8 | % | false | DEVICE_DATA | 24 | 1 | 1 |
| solarpump2 | pump 2 (PS4) | boolean | | false | DEVICE_DATA | 25 | 1 | 1 |
| solarpump2mod | pump 2 modulation (PS4) | uint8 | % | false | DEVICE_DATA | 26 | 1 | 1 |
| cyl2bottomtemp | second cylinder bottom temperature (TS5) | int16 | C | false | DEVICE_DATA | 27 | 1 | 1/10 |
| cyl3bottomtemp | third cylinder bottom temperature (TS11) | int16 | C | false | DEVICE_DATA | 28 | 1 | 1/10 |
| cyltoptemp | cylinder top temperature (TS10) | int16 | C | false | DEVICE_DATA | 29 | 1 | 1/10 |
| heatexchangertemp | heat exchanger temperature (TS6) | int16 | C | false | DEVICE_DATA | 30 | 1 | 1/10 |
| cylpumpmod | cylinder pump modulation (PS5) | uint8 | % | false | DEVICE_DATA | 31 | 1 | 1 |
| valvestatus | valve status | boolean | | false | DEVICE_DATA | 32 | 1 | 1 |
| vs1status | valve status VS1 | boolean | | false | DEVICE_DATA | 33 | 1 | 1 |
| vs3status | valve status VS3 | boolean | | false | DEVICE_DATA | 34 | 1 | 1 |
| transferpump | transfer pump | boolean | | false | DEVICE_DATA | 35 | 1 | 1 |
| transferpumpmod | transfer pump modulation | uint8 | % | false | DEVICE_DATA | 36 | 1 | 1 |
uint8
| collectormaxtemp | maximum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 36 | 1 | 1 |
| collectormaxtemp | maximum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 37 | 1 | 1 |
uint8
| collectormintemp | minimum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 37 | 1 | 1 |
| collectormintemp | minimum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 38 | 1 | 1 |
| energylasthour | energy last hour | uint24 | Wh | false | DEVICE_DATA | 13 | 2 | 1/10 |
| energytoday | total energy today | uint24 | Wh | false | DEVICE_DATA | 38 | 2 | 1 |
| energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 40 | 2 | 1/10 |
| pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 42 | 2 | 1 |
| m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 44 | 2 | 1 |
| heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 46 | 1 | 1 |
| externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 47 | 1 | 1 |
| thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 48 | 1 | 1 |
| heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 49 | 1 | 1 |
| activated | activated | boolean | | true | DEVICE_DATA | 50 | 1 | 1 |
| solarpumpmode | solar pump mode | enum | | true | DEVICE_DATA | 51 | 1 | 1 |
| solarpumpkick | solar pump kick | boolean | | true | DEVICE_DATA | 52 | 1 | 1 |
| plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 53 | 1 | 1 |
| doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 54 | 1 | 1 |
| energytoday | total energy today | uint24 | Wh | false | DEVICE_DATA | 39 | 2 | 1 |
| energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 41 | 2 | 1/10 |
| pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 43 | 2 | 1 |
| m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 45 | 2 | 1 |
| heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 47 | 1 | 1 |
| externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 48 | 1 | 1 |
| thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 49 | 1 | 1 |
| heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 50 | 1 | 1 |
| activated | activated | boolean | | true | DEVICE_DATA | 51 | 1 | 1 |
| solarpumpmode | solar pump mode | enum | | true | DEVICE_DATA | 52 | 1 | 1 |
| solarpumpkick | solar pump kick | boolean | | true | DEVICE_DATA | 53 | 1 | 1 |
| plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 54 | 1 | 1 |
| doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 55 | 1 | 1 |
uint8
| pump2minmod | minimum pump 2 modulation | uint8 (&gt;=0&lt;=0) | % | true | DEVICE_DATA | 55 | 1 | 1 |
| pump2minmod | minimum pump 2 modulation | uint8 (&gt;=0&lt;=0) | % | true | DEVICE_DATA | 56 | 1 | 1 |
uint8
| turnondiff2 | pump 2 turn on difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 56 | 1 | 1/10 |
| turnondiff2 | pump 2 turn on difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 57 | 1 | 1/10 |
uint8
| turnoffdiff2 | pump 2 turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 57 | 1 | 1/10 |
| pump2kick | pump kick 2 | boolean | | true | DEVICE_DATA | 58 | 1 | 1 |
| turnoffdiff2 | pump 2 turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 58 | 1 | 1/10 |
| pump2kick | pump kick 2 | boolean | | true | DEVICE_DATA | 59 | 1 | 1 |
uint8
| climatezone | climate zone | uint8 (&gt;=0&lt;=0) | | true | DEVICE_DATA | 59 | 1 | 1 |
| climatezone | climate zone | uint8 (&gt;=0&lt;=0) | | true | DEVICE_DATA | 60 | 1 | 1 |
uint16
| collector1area | collector 1 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 60 | 1 | 1/10 |
| collector1type | collector 1 type | enum | | true | DEVICE_DATA | 61 | 1 | 1 |
| collector1area | collector 1 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 61 | 1 | 1/10 |
| collector1type | collector 1 type | enum | | true | DEVICE_DATA | 62 | 1 | 1 |
uint16
| collector2area | collector 2 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 62 | 1 | 1/10 |
| collector2type | collector 2 type | enum | | true | DEVICE_DATA | 63 | 1 | 1 |
| cylpriority | cylinder priority | enum | | true | DEVICE_DATA | 64 | 1 | 1 |
| heatcntflowtemp | heat counter flow temperature | uint16 | C | false | DEVICE_DATA | 65 | 1 | 1/10 |
| heatcntrettemp | heat counter return temperature | uint16 | C | false | DEVICE_DATA | 66 | 1 | 1/10 |
| heatcnt | heat counter impulses | uint8 | | false | DEVICE_DATA | 67 | 1 | 1 |
| swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 68 | 1 | 1/10 |
| swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 |
| collector2area | collector 2 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 63 | 1 | 1/10 |
| collector2type | collector 2 type | enum | | true | DEVICE_DATA | 64 | 1 | 1 |
| cylpriority | cylinder priority | enum | | true | DEVICE_DATA | 65 | 1 | 1 |
| heatcntflowtemp | heat counter flow temperature | uint16 | C | false | DEVICE_DATA | 66 | 1 | 1/10 |
| heatcntrettemp | heat counter return temperature | uint16 | C | false | DEVICE_DATA | 67 | 1 | 1/10 |
| heatcnt | heat counter impulses | uint8 | | false | DEVICE_DATA | 68 | 1 | 1 |
| swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 |
| swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 70 | 1 | 1/10 |
int8
| heatassiston | heat assistance on | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 70 | 1 | 1/10 |
| heatassiston | heat assistance on | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 71 | 1 | 1/10 |
int8
| heatassistoff | heat assistance off | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 71 | 1 | 1/10 |
| heatassistoff | heat assistance off | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 72 | 1 | 1/10 |
### SM200, MS200
@@ -7860,67 +7862,68 @@ uint8
uint8
| turnoffdiff | pump turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 11 | 1 | 1/10 |
| collector2temp | collector 2 temperature (TS7) | int16 | C | false | DEVICE_DATA | 20 | 1 | 1/10 |
| cylmiddletemp | cylinder middle temperature (TS3) | int16 | C | false | DEVICE_DATA | 17 | 1 | 1/10 |
| cylmiddletemp | cylinder middle temperature (TS14) | int16 | C | false | DEVICE_DATA | 17 | 1 | 1/10 |
| ts3 | cylinder middle temperature (TS3) | int16 | C | false | DEVICE_DATA | 21 | 1 | 1/10 |
| retheatassist | return temperature heat assistance (TS4) | int16 | C | false | DEVICE_DATA | 18 | 1 | 1/10 |
| ts8 | (TS8) | int16 | C | false | DEVICE_DATA | 21 | 1 | 1/10 |
| ts16 | (TS16) | int16 | C | false | DEVICE_DATA | 22 | 1 | 1/10 |
| ts8 | (TS8) | int16 | C | false | DEVICE_DATA | 22 | 1 | 1/10 |
| ts16 | (TS16) | int16 | C | false | DEVICE_DATA | 23 | 1 | 1/10 |
| heatassistvalve | heat assistance valve (M1) | boolean | | false | DEVICE_DATA | 19 | 1 | 1 |
| heatassistpower | heat assistance valve power (M1) | uint8 | % | false | DEVICE_DATA | 23 | 1 | 1 |
| solarpump2 | pump 2 (PS4) | boolean | | false | DEVICE_DATA | 24 | 1 | 1 |
| solarpump2mod | pump 2 modulation (PS4) | uint8 | % | false | DEVICE_DATA | 25 | 1 | 1 |
| cyl2bottomtemp | second cylinder bottom temperature (TS5) | int16 | C | false | DEVICE_DATA | 26 | 1 | 1/10 |
| cyl3bottomtemp | third cylinder bottom temperature (TS11) | int16 | C | false | DEVICE_DATA | 27 | 1 | 1/10 |
| cyltoptemp | cylinder top temperature (TS10) | int16 | C | false | DEVICE_DATA | 28 | 1 | 1/10 |
| heatexchangertemp | heat exchanger temperature (TS6) | int16 | C | false | DEVICE_DATA | 29 | 1 | 1/10 |
| cylpumpmod | cylinder pump modulation (PS5) | uint8 | % | false | DEVICE_DATA | 30 | 1 | 1 |
| valvestatus | valve status | boolean | | false | DEVICE_DATA | 31 | 1 | 1 |
| vs1status | valve status VS1 | boolean | | false | DEVICE_DATA | 32 | 1 | 1 |
| vs3status | valve status VS3 | boolean | | false | DEVICE_DATA | 33 | 1 | 1 |
| transferpump | transfer pump | boolean | | false | DEVICE_DATA | 34 | 1 | 1 |
| transferpumpmod | transfer pump modulation | uint8 | % | false | DEVICE_DATA | 35 | 1 | 1 |
| heatassistpower | heat assistance valve power (M1) | uint8 | % | false | DEVICE_DATA | 24 | 1 | 1 |
| solarpump2 | pump 2 (PS4) | boolean | | false | DEVICE_DATA | 25 | 1 | 1 |
| solarpump2mod | pump 2 modulation (PS4) | uint8 | % | false | DEVICE_DATA | 26 | 1 | 1 |
| cyl2bottomtemp | second cylinder bottom temperature (TS5) | int16 | C | false | DEVICE_DATA | 27 | 1 | 1/10 |
| cyl3bottomtemp | third cylinder bottom temperature (TS11) | int16 | C | false | DEVICE_DATA | 28 | 1 | 1/10 |
| cyltoptemp | cylinder top temperature (TS10) | int16 | C | false | DEVICE_DATA | 29 | 1 | 1/10 |
| heatexchangertemp | heat exchanger temperature (TS6) | int16 | C | false | DEVICE_DATA | 30 | 1 | 1/10 |
| cylpumpmod | cylinder pump modulation (PS5) | uint8 | % | false | DEVICE_DATA | 31 | 1 | 1 |
| valvestatus | valve status | boolean | | false | DEVICE_DATA | 32 | 1 | 1 |
| vs1status | valve status VS1 | boolean | | false | DEVICE_DATA | 33 | 1 | 1 |
| vs3status | valve status VS3 | boolean | | false | DEVICE_DATA | 34 | 1 | 1 |
| transferpump | transfer pump | boolean | | false | DEVICE_DATA | 35 | 1 | 1 |
| transferpumpmod | transfer pump modulation | uint8 | % | false | DEVICE_DATA | 36 | 1 | 1 |
uint8
| collectormaxtemp | maximum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 36 | 1 | 1 |
| collectormaxtemp | maximum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 37 | 1 | 1 |
uint8
| collectormintemp | minimum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 37 | 1 | 1 |
| collectormintemp | minimum collector temperature | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 38 | 1 | 1 |
| energylasthour | energy last hour | uint24 | Wh | false | DEVICE_DATA | 13 | 2 | 1/10 |
| energytoday | total energy today | uint24 | Wh | false | DEVICE_DATA | 38 | 2 | 1 |
| energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 40 | 2 | 1/10 |
| pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 42 | 2 | 1 |
| m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 44 | 2 | 1 |
| heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 46 | 1 | 1 |
| externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 47 | 1 | 1 |
| thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 48 | 1 | 1 |
| heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 49 | 1 | 1 |
| activated | activated | boolean | | true | DEVICE_DATA | 50 | 1 | 1 |
| solarpumpmode | solar pump mode | enum | | true | DEVICE_DATA | 51 | 1 | 1 |
| solarpumpkick | solar pump kick | boolean | | true | DEVICE_DATA | 52 | 1 | 1 |
| plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 53 | 1 | 1 |
| doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 54 | 1 | 1 |
| energytoday | total energy today | uint24 | Wh | false | DEVICE_DATA | 39 | 2 | 1 |
| energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 41 | 2 | 1/10 |
| pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 43 | 2 | 1 |
| m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 45 | 2 | 1 |
| heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 47 | 1 | 1 |
| externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 48 | 1 | 1 |
| thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 49 | 1 | 1 |
| heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 50 | 1 | 1 |
| activated | activated | boolean | | true | DEVICE_DATA | 51 | 1 | 1 |
| solarpumpmode | solar pump mode | enum | | true | DEVICE_DATA | 52 | 1 | 1 |
| solarpumpkick | solar pump kick | boolean | | true | DEVICE_DATA | 53 | 1 | 1 |
| plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 54 | 1 | 1 |
| doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 55 | 1 | 1 |
uint8
| pump2minmod | minimum pump 2 modulation | uint8 (&gt;=0&lt;=0) | % | true | DEVICE_DATA | 55 | 1 | 1 |
| pump2minmod | minimum pump 2 modulation | uint8 (&gt;=0&lt;=0) | % | true | DEVICE_DATA | 56 | 1 | 1 |
uint8
| turnondiff2 | pump 2 turn on difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 56 | 1 | 1/10 |
| turnondiff2 | pump 2 turn on difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 57 | 1 | 1/10 |
uint8
| turnoffdiff2 | pump 2 turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 57 | 1 | 1/10 |
| pump2kick | pump kick 2 | boolean | | true | DEVICE_DATA | 58 | 1 | 1 |
| turnoffdiff2 | pump 2 turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 58 | 1 | 1/10 |
| pump2kick | pump kick 2 | boolean | | true | DEVICE_DATA | 59 | 1 | 1 |
uint8
| climatezone | climate zone | uint8 (&gt;=0&lt;=0) | | true | DEVICE_DATA | 59 | 1 | 1 |
| climatezone | climate zone | uint8 (&gt;=0&lt;=0) | | true | DEVICE_DATA | 60 | 1 | 1 |
uint16
| collector1area | collector 1 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 60 | 1 | 1/10 |
| collector1type | collector 1 type | enum | | true | DEVICE_DATA | 61 | 1 | 1 |
| collector1area | collector 1 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 61 | 1 | 1/10 |
| collector1type | collector 1 type | enum | | true | DEVICE_DATA | 62 | 1 | 1 |
uint16
| collector2area | collector 2 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 62 | 1 | 1/10 |
| collector2type | collector 2 type | enum | | true | DEVICE_DATA | 63 | 1 | 1 |
| cylpriority | cylinder priority | enum | | true | DEVICE_DATA | 64 | 1 | 1 |
| heatcntflowtemp | heat counter flow temperature | uint16 | C | false | DEVICE_DATA | 65 | 1 | 1/10 |
| heatcntrettemp | heat counter return temperature | uint16 | C | false | DEVICE_DATA | 66 | 1 | 1/10 |
| heatcnt | heat counter impulses | uint8 | | false | DEVICE_DATA | 67 | 1 | 1 |
| swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 68 | 1 | 1/10 |
| swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 |
| collector2area | collector 2 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 63 | 1 | 1/10 |
| collector2type | collector 2 type | enum | | true | DEVICE_DATA | 64 | 1 | 1 |
| cylpriority | cylinder priority | enum | | true | DEVICE_DATA | 65 | 1 | 1 |
| heatcntflowtemp | heat counter flow temperature | uint16 | C | false | DEVICE_DATA | 66 | 1 | 1/10 |
| heatcntrettemp | heat counter return temperature | uint16 | C | false | DEVICE_DATA | 67 | 1 | 1/10 |
| heatcnt | heat counter impulses | uint8 | | false | DEVICE_DATA | 68 | 1 | 1 |
| swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 |
| swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 70 | 1 | 1/10 |
int8
| heatassiston | heat assistance on | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 70 | 1 | 1/10 |
| heatassiston | heat assistance on | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 71 | 1 | 1/10 |
int8
| heatassistoff | heat assistance off | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 71 | 1 | 1/10 |
| heatassistoff | heat assistance off | int8 (&gt;=0&lt;=0) | K | true | DEVICE_DATA | 72 | 1 | 1/10 |
## Devices of type \_heatpump

View File

@@ -5495,7 +5495,7 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/
"ISM1",solar,101,cylmaxtemp,maximum cylinder temperature,uint8 (>=0<=0),C,true,number.solar_maximum_cylinder_temperature,number.solar_cylmaxtemp,8,0,1,5,1
"ISM1",solar,101,collectorshutdown,collector shutdown,boolean, ,false,binary_sensor.solar_collector_shutdown,binary_sensor.solar_collectorshutdown,8,0,1,6,1
"ISM1",solar,101,cylheated,cyl heated,boolean, ,false,binary_sensor.solar_cyl_heated,binary_sensor.solar_cylheated,8,0,1,7,1
"ISM1",solar,101,cylmiddletemp,cylinder middle temperature (TS3),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS3),sensor.solar_cylmiddletemp,8,0,1/10,17,1
"ISM1",solar,101,cylmiddletemp,cylinder middle temperature (TS14),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS14),sensor.solar_cylmiddletemp,8,0,1/10,17,1
"ISM1",solar,101,retheatassist,return temperature heat assistance (TS4),int16,C,false,sensor.solar_return_temperature_heat_assistance_(TS4),sensor.solar_retheatassist,8,0,1/10,18,1
"ISM1",solar,101,heatassistvalve,heat assistance valve (M1),boolean, ,false,binary_sensor.solar_heat_assistance_valve_(M1),binary_sensor.solar_heatassistvalve,8,0,1,19,1
"ISM1",solar,101,energylasthour,energy last hour,uint24,Wh,false,sensor.solar_energy_last_hour,sensor.solar_energylasthour,8,0,1/10,13,2
@@ -5506,7 +5506,7 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/
"ISM2",solar,103,cylmaxtemp,maximum cylinder temperature,uint8 (>=0<=0),C,true,number.solar_maximum_cylinder_temperature,number.solar_cylmaxtemp,8,0,1,5,1
"ISM2",solar,103,collectorshutdown,collector shutdown,boolean, ,false,binary_sensor.solar_collector_shutdown,binary_sensor.solar_collectorshutdown,8,0,1,6,1
"ISM2",solar,103,cylheated,cyl heated,boolean, ,false,binary_sensor.solar_cyl_heated,binary_sensor.solar_cylheated,8,0,1,7,1
"ISM2",solar,103,cylmiddletemp,cylinder middle temperature (TS3),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS3),sensor.solar_cylmiddletemp,8,0,1/10,17,1
"ISM2",solar,103,cylmiddletemp,cylinder middle temperature (TS14),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS14),sensor.solar_cylmiddletemp,8,0,1/10,17,1
"ISM2",solar,103,retheatassist,return temperature heat assistance (TS4),int16,C,false,sensor.solar_return_temperature_heat_assistance_(TS4),sensor.solar_retheatassist,8,0,1/10,18,1
"ISM2",solar,103,heatassistvalve,heat assistance valve (M1),boolean, ,false,binary_sensor.solar_heat_assistance_valve_(M1),binary_sensor.solar_heatassistvalve,8,0,1,19,1
"ISM2",solar,103,energylasthour,energy last hour,uint24,Wh,false,sensor.solar_energy_last_hour,sensor.solar_energylasthour,8,0,1/10,13,2
@@ -5522,57 +5522,58 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/
"SM50",solar,162,turnondiff,pump turn on difference,uint8 (>=0<=0),C,true,number.solar_pump_turn_on_difference,number.solar_turnondiff,8,0,1/10,10,1
"SM50",solar,162,turnoffdiff,pump turn off difference,uint8 (>=0<=0),C,true,number.solar_pump_turn_off_difference,number.solar_turnoffdiff,8,0,1/10,11,1
"SM50",solar,162,collector2temp,collector 2 temperature (TS7),int16,C,false,sensor.solar_collector_2_temperature_(TS7),sensor.solar_collector2temp,8,0,1/10,20,1
"SM50",solar,162,cylmiddletemp,cylinder middle temperature (TS3),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS3),sensor.solar_cylmiddletemp,8,0,1/10,17,1
"SM50",solar,162,cylmiddletemp,cylinder middle temperature (TS14),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS14),sensor.solar_cylmiddletemp,8,0,1/10,17,1
"SM50",solar,162,ts3,cylinder middle temperature (TS3),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS3),sensor.solar_ts3,8,0,1/10,21,1
"SM50",solar,162,retheatassist,return temperature heat assistance (TS4),int16,C,false,sensor.solar_return_temperature_heat_assistance_(TS4),sensor.solar_retheatassist,8,0,1/10,18,1
"SM50",solar,162,ts8,(TS8),int16,C,false,sensor.solar_(TS8),sensor.solar_ts8,8,0,1/10,21,1
"SM50",solar,162,ts16,(TS16),int16,C,false,sensor.solar_(TS16),sensor.solar_ts16,8,0,1/10,22,1
"SM50",solar,162,ts8,(TS8),int16,C,false,sensor.solar_(TS8),sensor.solar_ts8,8,0,1/10,22,1
"SM50",solar,162,ts16,(TS16),int16,C,false,sensor.solar_(TS16),sensor.solar_ts16,8,0,1/10,23,1
"SM50",solar,162,heatassistvalve,heat assistance valve (M1),boolean, ,false,binary_sensor.solar_heat_assistance_valve_(M1),binary_sensor.solar_heatassistvalve,8,0,1,19,1
"SM50",solar,162,heatassistpower,heat assistance valve power (M1),uint8,%,false,sensor.solar_heat_assistance_valve_power_(M1),sensor.solar_heatassistpower,8,0,1,23,1
"SM50",solar,162,solarpump2,pump 2 (PS4),boolean, ,false,binary_sensor.solar_pump_2_(PS4),binary_sensor.solar_solarpump2,8,0,1,24,1
"SM50",solar,162,solarpump2mod,pump 2 modulation (PS4),uint8,%,false,sensor.solar_pump_2_modulation_(PS4),sensor.solar_solarpump2mod,8,0,1,25,1
"SM50",solar,162,cyl2bottomtemp,second cylinder bottom temperature (TS5),int16,C,false,sensor.solar_second_cylinder_bottom_temperature_(TS5),sensor.solar_cyl2bottomtemp,8,0,1/10,26,1
"SM50",solar,162,cyl3bottomtemp,third cylinder bottom temperature (TS11),int16,C,false,sensor.solar_third_cylinder_bottom_temperature_(TS11),sensor.solar_cyl3bottomtemp,8,0,1/10,27,1
"SM50",solar,162,cyltoptemp,cylinder top temperature (TS10),int16,C,false,sensor.solar_cylinder_top_temperature_(TS10),sensor.solar_cyltoptemp,8,0,1/10,28,1
"SM50",solar,162,heatexchangertemp,heat exchanger temperature (TS6),int16,C,false,sensor.solar_heat_exchanger_temperature_(TS6),sensor.solar_heatexchangertemp,8,0,1/10,29,1
"SM50",solar,162,cylpumpmod,cylinder pump modulation (PS5),uint8,%,false,sensor.solar_cylinder_pump_modulation_(PS5),sensor.solar_cylpumpmod,8,0,1,30,1
"SM50",solar,162,valvestatus,valve status,boolean, ,false,binary_sensor.solar_valve_status,binary_sensor.solar_valvestatus,8,0,1,31,1
"SM50",solar,162,vs1status,valve status VS1,boolean, ,false,binary_sensor.solar_valve_status_VS1,binary_sensor.solar_vs1status,8,0,1,32,1
"SM50",solar,162,vs3status,valve status VS3,boolean, ,false,binary_sensor.solar_valve_status_VS3,binary_sensor.solar_vs3status,8,0,1,33,1
"SM50",solar,162,transferpump,transfer pump,boolean, ,false,binary_sensor.solar_transfer_pump,binary_sensor.solar_transferpump,8,0,1,34,1
"SM50",solar,162,transferpumpmod,transfer pump modulation,uint8,%,false,sensor.solar_transfer_pump_modulation,sensor.solar_transferpumpmod,8,0,1,35,1
"SM50",solar,162,collectormaxtemp,maximum collector temperature,uint8 (>=0<=0),C,true,number.solar_maximum_collector_temperature,number.solar_collectormaxtemp,8,0,1,36,1
"SM50",solar,162,collectormintemp,minimum collector temperature,uint8 (>=0<=0),C,true,number.solar_minimum_collector_temperature,number.solar_collectormintemp,8,0,1,37,1
"SM50",solar,162,heatassistpower,heat assistance valve power (M1),uint8,%,false,sensor.solar_heat_assistance_valve_power_(M1),sensor.solar_heatassistpower,8,0,1,24,1
"SM50",solar,162,solarpump2,pump 2 (PS4),boolean, ,false,binary_sensor.solar_pump_2_(PS4),binary_sensor.solar_solarpump2,8,0,1,25,1
"SM50",solar,162,solarpump2mod,pump 2 modulation (PS4),uint8,%,false,sensor.solar_pump_2_modulation_(PS4),sensor.solar_solarpump2mod,8,0,1,26,1
"SM50",solar,162,cyl2bottomtemp,second cylinder bottom temperature (TS5),int16,C,false,sensor.solar_second_cylinder_bottom_temperature_(TS5),sensor.solar_cyl2bottomtemp,8,0,1/10,27,1
"SM50",solar,162,cyl3bottomtemp,third cylinder bottom temperature (TS11),int16,C,false,sensor.solar_third_cylinder_bottom_temperature_(TS11),sensor.solar_cyl3bottomtemp,8,0,1/10,28,1
"SM50",solar,162,cyltoptemp,cylinder top temperature (TS10),int16,C,false,sensor.solar_cylinder_top_temperature_(TS10),sensor.solar_cyltoptemp,8,0,1/10,29,1
"SM50",solar,162,heatexchangertemp,heat exchanger temperature (TS6),int16,C,false,sensor.solar_heat_exchanger_temperature_(TS6),sensor.solar_heatexchangertemp,8,0,1/10,30,1
"SM50",solar,162,cylpumpmod,cylinder pump modulation (PS5),uint8,%,false,sensor.solar_cylinder_pump_modulation_(PS5),sensor.solar_cylpumpmod,8,0,1,31,1
"SM50",solar,162,valvestatus,valve status,boolean, ,false,binary_sensor.solar_valve_status,binary_sensor.solar_valvestatus,8,0,1,32,1
"SM50",solar,162,vs1status,valve status VS1,boolean, ,false,binary_sensor.solar_valve_status_VS1,binary_sensor.solar_vs1status,8,0,1,33,1
"SM50",solar,162,vs3status,valve status VS3,boolean, ,false,binary_sensor.solar_valve_status_VS3,binary_sensor.solar_vs3status,8,0,1,34,1
"SM50",solar,162,transferpump,transfer pump,boolean, ,false,binary_sensor.solar_transfer_pump,binary_sensor.solar_transferpump,8,0,1,35,1
"SM50",solar,162,transferpumpmod,transfer pump modulation,uint8,%,false,sensor.solar_transfer_pump_modulation,sensor.solar_transferpumpmod,8,0,1,36,1
"SM50",solar,162,collectormaxtemp,maximum collector temperature,uint8 (>=0<=0),C,true,number.solar_maximum_collector_temperature,number.solar_collectormaxtemp,8,0,1,37,1
"SM50",solar,162,collectormintemp,minimum collector temperature,uint8 (>=0<=0),C,true,number.solar_minimum_collector_temperature,number.solar_collectormintemp,8,0,1,38,1
"SM50",solar,162,energylasthour,energy last hour,uint24,Wh,false,sensor.solar_energy_last_hour,sensor.solar_energylasthour,8,0,1/10,13,2
"SM50",solar,162,energytoday,total energy today,uint24,Wh,false,sensor.solar_total_energy_today,sensor.solar_energytoday,8,0,1,38,2
"SM50",solar,162,energytotal,total energy,uint24,kWh,false,sensor.solar_total_energy,sensor.solar_energytotal,8,0,1/10,40,2
"SM50",solar,162,pump2worktime,pump 2 working time,time,minutes,false,sensor.solar_pump_2_working_time,sensor.solar_pump2worktime,8,0,1,42,2
"SM50",solar,162,m1worktime,differential control working time,time,minutes,false,sensor.solar_differential_control_working_time,sensor.solar_m1worktime,8,0,1,44,2
"SM50",solar,162,heattransfersystem,heattransfer system,boolean (>=0<=0), ,true,switch.solar_heattransfer_system,switch.solar_heattransfersystem,8,0,1,46,1
"SM50",solar,162,externalcyl,external cylinder,boolean (>=0<=0), ,true,switch.solar_external_cylinder,switch.solar_externalcyl,8,0,1,47,1
"SM50",solar,162,thermaldisinfect,thermal disinfection,boolean (>=0<=0), ,true,switch.solar_thermal_disinfection,switch.solar_thermaldisinfect,8,0,1,48,1
"SM50",solar,162,heatmetering,heatmetering,boolean (>=0<=0), ,true,switch.solar_heatmetering,switch.solar_heatmetering,8,0,1,49,1
"SM50",solar,162,activated,activated,boolean (>=0<=0), ,true,switch.solar_activated,switch.solar_activated,8,0,1,50,1
"SM50",solar,162,solarpumpmode,solar pump mode,enum [constant\|pwm\|analog] (>=0<=0), ,true,select.solar_solar_pump_mode,select.solar_solarpumpmode,8,0,1,51,1
"SM50",solar,162,solarpumpkick,solar pump kick,boolean (>=0<=0), ,true,switch.solar_solar_pump_kick,switch.solar_solarpumpkick,8,0,1,52,1
"SM50",solar,162,plainwatermode,plain water mode,boolean (>=0<=0), ,true,switch.solar_plain_water_mode,switch.solar_plainwatermode,8,0,1,53,1
"SM50",solar,162,doublematchflow,doublematchflow,boolean (>=0<=0), ,true,switch.solar_doublematchflow,switch.solar_doublematchflow,8,0,1,54,1
"SM50",solar,162,pump2minmod,minimum pump 2 modulation,uint8 (>=0<=0),%,true,number.solar_minimum_pump_2_modulation,number.solar_pump2minmod,8,0,1,55,1
"SM50",solar,162,turnondiff2,pump 2 turn on difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_on_difference,number.solar_turnondiff2,8,0,1/10,56,1
"SM50",solar,162,turnoffdiff2,pump 2 turn off difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_off_difference,number.solar_turnoffdiff2,8,0,1/10,57,1
"SM50",solar,162,pump2kick,pump kick 2,boolean (>=0<=0), ,true,switch.solar_pump_kick_2,switch.solar_pump2kick,8,0,1,58,1
"SM50",solar,162,climatezone,climate zone,uint8 (>=0<=0), ,true,number.solar_climate_zone,number.solar_climatezone,8,0,1,59,1
"SM50",solar,162,collector1area,collector 1 area,uint16 (>=0<=0),,true,number.solar_collector_1_area,number.solar_collector1area,8,0,1/10,60,1
"SM50",solar,162,collector1type,collector 1 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_1_type,select.solar_collector1type,8,0,1,61,1
"SM50",solar,162,collector2area,collector 2 area,uint16 (>=0<=0),,true,number.solar_collector_2_area,number.solar_collector2area,8,0,1/10,62,1
"SM50",solar,162,collector2type,collector 2 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_2_type,select.solar_collector2type,8,0,1,63,1
"SM50",solar,162,cylpriority,cylinder priority,enum [cyl 1\|cyl 2] (>=0<=0), ,true,select.solar_cylinder_priority,select.solar_cylpriority,8,0,1,64,1
"SM50",solar,162,heatcntflowtemp,heat counter flow temperature,uint16,C,false,sensor.solar_heat_counter_flow_temperature,sensor.solar_heatcntflowtemp,8,0,1/10,65,1
"SM50",solar,162,heatcntrettemp,heat counter return temperature,uint16,C,false,sensor.solar_heat_counter_return_temperature,sensor.solar_heatcntrettemp,8,0,1/10,66,1
"SM50",solar,162,heatcnt,heat counter impulses,uint8, ,false,sensor.solar_heat_counter_impulses,sensor.solar_heatcnt,8,0,1,67,1
"SM50",solar,162,swapflowtemp,swap flow temperature (TS14),uint16,C,false,sensor.solar_swap_flow_temperature_(TS14),sensor.solar_swapflowtemp,8,0,1/10,68,1
"SM50",solar,162,swaprettemp,swap return temperature (TS15),uint16,C,false,sensor.solar_swap_return_temperature_(TS15),sensor.solar_swaprettemp,8,0,1/10,69,1
"SM50",solar,162,heatassiston,heat assistance on,int8 (>=0<=0),K,true,number.solar_heat_assistance_on,number.solar_heatassiston,8,0,1/10,70,1
"SM50",solar,162,heatassistoff,heat assistance off,int8 (>=0<=0),K,true,number.solar_heat_assistance_off,number.solar_heatassistoff,8,0,1/10,71,1
"SM50",solar,162,energytoday,total energy today,uint24,Wh,false,sensor.solar_total_energy_today,sensor.solar_energytoday,8,0,1,39,2
"SM50",solar,162,energytotal,total energy,uint24,kWh,false,sensor.solar_total_energy,sensor.solar_energytotal,8,0,1/10,41,2
"SM50",solar,162,pump2worktime,pump 2 working time,time,minutes,false,sensor.solar_pump_2_working_time,sensor.solar_pump2worktime,8,0,1,43,2
"SM50",solar,162,m1worktime,differential control working time,time,minutes,false,sensor.solar_differential_control_working_time,sensor.solar_m1worktime,8,0,1,45,2
"SM50",solar,162,heattransfersystem,heattransfer system,boolean (>=0<=0), ,true,switch.solar_heattransfer_system,switch.solar_heattransfersystem,8,0,1,47,1
"SM50",solar,162,externalcyl,external cylinder,boolean (>=0<=0), ,true,switch.solar_external_cylinder,switch.solar_externalcyl,8,0,1,48,1
"SM50",solar,162,thermaldisinfect,thermal disinfection,boolean (>=0<=0), ,true,switch.solar_thermal_disinfection,switch.solar_thermaldisinfect,8,0,1,49,1
"SM50",solar,162,heatmetering,heatmetering,boolean (>=0<=0), ,true,switch.solar_heatmetering,switch.solar_heatmetering,8,0,1,50,1
"SM50",solar,162,activated,activated,boolean (>=0<=0), ,true,switch.solar_activated,switch.solar_activated,8,0,1,51,1
"SM50",solar,162,solarpumpmode,solar pump mode,enum [constant\|pwm\|analog] (>=0<=0), ,true,select.solar_solar_pump_mode,select.solar_solarpumpmode,8,0,1,52,1
"SM50",solar,162,solarpumpkick,solar pump kick,boolean (>=0<=0), ,true,switch.solar_solar_pump_kick,switch.solar_solarpumpkick,8,0,1,53,1
"SM50",solar,162,plainwatermode,plain water mode,boolean (>=0<=0), ,true,switch.solar_plain_water_mode,switch.solar_plainwatermode,8,0,1,54,1
"SM50",solar,162,doublematchflow,doublematchflow,boolean (>=0<=0), ,true,switch.solar_doublematchflow,switch.solar_doublematchflow,8,0,1,55,1
"SM50",solar,162,pump2minmod,minimum pump 2 modulation,uint8 (>=0<=0),%,true,number.solar_minimum_pump_2_modulation,number.solar_pump2minmod,8,0,1,56,1
"SM50",solar,162,turnondiff2,pump 2 turn on difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_on_difference,number.solar_turnondiff2,8,0,1/10,57,1
"SM50",solar,162,turnoffdiff2,pump 2 turn off difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_off_difference,number.solar_turnoffdiff2,8,0,1/10,58,1
"SM50",solar,162,pump2kick,pump kick 2,boolean (>=0<=0), ,true,switch.solar_pump_kick_2,switch.solar_pump2kick,8,0,1,59,1
"SM50",solar,162,climatezone,climate zone,uint8 (>=0<=0), ,true,number.solar_climate_zone,number.solar_climatezone,8,0,1,60,1
"SM50",solar,162,collector1area,collector 1 area,uint16 (>=0<=0),,true,number.solar_collector_1_area,number.solar_collector1area,8,0,1/10,61,1
"SM50",solar,162,collector1type,collector 1 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_1_type,select.solar_collector1type,8,0,1,62,1
"SM50",solar,162,collector2area,collector 2 area,uint16 (>=0<=0),,true,number.solar_collector_2_area,number.solar_collector2area,8,0,1/10,63,1
"SM50",solar,162,collector2type,collector 2 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_2_type,select.solar_collector2type,8,0,1,64,1
"SM50",solar,162,cylpriority,cylinder priority,enum [cyl 1\|cyl 2] (>=0<=0), ,true,select.solar_cylinder_priority,select.solar_cylpriority,8,0,1,65,1
"SM50",solar,162,heatcntflowtemp,heat counter flow temperature,uint16,C,false,sensor.solar_heat_counter_flow_temperature,sensor.solar_heatcntflowtemp,8,0,1/10,66,1
"SM50",solar,162,heatcntrettemp,heat counter return temperature,uint16,C,false,sensor.solar_heat_counter_return_temperature,sensor.solar_heatcntrettemp,8,0,1/10,67,1
"SM50",solar,162,heatcnt,heat counter impulses,uint8, ,false,sensor.solar_heat_counter_impulses,sensor.solar_heatcnt,8,0,1,68,1
"SM50",solar,162,swapflowtemp,swap flow temperature (TS14),uint16,C,false,sensor.solar_swap_flow_temperature_(TS14),sensor.solar_swapflowtemp,8,0,1/10,69,1
"SM50",solar,162,swaprettemp,swap return temperature (TS15),uint16,C,false,sensor.solar_swap_return_temperature_(TS15),sensor.solar_swaprettemp,8,0,1/10,70,1
"SM50",solar,162,heatassiston,heat assistance on,int8 (>=0<=0),K,true,number.solar_heat_assistance_on,number.solar_heatassiston,8,0,1/10,71,1
"SM50",solar,162,heatassistoff,heat assistance off,int8 (>=0<=0),K,true,number.solar_heat_assistance_off,number.solar_heatassistoff,8,0,1/10,72,1
"SM100, MS100",solar,163,collectortemp,collector temperature (TS1),int16,C,false,sensor.solar_collector_temperature_(TS1),sensor.solar_collectortemp,8,0,1/10,0,1
"SM100, MS100",solar,163,cylbottomtemp,cylinder bottom temperature (TS2),int16,C,false,sensor.solar_cylinder_bottom_temperature_(TS2),sensor.solar_cylbottomtemp,8,0,1/10,1,1
"SM100, MS100",solar,163,solarpump,pump (PS1),boolean, ,false,binary_sensor.solar_pump_(PS1),binary_sensor.solar_solarpump,8,0,1,2,1
@@ -5585,57 +5586,58 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/
"SM100, MS100",solar,163,turnondiff,pump turn on difference,uint8 (>=0<=0),C,true,number.solar_pump_turn_on_difference,number.solar_turnondiff,8,0,1/10,10,1
"SM100, MS100",solar,163,turnoffdiff,pump turn off difference,uint8 (>=0<=0),C,true,number.solar_pump_turn_off_difference,number.solar_turnoffdiff,8,0,1/10,11,1
"SM100, MS100",solar,163,collector2temp,collector 2 temperature (TS7),int16,C,false,sensor.solar_collector_2_temperature_(TS7),sensor.solar_collector2temp,8,0,1/10,20,1
"SM100, MS100",solar,163,cylmiddletemp,cylinder middle temperature (TS3),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS3),sensor.solar_cylmiddletemp,8,0,1/10,17,1
"SM100, MS100",solar,163,cylmiddletemp,cylinder middle temperature (TS14),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS14),sensor.solar_cylmiddletemp,8,0,1/10,17,1
"SM100, MS100",solar,163,ts3,cylinder middle temperature (TS3),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS3),sensor.solar_ts3,8,0,1/10,21,1
"SM100, MS100",solar,163,retheatassist,return temperature heat assistance (TS4),int16,C,false,sensor.solar_return_temperature_heat_assistance_(TS4),sensor.solar_retheatassist,8,0,1/10,18,1
"SM100, MS100",solar,163,ts8,(TS8),int16,C,false,sensor.solar_(TS8),sensor.solar_ts8,8,0,1/10,21,1
"SM100, MS100",solar,163,ts16,(TS16),int16,C,false,sensor.solar_(TS16),sensor.solar_ts16,8,0,1/10,22,1
"SM100, MS100",solar,163,ts8,(TS8),int16,C,false,sensor.solar_(TS8),sensor.solar_ts8,8,0,1/10,22,1
"SM100, MS100",solar,163,ts16,(TS16),int16,C,false,sensor.solar_(TS16),sensor.solar_ts16,8,0,1/10,23,1
"SM100, MS100",solar,163,heatassistvalve,heat assistance valve (M1),boolean, ,false,binary_sensor.solar_heat_assistance_valve_(M1),binary_sensor.solar_heatassistvalve,8,0,1,19,1
"SM100, MS100",solar,163,heatassistpower,heat assistance valve power (M1),uint8,%,false,sensor.solar_heat_assistance_valve_power_(M1),sensor.solar_heatassistpower,8,0,1,23,1
"SM100, MS100",solar,163,solarpump2,pump 2 (PS4),boolean, ,false,binary_sensor.solar_pump_2_(PS4),binary_sensor.solar_solarpump2,8,0,1,24,1
"SM100, MS100",solar,163,solarpump2mod,pump 2 modulation (PS4),uint8,%,false,sensor.solar_pump_2_modulation_(PS4),sensor.solar_solarpump2mod,8,0,1,25,1
"SM100, MS100",solar,163,cyl2bottomtemp,second cylinder bottom temperature (TS5),int16,C,false,sensor.solar_second_cylinder_bottom_temperature_(TS5),sensor.solar_cyl2bottomtemp,8,0,1/10,26,1
"SM100, MS100",solar,163,cyl3bottomtemp,third cylinder bottom temperature (TS11),int16,C,false,sensor.solar_third_cylinder_bottom_temperature_(TS11),sensor.solar_cyl3bottomtemp,8,0,1/10,27,1
"SM100, MS100",solar,163,cyltoptemp,cylinder top temperature (TS10),int16,C,false,sensor.solar_cylinder_top_temperature_(TS10),sensor.solar_cyltoptemp,8,0,1/10,28,1
"SM100, MS100",solar,163,heatexchangertemp,heat exchanger temperature (TS6),int16,C,false,sensor.solar_heat_exchanger_temperature_(TS6),sensor.solar_heatexchangertemp,8,0,1/10,29,1
"SM100, MS100",solar,163,cylpumpmod,cylinder pump modulation (PS5),uint8,%,false,sensor.solar_cylinder_pump_modulation_(PS5),sensor.solar_cylpumpmod,8,0,1,30,1
"SM100, MS100",solar,163,valvestatus,valve status,boolean, ,false,binary_sensor.solar_valve_status,binary_sensor.solar_valvestatus,8,0,1,31,1
"SM100, MS100",solar,163,vs1status,valve status VS1,boolean, ,false,binary_sensor.solar_valve_status_VS1,binary_sensor.solar_vs1status,8,0,1,32,1
"SM100, MS100",solar,163,vs3status,valve status VS3,boolean, ,false,binary_sensor.solar_valve_status_VS3,binary_sensor.solar_vs3status,8,0,1,33,1
"SM100, MS100",solar,163,transferpump,transfer pump,boolean, ,false,binary_sensor.solar_transfer_pump,binary_sensor.solar_transferpump,8,0,1,34,1
"SM100, MS100",solar,163,transferpumpmod,transfer pump modulation,uint8,%,false,sensor.solar_transfer_pump_modulation,sensor.solar_transferpumpmod,8,0,1,35,1
"SM100, MS100",solar,163,collectormaxtemp,maximum collector temperature,uint8 (>=0<=0),C,true,number.solar_maximum_collector_temperature,number.solar_collectormaxtemp,8,0,1,36,1
"SM100, MS100",solar,163,collectormintemp,minimum collector temperature,uint8 (>=0<=0),C,true,number.solar_minimum_collector_temperature,number.solar_collectormintemp,8,0,1,37,1
"SM100, MS100",solar,163,heatassistpower,heat assistance valve power (M1),uint8,%,false,sensor.solar_heat_assistance_valve_power_(M1),sensor.solar_heatassistpower,8,0,1,24,1
"SM100, MS100",solar,163,solarpump2,pump 2 (PS4),boolean, ,false,binary_sensor.solar_pump_2_(PS4),binary_sensor.solar_solarpump2,8,0,1,25,1
"SM100, MS100",solar,163,solarpump2mod,pump 2 modulation (PS4),uint8,%,false,sensor.solar_pump_2_modulation_(PS4),sensor.solar_solarpump2mod,8,0,1,26,1
"SM100, MS100",solar,163,cyl2bottomtemp,second cylinder bottom temperature (TS5),int16,C,false,sensor.solar_second_cylinder_bottom_temperature_(TS5),sensor.solar_cyl2bottomtemp,8,0,1/10,27,1
"SM100, MS100",solar,163,cyl3bottomtemp,third cylinder bottom temperature (TS11),int16,C,false,sensor.solar_third_cylinder_bottom_temperature_(TS11),sensor.solar_cyl3bottomtemp,8,0,1/10,28,1
"SM100, MS100",solar,163,cyltoptemp,cylinder top temperature (TS10),int16,C,false,sensor.solar_cylinder_top_temperature_(TS10),sensor.solar_cyltoptemp,8,0,1/10,29,1
"SM100, MS100",solar,163,heatexchangertemp,heat exchanger temperature (TS6),int16,C,false,sensor.solar_heat_exchanger_temperature_(TS6),sensor.solar_heatexchangertemp,8,0,1/10,30,1
"SM100, MS100",solar,163,cylpumpmod,cylinder pump modulation (PS5),uint8,%,false,sensor.solar_cylinder_pump_modulation_(PS5),sensor.solar_cylpumpmod,8,0,1,31,1
"SM100, MS100",solar,163,valvestatus,valve status,boolean, ,false,binary_sensor.solar_valve_status,binary_sensor.solar_valvestatus,8,0,1,32,1
"SM100, MS100",solar,163,vs1status,valve status VS1,boolean, ,false,binary_sensor.solar_valve_status_VS1,binary_sensor.solar_vs1status,8,0,1,33,1
"SM100, MS100",solar,163,vs3status,valve status VS3,boolean, ,false,binary_sensor.solar_valve_status_VS3,binary_sensor.solar_vs3status,8,0,1,34,1
"SM100, MS100",solar,163,transferpump,transfer pump,boolean, ,false,binary_sensor.solar_transfer_pump,binary_sensor.solar_transferpump,8,0,1,35,1
"SM100, MS100",solar,163,transferpumpmod,transfer pump modulation,uint8,%,false,sensor.solar_transfer_pump_modulation,sensor.solar_transferpumpmod,8,0,1,36,1
"SM100, MS100",solar,163,collectormaxtemp,maximum collector temperature,uint8 (>=0<=0),C,true,number.solar_maximum_collector_temperature,number.solar_collectormaxtemp,8,0,1,37,1
"SM100, MS100",solar,163,collectormintemp,minimum collector temperature,uint8 (>=0<=0),C,true,number.solar_minimum_collector_temperature,number.solar_collectormintemp,8,0,1,38,1
"SM100, MS100",solar,163,energylasthour,energy last hour,uint24,Wh,false,sensor.solar_energy_last_hour,sensor.solar_energylasthour,8,0,1/10,13,2
"SM100, MS100",solar,163,energytoday,total energy today,uint24,Wh,false,sensor.solar_total_energy_today,sensor.solar_energytoday,8,0,1,38,2
"SM100, MS100",solar,163,energytotal,total energy,uint24,kWh,false,sensor.solar_total_energy,sensor.solar_energytotal,8,0,1/10,40,2
"SM100, MS100",solar,163,pump2worktime,pump 2 working time,time,minutes,false,sensor.solar_pump_2_working_time,sensor.solar_pump2worktime,8,0,1,42,2
"SM100, MS100",solar,163,m1worktime,differential control working time,time,minutes,false,sensor.solar_differential_control_working_time,sensor.solar_m1worktime,8,0,1,44,2
"SM100, MS100",solar,163,heattransfersystem,heattransfer system,boolean (>=0<=0), ,true,switch.solar_heattransfer_system,switch.solar_heattransfersystem,8,0,1,46,1
"SM100, MS100",solar,163,externalcyl,external cylinder,boolean (>=0<=0), ,true,switch.solar_external_cylinder,switch.solar_externalcyl,8,0,1,47,1
"SM100, MS100",solar,163,thermaldisinfect,thermal disinfection,boolean (>=0<=0), ,true,switch.solar_thermal_disinfection,switch.solar_thermaldisinfect,8,0,1,48,1
"SM100, MS100",solar,163,heatmetering,heatmetering,boolean (>=0<=0), ,true,switch.solar_heatmetering,switch.solar_heatmetering,8,0,1,49,1
"SM100, MS100",solar,163,activated,activated,boolean (>=0<=0), ,true,switch.solar_activated,switch.solar_activated,8,0,1,50,1
"SM100, MS100",solar,163,solarpumpmode,solar pump mode,enum [constant\|pwm\|analog] (>=0<=0), ,true,select.solar_solar_pump_mode,select.solar_solarpumpmode,8,0,1,51,1
"SM100, MS100",solar,163,solarpumpkick,solar pump kick,boolean (>=0<=0), ,true,switch.solar_solar_pump_kick,switch.solar_solarpumpkick,8,0,1,52,1
"SM100, MS100",solar,163,plainwatermode,plain water mode,boolean (>=0<=0), ,true,switch.solar_plain_water_mode,switch.solar_plainwatermode,8,0,1,53,1
"SM100, MS100",solar,163,doublematchflow,doublematchflow,boolean (>=0<=0), ,true,switch.solar_doublematchflow,switch.solar_doublematchflow,8,0,1,54,1
"SM100, MS100",solar,163,pump2minmod,minimum pump 2 modulation,uint8 (>=0<=0),%,true,number.solar_minimum_pump_2_modulation,number.solar_pump2minmod,8,0,1,55,1
"SM100, MS100",solar,163,turnondiff2,pump 2 turn on difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_on_difference,number.solar_turnondiff2,8,0,1/10,56,1
"SM100, MS100",solar,163,turnoffdiff2,pump 2 turn off difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_off_difference,number.solar_turnoffdiff2,8,0,1/10,57,1
"SM100, MS100",solar,163,pump2kick,pump kick 2,boolean (>=0<=0), ,true,switch.solar_pump_kick_2,switch.solar_pump2kick,8,0,1,58,1
"SM100, MS100",solar,163,climatezone,climate zone,uint8 (>=0<=0), ,true,number.solar_climate_zone,number.solar_climatezone,8,0,1,59,1
"SM100, MS100",solar,163,collector1area,collector 1 area,uint16 (>=0<=0),,true,number.solar_collector_1_area,number.solar_collector1area,8,0,1/10,60,1
"SM100, MS100",solar,163,collector1type,collector 1 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_1_type,select.solar_collector1type,8,0,1,61,1
"SM100, MS100",solar,163,collector2area,collector 2 area,uint16 (>=0<=0),,true,number.solar_collector_2_area,number.solar_collector2area,8,0,1/10,62,1
"SM100, MS100",solar,163,collector2type,collector 2 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_2_type,select.solar_collector2type,8,0,1,63,1
"SM100, MS100",solar,163,cylpriority,cylinder priority,enum [cyl 1\|cyl 2] (>=0<=0), ,true,select.solar_cylinder_priority,select.solar_cylpriority,8,0,1,64,1
"SM100, MS100",solar,163,heatcntflowtemp,heat counter flow temperature,uint16,C,false,sensor.solar_heat_counter_flow_temperature,sensor.solar_heatcntflowtemp,8,0,1/10,65,1
"SM100, MS100",solar,163,heatcntrettemp,heat counter return temperature,uint16,C,false,sensor.solar_heat_counter_return_temperature,sensor.solar_heatcntrettemp,8,0,1/10,66,1
"SM100, MS100",solar,163,heatcnt,heat counter impulses,uint8, ,false,sensor.solar_heat_counter_impulses,sensor.solar_heatcnt,8,0,1,67,1
"SM100, MS100",solar,163,swapflowtemp,swap flow temperature (TS14),uint16,C,false,sensor.solar_swap_flow_temperature_(TS14),sensor.solar_swapflowtemp,8,0,1/10,68,1
"SM100, MS100",solar,163,swaprettemp,swap return temperature (TS15),uint16,C,false,sensor.solar_swap_return_temperature_(TS15),sensor.solar_swaprettemp,8,0,1/10,69,1
"SM100, MS100",solar,163,heatassiston,heat assistance on,int8 (>=0<=0),K,true,number.solar_heat_assistance_on,number.solar_heatassiston,8,0,1/10,70,1
"SM100, MS100",solar,163,heatassistoff,heat assistance off,int8 (>=0<=0),K,true,number.solar_heat_assistance_off,number.solar_heatassistoff,8,0,1/10,71,1
"SM100, MS100",solar,163,energytoday,total energy today,uint24,Wh,false,sensor.solar_total_energy_today,sensor.solar_energytoday,8,0,1,39,2
"SM100, MS100",solar,163,energytotal,total energy,uint24,kWh,false,sensor.solar_total_energy,sensor.solar_energytotal,8,0,1/10,41,2
"SM100, MS100",solar,163,pump2worktime,pump 2 working time,time,minutes,false,sensor.solar_pump_2_working_time,sensor.solar_pump2worktime,8,0,1,43,2
"SM100, MS100",solar,163,m1worktime,differential control working time,time,minutes,false,sensor.solar_differential_control_working_time,sensor.solar_m1worktime,8,0,1,45,2
"SM100, MS100",solar,163,heattransfersystem,heattransfer system,boolean (>=0<=0), ,true,switch.solar_heattransfer_system,switch.solar_heattransfersystem,8,0,1,47,1
"SM100, MS100",solar,163,externalcyl,external cylinder,boolean (>=0<=0), ,true,switch.solar_external_cylinder,switch.solar_externalcyl,8,0,1,48,1
"SM100, MS100",solar,163,thermaldisinfect,thermal disinfection,boolean (>=0<=0), ,true,switch.solar_thermal_disinfection,switch.solar_thermaldisinfect,8,0,1,49,1
"SM100, MS100",solar,163,heatmetering,heatmetering,boolean (>=0<=0), ,true,switch.solar_heatmetering,switch.solar_heatmetering,8,0,1,50,1
"SM100, MS100",solar,163,activated,activated,boolean (>=0<=0), ,true,switch.solar_activated,switch.solar_activated,8,0,1,51,1
"SM100, MS100",solar,163,solarpumpmode,solar pump mode,enum [constant\|pwm\|analog] (>=0<=0), ,true,select.solar_solar_pump_mode,select.solar_solarpumpmode,8,0,1,52,1
"SM100, MS100",solar,163,solarpumpkick,solar pump kick,boolean (>=0<=0), ,true,switch.solar_solar_pump_kick,switch.solar_solarpumpkick,8,0,1,53,1
"SM100, MS100",solar,163,plainwatermode,plain water mode,boolean (>=0<=0), ,true,switch.solar_plain_water_mode,switch.solar_plainwatermode,8,0,1,54,1
"SM100, MS100",solar,163,doublematchflow,doublematchflow,boolean (>=0<=0), ,true,switch.solar_doublematchflow,switch.solar_doublematchflow,8,0,1,55,1
"SM100, MS100",solar,163,pump2minmod,minimum pump 2 modulation,uint8 (>=0<=0),%,true,number.solar_minimum_pump_2_modulation,number.solar_pump2minmod,8,0,1,56,1
"SM100, MS100",solar,163,turnondiff2,pump 2 turn on difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_on_difference,number.solar_turnondiff2,8,0,1/10,57,1
"SM100, MS100",solar,163,turnoffdiff2,pump 2 turn off difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_off_difference,number.solar_turnoffdiff2,8,0,1/10,58,1
"SM100, MS100",solar,163,pump2kick,pump kick 2,boolean (>=0<=0), ,true,switch.solar_pump_kick_2,switch.solar_pump2kick,8,0,1,59,1
"SM100, MS100",solar,163,climatezone,climate zone,uint8 (>=0<=0), ,true,number.solar_climate_zone,number.solar_climatezone,8,0,1,60,1
"SM100, MS100",solar,163,collector1area,collector 1 area,uint16 (>=0<=0),,true,number.solar_collector_1_area,number.solar_collector1area,8,0,1/10,61,1
"SM100, MS100",solar,163,collector1type,collector 1 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_1_type,select.solar_collector1type,8,0,1,62,1
"SM100, MS100",solar,163,collector2area,collector 2 area,uint16 (>=0<=0),,true,number.solar_collector_2_area,number.solar_collector2area,8,0,1/10,63,1
"SM100, MS100",solar,163,collector2type,collector 2 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_2_type,select.solar_collector2type,8,0,1,64,1
"SM100, MS100",solar,163,cylpriority,cylinder priority,enum [cyl 1\|cyl 2] (>=0<=0), ,true,select.solar_cylinder_priority,select.solar_cylpriority,8,0,1,65,1
"SM100, MS100",solar,163,heatcntflowtemp,heat counter flow temperature,uint16,C,false,sensor.solar_heat_counter_flow_temperature,sensor.solar_heatcntflowtemp,8,0,1/10,66,1
"SM100, MS100",solar,163,heatcntrettemp,heat counter return temperature,uint16,C,false,sensor.solar_heat_counter_return_temperature,sensor.solar_heatcntrettemp,8,0,1/10,67,1
"SM100, MS100",solar,163,heatcnt,heat counter impulses,uint8, ,false,sensor.solar_heat_counter_impulses,sensor.solar_heatcnt,8,0,1,68,1
"SM100, MS100",solar,163,swapflowtemp,swap flow temperature (TS14),uint16,C,false,sensor.solar_swap_flow_temperature_(TS14),sensor.solar_swapflowtemp,8,0,1/10,69,1
"SM100, MS100",solar,163,swaprettemp,swap return temperature (TS15),uint16,C,false,sensor.solar_swap_return_temperature_(TS15),sensor.solar_swaprettemp,8,0,1/10,70,1
"SM100, MS100",solar,163,heatassiston,heat assistance on,int8 (>=0<=0),K,true,number.solar_heat_assistance_on,number.solar_heatassiston,8,0,1/10,71,1
"SM100, MS100",solar,163,heatassistoff,heat assistance off,int8 (>=0<=0),K,true,number.solar_heat_assistance_off,number.solar_heatassistoff,8,0,1/10,72,1
"SM200, MS200",solar,164,collectortemp,collector temperature (TS1),int16,C,false,sensor.solar_collector_temperature_(TS1),sensor.solar_collectortemp,8,0,1/10,0,1
"SM200, MS200",solar,164,cylbottomtemp,cylinder bottom temperature (TS2),int16,C,false,sensor.solar_cylinder_bottom_temperature_(TS2),sensor.solar_cylbottomtemp,8,0,1/10,1,1
"SM200, MS200",solar,164,solarpump,pump (PS1),boolean, ,false,binary_sensor.solar_pump_(PS1),binary_sensor.solar_solarpump,8,0,1,2,1
@@ -5648,57 +5650,58 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/
"SM200, MS200",solar,164,turnondiff,pump turn on difference,uint8 (>=0<=0),C,true,number.solar_pump_turn_on_difference,number.solar_turnondiff,8,0,1/10,10,1
"SM200, MS200",solar,164,turnoffdiff,pump turn off difference,uint8 (>=0<=0),C,true,number.solar_pump_turn_off_difference,number.solar_turnoffdiff,8,0,1/10,11,1
"SM200, MS200",solar,164,collector2temp,collector 2 temperature (TS7),int16,C,false,sensor.solar_collector_2_temperature_(TS7),sensor.solar_collector2temp,8,0,1/10,20,1
"SM200, MS200",solar,164,cylmiddletemp,cylinder middle temperature (TS3),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS3),sensor.solar_cylmiddletemp,8,0,1/10,17,1
"SM200, MS200",solar,164,cylmiddletemp,cylinder middle temperature (TS14),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS14),sensor.solar_cylmiddletemp,8,0,1/10,17,1
"SM200, MS200",solar,164,ts3,cylinder middle temperature (TS3),int16,C,false,sensor.solar_cylinder_middle_temperature_(TS3),sensor.solar_ts3,8,0,1/10,21,1
"SM200, MS200",solar,164,retheatassist,return temperature heat assistance (TS4),int16,C,false,sensor.solar_return_temperature_heat_assistance_(TS4),sensor.solar_retheatassist,8,0,1/10,18,1
"SM200, MS200",solar,164,ts8,(TS8),int16,C,false,sensor.solar_(TS8),sensor.solar_ts8,8,0,1/10,21,1
"SM200, MS200",solar,164,ts16,(TS16),int16,C,false,sensor.solar_(TS16),sensor.solar_ts16,8,0,1/10,22,1
"SM200, MS200",solar,164,ts8,(TS8),int16,C,false,sensor.solar_(TS8),sensor.solar_ts8,8,0,1/10,22,1
"SM200, MS200",solar,164,ts16,(TS16),int16,C,false,sensor.solar_(TS16),sensor.solar_ts16,8,0,1/10,23,1
"SM200, MS200",solar,164,heatassistvalve,heat assistance valve (M1),boolean, ,false,binary_sensor.solar_heat_assistance_valve_(M1),binary_sensor.solar_heatassistvalve,8,0,1,19,1
"SM200, MS200",solar,164,heatassistpower,heat assistance valve power (M1),uint8,%,false,sensor.solar_heat_assistance_valve_power_(M1),sensor.solar_heatassistpower,8,0,1,23,1
"SM200, MS200",solar,164,solarpump2,pump 2 (PS4),boolean, ,false,binary_sensor.solar_pump_2_(PS4),binary_sensor.solar_solarpump2,8,0,1,24,1
"SM200, MS200",solar,164,solarpump2mod,pump 2 modulation (PS4),uint8,%,false,sensor.solar_pump_2_modulation_(PS4),sensor.solar_solarpump2mod,8,0,1,25,1
"SM200, MS200",solar,164,cyl2bottomtemp,second cylinder bottom temperature (TS5),int16,C,false,sensor.solar_second_cylinder_bottom_temperature_(TS5),sensor.solar_cyl2bottomtemp,8,0,1/10,26,1
"SM200, MS200",solar,164,cyl3bottomtemp,third cylinder bottom temperature (TS11),int16,C,false,sensor.solar_third_cylinder_bottom_temperature_(TS11),sensor.solar_cyl3bottomtemp,8,0,1/10,27,1
"SM200, MS200",solar,164,cyltoptemp,cylinder top temperature (TS10),int16,C,false,sensor.solar_cylinder_top_temperature_(TS10),sensor.solar_cyltoptemp,8,0,1/10,28,1
"SM200, MS200",solar,164,heatexchangertemp,heat exchanger temperature (TS6),int16,C,false,sensor.solar_heat_exchanger_temperature_(TS6),sensor.solar_heatexchangertemp,8,0,1/10,29,1
"SM200, MS200",solar,164,cylpumpmod,cylinder pump modulation (PS5),uint8,%,false,sensor.solar_cylinder_pump_modulation_(PS5),sensor.solar_cylpumpmod,8,0,1,30,1
"SM200, MS200",solar,164,valvestatus,valve status,boolean, ,false,binary_sensor.solar_valve_status,binary_sensor.solar_valvestatus,8,0,1,31,1
"SM200, MS200",solar,164,vs1status,valve status VS1,boolean, ,false,binary_sensor.solar_valve_status_VS1,binary_sensor.solar_vs1status,8,0,1,32,1
"SM200, MS200",solar,164,vs3status,valve status VS3,boolean, ,false,binary_sensor.solar_valve_status_VS3,binary_sensor.solar_vs3status,8,0,1,33,1
"SM200, MS200",solar,164,transferpump,transfer pump,boolean, ,false,binary_sensor.solar_transfer_pump,binary_sensor.solar_transferpump,8,0,1,34,1
"SM200, MS200",solar,164,transferpumpmod,transfer pump modulation,uint8,%,false,sensor.solar_transfer_pump_modulation,sensor.solar_transferpumpmod,8,0,1,35,1
"SM200, MS200",solar,164,collectormaxtemp,maximum collector temperature,uint8 (>=0<=0),C,true,number.solar_maximum_collector_temperature,number.solar_collectormaxtemp,8,0,1,36,1
"SM200, MS200",solar,164,collectormintemp,minimum collector temperature,uint8 (>=0<=0),C,true,number.solar_minimum_collector_temperature,number.solar_collectormintemp,8,0,1,37,1
"SM200, MS200",solar,164,heatassistpower,heat assistance valve power (M1),uint8,%,false,sensor.solar_heat_assistance_valve_power_(M1),sensor.solar_heatassistpower,8,0,1,24,1
"SM200, MS200",solar,164,solarpump2,pump 2 (PS4),boolean, ,false,binary_sensor.solar_pump_2_(PS4),binary_sensor.solar_solarpump2,8,0,1,25,1
"SM200, MS200",solar,164,solarpump2mod,pump 2 modulation (PS4),uint8,%,false,sensor.solar_pump_2_modulation_(PS4),sensor.solar_solarpump2mod,8,0,1,26,1
"SM200, MS200",solar,164,cyl2bottomtemp,second cylinder bottom temperature (TS5),int16,C,false,sensor.solar_second_cylinder_bottom_temperature_(TS5),sensor.solar_cyl2bottomtemp,8,0,1/10,27,1
"SM200, MS200",solar,164,cyl3bottomtemp,third cylinder bottom temperature (TS11),int16,C,false,sensor.solar_third_cylinder_bottom_temperature_(TS11),sensor.solar_cyl3bottomtemp,8,0,1/10,28,1
"SM200, MS200",solar,164,cyltoptemp,cylinder top temperature (TS10),int16,C,false,sensor.solar_cylinder_top_temperature_(TS10),sensor.solar_cyltoptemp,8,0,1/10,29,1
"SM200, MS200",solar,164,heatexchangertemp,heat exchanger temperature (TS6),int16,C,false,sensor.solar_heat_exchanger_temperature_(TS6),sensor.solar_heatexchangertemp,8,0,1/10,30,1
"SM200, MS200",solar,164,cylpumpmod,cylinder pump modulation (PS5),uint8,%,false,sensor.solar_cylinder_pump_modulation_(PS5),sensor.solar_cylpumpmod,8,0,1,31,1
"SM200, MS200",solar,164,valvestatus,valve status,boolean, ,false,binary_sensor.solar_valve_status,binary_sensor.solar_valvestatus,8,0,1,32,1
"SM200, MS200",solar,164,vs1status,valve status VS1,boolean, ,false,binary_sensor.solar_valve_status_VS1,binary_sensor.solar_vs1status,8,0,1,33,1
"SM200, MS200",solar,164,vs3status,valve status VS3,boolean, ,false,binary_sensor.solar_valve_status_VS3,binary_sensor.solar_vs3status,8,0,1,34,1
"SM200, MS200",solar,164,transferpump,transfer pump,boolean, ,false,binary_sensor.solar_transfer_pump,binary_sensor.solar_transferpump,8,0,1,35,1
"SM200, MS200",solar,164,transferpumpmod,transfer pump modulation,uint8,%,false,sensor.solar_transfer_pump_modulation,sensor.solar_transferpumpmod,8,0,1,36,1
"SM200, MS200",solar,164,collectormaxtemp,maximum collector temperature,uint8 (>=0<=0),C,true,number.solar_maximum_collector_temperature,number.solar_collectormaxtemp,8,0,1,37,1
"SM200, MS200",solar,164,collectormintemp,minimum collector temperature,uint8 (>=0<=0),C,true,number.solar_minimum_collector_temperature,number.solar_collectormintemp,8,0,1,38,1
"SM200, MS200",solar,164,energylasthour,energy last hour,uint24,Wh,false,sensor.solar_energy_last_hour,sensor.solar_energylasthour,8,0,1/10,13,2
"SM200, MS200",solar,164,energytoday,total energy today,uint24,Wh,false,sensor.solar_total_energy_today,sensor.solar_energytoday,8,0,1,38,2
"SM200, MS200",solar,164,energytotal,total energy,uint24,kWh,false,sensor.solar_total_energy,sensor.solar_energytotal,8,0,1/10,40,2
"SM200, MS200",solar,164,pump2worktime,pump 2 working time,time,minutes,false,sensor.solar_pump_2_working_time,sensor.solar_pump2worktime,8,0,1,42,2
"SM200, MS200",solar,164,m1worktime,differential control working time,time,minutes,false,sensor.solar_differential_control_working_time,sensor.solar_m1worktime,8,0,1,44,2
"SM200, MS200",solar,164,heattransfersystem,heattransfer system,boolean (>=0<=0), ,true,switch.solar_heattransfer_system,switch.solar_heattransfersystem,8,0,1,46,1
"SM200, MS200",solar,164,externalcyl,external cylinder,boolean (>=0<=0), ,true,switch.solar_external_cylinder,switch.solar_externalcyl,8,0,1,47,1
"SM200, MS200",solar,164,thermaldisinfect,thermal disinfection,boolean (>=0<=0), ,true,switch.solar_thermal_disinfection,switch.solar_thermaldisinfect,8,0,1,48,1
"SM200, MS200",solar,164,heatmetering,heatmetering,boolean (>=0<=0), ,true,switch.solar_heatmetering,switch.solar_heatmetering,8,0,1,49,1
"SM200, MS200",solar,164,activated,activated,boolean (>=0<=0), ,true,switch.solar_activated,switch.solar_activated,8,0,1,50,1
"SM200, MS200",solar,164,solarpumpmode,solar pump mode,enum [constant\|pwm\|analog] (>=0<=0), ,true,select.solar_solar_pump_mode,select.solar_solarpumpmode,8,0,1,51,1
"SM200, MS200",solar,164,solarpumpkick,solar pump kick,boolean (>=0<=0), ,true,switch.solar_solar_pump_kick,switch.solar_solarpumpkick,8,0,1,52,1
"SM200, MS200",solar,164,plainwatermode,plain water mode,boolean (>=0<=0), ,true,switch.solar_plain_water_mode,switch.solar_plainwatermode,8,0,1,53,1
"SM200, MS200",solar,164,doublematchflow,doublematchflow,boolean (>=0<=0), ,true,switch.solar_doublematchflow,switch.solar_doublematchflow,8,0,1,54,1
"SM200, MS200",solar,164,pump2minmod,minimum pump 2 modulation,uint8 (>=0<=0),%,true,number.solar_minimum_pump_2_modulation,number.solar_pump2minmod,8,0,1,55,1
"SM200, MS200",solar,164,turnondiff2,pump 2 turn on difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_on_difference,number.solar_turnondiff2,8,0,1/10,56,1
"SM200, MS200",solar,164,turnoffdiff2,pump 2 turn off difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_off_difference,number.solar_turnoffdiff2,8,0,1/10,57,1
"SM200, MS200",solar,164,pump2kick,pump kick 2,boolean (>=0<=0), ,true,switch.solar_pump_kick_2,switch.solar_pump2kick,8,0,1,58,1
"SM200, MS200",solar,164,climatezone,climate zone,uint8 (>=0<=0), ,true,number.solar_climate_zone,number.solar_climatezone,8,0,1,59,1
"SM200, MS200",solar,164,collector1area,collector 1 area,uint16 (>=0<=0),,true,number.solar_collector_1_area,number.solar_collector1area,8,0,1/10,60,1
"SM200, MS200",solar,164,collector1type,collector 1 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_1_type,select.solar_collector1type,8,0,1,61,1
"SM200, MS200",solar,164,collector2area,collector 2 area,uint16 (>=0<=0),,true,number.solar_collector_2_area,number.solar_collector2area,8,0,1/10,62,1
"SM200, MS200",solar,164,collector2type,collector 2 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_2_type,select.solar_collector2type,8,0,1,63,1
"SM200, MS200",solar,164,cylpriority,cylinder priority,enum [cyl 1\|cyl 2] (>=0<=0), ,true,select.solar_cylinder_priority,select.solar_cylpriority,8,0,1,64,1
"SM200, MS200",solar,164,heatcntflowtemp,heat counter flow temperature,uint16,C,false,sensor.solar_heat_counter_flow_temperature,sensor.solar_heatcntflowtemp,8,0,1/10,65,1
"SM200, MS200",solar,164,heatcntrettemp,heat counter return temperature,uint16,C,false,sensor.solar_heat_counter_return_temperature,sensor.solar_heatcntrettemp,8,0,1/10,66,1
"SM200, MS200",solar,164,heatcnt,heat counter impulses,uint8, ,false,sensor.solar_heat_counter_impulses,sensor.solar_heatcnt,8,0,1,67,1
"SM200, MS200",solar,164,swapflowtemp,swap flow temperature (TS14),uint16,C,false,sensor.solar_swap_flow_temperature_(TS14),sensor.solar_swapflowtemp,8,0,1/10,68,1
"SM200, MS200",solar,164,swaprettemp,swap return temperature (TS15),uint16,C,false,sensor.solar_swap_return_temperature_(TS15),sensor.solar_swaprettemp,8,0,1/10,69,1
"SM200, MS200",solar,164,heatassiston,heat assistance on,int8 (>=0<=0),K,true,number.solar_heat_assistance_on,number.solar_heatassiston,8,0,1/10,70,1
"SM200, MS200",solar,164,heatassistoff,heat assistance off,int8 (>=0<=0),K,true,number.solar_heat_assistance_off,number.solar_heatassistoff,8,0,1/10,71,1
"SM200, MS200",solar,164,energytoday,total energy today,uint24,Wh,false,sensor.solar_total_energy_today,sensor.solar_energytoday,8,0,1,39,2
"SM200, MS200",solar,164,energytotal,total energy,uint24,kWh,false,sensor.solar_total_energy,sensor.solar_energytotal,8,0,1/10,41,2
"SM200, MS200",solar,164,pump2worktime,pump 2 working time,time,minutes,false,sensor.solar_pump_2_working_time,sensor.solar_pump2worktime,8,0,1,43,2
"SM200, MS200",solar,164,m1worktime,differential control working time,time,minutes,false,sensor.solar_differential_control_working_time,sensor.solar_m1worktime,8,0,1,45,2
"SM200, MS200",solar,164,heattransfersystem,heattransfer system,boolean (>=0<=0), ,true,switch.solar_heattransfer_system,switch.solar_heattransfersystem,8,0,1,47,1
"SM200, MS200",solar,164,externalcyl,external cylinder,boolean (>=0<=0), ,true,switch.solar_external_cylinder,switch.solar_externalcyl,8,0,1,48,1
"SM200, MS200",solar,164,thermaldisinfect,thermal disinfection,boolean (>=0<=0), ,true,switch.solar_thermal_disinfection,switch.solar_thermaldisinfect,8,0,1,49,1
"SM200, MS200",solar,164,heatmetering,heatmetering,boolean (>=0<=0), ,true,switch.solar_heatmetering,switch.solar_heatmetering,8,0,1,50,1
"SM200, MS200",solar,164,activated,activated,boolean (>=0<=0), ,true,switch.solar_activated,switch.solar_activated,8,0,1,51,1
"SM200, MS200",solar,164,solarpumpmode,solar pump mode,enum [constant\|pwm\|analog] (>=0<=0), ,true,select.solar_solar_pump_mode,select.solar_solarpumpmode,8,0,1,52,1
"SM200, MS200",solar,164,solarpumpkick,solar pump kick,boolean (>=0<=0), ,true,switch.solar_solar_pump_kick,switch.solar_solarpumpkick,8,0,1,53,1
"SM200, MS200",solar,164,plainwatermode,plain water mode,boolean (>=0<=0), ,true,switch.solar_plain_water_mode,switch.solar_plainwatermode,8,0,1,54,1
"SM200, MS200",solar,164,doublematchflow,doublematchflow,boolean (>=0<=0), ,true,switch.solar_doublematchflow,switch.solar_doublematchflow,8,0,1,55,1
"SM200, MS200",solar,164,pump2minmod,minimum pump 2 modulation,uint8 (>=0<=0),%,true,number.solar_minimum_pump_2_modulation,number.solar_pump2minmod,8,0,1,56,1
"SM200, MS200",solar,164,turnondiff2,pump 2 turn on difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_on_difference,number.solar_turnondiff2,8,0,1/10,57,1
"SM200, MS200",solar,164,turnoffdiff2,pump 2 turn off difference,uint8 (>=0<=0),C,true,number.solar_pump_2_turn_off_difference,number.solar_turnoffdiff2,8,0,1/10,58,1
"SM200, MS200",solar,164,pump2kick,pump kick 2,boolean (>=0<=0), ,true,switch.solar_pump_kick_2,switch.solar_pump2kick,8,0,1,59,1
"SM200, MS200",solar,164,climatezone,climate zone,uint8 (>=0<=0), ,true,number.solar_climate_zone,number.solar_climatezone,8,0,1,60,1
"SM200, MS200",solar,164,collector1area,collector 1 area,uint16 (>=0<=0),,true,number.solar_collector_1_area,number.solar_collector1area,8,0,1/10,61,1
"SM200, MS200",solar,164,collector1type,collector 1 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_1_type,select.solar_collector1type,8,0,1,62,1
"SM200, MS200",solar,164,collector2area,collector 2 area,uint16 (>=0<=0),,true,number.solar_collector_2_area,number.solar_collector2area,8,0,1/10,63,1
"SM200, MS200",solar,164,collector2type,collector 2 type,enum [flat\|vacuum] (>=0<=0), ,true,select.solar_collector_2_type,select.solar_collector2type,8,0,1,64,1
"SM200, MS200",solar,164,cylpriority,cylinder priority,enum [cyl 1\|cyl 2] (>=0<=0), ,true,select.solar_cylinder_priority,select.solar_cylpriority,8,0,1,65,1
"SM200, MS200",solar,164,heatcntflowtemp,heat counter flow temperature,uint16,C,false,sensor.solar_heat_counter_flow_temperature,sensor.solar_heatcntflowtemp,8,0,1/10,66,1
"SM200, MS200",solar,164,heatcntrettemp,heat counter return temperature,uint16,C,false,sensor.solar_heat_counter_return_temperature,sensor.solar_heatcntrettemp,8,0,1/10,67,1
"SM200, MS200",solar,164,heatcnt,heat counter impulses,uint8, ,false,sensor.solar_heat_counter_impulses,sensor.solar_heatcnt,8,0,1,68,1
"SM200, MS200",solar,164,swapflowtemp,swap flow temperature (TS14),uint16,C,false,sensor.solar_swap_flow_temperature_(TS14),sensor.solar_swapflowtemp,8,0,1/10,69,1
"SM200, MS200",solar,164,swaprettemp,swap return temperature (TS15),uint16,C,false,sensor.solar_swap_return_temperature_(TS15),sensor.solar_swaprettemp,8,0,1/10,70,1
"SM200, MS200",solar,164,heatassiston,heat assistance on,int8 (>=0<=0),K,true,number.solar_heat_assistance_on,number.solar_heatassiston,8,0,1/10,71,1
"SM200, MS200",solar,164,heatassistoff,heat assistance off,int8 (>=0<=0),K,true,number.solar_heat_assistance_off,number.solar_heatassistoff,8,0,1/10,72,1
"HP Module",heatpump,252,airhumidity,relative air humidity,uint8,%,false,sensor.heatpump_relative_air_humidity,sensor.heatpump_airhumidity,9,0,1,0,1
"HP Module",heatpump,252,dewtemperature,dew point temperature,uint8,C,false,sensor.heatpump_dew_point_temperature,sensor.heatpump_dewtemperature,9,0,1,1,1
"HP Module",heatpump,252,curflowtemp,current flow temperature,int16,C,false,sensor.heatpump_current_flow_temperature,sensor.heatpump_curflowtemp,9,0,1/10,2,1
Can't render this file because it is too large.

View File

@@ -171,6 +171,7 @@ telegram_type_id,name,is_fetched
0x0468,HPSet,
0x0469,HPSet,
0x046A,HPSet,
0x0470,RC300Summer2,
0x0471,RC300Summer2,
0x0472,RC300Summer2,
0x0473,RC300Summer2,
@@ -178,7 +179,6 @@ telegram_type_id,name,is_fetched
0x0475,RC300Summer2,
0x0476,RC300Summer2,
0x0477,RC300Summer2,
0x0478,RC300Summer2,
0x047B,HP2,
0x0484,HPSilentMode,fetched
0x0485,HpCooling,fetched
1 telegram_type_id name is_fetched
171 0x0468 HPSet
172 0x0469 HPSet
173 0x046A HPSet
174 0x0470 RC300Summer2
175 0x0471 RC300Summer2
176 0x0472 RC300Summer2
177 0x0473 RC300Summer2
179 0x0475 RC300Summer2
180 0x0476 RC300Summer2
181 0x0477 RC300Summer2
0x0478 RC300Summer2
182 0x047B HP2
183 0x0484 HPSilentMode fetched
184 0x0485 HpCooling fetched

View File

@@ -1,9 +1,9 @@
{
"name": "EMS-ESP",
"version": "3.8.0",
"version": "3.9.0",
"description": "EMS-ESP WebUI",
"homepage": "https://emsesp.org",
"author": "proddy, emsesp.org",
"author": "emsesp.org",
"license": "MIT",
"private": true,
"type": "module",
@@ -17,7 +17,7 @@
"preview-standalone": "typesafe-i18n --no-watch && vite build && concurrently -c \"auto\" \"pnpm:mock-rest\" \"vite preview\"",
"standalone": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite dev\"",
"typesafe-i18n": "typesafe-i18n --no-watch",
"build_webUI": "typesafe-i18n --no-watch && vite build && node progmem-generator.js",
"build-webUI": "typesafe-i18n --no-watch && vite build && node progmem-generator.js",
"format": "prettier -l -w '**/*.{ts,tsx,js,css,json,md}'",
"lint": "eslint . --fix",
"standalone-devcontainer": "concurrently -c \"auto\" \"typesafe-i18n\" \"pnpm:mock-rest\" \"vite --host\""
@@ -28,43 +28,37 @@
"@emotion/styled": "^11.14.1",
"@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",
"async-validator": "^4.2.5",
"etag": "^1.8.1",
"formidable": "^3.5.4",
"jwt-decode": "^4.0.0",
"magic-string": "^0.30.21",
"mime-types": "^3.0.2",
"preact": "^10.29.1",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-icons": "^5.6.0",
"react-router": "^7.14.1",
"react-toastify": "^11.0.5",
"react-router": "^7.14.2",
"react-toastify": "^11.1.0",
"typesafe-i18n": "^5.27.1",
"typescript": "^6.0.2"
"typescript": "^6.0.3"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@eslint/js": "^10.0.1",
"@preact/compat": "^18.3.2",
"@preact/preset-vite": "^2.10.5",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"axe-core": "^4.11.3",
"concurrently": "^9.2.1",
"eslint": "^10.2.0",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.3",
"rollup-plugin-visualizer": "^7.0.1",
"terser": "^5.46.1",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.8",
"terser": "^5.46.2",
"typescript-eslint": "^8.59.1",
"vite": "^8.0.10",
"vite-plugin-imagemin": "^0.6.1"
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8"
}

649
interface/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,20 +24,26 @@ let bundleStats = {
other: { count: 0, uncompressed: 0, compressed: 0 }
};
const generateWWWClass =
() => `typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler;
// Bundle Statistics:
// AsyncWebHandler that performs the lookup.
const generateWWWClass = () => `// Bundle Statistics:
// - Total compressed size: ${(totalSize / 1000).toFixed(1)} KB
// - Total uncompressed size: ${(Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) / 1000).toFixed(1)} KB
// - Compression ratio: ${(((Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0) - totalSize) / Object.values(bundleStats).reduce((sum, stat) => sum + stat.uncompressed, 0)) * 100).toFixed(1)}%
// - Generated on: ${new Date().toISOString()}
class WWWData {
${INDENT}public:
${INDENT.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) {
${fileInfo.map((f) => `${INDENT.repeat(3)}handler("${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, ${f.hash});`).join('\n')}
${INDENT.repeat(2)}}
struct WWWAsset {
${INDENT}const char * uri;
${INDENT}const char * contentType;
${INDENT}const uint8_t * content;
${INDENT}size_t len;
${INDENT}const char * etag; // already includes enclosing double quotes
};
static const WWWAsset WWW_ASSETS[] = {
${fileInfo.map((f) => `${INDENT}{"${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, "\\"${f.rawHash}\\""},`).join('\n')}
};
static constexpr size_t WWW_ASSETS_COUNT = sizeof(WWW_ASSETS) / sizeof(WWW_ASSETS[0]);
`;
const getFilesSync = (dir, files = []) => {
@@ -72,6 +78,7 @@ const writeFile = (relativeFilePath, buffer) => {
const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
// const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex');
const hash = etag(zipBuffer); // use smaller md5 instead of sha256
const rawHash = hash.replace(/^"|"$/g, '');
zipBuffer.forEach((b) => {
if (!(size % bytesPerLine)) {
@@ -94,7 +101,8 @@ const writeFile = (relativeFilePath, buffer) => {
mimeType,
variable,
size,
hash
hash,
rawHash
});
totalSize += size;

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { memo, useEffect, useState } from 'react';
import { ToastContainer, Zoom } from 'react-toastify';
import AppRouting from 'AppRouting';
@@ -46,19 +46,17 @@ const App = memo(() => {
const [wasLoaded, setWasLoaded] = useState(false);
const [locale, setLocale] = useState<Locales>('en');
// Memoize locale initialization to prevent unnecessary re-runs
const initializeLocale = useCallback(async () => {
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
localStorage.setItem('lang', newLocale);
setLocale(newLocale);
await loadLocaleAsync(newLocale);
setWasLoaded(true);
}, []);
useEffect(() => {
const initializeLocale = async () => {
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
localStorage.setItem('lang', newLocale);
setLocale(newLocale);
await loadLocaleAsync(newLocale);
setWasLoaded(true);
};
void initializeLocale();
}, [initializeLocale]);
}, []);
if (!wasLoaded) return null;

View File

@@ -1,19 +1,13 @@
import { type FC, Suspense, lazy, memo, useContext, useEffect, useRef } from 'react';
import { type FC, memo, useContext, useEffect, useRef } from 'react';
import { Navigate, Route, Routes } from 'react-router';
import { toast } from 'react-toastify';
import {
LoadingSpinner,
RequireAuthenticated,
RequireUnauthenticated
} from 'components';
import AuthenticatedRouting from 'AuthenticatedRouting';
import SignIn from 'SignIn';
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
import { Authentication, AuthenticationContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
// Lazy load route components for better code splitting
const SignIn = lazy(() => import('SignIn'));
const AuthenticatedRouting = lazy(() => import('AuthenticatedRouting'));
interface SecurityRedirectProps {
readonly message: string;
readonly signOut?: boolean;
@@ -45,34 +39,32 @@ const AppRouting: FC = memo(() => {
return (
<Authentication>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route
path="/unauthorized"
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
/>
<Route
path="/fileUpdated"
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
/>
<Route
path="/"
element={
<RequireUnauthenticated>
<SignIn />
</RequireUnauthenticated>
}
/>
<Route
path="/*"
element={
<RequireAuthenticated>
<AuthenticatedRouting />
</RequireAuthenticated>
}
/>
</Routes>
</Suspense>
<Routes>
<Route
path="/unauthorized"
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
/>
<Route
path="/fileUpdated"
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
/>
<Route
path="/"
element={
<RequireUnauthenticated>
<SignIn />
</RequireUnauthenticated>
}
/>
<Route
path="/*"
element={
<RequireAuthenticated>
<AuthenticatedRouting />
</RequireAuthenticated>
}
/>
</Routes>
</Authentication>
);
});

View File

@@ -1,86 +1,77 @@
import { Suspense, lazy, memo, useContext } from 'react';
import { memo, useContext } from 'react';
import { Navigate, Route, Routes } from 'react-router';
import { Layout, LoadingSpinner } from 'components';
import CustomEntities from 'app/main/CustomEntities';
import Customizations from 'app/main/Customizations';
import Dashboard from 'app/main/Dashboard';
import Devices from 'app/main/Devices';
import Help from 'app/main/Help';
import Modules from 'app/main/Modules';
import Scheduler from 'app/main/Scheduler';
import Sensors from 'app/main/Sensors';
import UserProfile from 'app/main/UserProfile';
import APSettings from 'app/settings/APSettings';
import ApplicationSettings from 'app/settings/ApplicationSettings';
import DownloadUpload from 'app/settings/DownloadUpload';
import MqttSettings from 'app/settings/MqttSettings';
import NTPSettings from 'app/settings/NTPSettings';
import Settings from 'app/settings/Settings';
import Version from 'app/settings/Version';
import Network from 'app/settings/network/Network';
import Security from 'app/settings/security/Security';
import APStatus from 'app/status/APStatus';
import Activity from 'app/status/Activity';
import HardwareStatus from 'app/status/HardwareStatus';
import MqttStatus from 'app/status/MqttStatus';
import NTPStatus from 'app/status/NTPStatus';
import NetworkStatus from 'app/status/NetworkStatus';
import Status from 'app/status/Status';
import SystemLog from 'app/status/SystemLog';
import { Layout } from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
// Lazy load all route components for better code splitting
const Dashboard = lazy(() => import('app/main/Dashboard'));
const Devices = lazy(() => import('app/main/Devices'));
const Sensors = lazy(() => import('app/main/Sensors'));
const Help = lazy(() => import('app/main/Help'));
const Customizations = lazy(() => import('app/main/Customizations'));
const Scheduler = lazy(() => import('app/main/Scheduler'));
const CustomEntities = lazy(() => import('app/main/CustomEntities'));
const Modules = lazy(() => import('app/main/Modules'));
const UserProfile = lazy(() => import('app/main/UserProfile'));
const Status = lazy(() => import('app/status/Status'));
const HardwareStatus = lazy(() => import('app/status/HardwareStatus'));
const Activity = lazy(() => import('app/status/Activity'));
const SystemLog = lazy(() => import('app/status/SystemLog'));
const MqttStatus = lazy(() => import('app/status/MqttStatus'));
const NTPStatus = lazy(() => import('app/status/NTPStatus'));
const APStatus = lazy(() => import('app/status/APStatus'));
const NetworkStatus = lazy(() => import('app/status/NetworkStatus'));
const Version = lazy(() => import('app/status/Version'));
const Settings = lazy(() => import('app/settings/Settings'));
const ApplicationSettings = lazy(() => import('app/settings/ApplicationSettings'));
const MqttSettings = lazy(() => import('app/settings/MqttSettings'));
const NTPSettings = lazy(() => import('app/settings/NTPSettings'));
const APSettings = lazy(() => import('app/settings/APSettings'));
const DownloadUpload = lazy(() => import('app/settings/DownloadUpload'));
const Network = lazy(() => import('app/settings/network/Network'));
const Security = lazy(() => import('app/settings/security/Security'));
const AuthenticatedRouting = memo(() => {
const { me } = useContext(AuthenticatedContext);
return (
<Layout>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard/*" element={<Dashboard />} />
<Route path="/devices/*" element={<Devices />} />
<Route path="/sensors/*" element={<Sensors />} />
<Route path="/help/*" element={<Help />} />
<Route path="/user/*" element={<UserProfile />} />
<Routes>
<Route path="/dashboard/*" element={<Dashboard />} />
<Route path="/devices/*" element={<Devices />} />
<Route path="/sensors/*" element={<Sensors />} />
<Route path="/help/*" element={<Help />} />
<Route path="/user/*" element={<UserProfile />} />
<Route path="/status/*" element={<Status />} />
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
<Route path="/status/activity" element={<Activity />} />
<Route path="/status/log" element={<SystemLog />} />
<Route path="/status/mqtt" element={<MqttStatus />} />
<Route path="/status/ntp" element={<NTPStatus />} />
<Route path="/status/ap" element={<APStatus />} />
<Route path="/status/network" element={<NetworkStatus />} />
<Route path="/status/version" element={<Version />} />
<Route path="/status/*" element={<Status />} />
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
<Route path="/status/activity" element={<Activity />} />
<Route path="/status/log" element={<SystemLog />} />
<Route path="/status/mqtt" element={<MqttStatus />} />
<Route path="/status/ntp" element={<NTPStatus />} />
<Route path="/status/ap" element={<APStatus />} />
<Route path="/status/network" element={<NetworkStatus />} />
{me.admin && (
<>
<Route path="/settings" element={<Settings />} />
<Route
path="/settings/application"
element={<ApplicationSettings />}
/>
<Route path="/settings/mqtt" element={<MqttSettings />} />
<Route path="/settings/ntp" element={<NTPSettings />} />
<Route path="/settings/ap" element={<APSettings />} />
<Route path="/settings/modules" element={<Modules />} />
<Route path="/settings/downloadUpload" element={<DownloadUpload />} />
{me.admin && (
<>
<Route path="/settings" element={<Settings />} />
<Route path="/settings/version" element={<Version />} />
<Route path="/settings/application" element={<ApplicationSettings />} />
<Route path="/settings/mqtt" element={<MqttSettings />} />
<Route path="/settings/ntp" element={<NTPSettings />} />
<Route path="/settings/ap" element={<APSettings />} />
<Route path="/settings/modules" element={<Modules />} />
<Route path="/settings/downloadUpload" element={<DownloadUpload />} />
<Route path="/settings/network/*" element={<Network />} />
<Route path="/settings/security/*" element={<Security />} />
<Route path="/settings/network/*" element={<Network />} />
<Route path="/settings/security/*" element={<Security />} />
<Route path="/customizations" element={<Customizations />} />
<Route path="/scheduler" element={<Scheduler />} />
<Route path="/customentities" element={<CustomEntities />} />
</>
)}
<Route path="/customizations" element={<Customizations />} />
<Route path="/scheduler" element={<Scheduler />} />
<Route path="/customentities" element={<CustomEntities />} />
</>
)}
<Route path="/*" element={<Navigate to="/" />} />
</Routes>
</Suspense>
<Route path="/*" element={<Navigate to="/" />} />
</Routes>
</Layout>
);
});

View File

@@ -18,7 +18,7 @@ import { PROJECT_NAME } from 'env';
import { useI18nContext } from 'i18n/i18n-react';
import type { SignInRequest } from 'types';
import { onEnterCallback, updateValue } from 'utils';
import { SIGN_IN_REQUEST_VALIDATOR, validate } from 'validators';
import { SIGN_IN_REQUEST_VALIDATOR, ValidationError, validate } from 'validators';
const SignIn = memo(() => {
const authenticationContext = useContext(AuthenticationContext);
@@ -43,7 +43,6 @@ const SignIn = memo(() => {
}
});
// Memoize callback to prevent recreation on every render
const updateLoginRequestValue = useMemo(
() =>
updateValue((updater) =>
@@ -65,7 +64,7 @@ const SignIn = memo(() => {
});
}, [callSignIn, signInRequest, LL]);
const validateAndSignIn = useCallback(async () => {
const validateAndSignIn = async () => {
setProcessing(true);
SIGN_IN_REQUEST_VALIDATOR.messages({
required: LL.IS_REQUIRED('%s')
@@ -74,10 +73,10 @@ const SignIn = memo(() => {
await validate(SIGN_IN_REQUEST_VALIDATOR, signInRequest);
await signIn();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
setProcessing(false);
}
}, [signInRequest, signIn, LL]);
};
const submitOnEnter = useMemo(() => onEnterCallback(signIn), [signIn]);

View File

@@ -57,12 +57,3 @@ export const alovaInstance = createAlova({
onSuccess: handleResponse
}
});
export const alovaInstanceGH = createAlova({
baseURL:
process.env.NODE_ENV === 'development'
? '/gh'
: 'https://api.github.com/repos/emsesp/EMS-ESP32/releases',
statesHook: ReactHook,
requestAdapter: xhrRequestAdapter()
});

View File

@@ -1,6 +1,6 @@
import type { LogSettings, SystemStatus } from 'types';
import { alovaInstance, alovaInstanceGH } from './endpoints';
import { alovaInstance } from './endpoints';
// systemStatus - also used to ping in System Monitor for pinging
export const readSystemStatus = () =>
@@ -13,29 +13,6 @@ export const updateLogSettings = (data: LogSettings) =>
alovaInstance.Post('/rest/logSettings', data);
export const fetchLogES = () => alovaInstance.Get('/es/log');
// Get versions from GitHub
// cache for 10 minutes to stop getting the IP blocked by GitHub
export const getStableVersion = () =>
alovaInstanceGH.Get('latest', {
cacheFor: 60 * 10 * 1000,
transform(response: { data: { name: string; published_at: string } }) {
return {
name: response.data.name.substring(1),
published_at: response.data.published_at
};
}
});
export const getDevVersion = () =>
alovaInstanceGH.Get('tags/latest', {
cacheFor: 60 * 10 * 1000,
transform(response: { data: { name: string; published_at: string } }) {
return {
name: response.data.name.split(/\s+/).splice(-1)[0]?.substring(1) || '',
published_at: response.data.published_at
};
}
});
const UPLOAD_TIMEOUT = 60000; // 1 minute
export const uploadFile = (file: File) => {

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useState } from 'react';
import { useBlocker } from 'react-router';
import { toast } from 'react-toastify';
@@ -57,20 +57,18 @@ const CustomEntities = () => {
initialData: []
});
const intervalCallback = useCallback(() => {
useInterval(() => {
if (!dialogOpen && !numChanges) {
void fetchEntities();
}
}, [dialogOpen, numChanges, fetchEntities]);
useInterval(intervalCallback);
});
const { send: writeEntities } = useRequest(
(data: Entities) => writeCustomEntities(data),
{ immediate: false }
);
const hasEntityChanged = useCallback((ei: EntityItem) => {
const hasEntityChanged = (ei: EntityItem) => {
return (
ei.id !== ei.o_id ||
ei.ram !== ei.o_ram ||
@@ -86,21 +84,19 @@ const CustomEntities = () => {
ei.deleted !== ei.o_deleted ||
(ei.value || '') !== (ei.o_value || '')
);
}, []);
};
const entity_theme = useMemo(
() =>
useTheme({
Table: `
const entity_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 120px;
`,
BaseRow: `
BaseRow: `
font-size: 14px;
.td {
height: 32px;
}
`,
BaseCell: `
BaseCell: `
&:nth-of-type(1) {
padding: 8px;
}
@@ -120,7 +116,7 @@ const CustomEntities = () => {
text-align: center;
}
`,
HeaderRow: `
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
@@ -129,7 +125,7 @@ const CustomEntities = () => {
height: 36px;
}
`,
Row: `
Row: `
background-color: #1e1e1e;
position: relative;
cursor: pointer;
@@ -140,11 +136,9 @@ const CustomEntities = () => {
background-color: #177ac9;
}
`
}),
[]
);
});
const saveEntities = useCallback(async () => {
const saveEntities = async () => {
await writeEntities({
entities: entities
.filter((ei: EntityItem) => !ei.deleted)
@@ -173,44 +167,41 @@ const CustomEntities = () => {
await fetchEntities();
setNumChanges(0);
});
}, [entities, writeEntities, LL, fetchEntities]);
};
const editEntityItem = useCallback((ei: EntityItem) => {
const editEntityItem = (ei: EntityItem) => {
setCreating(false);
setSelectedEntityItem(ei);
setDialogOpen(true);
}, []);
};
const onDialogClose = useCallback(() => {
const onDialogClose = () => {
setDialogOpen(false);
}, []);
};
const onDialogCancel = useCallback(async () => {
const onDialogCancel = async () => {
await fetchEntities().then(() => {
setNumChanges(0);
});
}, [fetchEntities]);
};
const onDialogSave = useCallback(
(updatedItem: EntityItem) => {
setDialogOpen(false);
void updateState(readCustomEntities(), (data: EntityItem[]) => {
const new_data = creating
? [
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
updatedItem
]
: data.map((ei) =>
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei
);
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
return new_data;
});
},
[creating, hasEntityChanged]
);
const onDialogSave = (updatedItem: EntityItem) => {
setDialogOpen(false);
void updateState(readCustomEntities(), (data: EntityItem[]) => {
const new_data = creating
? [
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
updatedItem
]
: data.map((ei) =>
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei
);
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
return new_data;
});
};
const onDialogDup = useCallback((item: EntityItem) => {
const onDialogDup = (item: EntityItem) => {
setCreating(true);
setSelectedEntityItem({
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
@@ -228,9 +219,9 @@ const CustomEntities = () => {
value: item.value
});
setDialogOpen(true);
}, []);
};
const addEntityItem = useCallback(() => {
const addEntityItem = () => {
setCreating(true);
setSelectedEntityItem({
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
@@ -248,30 +239,27 @@ const CustomEntities = () => {
value: ''
});
setDialogOpen(true);
}, []);
};
const formatValue = useCallback((value: unknown, uom: number) => {
const formatValue = (value: unknown, uom: number) => {
return value === undefined
? ''
: typeof value === 'number'
? new Intl.NumberFormat().format(value) +
(uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`)
: `${value as string}${uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`}`;
}, []);
};
const showHex = useCallback((value: number, digit: number) => {
const showHex = (value: number, digit: number) => {
return `0x${value.toString(16).toUpperCase().padStart(digit, '0')}`;
}, []);
};
const filteredAndSortedEntities = useMemo(
() =>
entities
?.filter((ei: EntityItem) => !ei.deleted)
.sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name)) ?? [],
[entities]
);
const filteredAndSortedEntities =
entities
?.filter((ei: EntityItem) => !ei.deleted)
.sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name)) ?? [];
const renderEntity = useCallback(() => {
const renderEntity = () => {
if (!entities) {
return (
<FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} />
@@ -328,17 +316,7 @@ const CustomEntities = () => {
)}
</Table>
);
}, [
entities,
error,
fetchEntities,
entity_theme,
editEntityItem,
LL,
filteredAndSortedEntities,
showHex,
formatValue
]);
};
return (
<SectionContent>

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -28,7 +28,7 @@ import type { ValidateFieldsError } from 'async-validator';
import { BlockFormControlLabel, ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils';
import { validate } from 'validators';
import { ValidationError, validate } from 'validators';
import { DeviceValueType, DeviceValueTypeNames, DeviceValueUOM_s } from './types';
import type { EntityItem } from './types';
@@ -68,14 +68,10 @@ const CustomEntitiesDialog = ({
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = useMemo(
() =>
updateValue(
setEditItem as unknown as React.Dispatch<
React.SetStateAction<Record<string, unknown>>
>
),
[]
const updateFormValue = updateValue(
setEditItem as unknown as React.Dispatch<
React.SetStateAction<Record<string, unknown>>
>
);
useEffect(() => {
@@ -105,16 +101,16 @@ const CustomEntitiesDialog = ({
}
}, [open, selectedItem]);
const handleClose = useCallback(
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
if (reason !== 'backdropClick') {
onClose();
}
},
[onClose]
);
const handleClose = (
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') {
onClose();
}
};
const save = useCallback(async () => {
const save = async () => {
try {
setFieldErrors(undefined);
await validate(validator, editItem);
@@ -136,29 +132,23 @@ const CustomEntitiesDialog = ({
}
onSave(processedItem);
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
}
}, [validator, editItem, onSave]);
};
const remove = useCallback(() => {
const itemWithDeleted = { ...editItem, deleted: true };
onSave(itemWithDeleted);
}, [editItem, onSave]);
const remove = () => {
onSave({ ...editItem, deleted: true });
};
const dup = useCallback(() => {
const dup = () => {
onDup(editItem);
}, [editItem, onDup]);
};
// Memoize UOM menu items to avoid recreating on every render
const uomMenuItems = useMemo(
() =>
DeviceValueUOM_s.map((val, i) => (
<MenuItem key={val} value={i}>
{val}
</MenuItem>
)),
[]
);
const uomMenuItems = DeviceValueUOM_s.map((val, i) => (
<MenuItem key={val} value={i}>
{val}
</MenuItem>
));
return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
@@ -215,7 +205,7 @@ const CustomEntitiesDialog = ({
name="value"
label={LL.DEFAULT(0) + ' ' + LL.VALUE(0)}
type="string"
value={editItem.value as string}
value={editItem.value}
variant="outlined"
onChange={updateFormValue}
fullWidth
@@ -260,7 +250,7 @@ const CustomEntitiesDialog = ({
margin="normal"
sx={{ width: '11ch' }}
type="string"
value={editItem.device_id as string}
value={editItem.device_id}
onChange={updateFormValue}
slotProps={{
input: {
@@ -280,7 +270,7 @@ const CustomEntitiesDialog = ({
margin="normal"
sx={{ width: '11ch' }}
type="string"
value={editItem.type_id as string}
value={editItem.type_id}
onChange={updateFormValue}
slotProps={{
input: {
@@ -381,7 +371,7 @@ const CustomEntitiesDialog = ({
fieldErrors={fieldErrors || {}}
name="factor"
label={LL.BITMASK()}
value={editItem.factor as string}
value={editItem.factor}
sx={{ width: '11ch' }}
variant="outlined"
onChange={updateFormValue}

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { useBlocker, useLocation } from 'react-router';
import { toast } from 'react-toastify';
@@ -171,19 +171,17 @@ const Customizations = () => {
);
};
const entities_theme = useMemo(
() =>
useTheme({
Table: `
const entities_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
`,
BaseRow: `
BaseRow: `
font-size: 14px;
.td {
height: 32px;
}
`,
BaseCell: `
BaseCell: `
&:nth-of-type(3) {
text-align: right;
}
@@ -194,7 +192,7 @@ const Customizations = () => {
text-align: right;
}
`,
HeaderRow: `
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
@@ -206,7 +204,7 @@ const Customizations = () => {
text-align: center;
}
`,
Row: `
Row: `
background-color: #1e1e1e;
position: relative;
cursor: pointer;
@@ -222,7 +220,7 @@ const Customizations = () => {
background-color: #177ac9;
}
`,
Cell: `
Cell: `
&:nth-of-type(2) {
padding: 8px;
}
@@ -236,9 +234,7 @@ const Customizations = () => {
padding-right: 8px;
}
`
}),
[]
);
});
function hasEntityChanged(de: DeviceEntity) {
return (
@@ -287,26 +283,23 @@ const Customizations = () => {
return value as string;
}
const isCommand = useCallback((de: DeviceEntity) => {
const isCommand = (de: DeviceEntity) => {
return de.n && de.n[0] === '!';
}, []);
};
const formatName = useCallback(
(de: DeviceEntity, withShortname: boolean) => {
let name: string;
if (isCommand(de)) {
name = de.t
? `${LL.COMMAND(1)}: ${de.t} ${de.n?.slice(1)}`
: `${LL.COMMAND(1)}: ${de.n?.slice(1)}`;
} else if (de.cn && de.cn !== '') {
name = de.t ? `${de.t} ${de.cn}` : de.cn;
} else {
name = de.t ? `${de.t} ${de.n}` : de.n || '';
}
return withShortname ? `${name} ${de.id}` : name;
},
[LL]
);
const formatName = (de: DeviceEntity, withShortname: boolean) => {
let name: string;
if (isCommand(de)) {
name = de.t
? `${LL.COMMAND(1)}: ${de.t} ${de.n?.slice(1)}`
: `${LL.COMMAND(1)}: ${de.n?.slice(1)}`;
} else if (de.cn && de.cn !== '') {
name = de.t ? `${de.t} ${de.cn}` : de.cn;
} else {
name = de.t ? `${de.t} ${de.n}` : de.n || '';
}
return withShortname ? `${name} ${de.id}` : name;
};
const getMaskNumber = (newMask: string[]) => {
let new_mask = 0;
@@ -336,33 +329,27 @@ const Customizations = () => {
return new_masks;
};
const filter_entity = useCallback(
(de: DeviceEntity) =>
(de.m & selectedFilters || !selectedFilters) &&
formatName(de, true).toLowerCase().includes(search.toLowerCase()),
[selectedFilters, search, formatName]
);
const filter_entity = (de: DeviceEntity) =>
(de.m & selectedFilters || !selectedFilters) &&
formatName(de, true).toLowerCase().includes(search.toLowerCase());
const maskDisabled = useCallback(
(set: boolean) => {
setDeviceEntities((prev) =>
prev.map((de) => {
if (filter_entity(de)) {
const excludeMask =
DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE;
return {
...de,
m: set ? de.m | excludeMask : de.m & ~excludeMask
};
}
return de;
})
);
},
[filter_entity]
);
const maskDisabled = (set: boolean) => {
setDeviceEntities((prev) =>
prev.map((de) => {
if (filter_entity(de)) {
const excludeMask =
DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE;
return {
...de,
m: set ? de.m | excludeMask : de.m & ~excludeMask
};
}
return de;
})
);
};
const resetCustomization = useCallback(async () => {
const resetCustomization = async () => {
try {
await sendResetCustomizations();
toast.info(LL.CUSTOMIZATIONS_RESTART());
@@ -372,30 +359,27 @@ const Customizations = () => {
setConfirmReset(false);
setRestarting(true);
}
}, [sendResetCustomizations, LL]);
};
const onDialogClose = () => {
setDialogOpen(false);
};
const updateDeviceEntity = useCallback((updatedItem: DeviceEntity) => {
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
setDeviceEntities(
(prev) =>
prev?.map((de) =>
de.id === updatedItem.id ? { ...de, ...updatedItem } : de
) ?? []
);
}, []);
};
const onDialogSave = useCallback(
(updatedItem: DeviceEntity) => {
setDialogOpen(false);
updateDeviceEntity(updatedItem);
},
[updateDeviceEntity]
);
const onDialogSave = (updatedItem: DeviceEntity) => {
setDialogOpen(false);
updateDeviceEntity(updatedItem);
};
const editDeviceEntity = useCallback((de: DeviceEntity) => {
const editDeviceEntity = (de: DeviceEntity) => {
if (de.n === undefined || (de.n && de.n[0] === '!')) {
return;
}
@@ -406,9 +390,9 @@ const Customizations = () => {
setSelectedDeviceEntity(de);
setDialogOpen(true);
}, []);
};
const saveCustomization = useCallback(async () => {
const saveCustomization = async () => {
if (!devices || !deviceEntities || selectedDevice === -1) {
return;
}
@@ -441,9 +425,9 @@ const Customizations = () => {
.finally(() => {
setOriginalSettings(deviceEntities);
});
}, [devices, deviceEntities, selectedDevice, sendCustomizationEntities, LL]);
};
const renameDevice = useCallback(async () => {
const renameDevice = async () => {
await sendDeviceName({
id: selectedDevice,
name: selectedDeviceName,
@@ -459,14 +443,7 @@ const Customizations = () => {
setRename(false);
await fetchCoreData();
});
}, [
selectedDevice,
selectedDeviceName,
selectedDeviceBrand,
sendDeviceName,
LL,
fetchCoreData
]);
};
const renderDeviceList = () => (
<>
@@ -562,10 +539,7 @@ const Customizations = () => {
</>
);
const filteredEntities = useMemo(
() => deviceEntities.filter((de) => filter_entity(de)),
[deviceEntities, filter_entity]
);
const filteredEntities = deviceEntities.filter((de) => filter_entity(de));
const renderDeviceData = () => {
return (

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { memo, useEffect, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import CloseIcon from '@mui/icons-material/Close';
@@ -57,23 +57,16 @@ const CustomizationsDialog = ({
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
const [error, setError] = useState<boolean>(false);
const updateFormValue = useMemo(
() =>
updateValue(
setEditItem as unknown as React.Dispatch<
React.SetStateAction<Record<string, unknown>>
>
),
[]
const updateFormValue = updateValue(
setEditItem as unknown as React.Dispatch<
React.SetStateAction<Record<string, unknown>>
>
);
const isWriteableNumber = useMemo(
() =>
typeof editItem.v === 'number' &&
editItem.w &&
!(editItem.m & DeviceEntityMask.DV_READONLY),
[editItem.v, editItem.w, editItem.m]
);
const isWriteableNumber =
typeof editItem.v === 'number' &&
editItem.w &&
!(editItem.m & DeviceEntityMask.DV_READONLY);
useEffect(() => {
if (open) {
@@ -82,16 +75,16 @@ const CustomizationsDialog = ({
}
}, [open, selectedItem]);
const handleClose = useCallback(
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
if (reason !== 'backdropClick') {
onClose();
}
},
[onClose]
);
const handleClose = (
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') {
onClose();
}
};
const save = useCallback(() => {
const save = () => {
if (
isWriteableNumber &&
editItem.mi &&
@@ -102,34 +95,31 @@ const CustomizationsDialog = ({
} else {
onSave(editItem);
}
}, [isWriteableNumber, editItem, onSave]);
};
const updateDeviceEntity = useCallback((updatedItem: DeviceEntity) => {
const updateDeviceEntity = (updatedItem: DeviceEntity) => {
setEditItem((prev) => ({ ...prev, m: updatedItem.m }));
}, []);
const dialogTitle = useMemo(() => `${LL.EDIT()} ${LL.ENTITY()}`, [LL]);
const writeableIcon = useMemo(
() =>
editItem.w ? (
<DoneIcon color="success" sx={{ fontSize: ICON_SIZE }} />
) : (
<CloseIcon color="error" sx={{ fontSize: ICON_SIZE }} />
),
[editItem.w]
);
};
return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
<DialogTitle>{dialogTitle}</DialogTitle>
<DialogTitle>{`${LL.EDIT()} ${LL.ENTITY()}`}</DialogTitle>
<DialogContent dividers>
<LabelValue label={LL.ID_OF(LL.ENTITY())} value={editItem.id} />
<LabelValue
label={`${LL.DEFAULT(1)} ${LL.ENTITY_NAME(1)}`}
value={editItem.n}
/>
<LabelValue label={LL.WRITEABLE()} value={writeableIcon} />
<LabelValue
label={LL.WRITEABLE()}
value={
editItem.w ? (
<DoneIcon color="success" sx={{ fontSize: ICON_SIZE }} />
) : (
<CloseIcon color="error" sx={{ fontSize: ICON_SIZE }} />
)
}
/>
<Box sx={{ mt: 1, mb: 2 }}>
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { memo, useContext, useEffect, useState } from 'react';
import { IconContext } from 'react-icons/lib';
import { Link } from 'react-router';
import { toast } from 'react-toastify';
@@ -77,40 +77,35 @@ const Dashboard = memo(() => {
}
);
const deviceValueDialogSave = useCallback(
async (devicevalue: DeviceValue) => {
if (!selectedDashboardItem) {
return;
}
const id = selectedDashboardItem.parentNode.id; // this is the parent ID
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
.then(() => {
toast.success(LL.WRITE_CMD_SENT());
})
.catch((error: Error) => {
toast.error(error.message);
})
.finally(() => {
setDeviceValueDialogOpen(false);
setSelectedDashboardItem(undefined);
});
},
[selectedDashboardItem, sendDeviceValue, LL]
);
const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
if (!selectedDashboardItem) {
return;
}
const id = selectedDashboardItem.parentNode.id; // this is the parent ID
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
.then(() => {
toast.success(LL.WRITE_CMD_SENT());
})
.catch((error: Error) => {
toast.error(error.message);
})
.finally(() => {
setDeviceValueDialogOpen(false);
setSelectedDashboardItem(undefined);
});
};
const dashboard_theme = useMemo(
() =>
useTheme({
Table: `
const dashboard_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
`,
BaseRow: `
BaseRow: `
font-size: 14px;
.td {
height: 28px;
}
`,
Row: `
Row: `
cursor: pointer;
background-color: #1e1e1e;
&:nth-of-type(odd) .td {
@@ -120,7 +115,7 @@ const Dashboard = memo(() => {
background-color: #177ac9;
},
`,
BaseCell: `
BaseCell: `
&:nth-of-type(2) {
text-align: right;
}
@@ -128,9 +123,7 @@ const Dashboard = memo(() => {
text-align: right;
}
`
}),
[]
);
});
const tree = useTree(
{ nodes: [...data.nodes] },
@@ -164,79 +157,64 @@ const Dashboard = memo(() => {
}
});
const nodeIds = useMemo(
() => data.nodes.map((item: DashboardItem) => item.id),
[data.nodes]
);
useEffect(() => {
const nodeIds = data.nodes.map((item: DashboardItem) => item.id);
showAll
? tree.fns.onAddAll(nodeIds) // expand tree
: tree.fns.onRemoveAll(); // collapse tree
}, [parentNodes]);
const showType = useCallback(
(n?: string, t?: number) => {
// if we have a name show it
if (n) {
return n;
const showType = (n?: string, t?: number) => {
// if we have a name show it
if (n) {
return n;
}
if (t) {
// otherwise pick translation based on type
switch (t) {
case DeviceType.CUSTOM:
return LL.CUSTOM_ENTITIES(0);
case DeviceType.ANALOGSENSOR:
return LL.ANALOG_SENSORS();
case DeviceType.TEMPERATURESENSOR:
return LL.TEMP_SENSORS();
case DeviceType.SCHEDULER:
return LL.SCHEDULER();
default:
break;
}
if (t) {
// otherwise pick translation based on type
switch (t) {
case DeviceType.CUSTOM:
return LL.CUSTOM_ENTITIES(0);
case DeviceType.ANALOGSENSOR:
return LL.ANALOG_SENSORS();
case DeviceType.TEMPERATURESENSOR:
return LL.TEMP_SENSORS();
case DeviceType.SCHEDULER:
return LL.SCHEDULER();
default:
break;
}
}
return '';
},
[LL]
);
}
return '';
};
const showName = useCallback(
(di: DashboardItem) => {
if (di.id < 100) {
// if its a device (parent node) and has entities
if (di.nodes?.length) {
return (
<span style={{ fontSize: '15px' }}>
<DeviceIcon type_id={di.t ?? 0} />
&nbsp;&nbsp;{showType(di.n, di.t)}
<span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span>
</span>
);
}
const showName = (di: DashboardItem) => {
if (di.id < 100) {
// if its a device (parent node) and has entities
if (di.nodes?.length) {
return (
<span style={{ fontSize: '15px' }}>
<DeviceIcon type_id={di.t ?? 0} />
&nbsp;&nbsp;{showType(di.n, di.t)}
<span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span>
</span>
);
}
if (di.dv) {
return <span>{di.dv.id.slice(2)}</span>;
}
return null;
},
[showType]
);
}
if (di.dv) {
return <span>{di.dv.id.slice(2)}</span>;
}
return null;
};
const hasMask = useCallback(
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
[]
);
const hasMask = (id: string, mask: number) =>
(parseInt(id.slice(0, 2), 16) & mask) === mask;
const editDashboardValue = useCallback(
(di: DashboardItem) => {
if (me.admin && di.dv?.c) {
setSelectedDashboardItem(di);
setDeviceValueDialogOpen(true);
}
},
[me.admin]
);
const editDashboardValue = (di: DashboardItem) => {
if (me.admin && di.dv?.c) {
setSelectedDashboardItem(di);
setDeviceValueDialogOpen(true);
}
};
const handleShowAll = (
_event: React.MouseEvent<HTMLElement>,
@@ -248,10 +226,9 @@ const Dashboard = memo(() => {
}
};
const hasFavEntities = useMemo(
() => data.nodes.filter((item: DashboardItem) => item.id <= 90).length,
[data.nodes]
);
const hasFavEntities = data.nodes.filter(
(item: DashboardItem) => item.id <= 90
).length;
const renderContent = () => {
if (!data) {

View File

@@ -4,7 +4,6 @@ import {
useContext,
useEffect,
useLayoutEffect,
useMemo,
useState
} from 'react';
import { IconContext } from 'react-icons';
@@ -133,21 +132,19 @@ const Devices = memo(() => {
};
}, []);
const leftOffset = useCallback(() => {
const leftOffset = () => {
const devicesWindow = document.getElementById('devices-window');
if (!devicesWindow) return 0;
const { left, right } = devicesWindow.getBoundingClientRect();
if (!left || !right) return 0;
return left + (right - left < 400 ? 0 : 200);
}, []);
};
const common_theme = useMemo(
() =>
useTheme({
BaseRow: `
const common_theme = useTheme({
BaseRow: `
font-size: 14px;
`,
HeaderRow: `
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
@@ -155,7 +152,7 @@ const Devices = memo(() => {
border-bottom: 1px solid #565656;
}
`,
Row: `
Row: `
cursor: pointer;
background-color: #1E1E1E;
.td {
@@ -165,88 +162,78 @@ const Devices = memo(() => {
background-color: #177ac9;
}
`
}),
[]
);
});
const device_theme = useMemo(
() =>
useTheme([
common_theme,
{
BaseRow: `
font-size: 15px;
.td {
height: 28px;
}
`,
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
`,
HeaderRow: `
.th {
padding: 8px;
`,
Row: `
&:nth-of-type(odd) .td {
background-color: #303030;
},
&:hover .td {
background-color: #177ac9;
},
`
}
]),
[common_theme]
);
const data_theme = useMemo(
() =>
useTheme([
common_theme,
{
Table: `
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
height: auto;
max-height: 100%;
overflow-y: scroll;
::-webkit-scrollbar {
display:none;
const device_theme = useTheme([
common_theme,
{
BaseRow: `
font-size: 15px;
.td {
height: 28px;
}
`,
BaseRow: `
.td {
height: 32px;
}
`,
BaseCell: `
&:nth-of-type(1) {
border-left: 1px solid #177ac9;
},
&:nth-of-type(2) {
text-align: right;
},
&:nth-of-type(3) {
border-right: 1px solid #177ac9;
}
`,
HeaderRow: `
.th {
border-top: 1px solid #565656;
}
`,
Row: `
&:nth-of-type(odd) .td {
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
`,
HeaderRow: `
.th {
padding: 8px;
`,
Row: `
&:nth-of-type(odd) .td {
background-color: #303030;
},
&:hover .td {
background-color: #177ac9;
},
&:hover .td {
background-color: #177ac9;
},
`
}
]);
const data_theme = useTheme([
common_theme,
{
Table: `
--data-table-library_grid-template-columns: minmax(200px, auto) minmax(150px, auto) 40px;
height: auto;
max-height: 100%;
overflow-y: scroll;
::-webkit-scrollbar {
display:none;
}
`
}
]),
[common_theme]
);
`,
BaseRow: `
.td {
height: 32px;
}
`,
BaseCell: `
&:nth-of-type(1) {
border-left: 1px solid #177ac9;
},
&:nth-of-type(2) {
text-align: right;
},
&:nth-of-type(3) {
border-right: 1px solid #177ac9;
}
`,
HeaderRow: `
.th {
border-top: 1px solid #565656;
}
`,
Row: `
&:nth-of-type(odd) .td {
background-color: #303030;
},
&:hover .td {
background-color: #177ac9;
}
`
}
]);
const getSortIcon = (state: State, sortKey: unknown) => {
if (state.sortKey === sortKey && state.reverse) {
@@ -345,10 +332,8 @@ const Devices = memo(() => {
return sc;
};
const hasMask = useCallback(
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
[]
);
const hasMask = (id: string, mask: number) =>
(parseInt(id.slice(0, 2), 16) & mask) === mask;
const handleDownloadCsv = () => {
const deviceIndex = coreData.devices.findIndex(
@@ -607,41 +592,35 @@ const Devices = memo(() => {
return;
}
const showDeviceValue = useCallback((dv: DeviceValue) => {
const showDeviceValue = (dv: DeviceValue) => {
setSelectedDeviceValue(dv);
setDeviceValueDialogOpen(true);
}, []);
};
const renderNameCell = useCallback(
(dv: DeviceValue) => (
<>
{dv.id.slice(2)}&nbsp;
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
<StarIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
</>
),
[hasMask]
const renderNameCell = (dv: DeviceValue) => (
<>
{dv.id.slice(2)}&nbsp;
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
<StarIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
</>
);
const shown_data = useMemo(() => {
if (onlyFav) {
return deviceData.nodes.filter(
const shown_data = onlyFav
? deviceData.nodes.filter(
(dv: DeviceValue) =>
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
)
: deviceData.nodes.filter((dv: DeviceValue) =>
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
);
}
return deviceData.nodes.filter((dv: DeviceValue) =>
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
);
}, [deviceData.nodes, onlyFav, search]);
const deviceIndex = coreData.devices.findIndex(
(d: Device) => d.id === device_select.state.id

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
@@ -24,7 +24,7 @@ import type { ValidateFieldsError } from 'async-validator';
import { ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils';
import { validate } from 'validators';
import { ValidationError, validate } from 'validators';
import { DeviceValueUOM, DeviceValueUOM_s } from './types';
import type { DeviceValue } from './types';
@@ -52,7 +52,7 @@ const DevicesDialog = ({
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = useMemo(() => updateValue(setEditItem), [setEditItem]);
const updateFormValue = updateValue(setEditItem);
useEffect(() => {
if (open) {
@@ -61,36 +61,33 @@ const DevicesDialog = ({
}
}, [open, selectedItem]);
const save = useCallback(async () => {
const save = async () => {
try {
setFieldErrors(undefined);
await validate(validator, editItem);
onSave(editItem);
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
}
}, [validator, editItem, onSave]);
};
const setUom = useCallback(
(uom?: DeviceValueUOM) => {
if (uom === undefined) {
return;
}
switch (uom) {
case DeviceValueUOM.HOURS:
return LL.HOURS();
case DeviceValueUOM.MINUTES:
return LL.MINUTES();
case DeviceValueUOM.SECONDS:
return LL.SECONDS();
default:
return DeviceValueUOM_s[uom];
}
},
[LL]
);
const setUom = (uom?: DeviceValueUOM) => {
if (uom === undefined) {
return;
}
switch (uom) {
case DeviceValueUOM.HOURS:
return LL.HOURS();
case DeviceValueUOM.MINUTES:
return LL.MINUTES();
case DeviceValueUOM.SECONDS:
return LL.SECONDS();
default:
return DeviceValueUOM_s[uom];
}
};
const showHelperText = useCallback((dv: DeviceValue) => {
const showHelperText = (dv: DeviceValue) => {
if (dv.h) return dv.h;
if (dv.l) return dv.l.join(' | ');
if (dv.m !== undefined && dv.x !== undefined) {
@@ -101,26 +98,16 @@ const DevicesDialog = ({
);
}
return undefined;
}, []);
};
const isCommand = useMemo(
() => selectedItem.v === '' && selectedItem.c,
[selectedItem.v, selectedItem.c]
);
const dialogTitle = useMemo(() => {
if (isCommand) return LL.RUN_COMMAND();
return writeable ? LL.CHANGE_VALUE() : LL.VALUE(0);
}, [isCommand, writeable, LL]);
const buttonLabel = useMemo(() => {
return isCommand ? LL.EXECUTE() : LL.UPDATE();
}, [isCommand, LL]);
const helperText = useMemo(
() => showHelperText(editItem),
[editItem, showHelperText]
);
const isCommand = selectedItem.v === '' && selectedItem.c;
const dialogTitle = isCommand
? LL.RUN_COMMAND()
: writeable
? LL.CHANGE_VALUE()
: LL.VALUE(0);
const buttonLabel = isCommand ? LL.EXECUTE() : LL.UPDATE();
const helperText = showHelperText(editItem);
const valueLabel = LL.VALUE(0);

View File

@@ -1,5 +1,3 @@
import { useCallback, useMemo } from 'react';
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
import OptionIcon from './OptionIcon';
@@ -11,7 +9,6 @@ interface EntityMaskToggleProps {
de: DeviceEntity;
}
// Available mask values
const MASK_VALUES = [
DeviceEntityMask.DV_WEB_EXCLUDE, // 1
DeviceEntityMask.DV_API_MQTT_EXCLUDE, // 2
@@ -20,123 +17,95 @@ const MASK_VALUES = [
DeviceEntityMask.DV_DELETED // 128
];
/**
* Converts an array of mask strings to a bitmask number
*/
const getMaskNumber = (newMask: string[]): number => {
return newMask.reduce((mask, entry) => mask | Number(entry), 0);
};
const getMaskNumber = (newMask: string[]): number =>
newMask.reduce((mask, entry) => mask | Number(entry), 0);
/**
* Converts a bitmask number to an array of mask strings
*/
const getMaskString = (mask: number): string[] => {
return MASK_VALUES.filter((value) => (mask & value) === value).map((value) =>
const getMaskString = (mask: number): string[] =>
MASK_VALUES.filter((value) => (mask & value) === value).map((value) =>
String(value)
);
};
/**
* Checks if a specific mask bit is set
*/
const hasMask = (mask: number, flag: number): boolean => (mask & flag) === flag;
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
const handleChange = useCallback(
(_event: unknown, mask: string[]) => {
// Convert selected masks to a number
const newMask = getMaskNumber(mask);
const updatedDe = { ...de };
const handleChange = (_event: unknown, mask: string[]) => {
const newMask = getMaskNumber(mask);
const updatedDe = { ...de };
// Apply business logic for mask interactions
// If entity has no name and is set to readonly, also exclude from web
if (updatedDe.n === '' && hasMask(newMask, DeviceEntityMask.DV_READONLY)) {
updatedDe.m = newMask | DeviceEntityMask.DV_WEB_EXCLUDE;
} else {
updatedDe.m = newMask;
}
// If entity has no name and is set to readonly, also exclude from web
if (updatedDe.n === '' && hasMask(newMask, DeviceEntityMask.DV_READONLY)) {
updatedDe.m = newMask | DeviceEntityMask.DV_WEB_EXCLUDE;
} else {
updatedDe.m = newMask;
}
// If excluded from web, cannot be favorite
if (hasMask(updatedDe.m, DeviceEntityMask.DV_WEB_EXCLUDE)) {
updatedDe.m = updatedDe.m & ~DeviceEntityMask.DV_FAVORITE;
}
// If excluded from web, cannot be favorite
if (hasMask(updatedDe.m, DeviceEntityMask.DV_WEB_EXCLUDE)) {
updatedDe.m = updatedDe.m & ~DeviceEntityMask.DV_FAVORITE;
}
onUpdate(updatedDe);
},
[de, onUpdate]
);
// Memoize mask string value
const maskStringValue = useMemo(() => getMaskString(de.m), [de.m]);
// Memoize disabled states
const isFavoriteDisabled = useMemo(
() =>
hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_DELETED) ||
de.n === undefined,
[de.m, de.n]
);
const isReadonlyDisabled = useMemo(
() =>
!de.w ||
hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_FAVORITE),
[de.w, de.m]
);
const isApiMqttExcludeDisabled = useMemo(
() => de.n === '' || hasMask(de.m, DeviceEntityMask.DV_DELETED),
[de.n, de.m]
);
const isWebExcludeDisabled = useMemo(
() => de.n === undefined || hasMask(de.m, DeviceEntityMask.DV_DELETED),
[de.n, de.m]
);
// Memoize mask flag checks
const isFavoriteSet = useMemo(
() => hasMask(de.m, DeviceEntityMask.DV_FAVORITE),
[de.m]
);
const isReadonlySet = useMemo(
() => hasMask(de.m, DeviceEntityMask.DV_READONLY),
[de.m]
);
const isApiMqttExcludeSet = useMemo(
() => hasMask(de.m, DeviceEntityMask.DV_API_MQTT_EXCLUDE),
[de.m]
);
const isWebExcludeSet = useMemo(
() => hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE),
[de.m]
);
const isDeletedSet = useMemo(
() => hasMask(de.m, DeviceEntityMask.DV_DELETED),
[de.m]
);
onUpdate(updatedDe);
};
return (
<ToggleButtonGroup
size="small"
color="secondary"
value={maskStringValue}
value={getMaskString(de.m)}
onChange={handleChange}
>
<ToggleButton value="8" disabled={isFavoriteDisabled}>
<OptionIcon type="favorite" isSet={isFavoriteSet} />
<ToggleButton
value="8"
disabled={
hasMask(
de.m,
DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_DELETED
) || de.n === undefined
}
>
<OptionIcon
type="favorite"
isSet={hasMask(de.m, DeviceEntityMask.DV_FAVORITE)}
/>
</ToggleButton>
<ToggleButton value="4" disabled={isReadonlyDisabled}>
<OptionIcon type="readonly" isSet={isReadonlySet} />
<ToggleButton
value="4"
disabled={
!de.w ||
hasMask(
de.m,
DeviceEntityMask.DV_WEB_EXCLUDE | DeviceEntityMask.DV_FAVORITE
)
}
>
<OptionIcon
type="readonly"
isSet={hasMask(de.m, DeviceEntityMask.DV_READONLY)}
/>
</ToggleButton>
<ToggleButton value="2" disabled={isApiMqttExcludeDisabled}>
<OptionIcon type="api_mqtt_exclude" isSet={isApiMqttExcludeSet} />
<ToggleButton
value="2"
disabled={de.n === '' || hasMask(de.m, DeviceEntityMask.DV_DELETED)}
>
<OptionIcon
type="api_mqtt_exclude"
isSet={hasMask(de.m, DeviceEntityMask.DV_API_MQTT_EXCLUDE)}
/>
</ToggleButton>
<ToggleButton value="1" disabled={isWebExcludeDisabled}>
<OptionIcon type="web_exclude" isSet={isWebExcludeSet} />
<ToggleButton
value="1"
disabled={de.n === undefined || hasMask(de.m, DeviceEntityMask.DV_DELETED)}
>
<OptionIcon
type="web_exclude"
isSet={hasMask(de.m, DeviceEntityMask.DV_WEB_EXCLUDE)}
/>
</ToggleButton>
<ToggleButton value="128">
<OptionIcon type="deleted" isSet={isDeletedSet} />
<OptionIcon
type="deleted"
isSet={hasMask(de.m, DeviceEntityMask.DV_DELETED)}
/>
</ToggleButton>
</ToggleButtonGroup>
);

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useContext, useMemo, useState } from 'react';
import { memo, useContext, useState } from 'react';
import type { ReactElement } from 'react';
import { toast } from 'react-toastify';
@@ -60,6 +60,8 @@ const AVATAR_STYLES: SxProps<Theme> = {
bgcolor: '#72caf9'
};
const SYSTEM_INFO_API: APIcall = { device: 'system', cmd: 'info', id: 0 };
const HelpComponent = () => {
const { LL } = useI18nContext();
useLayoutTitle(LL.HELP());
@@ -72,12 +74,7 @@ const HelpComponent = () => {
});
const [imgError, setImgError] = useState<boolean>(false);
const getCustomSupportMethod = useMemo(
() => callAction({ action: 'getCustomSupport' }),
[]
);
useRequest(getCustomSupportMethod).onSuccess((event) => {
useRequest(callAction({ action: 'getCustomSupport' })).onSuccess((event) => {
if (event?.data && Object.keys(event.data).length !== 0) {
const { Support } = event.data as {
Support: { img_url?: string; html?: string[] };
@@ -100,47 +97,26 @@ const HelpComponent = () => {
toast.error(String(error.error?.message || 'An error occurred'));
});
// Optimize API call memoization
const apiCall = useMemo(() => ({ device: 'system', cmd: 'info', id: 0 }), []);
const helpLinks: HelpLink[] = [
{
href: 'https://emsesp.org',
icon: <MenuBookIcon />,
label: () => LL.HELP_INFORMATION_1()
},
{
href: 'https://discord.gg/GP9DPSgeJq',
icon: <CommentIcon />,
label: () => LL.HELP_INFORMATION_2()
},
{
href: 'https://github.com/emsesp/EMS-ESP32/issues/new/choose',
icon: <GitHubIcon />,
label: () => LL.HELP_INFORMATION_3()
}
];
const handleDownloadSystemInfo = useCallback(() => {
void sendAPI(apiCall);
}, [sendAPI, apiCall]);
const handleImageError = useCallback(() => {
setImgError(true);
}, []);
// Memoize help links to prevent recreation on every render
const helpLinks: HelpLink[] = useMemo(
() => [
{
href: 'https://emsesp.org',
icon: <MenuBookIcon />,
label: () => LL.HELP_INFORMATION_1()
},
{
href: 'https://discord.gg/GP9DPSgeJq',
icon: <CommentIcon />,
label: () => LL.HELP_INFORMATION_2()
},
{
href: 'https://github.com/emsesp/EMS-ESP32/issues/new/choose',
icon: <GitHubIcon />,
label: () => LL.HELP_INFORMATION_3()
}
],
[LL]
);
const isAdmin = useMemo(() => me?.admin ?? false, [me?.admin]);
// Memoize image source computation
const imageSrc = useMemo(
() =>
imgError || !customSupport.img_url ? DEFAULT_IMAGE_URL : customSupport.img_url,
[imgError, customSupport.img_url]
);
const imageSrc =
imgError || !customSupport.img_url ? DEFAULT_IMAGE_URL : customSupport.img_url;
return (
<SectionContent>
@@ -157,13 +133,13 @@ const HelpComponent = () => {
component="img"
referrerPolicy="no-referrer"
sx={IMAGE_STYLES}
onError={handleImageError}
onError={() => setImgError(true)}
src={imageSrc}
/>
</Stack>
)}
{isAdmin && (
{me?.admin && (
<List>
{helpLinks.map(({ href, icon, label }) => (
<ListItem key={href}>
@@ -191,7 +167,7 @@ const HelpComponent = () => {
startIcon={<DownloadIcon />}
variant="outlined"
color="primary"
onClick={handleDownloadSystemInfo}
onClick={() => void sendAPI(SYSTEM_INFO_API)}
>
{LL.SUPPORT_INFORMATION(0)}
</Button>
@@ -214,7 +190,6 @@ const HelpComponent = () => {
);
};
// Memoize the component to prevent unnecessary re-renders
const Help = memo(HelpComponent);
export default Help;

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useMemo, useState } from 'react';
import { memo, useState } from 'react';
import { useBlocker } from 'react-router';
import { toast } from 'react-toastify';
@@ -69,58 +69,53 @@ const Modules = () => {
}
);
const modules_theme = useTheme(
useMemo(
() => ({
Table: `
--data-table-library_grid-template-columns: 48px 180px 120px 100px repeat(1, minmax(160px, 1fr)) 180px;
`,
BaseRow: `
font-size: 14px;
.td {
height: 32px;
}
`,
BaseCell: `
&:nth-of-type(1) {
text-align: center;
}
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
.th {
border-bottom: 1px solid #565656;
height: 36px;
}
`,
Row: `
background-color: #1e1e1e;
position: relative;
cursor: pointer;
.td {
border-top: 1px solid #565656;
border-bottom: 1px solid #565656;
}
&:hover .td {
border-top: 1px solid #177ac9;
border-bottom: 1px solid #177ac9;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
`
}),
[]
)
);
const modules_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: 48px 180px 120px 100px repeat(1, minmax(160px, 1fr)) 180px;
`,
BaseRow: `
font-size: 14px;
.td {
height: 32px;
}
`,
BaseCell: `
&:nth-of-type(1) {
text-align: center;
}
`,
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
.th {
border-bottom: 1px solid #565656;
height: 36px;
}
`,
Row: `
background-color: #1e1e1e;
position: relative;
cursor: pointer;
.td {
border-top: 1px solid #565656;
border-bottom: 1px solid #565656;
}
&:hover .td {
border-top: 1px solid #177ac9;
border-bottom: 1px solid #177ac9;
}
&:nth-of-type(odd) .td {
background-color: #303030;
}
`
});
const onDialogClose = useCallback(() => {
const onDialogClose = () => {
setDialogOpen(false);
}, []);
};
const updateModuleItem = useCallback((updatedItem: ModuleItem) => {
const updateModuleItem = (updatedItem: ModuleItem) => {
void updateState(readModules(), (data: ModuleItem[]) => {
const new_data = data.map((mi) =>
mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi
@@ -128,28 +123,25 @@ const Modules = () => {
setNumChanges(new_data.filter(hasModulesChanged).length);
return new_data;
});
}, []);
};
const onDialogSave = useCallback(
(updatedItem: ModuleItem) => {
setDialogOpen(false);
updateModuleItem(updatedItem);
},
[updateModuleItem]
);
const onDialogSave = (updatedItem: ModuleItem) => {
setDialogOpen(false);
updateModuleItem(updatedItem);
};
const editModuleItem = useCallback((mi: ModuleItem) => {
const editModuleItem = (mi: ModuleItem) => {
setSelectedModuleItem(mi);
setDialogOpen(true);
}, []);
};
const onCancel = useCallback(async () => {
const onCancel = async () => {
await fetchModules().then(() => {
setNumChanges(0);
});
}, [fetchModules]);
};
const saveModules = useCallback(async () => {
const saveModules = async () => {
try {
await Promise.all(
modules.map((condensed_mi: ModuleItem) =>
@@ -167,9 +159,9 @@ const Modules = () => {
await fetchModules();
setNumChanges(0);
}
}, [modules, updateModules, LL, fetchModules]);
};
const content = useMemo(() => {
const renderContent = () => {
if (!modules) {
return (
<FormLoader onRetry={fetchModules} errorMessage={error?.message || ''} />
@@ -262,22 +254,12 @@ const Modules = () => {
</Box>
</>
);
}, [
modules,
fetchModules,
error,
modules_theme,
editModuleItem,
LL,
numChanges,
onCancel,
saveModules
]);
};
return (
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{content}
{renderContent()}
{selectedModuleItem && (
<ModulesDialog
open={dialogOpen}

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done';
@@ -37,14 +37,10 @@ const ModulesDialog = ({
const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<ModuleItem>(selectedItem);
const updateFormValue = useMemo(
() =>
updateValue(
setEditItem as unknown as React.Dispatch<
React.SetStateAction<Record<string, unknown>>
>
),
[]
const updateFormValue = updateValue(
setEditItem as unknown as React.Dispatch<
React.SetStateAction<Record<string, unknown>>
>
);
// Sync form state when dialog opens or selected item changes
@@ -54,18 +50,13 @@ const ModulesDialog = ({
}
}, [open, selectedItem]);
const handleSave = useCallback(() => {
const handleSave = () => {
onSave(editItem);
}, [editItem, onSave]);
const dialogTitle = useMemo(
() => `${LL.EDIT()} ${editItem.key}`,
[LL, editItem.key]
);
};
return (
<Dialog sx={dialogStyle} fullWidth maxWidth="xs" open={open} onClose={onClose}>
<DialogTitle>{dialogTitle}</DialogTitle>
<DialogTitle>{`${LL.EDIT()} ${editItem.key}`}</DialogTitle>
<DialogContent dividers>
<Grid container>
<BlockFormControlLabel

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { useBlocker } from 'react-router';
import { toast } from 'react-toastify';
@@ -132,7 +132,7 @@ const Scheduler = () => {
}
);
const hasScheduleChanged = useCallback((si: ScheduleItem) => {
const hasScheduleChanged = (si: ScheduleItem) => {
return (
si.id !== si.o_id ||
(si.name || '') !== (si.o_name || '') ||
@@ -143,15 +143,13 @@ const Scheduler = () => {
si.cmd !== si.o_cmd ||
si.value !== si.o_value
);
}, []);
};
const intervalCallback = useCallback(() => {
useInterval(() => {
if (numChanges === 0) {
void fetchSchedule();
}
}, [numChanges, fetchSchedule]);
useInterval(intervalCallback, INTERVAL_DELAY);
}, INTERVAL_DELAY);
useEffect(() => {
const formatter = new Intl.DateTimeFormat(locale, {
@@ -169,7 +167,7 @@ const Scheduler = () => {
const schedule_theme = useTheme(scheduleTheme);
const saveSchedule = useCallback(async () => {
const saveSchedule = async () => {
try {
await updateSchedule({
schedule: schedule
@@ -192,46 +190,43 @@ const Scheduler = () => {
await fetchSchedule();
setNumChanges(0);
}
}, [LL, schedule, updateSchedule, fetchSchedule]);
};
const editScheduleItem = useCallback((si: ScheduleItem) => {
const editScheduleItem = (si: ScheduleItem) => {
setCreating(false);
setSelectedScheduleItem(si);
setDialogOpen(true);
if (si.o_name === undefined) {
si.o_name = si.name;
}
}, []);
};
const onDialogClose = useCallback(() => {
const onDialogClose = () => {
setDialogOpen(false);
}, []);
};
const onDialogCancel = useCallback(async () => {
const onDialogCancel = async () => {
await fetchSchedule().then(() => {
setNumChanges(0);
});
}, [fetchSchedule]);
};
const onDialogSave = useCallback(
(updatedItem: ScheduleItem) => {
setDialogOpen(false);
void updateState(readSchedule(), (data: ScheduleItem[]) => {
const new_data = creating
? [...data, updatedItem]
: data.map((si) =>
si.id === updatedItem.id ? { ...si, ...updatedItem } : si
);
const onDialogSave = (updatedItem: ScheduleItem) => {
setDialogOpen(false);
void updateState(readSchedule(), (data: ScheduleItem[]) => {
const new_data = creating
? [...data, updatedItem]
: data.map((si) =>
si.id === updatedItem.id ? { ...si, ...updatedItem } : si
);
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
setNumChanges(new_data.filter((si) => hasScheduleChanged(si)).length);
return new_data;
});
},
[creating, hasScheduleChanged]
);
return new_data;
});
};
const addScheduleItem = useCallback(() => {
const addScheduleItem = () => {
setCreating(true);
const newItem: ScheduleItem = {
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
@@ -239,36 +234,29 @@ const Scheduler = () => {
};
setSelectedScheduleItem(newItem);
setDialogOpen(true);
}, []);
};
const filteredAndSortedSchedule = useMemo(
() =>
schedule
.filter((si: ScheduleItem) => !si.deleted)
.sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags),
[schedule]
);
const filteredAndSortedSchedule = schedule
.filter((si: ScheduleItem) => !si.deleted)
.sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags);
const dayBox = useCallback(
(si: ScheduleItem, flag: number) => {
const dayIndex = Math.log(flag) / LOG_2;
const isActive = (si.flags & flag) === flag;
const dayBox = (si: ScheduleItem, flag: number) => {
const dayIndex = Math.log(flag) / LOG_2;
const isActive = (si.flags & flag) === flag;
return (
<>
<Box>
<Typography sx={{ fontSize: 11 }} color={isActive ? 'primary' : 'grey'}>
{dow[dayIndex]}
</Typography>
</Box>
<Divider orientation="vertical" flexItem />
</>
);
},
[dow]
);
return (
<>
<Box>
<Typography sx={{ fontSize: 11 }} color={isActive ? 'primary' : 'grey'}>
{dow[dayIndex]}
</Typography>
</Box>
<Divider orientation="vertical" flexItem />
</>
);
};
const scheduleType = useCallback((si: ScheduleItem) => {
const scheduleType = (si: ScheduleItem) => {
const label = scheduleTypeLabels[si.flags];
return (
@@ -278,9 +266,9 @@ const Scheduler = () => {
</Typography>
</Box>
);
}, []);
};
const renderSchedule = useCallback(() => {
const renderSchedule = () => {
if (!schedule) {
return (
<FormLoader onRetry={fetchSchedule} errorMessage={error?.message || ''} />
@@ -343,17 +331,7 @@ const Scheduler = () => {
)}
</Table>
);
}, [
schedule,
error,
fetchSchedule,
filteredAndSortedSchedule,
schedule_theme,
editScheduleItem,
LL,
dayBox,
scheduleType
]);
};
return (
<SectionContent>

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -26,7 +26,7 @@ import type { ValidateFieldsError } from 'async-validator';
import { BlockFormControlLabel, ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { updateValue } from 'utils';
import { validate } from 'validators';
import { ValidationError, validate } from 'validators';
import { ScheduleFlag } from './types';
import type { ScheduleItem } from './types';
@@ -60,6 +60,12 @@ const FLAG_VALUES = [
ScheduleFlag.SCHEDULE_SAT
] as const;
const getFlagDOWnumber = (flags: string[]) =>
flags.reduce((acc, flag) => acc | Number(flag), 0) & FLAG_MASK_127;
const getFlagDOWstring = (f: number) =>
FLAG_VALUES.filter((flag) => (f & flag) === flag).map((flag) => String(flag));
interface SchedulerDialogProps {
open: boolean;
creating: boolean;
@@ -84,14 +90,10 @@ const SchedulerDialog = ({
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const [scheduleType, setScheduleType] = useState<ScheduleFlag>();
const updateFormValue = useMemo(
() =>
updateValue(
setEditItem as unknown as React.Dispatch<
React.SetStateAction<Record<string, unknown>>
>
),
[]
const updateFormValue = updateValue(
setEditItem as unknown as React.Dispatch<
React.SetStateAction<Record<string, unknown>>
>
);
useEffect(() => {
@@ -112,129 +114,95 @@ const SchedulerDialog = ({
}
}, [open, selectedItem]);
// Helper function to handle save operations
const handleSave = useCallback(
async (itemToSave: ScheduleItem) => {
try {
setFieldErrors(undefined);
await validate(validator, itemToSave);
onSave(itemToSave);
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
}
},
[validator, onSave]
);
const save = useCallback(async () => {
await handleSave(editItem);
}, [editItem, handleSave]);
const saveandactivate = useCallback(async () => {
await handleSave({ ...editItem, active: true });
}, [editItem, handleSave]);
const remove = useCallback(() => {
onSave({ ...editItem, deleted: true });
}, [editItem, onSave]);
// Optimize DOW flag conversion
const getFlagDOWnumber = useCallback((flags: string[]) => {
return flags.reduce((acc, flag) => acc | Number(flag), 0) & FLAG_MASK_127;
}, []);
const getFlagDOWstring = useCallback((f: number) => {
return FLAG_VALUES.filter((flag) => (f & flag) === flag).map((flag) =>
String(flag)
);
}, []);
// Day of week display component
const DayOfWeekButton = useCallback(
(flag: number) => {
const dayIndex = Math.log2(flag);
const isSelected = (editItem.flags & flag) === flag;
return (
<Typography
sx={{ fontSize: TYPOGRAPHY_FONT_SIZE }}
color={isSelected ? 'primary' : 'grey'}
>
{dow[dayIndex]}
</Typography>
);
},
[editItem.flags, dow]
);
const handleClose = useCallback(
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
if (reason !== 'backdropClick') {
onClose();
}
},
[onClose]
);
const handleScheduleTypeChange = useCallback(
(_event: React.SyntheticEvent<HTMLElement>, flag: ScheduleFlag | null) => {
if (flag !== null) {
setFieldErrors(undefined); // clear any validation errors
setScheduleType(flag);
// wipe the time field when changing the schedule type
// set the flags based on type
const newFlags = flag === ScheduleFlag.SCHEDULE_DAY ? FLAG_ALL_DAYS : flag;
setEditItem((prev) => ({ ...prev, time: '', flags: newFlags }));
}
},
[]
);
const handleDOWChange = useCallback(
(_event: React.SyntheticEvent<HTMLElement>, flags: string[]) => {
const newFlags =
getFlagDOWnumber(flags) === 0 ? FLAG_ALL_DAYS : getFlagDOWnumber(flags);
setEditItem((prev) => ({ ...prev, flags: newFlags }));
},
[getFlagDOWnumber]
);
// Memoize derived values
const isDaySchedule = useMemo(
() => scheduleType === ScheduleFlag.SCHEDULE_DAY,
[scheduleType]
);
const isTimerSchedule = useMemo(
() => scheduleType === ScheduleFlag.SCHEDULE_TIMER,
[scheduleType]
);
const isImmediateSchedule = useMemo(
() => scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE,
[scheduleType]
);
const needsTimeField = useMemo(
() => isDaySchedule || isTimerSchedule,
[isDaySchedule, isTimerSchedule]
);
const dowFlags = useMemo(
() => getFlagDOWstring(editItem.flags),
[editItem.flags, getFlagDOWstring]
);
const timeFieldValue = useMemo(() => {
if (needsTimeField) {
return editItem.time === '' ? DEFAULT_TIME : editItem.time;
const handleSave = async (itemToSave: ScheduleItem) => {
try {
setFieldErrors(undefined);
await validate(validator, itemToSave);
onSave(itemToSave);
} catch (error) {
setFieldErrors((error as ValidationError).fieldErrors);
}
return editItem.time === DEFAULT_TIME ? '' : editItem.time;
}, [editItem.time, needsTimeField]);
};
const timeFieldLabel = useMemo(() => {
const save = async () => {
await handleSave(editItem);
};
const saveandactivate = async () => {
await handleSave({ ...editItem, active: true });
};
const remove = () => {
onSave({ ...editItem, deleted: true });
};
const DayOfWeekButton = (flag: number) => {
const dayIndex = Math.log2(flag);
const isSelected = (editItem.flags & flag) === flag;
return (
<Typography
sx={{ fontSize: TYPOGRAPHY_FONT_SIZE }}
color={isSelected ? 'primary' : 'grey'}
>
{dow[dayIndex]}
</Typography>
);
};
const handleClose = (
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') {
onClose();
}
};
const handleScheduleTypeChange = (
_event: React.SyntheticEvent<HTMLElement>,
flag: ScheduleFlag | null
) => {
if (flag !== null) {
setFieldErrors(undefined); // clear any validation errors
setScheduleType(flag);
// wipe the time field when changing the schedule type
// set the flags based on type
const newFlags = flag === ScheduleFlag.SCHEDULE_DAY ? FLAG_ALL_DAYS : flag;
setEditItem((prev) => ({ ...prev, time: '', flags: newFlags }));
}
};
const handleDOWChange = (
_event: React.SyntheticEvent<HTMLElement>,
flags: string[]
) => {
const newFlags =
getFlagDOWnumber(flags) === 0 ? FLAG_ALL_DAYS : getFlagDOWnumber(flags);
setEditItem((prev) => ({ ...prev, flags: newFlags }));
};
const isDaySchedule = scheduleType === ScheduleFlag.SCHEDULE_DAY;
const isTimerSchedule = scheduleType === ScheduleFlag.SCHEDULE_TIMER;
const isImmediateSchedule = scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE;
const needsTimeField = isDaySchedule || isTimerSchedule;
const dowFlags = getFlagDOWstring(editItem.flags);
const timeFieldValue = needsTimeField
? editItem.time === ''
? DEFAULT_TIME
: editItem.time
: editItem.time === DEFAULT_TIME
? ''
: editItem.time;
const timeFieldLabel = (() => {
if (scheduleType === ScheduleFlag.SCHEDULE_TIMER) return LL.TIMER(1);
if (scheduleType === ScheduleFlag.SCHEDULE_CONDITION) return LL.CONDITION();
if (scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE) return LL.ONCHANGE();
if (scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE) return LL.IMMEDIATE();
return LL.TIME(1);
}, [scheduleType, LL]);
})();
return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>

View File

@@ -1,4 +1,4 @@
import { useCallback, useContext, useMemo, useRef, useState } from 'react';
import { useContext, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
@@ -158,18 +158,16 @@ const Sensors = () => {
}
);
const intervalCallback = useCallback(() => {
useInterval(() => {
if (!temperatureDialogOpen && !analogDialogOpen) {
void fetchSensorData();
}
}, [temperatureDialogOpen, analogDialogOpen, fetchSensorData]);
useInterval(intervalCallback);
});
const temperature_theme = useTheme([common_theme, temperature_theme_config]);
const analog_theme = useTheme([common_theme, analog_theme_config]);
const getSortIcon = useCallback((state: State, sortKey: unknown) => {
const getSortIcon = (state: State, sortKey: unknown) => {
if (state.sortKey === sortKey && state.reverse) {
return <KeyboardArrowDownOutlinedIcon />;
}
@@ -177,7 +175,7 @@ const Sensors = () => {
return <KeyboardArrowUpOutlinedIcon />;
}
return <UnfoldMoreOutlinedIcon />;
}, []);
};
const analog_sort = useSort(
{ nodes: sensorData.as },
@@ -234,121 +232,106 @@ const Sensors = () => {
useLayoutTitle(LL.SENSORS());
const formatDurationMin = useCallback(
(duration_min: number) => {
const totalMs = duration_min * MS_PER_MINUTE;
const days = Math.trunc(totalMs / MS_PER_DAY);
const hours = Math.trunc(totalMs / MS_PER_HOUR) % 24;
const minutes = Math.trunc(totalMs / MS_PER_MINUTE) % 60;
const formatDurationMin = (duration_min: number) => {
const totalMs = duration_min * MS_PER_MINUTE;
const days = Math.trunc(totalMs / MS_PER_DAY);
const hours = Math.trunc(totalMs / MS_PER_HOUR) % 24;
const minutes = Math.trunc(totalMs / MS_PER_MINUTE) % 60;
const parts: string[] = [];
if (days > 0) {
parts.push(LL.NUM_DAYS({ num: days }));
}
if (hours > 0) {
parts.push(LL.NUM_HOURS({ num: hours }));
}
if (minutes > 0) {
parts.push(LL.NUM_MINUTES({ num: minutes }));
}
return parts.join(' ');
},
[LL]
);
const parts: string[] = [];
if (days > 0) {
parts.push(LL.NUM_DAYS({ num: days }));
}
if (hours > 0) {
parts.push(LL.NUM_HOURS({ num: hours }));
}
if (minutes > 0) {
parts.push(LL.NUM_MINUTES({ num: minutes }));
}
return parts.join(' ');
};
const formatValue = useCallback(
(value: unknown, uom: DeviceValueUOM) => {
if (value === undefined) {
return '';
}
if (typeof value !== 'number') {
return value as string;
}
switch (uom) {
case DeviceValueUOM.HOURS:
return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 });
case DeviceValueUOM.MINUTES:
return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 });
case DeviceValueUOM.SECONDS:
return LL.NUM_SECONDS({ num: value });
case DeviceValueUOM.NONE:
return new Intl.NumberFormat().format(value);
case DeviceValueUOM.DEGREES:
case DeviceValueUOM.DEGREES_R:
case DeviceValueUOM.FAHRENHEIT:
return (
new Intl.NumberFormat(undefined, {
minimumFractionDigits: 1
}).format(value) +
' ' +
DeviceValueUOM_s[uom]
);
default:
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
}
},
[formatDurationMin, LL]
);
const formatValue = (value: unknown, uom: DeviceValueUOM) => {
if (value === undefined) {
return '';
}
if (typeof value !== 'number') {
return value as string;
}
switch (uom) {
case DeviceValueUOM.HOURS:
return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 });
case DeviceValueUOM.MINUTES:
return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 });
case DeviceValueUOM.SECONDS:
return LL.NUM_SECONDS({ num: value });
case DeviceValueUOM.NONE:
return new Intl.NumberFormat().format(value);
case DeviceValueUOM.DEGREES:
case DeviceValueUOM.DEGREES_R:
case DeviceValueUOM.FAHRENHEIT:
return (
new Intl.NumberFormat(undefined, {
minimumFractionDigits: 1
}).format(value) +
' ' +
DeviceValueUOM_s[uom]
);
default:
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
}
};
const updateTemperatureSensor = useCallback(
(ts: TemperatureSensor) => {
if (me.admin) {
ts.o_n = ts.n;
setSelectedTemperatureSensor(ts);
setTemperatureDialogOpen(true);
}
},
[me.admin]
);
const updateTemperatureSensor = (ts: TemperatureSensor) => {
if (me.admin) {
ts.o_n = ts.n;
setSelectedTemperatureSensor(ts);
setTemperatureDialogOpen(true);
}
};
const onTemperatureDialogClose = useCallback(() => {
const onTemperatureDialogClose = () => {
setTemperatureDialogOpen(false);
void fetchSensorData();
}, [fetchSensorData]);
};
const onTemperatureDialogSave = useCallback(
async (ts: TemperatureSensor) => {
await sendTemperatureSensor({
id: ts.id,
name: ts.n,
offset: ts.o,
is_system: ts.s
const onTemperatureDialogSave = async (ts: TemperatureSensor) => {
await sendTemperatureSensor({
id: ts.id,
name: ts.n,
offset: ts.o,
is_system: ts.s
})
.then(() => {
toast.success(LL.UPDATED_OF(LL.SENSOR(1)));
})
.then(() => {
toast.success(LL.UPDATED_OF(LL.SENSOR(1)));
})
.catch(() => {
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
})
.finally(() => {
setTemperatureDialogOpen(false);
setSelectedTemperatureSensor(undefined);
void fetchSensorData();
});
},
[sendTemperatureSensor, LL, fetchSensorData]
);
.catch(() => {
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
})
.finally(() => {
setTemperatureDialogOpen(false);
setSelectedTemperatureSensor(undefined);
void fetchSensorData();
});
};
const updateAnalogSensor = useCallback(
(as: AnalogSensor) => {
if (me.admin) {
setCreating(false);
as.o_n = as.n;
setSelectedAnalogSensor(as);
setAnalogDialogOpen(true);
}
},
[me.admin]
);
const updateAnalogSensor = (as: AnalogSensor) => {
if (me.admin) {
setCreating(false);
as.o_n = as.n;
setSelectedAnalogSensor(as);
setAnalogDialogOpen(true);
}
};
const onAnalogDialogClose = useCallback(() => {
const onAnalogDialogClose = () => {
setAnalogDialogOpen(false);
void fetchSensorData();
}, [fetchSensorData]);
};
const addAnalogSensor = useCallback(() => {
const addAnalogSensor = () => {
if (firstAvailableGPIO.current === undefined) {
toast.error('No available GPIO found');
toast.error(LL.NO_GPIO());
return;
}
setCreating(true);
@@ -366,194 +349,167 @@ const Sensors = () => {
o_n: ''
});
setAnalogDialogOpen(true);
}, []);
};
const onAnalogDialogSave = useCallback(
async (as: AnalogSensor) => {
await sendAnalogSensor({
id: as.id,
gpio: as.g,
name: as.n,
offset: as.o,
factor: as.f,
uom: as.u,
type: as.t,
deleted: as.d,
is_system: as.s
const onAnalogDialogSave = async (as: AnalogSensor) => {
await sendAnalogSensor({
id: as.id,
gpio: as.g,
name: as.n,
offset: as.o,
factor: as.f,
uom: as.u,
type: as.t,
deleted: as.d,
is_system: as.s
})
.then(() => {
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2)));
})
.then(() => {
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2)));
})
.catch(() => {
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
})
.finally(() => {
setAnalogDialogOpen(false);
setSelectedAnalogSensor(undefined);
void fetchSensorData();
});
},
[sendAnalogSensor, LL, fetchSensorData]
.catch(() => {
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
})
.finally(() => {
setAnalogDialogOpen(false);
setSelectedAnalogSensor(undefined);
void fetchSensorData();
});
};
const RenderAnalogSensors = (
<Table
data={{ nodes: sensorData.as }}
theme={analog_theme}
sort={analog_sort}
layout={{ custom: true }}
>
{(tableList: AnalogSensor[]) => (
<>
<Header>
<HeaderRow>
<HeaderCell stiff>
<Button
fullWidth
style={HEADER_BUTTON_STYLE}
endIcon={getSortIcon(analog_sort.state, 'GPIO')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'GPIO' })}
>
GPIO
</Button>
</HeaderCell>
<HeaderCell resize>
<Button
fullWidth
style={HEADER_BUTTON_STYLE}
endIcon={getSortIcon(analog_sort.state, 'NAME')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
{LL.NAME(0)}
</Button>
</HeaderCell>
<HeaderCell stiff>
<Button
fullWidth
style={HEADER_BUTTON_STYLE}
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
>
{LL.TYPE(0)}
</Button>
</HeaderCell>
<HeaderCell stiff>
<Button
fullWidth
style={HEADER_BUTTON_STYLE_END}
endIcon={getSortIcon(analog_sort.state, 'VALUE')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'VALUE' })}
>
{LL.VALUE(0)}
</Button>
</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((as: AnalogSensor) => (
<Row
style={{ color: as.s ? 'grey' : 'inherit' }}
key={as.id}
item={as}
onClick={() => updateAnalogSensor(as)}
>
<Cell stiff>{as.g}</Cell>
<Cell>{as.n}</Cell>
<Cell stiff>{AnalogTypeNames[as.t - 1]} </Cell>
{(as.t === AnalogType.DIGITAL_OUT &&
as.g !== GPIO_25 &&
as.g !== GPIO_26) ||
as.t === AnalogType.DIGITAL_IN ||
as.t === AnalogType.PULSE ? (
<Cell stiff>{as.v ? LL.ON() : LL.OFF()}</Cell>
) : (
<Cell stiff>{formatValue(as.v, as.u)}</Cell>
)}
</Row>
))}
</Body>
</>
)}
</Table>
);
const RenderAnalogSensors = useMemo(
() => (
<Table
data={{ nodes: sensorData.as }}
theme={analog_theme}
sort={analog_sort}
layout={{ custom: true }}
>
{(tableList: AnalogSensor[]) => (
<>
<Header>
<HeaderRow>
<HeaderCell stiff>
<Button
fullWidth
style={HEADER_BUTTON_STYLE}
endIcon={getSortIcon(analog_sort.state, 'GPIO')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'GPIO' })}
>
GPIO
</Button>
</HeaderCell>
<HeaderCell resize>
<Button
fullWidth
style={HEADER_BUTTON_STYLE}
endIcon={getSortIcon(analog_sort.state, 'NAME')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'NAME' })}
>
{LL.NAME(0)}
</Button>
</HeaderCell>
<HeaderCell stiff>
<Button
fullWidth
style={HEADER_BUTTON_STYLE}
endIcon={getSortIcon(analog_sort.state, 'TYPE')}
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'TYPE' })}
>
{LL.TYPE(0)}
</Button>
</HeaderCell>
<HeaderCell stiff>
<Button
fullWidth
style={HEADER_BUTTON_STYLE_END}
endIcon={getSortIcon(analog_sort.state, 'VALUE')}
onClick={() =>
analog_sort.fns.onToggleSort({ sortKey: 'VALUE' })
}
>
{LL.VALUE(0)}
</Button>
</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((as: AnalogSensor) => (
<Row
style={{ color: as.s ? 'grey' : 'inherit' }}
key={as.id}
item={as}
onClick={() => updateAnalogSensor(as)}
const RenderTemperatureSensors = (
<Table
data={{ nodes: sensorData.ts }}
theme={temperature_theme}
sort={temperature_sort}
layout={{ custom: true }}
>
{(tableList: TemperatureSensor[]) => (
<>
<Header>
<HeaderRow>
<HeaderCell resize>
<Button
fullWidth
style={HEADER_BUTTON_STYLE}
endIcon={getSortIcon(temperature_sort.state, 'NAME')}
onClick={() =>
temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })
}
>
<Cell stiff>{as.g}</Cell>
<Cell>{as.n}</Cell>
<Cell stiff>{AnalogTypeNames[as.t - 1]} </Cell>
{(as.t === AnalogType.DIGITAL_OUT &&
as.g !== GPIO_25 &&
as.g !== GPIO_26) ||
as.t === AnalogType.DIGITAL_IN ||
as.t === AnalogType.PULSE ? (
<Cell stiff>{as.v ? LL.ON() : LL.OFF()}</Cell>
) : (
<Cell stiff>{formatValue(as.v, as.u)}</Cell>
)}
</Row>
))}
</Body>
</>
)}
</Table>
),
[
analog_sort,
analog_theme,
getSortIcon,
sensorData.as,
LL,
updateAnalogSensor,
formatValue
]
);
const RenderTemperatureSensors = useMemo(
() => (
<Table
data={{ nodes: sensorData.ts }}
theme={temperature_theme}
sort={temperature_sort}
layout={{ custom: true }}
>
{(tableList: TemperatureSensor[]) => (
<>
<Header>
<HeaderRow>
<HeaderCell resize>
<Button
fullWidth
style={HEADER_BUTTON_STYLE}
endIcon={getSortIcon(temperature_sort.state, 'NAME')}
onClick={() =>
temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })
}
>
{LL.NAME(0)}
</Button>
</HeaderCell>
<HeaderCell stiff>
<Button
fullWidth
style={HEADER_BUTTON_STYLE_END}
endIcon={getSortIcon(temperature_sort.state, 'VALUE')}
onClick={() =>
temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })
}
>
{LL.VALUE(0)}
</Button>
</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((ts: TemperatureSensor) => (
<Row
style={{ color: ts.s ? 'grey' : 'inherit' }}
key={ts.id}
item={ts}
onClick={() => updateTemperatureSensor(ts)}
{LL.NAME(0)}
</Button>
</HeaderCell>
<HeaderCell stiff>
<Button
fullWidth
style={HEADER_BUTTON_STYLE_END}
endIcon={getSortIcon(temperature_sort.state, 'VALUE')}
onClick={() =>
temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })
}
>
<Cell>{ts.n}</Cell>
<Cell>{formatValue(ts.t, ts.u)}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
),
[
temperature_sort,
temperature_theme,
getSortIcon,
sensorData.ts,
LL,
updateTemperatureSensor,
formatValue
]
{LL.VALUE(0)}
</Button>
</HeaderCell>
</HeaderRow>
</Header>
<Body>
{tableList.map((ts: TemperatureSensor) => (
<Row
style={{ color: ts.s ? 'grey' : 'inherit' }}
key={ts.id}
item={ts}
onClick={() => updateTemperatureSensor(ts)}
>
<Cell>{ts.n}</Cell>
<Cell>{formatValue(ts.t, ts.u)}</Cell>
</Row>
))}
</Body>
</>
)}
</Table>
);
return (

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done';
@@ -23,7 +23,7 @@ import type { ValidateFieldsError } from 'async-validator';
import { ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils';
import { validate } from 'validators';
import { ValidationError, validate } from 'validators';
import { AnalogType, AnalogTypeNames, DeviceValueUOM_s } from './types';
import type { AnalogSensor } from './types';
@@ -53,84 +53,54 @@ const SensorsAnalogDialog = ({
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const [editItem, setEditItem] = useState<AnalogSensor>(selectedItem);
const updateFormValue = useMemo(
() =>
updateValue((updater) =>
setEditItem(
(prev) =>
updater(
prev as unknown as Record<string, unknown>
) as unknown as AnalogSensor
)
),
[setEditItem]
const updateFormValue = updateValue((updater) =>
setEditItem(
(prev) =>
updater(
prev as unknown as Record<string, unknown>
) as unknown as AnalogSensor
)
);
// Memoize helper functions to check sensor type conditions
const isCounterOrRate = useMemo(
() =>
editItem.t === AnalogType.COUNTER ||
editItem.t === AnalogType.RATE ||
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2),
[editItem.t]
);
const isCounter = useMemo(
() =>
editItem.t === AnalogType.COUNTER ||
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2),
[editItem.t]
);
const isFreqType = useMemo(
() => editItem.t >= AnalogType.FREQ_0 && editItem.t <= AnalogType.FREQ_2,
[editItem.t]
);
const isPWM = useMemo(
() =>
editItem.t === AnalogType.PWM_0 ||
editItem.t === AnalogType.PWM_1 ||
editItem.t === AnalogType.PWM_2,
[editItem.t]
);
const isDACOutGPIO = useMemo(
() =>
editItem.t === AnalogType.DIGITAL_OUT &&
(editItem.g === 25 || editItem.g === 26),
[editItem.t, editItem.g]
);
const isDigitalOutGPIO = useMemo(
() =>
editItem.t === AnalogType.DIGITAL_OUT &&
editItem.g !== 25 &&
editItem.g !== 26,
[editItem.t, editItem.g]
);
const isCounterOrRate =
editItem.t === AnalogType.COUNTER ||
editItem.t === AnalogType.RATE ||
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2);
const isCounter =
editItem.t === AnalogType.COUNTER ||
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2);
const isFreqType =
editItem.t >= AnalogType.FREQ_0 && editItem.t <= AnalogType.FREQ_2;
const isPWM =
editItem.t === AnalogType.PWM_0 ||
editItem.t === AnalogType.PWM_1 ||
editItem.t === AnalogType.PWM_2;
const isDACOutGPIO =
editItem.t === AnalogType.DIGITAL_OUT &&
(editItem.g === 25 || editItem.g === 26);
const isDigitalOutGPIO =
editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26;
// Memoize menu items to avoid recreation on each render
const analogTypeMenuItems = useMemo(
() =>
AnalogTypeNames.map((val, i) => ({ name: val, value: i + 1 }))
.sort((a, b) => a.name.localeCompare(b.name))
.map(({ name, value }) => (
<MenuItem
key={name}
value={value}
disabled={disabledTypeList?.includes(value)}
>
{name}
</MenuItem>
)),
[disabledTypeList]
);
const analogTypeMenuItems = AnalogTypeNames.map((val, i) => ({
name: val,
value: i + 1
}))
.sort((a, b) => a.name.localeCompare(b.name))
.map(({ name, value }) => (
<MenuItem
key={name}
value={value}
disabled={disabledTypeList?.includes(value)}
>
{name}
</MenuItem>
));
const uomMenuItems = useMemo(
() =>
DeviceValueUOM_s.map((val, i) => (
<MenuItem key={val} value={i}>
{val}
</MenuItem>
)),
[]
);
const uomMenuItems = DeviceValueUOM_s.map((val, i) => (
<MenuItem key={val} value={i}>
{val}
</MenuItem>
));
const analogGPIOMenuItems = () =>
// add selectedItem.g to the list
@@ -157,34 +127,30 @@ const SensorsAnalogDialog = ({
}
}, [open, selectedItem]);
const handleClose = useCallback(
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => {
if (reason !== 'backdropClick') {
onClose();
}
},
[onClose]
);
const handleClose = (
_event: React.SyntheticEvent,
reason: 'backdropClick' | 'escapeKeyDown'
) => {
if (reason !== 'backdropClick') {
onClose();
}
};
const save = useCallback(async () => {
const save = async () => {
try {
setFieldErrors(undefined);
await validate(validator, editItem);
onSave(editItem);
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
}
}, [validator, editItem, onSave]);
};
const remove = useCallback(() => {
const remove = () => {
onSave({ ...editItem, d: true });
}, [editItem, onSave]);
};
const dialogTitle = useMemo(
() =>
`${creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()} ${LL.ANALOG_SENSOR(0)}`,
[creating, LL]
);
const dialogTitle = `${creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()} ${LL.ANALOG_SENSOR(0)}`;
return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done';
@@ -21,7 +21,7 @@ import type { ValidateFieldsError } from 'async-validator';
import { ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils';
import { validate } from 'validators';
import { ValidationError, validate } from 'validators';
import type { TemperatureSensor } from './types';
@@ -50,16 +50,12 @@ const SensorsTemperatureDialog = ({
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
const updateFormValue = useMemo(
() =>
updateValue(
setEditItem as unknown as (
updater: (
prevState: Readonly<Record<string, unknown>>
) => Record<string, unknown>
) => void
),
[setEditItem]
const updateFormValue = updateValue(
setEditItem as unknown as (
updater: (
prevState: Readonly<Record<string, unknown>>
) => Record<string, unknown>
) => void
);
useEffect(() => {
@@ -69,46 +65,25 @@ const SensorsTemperatureDialog = ({
}
}, [open, selectedItem]);
const handleClose = useCallback(
(_event: React.SyntheticEvent, reason?: string) => {
if (reason !== 'backdropClick') {
onClose();
}
},
[onClose]
);
const handleClose = (_event: React.SyntheticEvent, reason?: string) => {
if (reason !== 'backdropClick') {
onClose();
}
};
const save = useCallback(async () => {
const save = async () => {
try {
setFieldErrors(undefined);
await validate(validator, editItem);
onSave(editItem);
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
}
}, [validator, editItem, onSave]);
const dialogTitle = useMemo(() => `${LL.EDIT()} ${LL.TEMP_SENSOR()}`, [LL]);
const offsetValue = useMemo(() => numberValue(editItem.o), [editItem.o]);
const slotProps = useMemo(
() => ({
input: {
startAdornment: <InputAdornment position="start">{TEMP_UNIT}</InputAdornment>
},
htmlInput: {
min: OFFSET_MIN,
max: OFFSET_MAX,
step: OFFSET_STEP
}
}),
[]
);
};
return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}>
<DialogTitle>{dialogTitle}</DialogTitle>
<DialogTitle>{`${LL.EDIT()} ${LL.TEMP_SENSOR()}`}</DialogTitle>
<DialogContent dividers>
<Typography sx={{ mb: 2 }} color="warning" variant="body2">
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
@@ -128,12 +103,23 @@ const SensorsTemperatureDialog = ({
<TextField
name="o"
label={LL.OFFSET()}
value={offsetValue}
value={numberValue(editItem.o)}
sx={{ width: '11ch' }}
type="number"
variant="outlined"
onChange={updateFormValue}
slotProps={slotProps}
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">{TEMP_UNIT}</InputAdornment>
)
},
htmlInput: {
min: OFFSET_MIN,
max: OFFSET_MAX,
step: OFFSET_STEP
}
}}
/>
</Grid>
</Grid>

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useContext } from 'react';
import { memo, useContext } from 'react';
import PersonIcon from '@mui/icons-material/Person';
import {
@@ -23,9 +23,9 @@ const UserProfileComponent = () => {
useLayoutTitle(LL.USER_PROFILE());
const handleSignOut = useCallback(() => {
const handleSignOut = () => {
signOut(true);
}, [signOut]);
};
return (
<SectionContent>

View File

@@ -43,6 +43,16 @@ export interface Settings {
modbus_port: number;
modbus_max_clients: number;
modbus_timeout: number;
email_enabled: boolean;
email_ssl?: boolean;
email_starttls?: boolean;
email_server: string;
email_port: number;
email_login: string;
email_pass: string;
email_sender: string;
email_recp: string;
email_subject: string;
developer_mode: boolean;
}

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning';
@@ -21,10 +21,9 @@ import { useI18nContext } from 'i18n/i18n-react';
import type { APSettingsType } from 'types';
import { APProvisionMode } from 'types';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { createAPSettingsValidator, validate } from 'validators';
import { ValidationError, createAPSettingsValidator, validate } from 'validators';
export const isAPEnabled = ({ provision_mode }: APSettingsType) =>
provision_mode === APProvisionMode.AP_MODE_ALWAYS ||
provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
// Efficient range function without recursion
@@ -63,22 +62,16 @@ const APSettings = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = useMemo(
() =>
updateValueDirty(
origData as unknown as Record<string, unknown>,
dirtyFlags,
setDirtyFlags,
updateDataValue as (value: unknown) => void
),
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
const updateFormValue = updateValueDirty(
origData as unknown as Record<string, unknown>,
dirtyFlags,
setDirtyFlags,
updateDataValue as (value: unknown) => void
);
// Memoize AP enabled state
const apEnabled = useMemo(() => (data ? isAPEnabled(data) : false), [data]);
const apEnabled = data ? isAPEnabled(data) : false;
// Memoize validation and submit handler
const validateAndSubmit = useCallback(async () => {
const validateAndSubmit = async () => {
if (!data) return;
try {
@@ -86,9 +79,9 @@ const APSettings = () => {
await validate(createAPSettingsValidator(data), data);
await saveData();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
}
}, [data, saveData]);
};
const content = () => {
if (!data) {
@@ -108,9 +101,6 @@ const APSettings = () => {
onChange={updateFormValue}
margin="normal"
>
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>
{LL.AP_PROVIDE_TEXT_1()}
</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>
{LL.AP_PROVIDE_TEXT_2()}
</MenuItem>

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useState } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -28,12 +28,13 @@ import {
FormLoader,
MessageBox,
SectionContent,
ValidatedPasswordField,
ValidatedTextField,
useLayoutTitle
} from 'components';
import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
import { ValidationError, validate } from 'validators';
import { API, getBoardProfile, readSettings, writeSettings } from '../../api/app';
import { BOARD_PROFILES } from '../main/types';
@@ -106,82 +107,65 @@ const ApplicationSettings = () => {
});
});
// Memoized input props to prevent recreation on every render
const SecondsInputProps = useMemo(
() => ({
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}),
[LL]
);
const SecondsInputProps = {
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
};
const MinutesInputProps = useMemo(
() => ({
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
}),
[LL]
);
const MinutesInputProps = {
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
};
const HoursInputProps = useMemo(
() => ({
endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment>
}),
[LL]
);
const HoursInputProps = {
endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment>
};
const doRestart = useCallback(async () => {
const doRestart = async () => {
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
}, [sendAPI]);
};
const updateBoardProfile = useCallback(
async (board_profile: string) => {
await readBoardProfile(board_profile).catch((error: Error) => {
toast.error(error.message);
});
},
[readBoardProfile]
);
const updateBoardProfile = async (board_profile: string) => {
await readBoardProfile(board_profile).catch((error: Error) => {
toast.error(error.message);
});
};
useLayoutTitle(LL.APPLICATION());
const validateAndSubmit = useCallback(async () => {
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(createSettingsValidator(data), data);
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
} finally {
await saveData();
}
}, [data, saveData]);
};
const changeBoardProfile = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const boardProfile = event.target.value;
updateFormValue(event);
if (boardProfile === 'CUSTOM') {
updateDataValue({
...data,
board_profile: boardProfile
});
} else {
void updateBoardProfile(boardProfile);
}
},
[data, updateBoardProfile, updateFormValue, updateDataValue]
);
const changeBoardProfile = (event: React.ChangeEvent<HTMLInputElement>) => {
const boardProfile = event.target.value;
updateFormValue(event);
if (boardProfile === 'CUSTOM') {
updateDataValue({
...data,
board_profile: boardProfile
});
} else {
void updateBoardProfile(boardProfile);
}
};
const restart = useCallback(async () => {
const restart = async () => {
await validateAndSubmit();
await doRestart();
}, [validateAndSubmit, doRestart]);
};
// Memoize board profile select items to prevent recreation
const boardProfileItems = useMemo(() => boardProfileSelectItems(), []);
const boardProfileItems = boardProfileSelectItems();
const content = () => {
if (!data || !hardwareData) {
@@ -351,6 +335,156 @@ const ApplicationSettings = () => {
</Grid>
</Grid>
)}
<Typography color="secondary">eMail</Typography>
<BlockFormControlLabel
control={
<Checkbox
checked={data.email_enabled}
onChange={updateFormValue}
name="email_enabled"
disabled={!hardwareData.psram}
/>
}
label={
<Typography color={!hardwareData.psram ? 'grey' : 'default'}>
Enable eMail notification
{!hardwareData.psram && (
<Typography variant="caption">
&nbsp; &#40;{LL.IS_REQUIRED('PSRAM')}&#41;
</Typography>
)}
</Typography>
}
/>
{data.email_enabled && (
<>
<Grid
container
spacing={2}
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_server"
label="SMTP Server"
variant="outlined"
value={data.email_server}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
sx={{ width: '12ch' }}
name="email_port"
variant="outlined"
label="Port"
value={numberValue(data.email_port)}
type="number"
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid size={4} mt={!data.email_ssl && !data.email_starttls ? 0 : 3}>
{!data.email_starttls && (
<BlockFormControlLabel
sx={{ width: '12ch' }}
control={
<Checkbox
checked={data.email_ssl}
onChange={updateFormValue}
name="email_ssl"
disabled={
data.email_starttls || data.email_ssl === undefined
}
/>
}
label="SSL/TLS"
/>
)}
{!data.email_ssl && (
<BlockFormControlLabel
sx={{ width: '12ch' }}
control={
<Checkbox
checked={data.email_starttls}
onChange={updateFormValue}
name="email_starttls"
disabled={
data.email_ssl || data.email_starttls === undefined
}
/>
}
label="STARTTLS"
/>
)}
</Grid>
</Grid>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_login"
label="Login"
variant="outlined"
value={data.email_login}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedPasswordField
fieldErrors={fieldErrors || {}}
name="email_pass"
label="Password"
variant="outlined"
value={data.email_pass}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
</Grid>
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_sender"
label="From"
variant="outlined"
value={data.email_sender}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_recp"
label="To"
variant="outlined"
value={data.email_recp}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="email_subject"
label="Subject"
variant="outlined"
value={data.email_subject}
onChange={updateFormValue}
margin="normal"
/>
</Grid>
</Grid>
</>
)}
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.SENSORS()}
</Typography>

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useState } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -57,7 +57,7 @@ const DownloadUpload = () => {
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
const doRestart = useCallback(async () => {
const doRestart = async () => {
setRestarting(true);
try {
await sendAPI({ device: 'system', cmd: 'restart', id: 0 });
@@ -65,16 +65,33 @@ const DownloadUpload = () => {
toast.error((error as Error).message);
setRestarting(false);
}
}, [sendAPI]);
};
useLayoutTitle(LL.DOWNLOAD_UPLOAD());
const handleCloseBackupDialog = useCallback(() => {
const handleCloseBackupDialog = () => {
setConfirmBackup(false);
}, []);
};
const renderBackupDialog = useMemo(
() => (
const handleDownload = (type: string) => () => {
void sendExportData(type);
setConfirmBackup(false);
};
if (restarting) {
return <SystemMonitor />;
}
if (!data) {
return (
<SectionContent>
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
</SectionContent>
);
}
return (
<SectionContent>
<Dialog
sx={dialogStyle}
open={confirmBackup}
@@ -98,39 +115,13 @@ const DownloadUpload = () => {
<Button
startIcon={<DownloadIcon />}
variant="outlined"
onClick={() => handleDownload('systembackup')()}
onClick={handleDownload('systembackup')}
color="primary"
>
{LL.DOWNLOAD(0)}
</Button>
</DialogActions>
</Dialog>
),
[confirmBackup, handleCloseBackupDialog, LL]
);
const handleDownload = useCallback(
(type: string) => () => {
void sendExportData(type);
},
[sendExportData]
);
if (restarting) {
return <SystemMonitor />;
}
if (!data) {
return (
<SectionContent>
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
</SectionContent>
);
}
return (
<SectionContent>
{renderBackupDialog}
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)}

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useState } from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -31,7 +31,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react';
import type { MqttSettingsType } from 'types';
import { numberValue, updateValueDirty, useRest } from 'utils';
import { createMqttSettingsValidator, validate } from 'validators';
import { ValidationError, createMqttSettingsValidator, validate } from 'validators';
import { callAction } from '../../api/app';
@@ -57,7 +57,7 @@ const MqttSettings = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const sendResetMQTT = useCallback(() => {
const sendResetMQTT = () => {
void callAction({ action: 'resetMQTT' })
.then(() => {
toast.success('MQTT ' + LL.REFRESH() + ' successful');
@@ -65,56 +65,44 @@ const MqttSettings = () => {
.catch((error) => {
toast.error(String(error.error?.message || 'An error occurred'));
});
}, []);
};
const updateFormValue = useMemo(
() =>
updateValueDirty(
origData as unknown as Record<string, unknown>,
dirtyFlags,
setDirtyFlags,
updateDataValue as (value: unknown) => void
),
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
const updateFormValue = updateValueDirty(
origData as unknown as Record<string, unknown>,
dirtyFlags,
setDirtyFlags,
updateDataValue as (value: unknown) => void
);
const SecondsInputProps = useMemo(
() => ({
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}),
[LL]
);
const SecondsInputProps = {
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
};
const emptyFieldErrors = useMemo(() => ({}), []);
const validateAndSubmit = useCallback(async () => {
const validateAndSubmit = async () => {
if (!data) return;
try {
setFieldErrors(undefined);
await validate(createMqttSettingsValidator(data), data);
await saveData();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
}
}, [data, saveData]);
};
const publishIntervalFields = useMemo(
() => [
{ name: 'publish_time_heartbeat', label: 'Heartbeat', validated: true },
{ name: 'publish_time_boiler', label: LL.MQTT_INT_BOILER(), validated: false },
{
name: 'publish_time_thermostat',
label: LL.MQTT_INT_THERMOSTATS(),
validated: false
},
{ name: 'publish_time_solar', label: LL.MQTT_INT_SOLAR(), validated: false },
{ name: 'publish_time_mixer', label: LL.MQTT_INT_MIXER(), validated: false },
{ name: 'publish_time_water', label: LL.MQTT_INT_WATER(), validated: false },
{ name: 'publish_time_sensor', label: LL.SENSORS(), validated: false },
{ name: 'publish_time_other', label: LL.DEFAULT(0), validated: false }
],
[LL]
);
const publishIntervalFields = [
{ name: 'publish_time_heartbeat', label: 'Heartbeat', validated: true },
{ name: 'publish_time_boiler', label: LL.MQTT_INT_BOILER(), validated: false },
{
name: 'publish_time_thermostat',
label: LL.MQTT_INT_THERMOSTATS(),
validated: false
},
{ name: 'publish_time_solar', label: LL.MQTT_INT_SOLAR(), validated: false },
{ name: 'publish_time_mixer', label: LL.MQTT_INT_MIXER(), validated: false },
{ name: 'publish_time_water', label: LL.MQTT_INT_WATER(), validated: false },
{ name: 'publish_time_sensor', label: LL.SENSORS(), validated: false },
{ name: 'publish_time_other', label: LL.DEFAULT(0), validated: false }
];
if (!data) {
return (
@@ -154,7 +142,7 @@ const MqttSettings = () => {
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors ?? emptyFieldErrors}
fieldErrors={fieldErrors ?? {}}
name="host"
label={LL.ADDRESS_OF(LL.BROKER())}
multiline
@@ -166,7 +154,7 @@ const MqttSettings = () => {
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors ?? emptyFieldErrors}
fieldErrors={fieldErrors ?? {}}
name="port"
label="Port"
variant="outlined"
@@ -178,7 +166,7 @@ const MqttSettings = () => {
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors ?? emptyFieldErrors}
fieldErrors={fieldErrors ?? {}}
name="base"
label={LL.BASE_TOPIC()}
variant="outlined"
@@ -219,7 +207,7 @@ const MqttSettings = () => {
</Grid>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors ?? emptyFieldErrors}
fieldErrors={fieldErrors ?? {}}
name="keep_alive"
label="Keep Alive"
slotProps={{
@@ -438,7 +426,7 @@ const MqttSettings = () => {
<Grid key={field.name}>
{field.validated ? (
<ValidatedTextField
fieldErrors={fieldErrors ?? emptyFieldErrors}
fieldErrors={fieldErrors ?? {}}
name={field.name}
label={field.label}
slotProps={{

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useState } from 'react';
import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
@@ -36,7 +36,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react';
import type { NTPSettingsType, Time } from 'types';
import { formatLocalDateTime, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
import { ValidationError, validate } from 'validators';
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
import { TIME_ZONES, selectedTimeZone, useTimeZoneSelectItems } from './TZ';
@@ -61,14 +61,11 @@ const NTPSettings = () => {
const { LL } = useI18nContext();
useLayoutTitle('NTP');
// Memoized timezone select items for better performance
const timeZoneItems = useTimeZoneSelectItems();
// Memoized selected timezone value
const selectedTzValue = useMemo(
() => (data ? selectedTimeZone(data.tz_label, data.tz_format) : undefined),
[data?.tz_label, data?.tz_format]
);
const selectedTzValue = data
? selectedTimeZone(data.tz_label, data.tz_format)
: undefined;
const [localTime, setLocalTime] = useState<string>('');
const [settingTime, setSettingTime] = useState<boolean>(false);
@@ -82,32 +79,22 @@ const NTPSettings = () => {
}
);
// Memoize updateFormValue to prevent recreation on every render
const updateFormValue = useMemo(
() =>
updateValueDirty(
origData as unknown as Record<string, unknown>,
dirtyFlags,
setDirtyFlags,
updateDataValue as (value: unknown) => void
),
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
const updateFormValue = updateValueDirty(
origData as unknown as Record<string, unknown>,
dirtyFlags,
setDirtyFlags,
updateDataValue as (value: unknown) => void
);
// Memoize updateLocalTime handler
const updateLocalTime = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value),
[]
);
const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
setLocalTime(event.target.value);
// Memoize openSetTime handler
const openSetTime = useCallback(() => {
const openSetTime = () => {
setLocalTime(formatLocalDateTime(new Date()));
setSettingTime(true);
}, []);
};
// Memoize configureTime handler
const configureTime = useCallback(async () => {
const configureTime = async () => {
setProcessing(true);
try {
@@ -120,38 +107,31 @@ const NTPSettings = () => {
} finally {
setProcessing(false);
}
}, [localTime, updateTime, LL, loadData]);
};
// Memoize close dialog handler
const handleCloseSetTime = useCallback(() => setSettingTime(false), []);
const handleCloseSetTime = () => setSettingTime(false);
// Memoize validate and submit handler
const validateAndSubmit = useCallback(async () => {
const validateAndSubmit = async () => {
if (!data) return;
try {
setFieldErrors(undefined);
await validate(NTP_SETTINGS_VALIDATOR, data);
await saveData();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
}
}, [data, saveData]);
};
// Memoize timezone change handler
const changeTimeZone = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
...settings,
tz_label: event.target.value,
tz_format: TIME_ZONES[event.target.value]
}));
updateFormValue(event);
},
[updateFormValue]
);
const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
...settings,
tz_label: event.target.value,
tz_format: TIME_ZONES[event.target.value]
}));
updateFormValue(event);
};
// Memoize render content to prevent unnecessary re-renders
const renderContent = useMemo(() => {
const renderContent = () => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
}
@@ -236,26 +216,12 @@ const NTPSettings = () => {
)}
</>
);
}, [
data,
errorMessage,
loadData,
updateFormValue,
fieldErrors,
selectedTzValue,
changeTimeZone,
timeZoneItems,
dirtyFlags,
openSetTime,
saving,
validateAndSubmit,
LL
]);
};
return (
<SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null}
{renderContent}
{renderContent()}
<Dialog sx={dialogStyle} open={settingTime} onClose={handleCloseSetTime}>
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
<DialogContent dividers>

View File

@@ -1,190 +1,108 @@
import { useCallback, useMemo, useState } from 'react';
import { useContext } from 'react';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import CancelIcon from '@mui/icons-material/Cancel';
import BuildIcon from '@mui/icons-material/Build';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import ImportExportIcon from '@mui/icons-material/ImportExport';
import LockIcon from '@mui/icons-material/Lock';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import TuneIcon from '@mui/icons-material/Tune';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
List
} from '@mui/material';
import { List } from '@mui/material';
import { API } from 'api/app';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types';
import { SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import SystemMonitor from '../status/SystemMonitor';
const Settings = () => {
const { LL } = useI18nContext();
const { versions } = useContext(AuthenticatedContext);
useLayoutTitle(LL.SETTINGS(0));
const [confirmFactoryReset, setConfirmFactoryReset] = useState(false);
const [restarting, setRestarting] = useState<boolean>();
const upgradeAvailable = versions?.current?.upgradeable ?? false;
const firmwareText = versions?.current?.version
? `v${versions.current.version}${upgradeAvailable ? ` (${LL.UPDATE_AVAILABLE()})` : ''}`
: '';
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
return (
<SectionContent>
<List>
<ListMenuItem
icon={BuildIcon}
bgcolor="#72caf9"
label="EMS-ESP Firmware"
text={firmwareText}
to="/settings/version"
badge={upgradeAvailable}
/>
const doFormat = useCallback(async () => {
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
setRestarting(true);
setConfirmFactoryReset(false);
});
}, [sendAPI]);
<ListMenuItem
icon={TuneIcon}
bgcolor="#134ba2"
label={LL.APPLICATION()}
text={LL.APPLICATION_SETTINGS_1()}
to="application"
/>
const handleFactoryResetClose = useCallback(() => {
setConfirmFactoryReset(false);
}, []);
<ListMenuItem
icon={SettingsEthernetIcon}
bgcolor="#40828f"
label={LL.NETWORK(0)}
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))}
to="network"
/>
const handleFactoryResetClick = useCallback(() => {
setConfirmFactoryReset(true);
}, []);
<ListMenuItem
icon={SettingsInputAntennaIcon}
bgcolor="#5f9a5f"
label={LL.ACCESS_POINT(0)}
text={LL.CONFIGURE(LL.ACCESS_POINT(1))}
to="ap"
/>
const content = useMemo(() => {
return (
<>
<List>
<ListMenuItem
icon={TuneIcon}
bgcolor="#134ba2"
label={LL.APPLICATION()}
text={LL.APPLICATION_SETTINGS_1()}
to="application"
/>
<ListMenuItem
icon={AccessTimeIcon}
bgcolor="#c5572c"
label="NTP"
text={LL.CONFIGURE(LL.LOCAL_TIME(1))}
to="ntp"
/>
<ListMenuItem
icon={SettingsEthernetIcon}
bgcolor="#40828f"
label={LL.NETWORK(0)}
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))}
to="network"
/>
<ListMenuItem
icon={DeviceHubIcon}
bgcolor="#68374d"
label="MQTT"
text={LL.CONFIGURE('MQTT')}
to="mqtt"
/>
<ListMenuItem
icon={SettingsInputAntennaIcon}
bgcolor="#5f9a5f"
label={LL.ACCESS_POINT(0)}
text={LL.CONFIGURE(LL.ACCESS_POINT(1))}
to="ap"
/>
<ListMenuItem
icon={LockIcon}
label={LL.SECURITY(0)}
text={LL.SECURITY_1()}
to="security"
/>
<ListMenuItem
icon={AccessTimeIcon}
bgcolor="#c5572c"
label="NTP"
text={LL.CONFIGURE(LL.LOCAL_TIME(1))}
to="ntp"
/>
<ListMenuItem
icon={ViewModuleIcon}
bgcolor="#efc34b"
label={LL.MODULES()}
text={LL.MODULES_1()}
to="modules"
/>
<ListMenuItem
icon={DeviceHubIcon}
bgcolor="#68374d"
label="MQTT"
text={LL.CONFIGURE('MQTT')}
to="mqtt"
/>
<ListMenuItem
icon={LockIcon}
label={LL.SECURITY(0)}
text={LL.SECURITY_1()}
to="security"
/>
<ListMenuItem
icon={ViewModuleIcon}
bgcolor="#efc34b"
label={LL.MODULES()}
text={LL.MODULES_1()}
to="modules"
/>
<ListMenuItem
icon={ImportExportIcon}
bgcolor="#5d89f7"
label={LL.DOWNLOAD_UPLOAD()}
text={LL.DOWNLOAD_UPLOAD_1()}
to="downloadUpload"
/>
</List>
<Dialog
sx={dialogStyle}
open={confirmFactoryReset}
onClose={handleFactoryResetClose}
>
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={handleFactoryResetClose}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={doFormat}
color="error"
>
{LL.FACTORY_RESET()}
</Button>
</DialogActions>
</Dialog>
<Divider />
<Box
sx={{
mt: 2,
display: 'flex',
justifyContent: 'flex-end',
flexWrap: 'nowrap',
whiteSpace: 'nowrap'
}}
>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={handleFactoryResetClick}
color="error"
>
{LL.FACTORY_RESET()}
</Button>
</Box>
</>
);
}, [
LL,
handleFactoryResetClick,
handleFactoryResetClose,
doFormat,
confirmFactoryReset,
restarting
]);
return restarting ? <SystemMonitor /> : <SectionContent>{content}</SectionContent>;
<ListMenuItem
icon={ImportExportIcon}
bgcolor="#5d89f7"
label={LL.DOWNLOAD_UPLOAD()}
text={LL.DOWNLOAD_UPLOAD_1()}
to="downloadUpload"
/>
</List>
</SectionContent>
);
};
export default Settings;

View File

@@ -1,5 +1,3 @@
import { useMemo } from 'react';
import { MenuItem } from '@mui/material';
export const TIME_ZONES: Record<string, string> = {
@@ -472,26 +470,16 @@ export function selectedTimeZone(label: string, format: string) {
return TIME_ZONES[label] === format ? label : undefined;
}
// Memoized version for use in components
export function useTimeZoneSelectItems() {
return useMemo(
() =>
TIME_ZONE_LABELS.map((label) => (
<MenuItem key={label} value={label}>
{label}
</MenuItem>
)),
[]
);
}
// Fallback export for backward compatibility - now memoized
const precomputedTimeZoneItems = TIME_ZONE_LABELS.map((label) => (
<MenuItem key={label} value={label}>
{label}
</MenuItem>
));
export function useTimeZoneSelectItems() {
return precomputedTimeZoneItems;
}
export function timeZoneSelectItems() {
return precomputedTimeZoneItems;
}

View File

@@ -0,0 +1,958 @@
import { memo, useContext, useMemo, useState } from 'react';
import { Link } from 'react-router';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import CloseIcon from '@mui/icons-material/Close';
import CheckIcon from '@mui/icons-material/Done';
import DownloadIcon from '@mui/icons-material/GetApp';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import WarningIcon from '@mui/icons-material/Warning';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
IconButton,
Table,
TableBody,
TableCell,
TableRow,
Typography
} from '@mui/material';
import * as SystemApi from 'api/system';
import { API, callAction } from 'api/app';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types';
import SystemMonitor from 'app/status/SystemMonitor';
import {
FormLoader,
SectionContent,
SingleUpload,
useLayoutTitle
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { TranslationFunctions } from 'i18n/i18n-types';
import type { VersionInfo } from 'types';
import { prettyDateTime } from 'utils/time';
// Constants moved outside component to avoid recreation
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const STABLE_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const DEV_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
// Types for better type safety
interface PartitionData {
partition: string;
version: string;
install_date?: string;
size: number;
}
interface VersionData {
emsesp_version: string;
arduino_version: string;
esp_platform: string;
flash_chip_size: number;
psram: boolean;
build_flags?: string;
partition: string;
partitions: PartitionData[];
developer_mode: boolean;
}
// Memoized components for better performance
const VersionInfoDialog = memo(
({
showVersionInfo,
latestVersion,
latestDevVersion,
partitionVersion,
partition,
currentPartition,
size,
locale,
LL,
onClose
}: {
showVersionInfo: number;
latestVersion: VersionInfo | undefined;
latestDevVersion: VersionInfo | undefined;
partitionVersion: VersionInfo | undefined;
partition: string;
currentPartition: string;
size: number;
locale: string;
LL: TranslationFunctions;
onClose: () => void;
}) => {
if (showVersionInfo === 0) return null;
const isStable = showVersionInfo === 1;
const isDev = showVersionInfo === 2;
const isPartition = showVersionInfo === 3;
const version = isStable
? latestVersion
: isDev
? latestDevVersion
: partitionVersion;
const relNotesUrl = isStable
? STABLE_RELNOTES_URL
: isDev
? DEV_RELNOTES_URL
: '';
return (
<Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
<DialogTitle>{LL.FIRMWARE_VERSION_INFO()}</DialogTitle>
<DialogContent dividers>
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
<TableBody>
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
{LL.VERSION()}
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{isPartition
? typeof version === 'string'
? version
: version?.version
: version?.version}
</TableCell>
</TableRow>
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13,
width: 140
}}
>
{isPartition ? LL.TYPE(0) : LL.RELEASE_TYPE()}
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{partition === currentPartition && LL.ACTIVE() + ' '}
{isStable
? LL.STABLE()
: isDev
? LL.DEVELOPMENT()
: 'Partition ' + LL.VERSION()}
</TableCell>
</TableRow>
{isPartition && (
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
Partition
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{partition}
</TableCell>
</TableRow>
)}
{isPartition && (
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
Size
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{size} KB
</TableCell>
</TableRow>
)}
{version && version.date && (
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
{isPartition ? 'Install Date' : 'Build Date'}
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{prettyDateTime(locale, new Date(version.date))}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
{!isPartition && (
<Button
variant="outlined"
component="a"
href={relNotesUrl}
target="_blank"
color="primary"
>
Changelog
</Button>
)}
<Button variant="outlined" onClick={onClose} color="secondary">
{LL.CLOSE()}
</Button>
</DialogActions>
</Dialog>
);
}
);
const InstallDialog = memo(
({
openInstallDialog,
fetchDevVersion,
latestVersion,
latestDevVersion,
upgradeImportantMessageType,
downloadOnly,
platform,
LL,
onClose,
onInstall
}: {
openInstallDialog: boolean;
fetchDevVersion: boolean;
latestVersion: VersionInfo | undefined;
latestDevVersion: VersionInfo | undefined;
upgradeImportantMessageType: number;
downloadOnly: boolean;
platform: string;
LL: TranslationFunctions;
onClose: () => void;
onInstall: (url: string) => void;
}) => {
const binURL = (() => {
if (!latestVersion || !latestDevVersion) return '';
const version = fetchDevVersion ? latestDevVersion : latestVersion;
const filename = `EMS-ESP-${version.version.replaceAll('.', '_')}-${platform}.bin`;
return fetchDevVersion
? `${DEV_URL}${filename}`
: `${STABLE_URL}v${version.version}/${filename}`;
})();
return (
<Dialog sx={dialogStyle} open={openInstallDialog} onClose={onClose}>
<DialogTitle>
{`${LL.INSTALL()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`}
</DialogTitle>
<DialogContent dividers>
<Typography sx={{ mb: 2 }}>
{LL.INSTALL_VERSION(
downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(),
fetchDevVersion ? latestDevVersion?.version : latestVersion?.version
)}
</Typography>
{upgradeImportantMessageType === 2 && LL.UPGRADE_IMPORTANT_MESSAGES_2()}
{upgradeImportantMessageType === 1 && (
<>
{LL.UPGRADE_IMPORTANT_MESSAGES_1()}
<Typography sx={{ mt: 2 }}>
<Link to="/settings/downloadUpload" style={{ color: 'lightblue' }}>
{LL.DOWNLOAD_SYSTEM_BACKUP()}
</Link>
</Typography>
</>
)}
<Typography sx={{ mt: 2 }}>
<Link
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
target="_blank"
rel="noreferrer"
style={{ color: 'lightblue' }}
>
{LL.ONLINE_HELP()}
</Link>
</Typography>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onClose}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
onClick={onClose}
color="primary"
>
<Link
to={binURL}
target="_blank"
rel="noreferrer"
style={{ color: 'lightblue', textDecoration: 'none' }}
>
{LL.DOWNLOAD(0)}
</Link>
</Button>
{!downloadOnly && (
<Button
startIcon={<WarningIcon color="warning" />}
variant="outlined"
onClick={() => onInstall(binURL)}
color="primary"
>
{LL.INSTALL()}
</Button>
)}
</DialogActions>
</Dialog>
);
}
);
const InstallPartitionDialog = memo(
({
openInstallPartitionDialog,
version,
partition,
LL,
onClose,
onInstall
}: {
openInstallPartitionDialog: boolean;
version: string;
partition: string;
LL: TranslationFunctions;
onClose: () => void;
onInstall: (partition: string) => void;
}) => {
return (
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
<DialogTitle>
{LL.INSTALL()} {LL.STORED_VERSIONS()}
</DialogTitle>
<DialogContent dividers>
<Typography sx={{ mb: 2 }}>
{LL.INSTALL_VERSION(LL.INSTALL(), version)}
</Typography>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onClose}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
variant="outlined"
onClick={() => onInstall(partition)}
color="primary"
>
{LL.INSTALL()}
</Button>
</DialogActions>
</Dialog>
);
}
);
// Helper function moved outside component
const getPlatform = (data: VersionData): string => {
return `${data.esp_platform}-${data.flash_chip_size >= 16384 ? '16MB' : '4MB'}${data.psram ? '+' : ''}`;
};
const Version = () => {
const { LL, locale } = useI18nContext();
const { me, versions } = useContext(AuthenticatedContext);
const [restarting, setRestarting] = useState<boolean>(false);
const [confirmFactoryReset, setConfirmFactoryReset] = useState<boolean>(false);
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
const [partitionVersion, setPartitionVersion] = useState<VersionInfo | undefined>(
undefined
);
const [partition, setPartition] = useState<string>('');
const [openInstallPartitionDialog, setOpenInstallPartitionDialog] =
useState<boolean>(false);
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
const [showVersionInfo, setShowVersionInfo] = useState<number>(0); // 1 = stable, 2 = dev, 3 = partition
const [firmwareSize, setFirmwareSize] = useState<number>(0);
const latestVersion = useMemo<VersionInfo | undefined>(
() =>
versions?.stable
? { version: versions.stable.version, date: versions.stable.date }
: undefined,
[versions?.stable]
);
const latestDevVersion = useMemo<VersionInfo | undefined>(
() =>
versions?.dev
? { version: versions.dev.version, date: versions.dev.date }
: undefined,
[versions?.dev]
);
const usingDevVersion = versions?.current?.type === 'dev';
const stableUpgradeAvailable = versions?.stable?.upgradeable ?? false;
const devUpgradeAvailable = versions?.dev?.upgradeable ?? false;
const internetLive = Boolean(versions?.stable || versions?.dev);
const { send: sendSetPartition } = useRequest(
(partition: string) => callAction({ action: 'setPartition', param: partition }),
{ immediate: false }
).onError((error) => {
toast.error(String(error.error?.message || 'An error occurred'));
});
const {
data,
send: loadData,
error
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
const systemData = event.data as VersionData;
if (systemData.arduino_version.startsWith('Tasmota')) {
setDownloadOnly(true);
}
});
const { send: sendUploadURL } = useRequest(
(url: string) => callAction({ action: 'uploadURL', param: url }),
{ immediate: false }
);
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const [upgradeImportantMessageType, setUpgradeImportantMessageType] =
useState<number>(0);
const { send: checkUpgradeImportantMessages } = useRequest(
(version: string) =>
callAction({ action: 'upgradeImportantMessages', param: version }),
{
immediate: false
}
)
.onSuccess((event) => {
const upgradeImportantMessageType_n = (
event.data as { upgradeImportantMessageType: number }
).upgradeImportantMessageType;
setUpgradeImportantMessageType(upgradeImportantMessageType_n);
})
.onError((error) => {
toast.error(String(error.error?.message || 'An error occurred'));
});
const platform = data ? getPlatform(data) : '';
const otherPartitions =
data?.partitions.filter((p) => p.partition !== data.partition) ?? [];
const setPartitionVersionInfo = (partition: string) => {
setShowVersionInfo(3);
const partitionData = data?.partitions.find((p) => p.partition === partition);
if (partitionData) {
setPartitionVersion({
version: partitionData.version,
date: partitionData.install_date ?? ''
});
setPartition(partitionData.partition);
setFirmwareSize(partitionData.size);
}
};
const doRestart = async () => {
setConfirmRestart(false);
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
setRestarting(true);
};
const doFormat = async () => {
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => {
setRestarting(true);
setConfirmFactoryReset(false);
});
};
const handleFactoryResetClose = () => setConfirmFactoryReset(false);
const handleFactoryResetClick = () => setConfirmFactoryReset(true);
const handleRestartClose = () => setConfirmRestart(false);
const handleRestartClick = () => setConfirmRestart(true);
const installFirmwareURL = async (url: string) => {
await sendUploadURL(url).catch((error: Error) => {
toast.error(error.message);
});
await doRestart();
};
const installPartitionFirmware = async (partition: string) => {
await sendSetPartition(partition).catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
};
const showPartitionDialog = (
version: string,
partition: string,
install_date: string
) => {
setOpenInstallPartitionDialog(true);
setPartitionVersion({ version: version, date: install_date });
setPartition(partition);
};
const showFirmwareDialog = (useDevVersion: boolean) => {
setFetchDevVersion(useDevVersion);
const targetVersion = useDevVersion
? latestDevVersion?.version
: latestVersion?.version;
if (targetVersion) {
void checkUpgradeImportantMessages(targetVersion);
}
setOpenInstallDialog(true);
};
const closeInstallDialog = () => setOpenInstallDialog(false);
const closeInstallPartitionDialog = () => setOpenInstallPartitionDialog(false);
const handleVersionInfoClose = () => {
setShowVersionInfo(0);
setPartitionVersion(undefined);
setPartition('');
};
useLayoutTitle('EMS-ESP Firmware');
const showButtons = (showingDev: boolean) => {
const choice = showingDev
? !usingDevVersion
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
: devUpgradeAvailable
? LL.UPDATE_AVAILABLE()
: undefined
: usingDevVersion
? LL.SWITCH_RELEASE_TYPE(LL.STABLE())
: stableUpgradeAvailable
? LL.UPDATE_AVAILABLE()
: undefined;
if (!choice) {
return (
<>
<CheckIcon
color="success"
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
/>
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
</span>
<Button
sx={{ ml: 1 }}
variant="outlined"
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{LL.REINSTALL()}
</Button>
</>
);
}
if (!me.admin) return null;
const isUpdateAvailable = choice === LL.UPDATE_AVAILABLE();
return (
<Button
sx={{ ml: 1 }}
variant="outlined"
color={isUpdateAvailable ? 'success' : 'warning'}
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{choice}
{isUpdateAvailable && (
<Box
component="span"
aria-label="update available"
sx={{
display: 'inline-block',
width: 8,
height: 8,
ml: 1,
verticalAlign: 'middle',
borderRadius: '50%',
backgroundColor: '#ffeb3b',
boxShadow: '0 0 6px rgba(255, 235, 59, 0.8)'
}}
/>
)}
</Button>
);
};
if (restarting) {
return <SystemMonitor />;
}
if (!data) {
return (
<SectionContent>
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
</SectionContent>
);
}
return (
<SectionContent>
<Box sx={{ p: 2, border: '1px solid #565656', borderRadius: 2 }}>
<Typography sx={{ mb: 1 }} variant="h6" color="primary">
{LL.THIS_VERSION()}
</Typography>
<Grid
container
direction="row"
sx={{
justifyContent: 'flex-start',
alignItems: 'baseline'
}}
>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.VERSION()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{data.emsesp_version}
{data.build_flags && (
<Typography variant="caption">
&nbsp; &#40;{data.build_flags}&#41;
</Typography>
)}
<IconButton
onClick={() => setPartitionVersionInfo(data.partition)}
aria-label={LL.FIRMWARE_VERSION_INFO()}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
</Typography>
</Grid>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.PLATFORM()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{platform}
<Typography variant="caption">
&nbsp; &#40;
{data.psram ? (
<CheckIcon
color="success"
sx={{
fontSize: '1.5em',
verticalAlign: 'middle'
}}
/>
) : (
<CloseIcon
color="error"
sx={{
fontSize: '1.5em',
verticalAlign: 'middle'
}}
/>
)}
PSRAM&#41;
</Typography>
</Typography>
</Grid>
</Grid>
{internetLive ? (
<>
<Typography sx={{ mt: 4, mb: 1 }} variant="h6" color="primary">
{LL.AVAILABLE_VERSION()}
</Typography>
<Grid
container
direction="row"
rowSpacing={1}
sx={{
justifyContent: 'flex-start',
alignItems: 'baseline'
}}
>
{otherPartitions.length > 0 && data.developer_mode && (
<>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.STORED_VERSIONS()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
{otherPartitions.map((partition) => (
<Typography key={partition.partition} sx={{ mb: 1 }}>
{partition.version}
<IconButton
onClick={() =>
setPartitionVersionInfo(partition.partition)
}
aria-label={LL.FIRMWARE_VERSION_INFO()}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
<Button
sx={{ ml: 0 }}
variant="outlined"
size="small"
onClick={() =>
showPartitionDialog(
partition.version,
partition.partition,
partition.install_date ?? ''
)
}
>
{LL.INSTALL()}
</Button>
</Typography>
))}
</Grid>
</>
)}
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.STABLE()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{latestVersion?.version}
<IconButton
onClick={() => setShowVersionInfo(1)}
aria-label={LL.FIRMWARE_VERSION_INFO()}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
{showButtons(false)}
</Typography>
</Grid>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.DEVELOPMENT()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{latestDevVersion?.version}
<IconButton
onClick={() => setShowVersionInfo(2)}
aria-label={LL.FIRMWARE_VERSION_INFO()}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
{showButtons(true)}
</Typography>
</Grid>
</Grid>
</>
) : (
<Typography sx={{ mt: 2 }} color="warning">
<WarningIcon color="warning" sx={{ verticalAlign: 'middle', mr: 2 }} />
{LL.INTERNET_CONNECTION_REQUIRED()}
</Typography>
)}
{me.admin && (
<>
<VersionInfoDialog
showVersionInfo={showVersionInfo}
latestVersion={latestVersion}
latestDevVersion={latestDevVersion}
partitionVersion={partitionVersion}
locale={locale}
partition={partition}
currentPartition={data?.partition ?? ''}
size={firmwareSize}
LL={LL}
onClose={handleVersionInfoClose}
/>
<InstallDialog
openInstallDialog={openInstallDialog}
fetchDevVersion={fetchDevVersion}
latestVersion={latestVersion}
latestDevVersion={latestDevVersion}
upgradeImportantMessageType={upgradeImportantMessageType}
downloadOnly={downloadOnly}
platform={platform}
LL={LL}
onClose={closeInstallDialog}
onInstall={installFirmwareURL}
/>
<InstallPartitionDialog
openInstallPartitionDialog={openInstallPartitionDialog}
version={partitionVersion?.version || ''}
partition={partition}
LL={LL}
onClose={closeInstallPartitionDialog}
onInstall={installPartitionFirmware}
/>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()}
</Typography>
<SingleUpload doRestart={doRestart} />
</>
)}
</Box>
{me.admin && (
<>
<Dialog
sx={dialogStyle}
open={confirmFactoryReset}
onClose={handleFactoryResetClose}
>
<DialogTitle>{LL.FACTORY_RESET()}</DialogTitle>
<DialogContent dividers>{LL.SYSTEM_FACTORY_TEXT_DIALOG()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={handleFactoryResetClose}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={doFormat}
color="error"
>
{LL.FACTORY_RESET()}
</Button>
</DialogActions>
</Dialog>
<Dialog
sx={dialogStyle}
open={confirmRestart}
onClose={handleRestartClose}
>
<DialogTitle>{LL.RESTART()}</DialogTitle>
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={handleRestartClose}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={doRestart}
color="error"
>
{LL.RESTART()}
</Button>
</DialogActions>
</Dialog>
<Box
sx={{
mt: 2,
display: 'flex',
justifyContent: 'flex-end',
flexWrap: 'nowrap',
whiteSpace: 'nowrap',
gap: 1
}}
>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={handleRestartClick}
color="error"
>
{LL.RESTART()}
</Button>
{data.developer_mode && (
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
onClick={handleFactoryResetClick}
color="error"
>
{LL.FACTORY_RESET()}
</Button>
)}
</Box>
</>
)}
</SectionContent>
);
};
export default memo(Version);

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useMemo, useState } from 'react';
import { memo, useState } from 'react';
import {
Navigate,
Route,
@@ -40,26 +40,20 @@ const Network = () => {
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
const selectNetwork = useCallback(
(network: WiFiNetwork) => {
setSelectedNetwork(network);
void navigate('/settings/network/settings');
},
[navigate]
);
const selectNetwork = (network: WiFiNetwork) => {
setSelectedNetwork(network);
void navigate('/settings/network/settings');
};
const deselectNetwork = useCallback(() => {
const deselectNetwork = () => {
setSelectedNetwork(undefined);
}, []);
};
const contextValue = useMemo(
() => ({
...(selectedNetwork && { selectedNetwork }),
selectNetwork,
deselectNetwork
}),
[selectedNetwork, selectNetwork, deselectNetwork]
);
const contextValue = {
...(selectedNetwork && { selectedNetwork }),
selectNetwork,
deselectNetwork
};
return (
<WiFiConnectionContext.Provider value={contextValue}>

View File

@@ -40,7 +40,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react';
import type { NetworkSettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators';
import { ValidationError, validate } from 'validators';
import { createNetworkSettingsValidator } from 'validators/network';
import SystemMonitor from '../../status/SystemMonitor';
@@ -116,24 +116,24 @@ const NetworkSettings = () => {
await validate(createNetworkSettingsValidator(data), data);
await saveData();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
}
deselectNetwork();
}, [data, saveData, deselectNetwork]);
const setCancel = useCallback(async () => {
const setCancel = async () => {
deselectNetwork();
await loadData();
}, [deselectNetwork, loadData]);
};
const doRestart = useCallback(async () => {
const doRestart = async () => {
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
}, [sendAPI]);
};
const content = () => {
if (!data) {

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useRef, useState } from 'react';
import { memo, useRef, useState } from 'react';
import PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import { Button } from '@mui/material';
@@ -48,12 +48,12 @@ const WiFiNetworkScanner = () => {
}
});
const renderNetworkScanner = useCallback(() => {
const renderNetworkScanner = () => {
if (!networkList) {
return <FormLoader errorMessage={errorMessage || ''} />;
}
return <WiFiNetworkSelector networkList={networkList} />;
}, [networkList, errorMessage]);
};
return (
<SectionContent>

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useContext } from 'react';
import { memo, useContext } from 'react';
import LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen';
@@ -63,34 +63,31 @@ const WiFiNetworkSelector = ({ networkList }: { networkList: WiFiNetworkList })
const wifiConnectionContext = useContext(WiFiConnectionContext);
const renderNetwork = useCallback(
(network: WiFiNetwork) => (
<ListItem
key={network.bssid}
onClick={() => wifiConnectionContext.selectNetwork(network)}
>
<ListItemAvatar>
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
</ListItemAvatar>
<ListItemText
primary={network.ssid}
secondary={
'Security: ' +
networkSecurityMode(network) +
', Ch: ' +
network.channel +
', bssid: ' +
network.bssid
}
/>
<ListItemIcon>
<Badge badgeContent={network.rssi + 'dBm'}>
<WifiIcon sx={{ color: networkQualityHighlight(network, theme) }} />
</Badge>
</ListItemIcon>
</ListItem>
),
[wifiConnectionContext, theme]
const renderNetwork = (network: WiFiNetwork) => (
<ListItem
key={network.bssid}
onClick={() => wifiConnectionContext.selectNetwork(network)}
>
<ListItemAvatar>
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
</ListItemAvatar>
<ListItemText
primary={network.ssid}
secondary={
'Security: ' +
networkSecurityMode(network) +
', Ch: ' +
network.channel +
', bssid: ' +
network.bssid
}
/>
<ListItemIcon>
<Badge badgeContent={network.rssi + 'dBm'}>
<WifiIcon sx={{ color: networkQualityHighlight(network, theme) }} />
</Badge>
</ListItemIcon>
</ListItem>
);
if (networkList.networks.length === 0) {

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useContext, useMemo, useState } from 'react';
import { memo, useCallback, useContext, useState } from 'react';
import { useBlocker } from 'react-router';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -55,16 +55,14 @@ const ManageUsers = () => {
const blocker = useBlocker(changed !== 0);
const { LL } = useI18nContext();
const table_theme = useMemo(
() =>
useTheme({
Table: `
const table_theme = useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) minmax(120px, max-content) 120px;
`,
BaseRow: `
BaseRow: `
font-size: 14px;
`,
HeaderRow: `
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
@@ -74,7 +72,7 @@ const ManageUsers = () => {
border-bottom: 1px solid #565656;
}
`,
Row: `
Row: `
.td {
padding: 8px;
border-top: 1px solid #565656;
@@ -87,7 +85,7 @@ const ManageUsers = () => {
background-color: #1e1e1e;
}
`,
BaseCell: `
BaseCell: `
&:nth-of-type(2) {
text-align: center;
}
@@ -95,44 +93,36 @@ const ManageUsers = () => {
text-align: right;
}
`
}),
[]
);
});
const noAdminConfigured = useCallback(
() => !data?.users.find((u) => u.admin),
[data]
);
const noAdminConfigured = () => !data?.users.find((u) => u.admin);
const removeUser = useCallback(
(toRemove: UserType) => {
if (!data) return;
const users = data.users.filter((u) => u.username !== toRemove.username);
updateDataValue({ ...data, users });
setChanged(changed + 1);
},
[data, updateDataValue, changed]
);
const removeUser = (toRemove: UserType) => {
if (!data) return;
const users = data.users.filter((u) => u.username !== toRemove.username);
updateDataValue({ ...data, users });
setChanged(changed + 1);
};
const createUser = useCallback(() => {
const createUser = () => {
setCreating(true);
setUser({
username: '',
password: '',
admin: true
});
}, []);
};
const editUser = useCallback((toEdit: UserType) => {
const editUser = (toEdit: UserType) => {
setCreating(false);
setUser({ ...toEdit });
}, []);
};
const cancelEditingUser = useCallback(() => {
const cancelEditingUser = () => {
setUser(undefined);
}, []);
};
const doneEditingUser = useCallback(() => {
const doneEditingUser = () => {
if (user && data) {
const users = [
...data.users.filter(
@@ -144,26 +134,26 @@ const ManageUsers = () => {
setUser(undefined);
setChanged(changed + 1);
}
}, [user, data, updateDataValue, changed]);
};
const closeGenerateToken = useCallback(() => {
setGeneratingToken(undefined);
}, []);
const generateTokenForUser = useCallback((username: string) => {
const generateTokenForUser = (username: string) => {
setGeneratingToken(username);
}, []);
};
const onSubmit = useCallback(async () => {
const onSubmit = async () => {
await saveData();
await authenticatedContext.refresh();
setChanged(0);
}, [saveData, authenticatedContext]);
};
const onCancelSubmit = useCallback(async () => {
const onCancelSubmit = async () => {
await loadData();
setChanged(0);
}, [loadData]);
};
const content = () => {
if (!data) {
@@ -177,15 +167,10 @@ const ManageUsers = () => {
admin: boolean;
}
// add id to the type, needed for the table
const user_table = useMemo(
() =>
data.users.map((u) => ({
...u,
id: u.username
})) as UserType2[],
[data.users]
);
const user_table = data.users.map((u) => ({
...u,
id: u.username
})) as UserType2[];
return (
<>

View File

@@ -1,4 +1,4 @@
import { memo, useMemo } from 'react';
import { memo } from 'react';
import { Navigate, Route, Routes, matchRoutes, useLocation } from 'react-router';
import { Tab } from '@mui/material';
@@ -15,19 +15,15 @@ const Security = () => {
const location = useLocation();
const matchedRoutes = useMemo(
() =>
matchRoutes(
[
{
path: '/settings/security/settings',
element: <ManageUsers />
},
{ path: '/settings/security/users', element: <SecuritySettings /> }
],
location
),
[location]
const matchedRoutes = matchRoutes(
[
{
path: '/settings/security/settings',
element: <ManageUsers />
},
{ path: '/settings/security/users', element: <SecuritySettings /> }
],
location
);
const routerTab = matchedRoutes?.[0]?.route.path || false;

View File

@@ -19,7 +19,7 @@ import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { SecuritySettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils';
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators';
import { SECURITY_SETTINGS_VALIDATOR, ValidationError, validate } from 'validators';
const SecuritySettings = () => {
const { LL } = useI18nContext();
@@ -58,7 +58,7 @@ const SecuritySettings = () => {
await saveData();
await authenticatedContext.refresh();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
}
}, [data, saveData, authenticatedContext]);
@@ -79,7 +79,7 @@ const SecuritySettings = () => {
onChange={updateFormValue}
margin="normal"
/>
<MessageBox level="info" message={LL.SU_TEXT()} mt={1} />
<MessageBox level="info" message={LL.SU_TEXT()} sx={{ mt: 1 }} />
{dirtyFlags && dirtyFlags.length !== 0 && (
<ButtonRow>
<Button

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect, useState } from 'react';
import { memo, useEffect, useState } from 'react';
import type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -24,7 +24,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react';
import type { UserType } from 'types';
import { updateValue } from 'utils';
import { validate } from 'validators';
import { ValidationError, validate } from 'validators';
interface UserFormProps {
creating: boolean;
@@ -62,17 +62,17 @@ const User: FC<UserFormProps> = ({
}
}, [open]);
const validateAndDone = useCallback(async () => {
const validateAndDone = async () => {
if (user) {
try {
setFieldErrors(undefined);
await validate(validator, user);
onDoneEditing();
} catch (error) {
setFieldErrors(error as ValidateFieldsError);
setFieldErrors((error as ValidationError).fieldErrors);
}
}
}, [user, validator, onDoneEditing]);
};
return (
<Dialog

View File

@@ -1,5 +1,3 @@
import { useCallback, useMemo } from 'react';
import {
Body,
Cell,
@@ -36,16 +34,14 @@ const SystemActivity = () => {
useLayoutTitle(LL.DATA_TRAFFIC());
const stats_theme = tableTheme(
useMemo(
() => ({
Table: `
const stats_theme = tableTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 90px 90px 80px;
`,
BaseRow: `
BaseRow: `
font-size: 14px;
`,
HeaderRow: `
HeaderRow: `
text-transform: uppercase;
background-color: black;
color: #90CAF9;
@@ -55,7 +51,7 @@ const SystemActivity = () => {
border-bottom: 1px solid #565656;
}
`,
Row: `
Row: `
.td {
padding: 8px;
border-top: 1px solid #565656;
@@ -69,26 +65,20 @@ const SystemActivity = () => {
background-color: #1e1e1e;
}
`,
BaseCell: `
BaseCell: `
&:not(:first-of-type) {
text-align: center;
}
`
}),
[]
)
);
});
const showName = useCallback(
(id: number) => {
const name: keyof Translation['STATUS_NAMES'] =
id.toString() as keyof Translation['STATUS_NAMES'];
return LL.STATUS_NAMES[name]();
},
[LL]
);
const showName = (id: number) => {
const name: keyof Translation['STATUS_NAMES'] =
id.toString() as keyof Translation['STATUS_NAMES'];
return LL.STATUS_NAMES[name]();
};
const showQuality = useCallback((stat: Stat) => {
const showQuality = (stat: Stat) => {
if (stat.q === 0 || stat.s + stat.f === 0) {
return;
}
@@ -100,14 +90,18 @@ const SystemActivity = () => {
} else {
return <div style={{ color: QUALITY_COLORS.POOR }}>{stat.q}%</div>;
}
}, []);
const content = useMemo(() => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
}
};
if (!data) {
return (
<SectionContent>
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
</SectionContent>
);
}
return (
<SectionContent>
<Table
data={{ nodes: data.stats }}
theme={stats_theme}
@@ -136,10 +130,8 @@ const SystemActivity = () => {
</>
)}
</Table>
);
}, [data, loadData, error?.message, stats_theme, LL, showName, showQuality]);
return <SectionContent>{content}</SectionContent>;
</SectionContent>
);
};
export default SystemActivity;

View File

@@ -1,4 +1,4 @@
import { type FC, memo, useMemo } from 'react';
import { type FC, memo } from 'react';
import AutoAwesomeMotionIcon from '@mui/icons-material/AutoAwesomeMotion';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
@@ -127,16 +127,15 @@ const MqttStatus = () => {
void loadData();
});
// Memoize error message separately to avoid re-renders on error object changes
const errorMessage = error?.message || '';
const mqttStatusText = useMemo(() => {
if (!data) return '';
if (!data.enabled) return LL.NOT_ENABLED();
return data.connected
? `${LL.CONNECTED(0)} (${data.connect_count})`
: `${LL.DISCONNECTED()} (${data.connect_count})`;
}, [data, LL]);
const mqttStatusText = !data
? ''
: !data.enabled
? LL.NOT_ENABLED()
: data.connected
? `${LL.CONNECTED(0)} (${data.connect_count})`
: `${LL.DISCONNECTED()} (${data.connect_count})`;
if (!data) {
return (

View File

@@ -1,5 +1,3 @@
import { useMemo } from 'react';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import DnsIcon from '@mui/icons-material/Dns';
import SwapVerticalCircleIcon from '@mui/icons-material/SwapVerticalCircle';
@@ -67,12 +65,16 @@ const NTPStatus = () => {
}
};
const content = useMemo(() => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
}
if (!data) {
return (
<SectionContent>
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
</SectionContent>
);
}
return (
<SectionContent>
<List>
<ListItem>
<ListItemAvatar>
@@ -121,10 +123,8 @@ const NTPStatus = () => {
</ListItem>
<Divider variant="inset" component="li" />
</List>
);
}, [data, error, loadData, LL, theme]);
return <SectionContent>{content}</SectionContent>;
</SectionContent>
);
};
export default NTPStatus;

View File

@@ -1,5 +1,3 @@
import { useMemo } from 'react';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DnsIcon from '@mui/icons-material/Dns';
import GiteIcon from '@mui/icons-material/Gite';
@@ -124,16 +122,20 @@ const NetworkStatus = () => {
const theme = useTheme();
const content = useMemo(() => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
}
const statusText = getNetworkStatusText(data.status, data.reconnect_count, LL);
const statusColor = networkStatusHighlight(data, theme);
const qualityColor = networkQualityHighlight(data, theme);
if (!data) {
return (
<SectionContent>
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
</SectionContent>
);
}
const statusText = getNetworkStatusText(data.status, data.reconnect_count, LL);
const statusColor = networkStatusHighlight(data, theme);
const qualityColor = networkQualityHighlight(data, theme);
return (
<SectionContent>
<List>
<ListItem>
<ListItemAvatar>
@@ -227,10 +229,8 @@ const NetworkStatus = () => {
</>
)}
</List>
);
}, [data, error, loadData, LL, theme]);
return <SectionContent>{content}</SectionContent>;
</SectionContent>
);
};
export default NetworkStatus;

View File

@@ -1,25 +1,16 @@
import { useCallback, useContext, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import { useContext } from 'react';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import BuildIcon from '@mui/icons-material/Build';
import CancelIcon from '@mui/icons-material/Cancel';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
import LogoDevIcon from '@mui/icons-material/LogoDev';
import MemoryIcon from '@mui/icons-material/Memory';
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import RouterIcon from '@mui/icons-material/Router';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import WifiIcon from '@mui/icons-material/Wifi';
import {
Avatar,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
List,
ListItem,
ListItemAvatar,
@@ -27,12 +18,10 @@ import {
useTheme
} from '@mui/material';
import { API } from 'api/app';
import { readSystemStatus } from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import { type APIcall, busConnectionStatus } from 'app/main/types';
import { busConnectionStatus } from 'app/main/types';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication';
@@ -41,9 +30,6 @@ import { NTPSyncStatus, NetworkConnectionStatus, SystemStatusCodes } from 'types
import { useInterval } from 'utils';
import { formatDateTime } from 'utils/time';
import SystemMonitor from './SystemMonitor';
// Pure functions moved outside component to avoid recreation on each render
const formatNumber = (num: number) => new Intl.NumberFormat().format(num);
const formatDurationSec = (
@@ -72,24 +58,7 @@ const SystemStatus = () => {
const { me } = useContext(AuthenticatedContext);
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
const [restarting, setRestarting] = useState<boolean>();
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const {
data,
send: loadData,
error
} = useRequest(readSystemStatus, {
async middleware(_, next) {
if (!restarting) {
await next();
}
}
});
const { data, send: loadData, error } = useRequest(readSystemStatus);
useInterval(() => {
void loadData();
@@ -97,10 +66,8 @@ const SystemStatus = () => {
const theme = useTheme();
// Memoize derived status values to avoid recalculation on every render
const busStatus = useMemo(() => {
const busStatus = (() => {
if (!data) return 'EMS state unknown';
switch (data.bus_status) {
case busConnectionStatus.BUS_STATUS_CONNECTED:
return `EMS ${LL.CONNECTED(0)} (${formatDurationSec(data.bus_uptime, LL)})`;
@@ -111,12 +78,10 @@ const SystemStatus = () => {
default:
return 'EMS state unknown';
}
}, [data?.bus_status, data?.bus_uptime, LL]);
})();
// Memoize derived status values to avoid recalculation on every render
const systemStatus = useMemo(() => {
const systemStatus = (() => {
if (!data) return '??';
switch (data.status) {
case SystemStatusCodes.SYSTEM_STATUS_PENDING_UPLOAD:
case SystemStatusCodes.SYSTEM_STATUS_UPLOADING:
@@ -129,14 +94,12 @@ const SystemStatus = () => {
case SystemStatusCodes.SYSTEM_STATUS_INVALID_GPIO:
return LL.GPIO_OF(LL.FAILED(0));
default:
// SystemStatusCodes.SYSTEM_STATUS_NORMAL
return 'OK';
}
}, [data?.status, LL]);
})();
const busStatusHighlight = useMemo(() => {
const busStatusHighlight = (() => {
if (!data) return theme.palette.warning.main;
switch (data.bus_status) {
case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return theme.palette.warning.main;
@@ -147,11 +110,10 @@ const SystemStatus = () => {
default:
return theme.palette.warning.main;
}
}, [data?.bus_status, theme.palette]);
})();
const ntpStatus = useMemo(() => {
const ntpStatus = (() => {
if (!data) return LL.UNKNOWN();
switch (data.ntp_status) {
case NTPSyncStatus.NTP_DISABLED:
return LL.NOT_ENABLED();
@@ -164,11 +126,10 @@ const SystemStatus = () => {
default:
return LL.UNKNOWN();
}
}, [data?.ntp_status, data?.ntp_time, LL]);
})();
const ntpStatusHighlight = useMemo(() => {
const ntpStatusHighlight = (() => {
if (!data) return theme.palette.error.main;
switch (data.ntp_status) {
case NTPSyncStatus.NTP_DISABLED:
return theme.palette.info.main;
@@ -179,11 +140,10 @@ const SystemStatus = () => {
default:
return theme.palette.error.main;
}
}, [data?.ntp_status, theme.palette]);
})();
const networkStatusHighlight = useMemo(() => {
const networkStatusHighlight = (() => {
if (!data) return theme.palette.warning.main;
switch (data.network_status) {
case NetworkConnectionStatus.WIFI_STATUS_IDLE:
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
@@ -198,11 +158,10 @@ const SystemStatus = () => {
default:
return theme.palette.warning.main;
}
}, [data?.network_status, theme.palette]);
})();
const networkStatus = useMemo(() => {
const networkStatus = (() => {
if (!data) return LL.UNKNOWN();
switch (data.network_status) {
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return LL.INACTIVE(1);
@@ -223,227 +182,103 @@ const SystemStatus = () => {
default:
return LL.UNKNOWN();
}
}, [data?.network_status, data?.wifi_rssi, LL]);
})();
const activeHighlight = useCallback(
(value: boolean) =>
value ? theme.palette.success.main : theme.palette.info.main,
[theme.palette]
);
const doRestart = useCallback(async () => {
setConfirmRestart(false);
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
}, [sendAPI]);
const handleCloseRestartDialog = useCallback(() => {
setConfirmRestart(false);
}, []);
const renderRestartDialog = useMemo(
() => (
<Dialog
sx={dialogStyle}
open={confirmRestart}
onClose={handleCloseRestartDialog}
>
<DialogTitle>{LL.RESTART()}</DialogTitle>
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={handleCloseRestartDialog}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={doRestart}
color="error"
>
{LL.RESTART()}
</Button>
</DialogActions>
</Dialog>
),
[confirmRestart, handleCloseRestartDialog, doRestart, LL]
);
// Memoize formatted values
const firmwareVersion = useMemo(
() => `v${data?.emsesp_version || ''}`,
[data?.emsesp_version]
);
const uptimeText = useMemo(
() => (data ? formatDurationSec(data.uptime, LL) : ''),
[data?.uptime, LL]
);
const freeMemoryText = useMemo(
() => (data ? `${formatNumber(data.free_heap)} KB ${LL.FREE_MEMORY()}` : ''),
[data?.free_heap, LL]
);
const networkIcon = useMemo(
() =>
data?.network_status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
? WifiIcon
: RouterIcon,
[data?.network_status]
);
const mqttStatusText = useMemo(
() => (data?.mqtt_status ? LL.CONNECTED(0) : LL.INACTIVE(0)),
[data?.mqtt_status, LL]
);
const apStatusText = useMemo(
() => (data?.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)),
[data?.ap_status, LL]
);
const handleRestartClick = useCallback(() => {
setConfirmRestart(true);
}, []);
const content = useMemo(() => {
if (!data || !LL) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
}
const activeHighlight = (value: boolean) =>
value ? theme.palette.success.main : theme.palette.info.main;
if (!data || !LL) {
return (
<>
<List>
<ListMenuItem
icon={BuildIcon}
bgcolor="#72caf9"
label="EMS-ESP Firmware"
text={firmwareVersion}
to="version"
/>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}>
<MonitorHeartIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.STATUS_OF(LL.SYSTEM(0))}
secondary={`${systemStatus} (${LL.UPTIME()}: ${uptimeText})`}
/>
{me.admin && (
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
color="error"
onClick={handleRestartClick}
>
{LL.RESTART()}
</Button>
)}
</ListItem>
<ListMenuItem
disabled={!me.admin}
icon={MemoryIcon}
bgcolor="#68374d"
label={LL.HARDWARE()}
text={freeMemoryText}
to="/status/hardwarestatus"
/>
<ListMenuItem
disabled={!me.admin}
icon={DirectionsBusIcon}
bgcolor={busStatusHighlight}
label={LL.DATA_TRAFFIC()}
text={busStatus}
to="/status/activity"
/>
<ListMenuItem
disabled={!me.admin}
icon={networkIcon}
bgcolor={networkStatusHighlight}
label={LL.NETWORK(1)}
text={networkStatus}
to="/status/network"
/>
<ListMenuItem
disabled={!me.admin}
icon={DeviceHubIcon}
bgcolor={activeHighlight(data.mqtt_status)}
label="MQTT"
text={mqttStatusText}
to="/status/mqtt"
/>
<ListMenuItem
disabled={!me.admin}
icon={AccessTimeIcon}
bgcolor={ntpStatusHighlight}
label="NTP"
text={ntpStatus}
to="/status/ntp"
/>
<ListMenuItem
disabled={!me.admin}
icon={SettingsInputAntennaIcon}
bgcolor={activeHighlight(data.ap_status)}
label={LL.ACCESS_POINT(0)}
text={apStatusText}
to="/status/ap"
/>
<ListMenuItem
disabled={!me.admin}
icon={LogoDevIcon}
bgcolor="#40828f"
label={LL.LOG_OF(LL.SYSTEM(0))}
text={LL.VIEW_LOG()}
to="/status/log"
/>
</List>
{renderRestartDialog}
</>
<SectionContent>
<FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
</SectionContent>
);
}, [
data,
LL,
firmwareVersion,
uptimeText,
freeMemoryText,
networkIcon,
mqttStatusText,
apStatusText,
busStatus,
busStatusHighlight,
networkStatusHighlight,
networkStatus,
ntpStatusHighlight,
ntpStatus,
activeHighlight,
me.admin,
handleRestartClick,
error,
loadData,
renderRestartDialog
]);
}
return restarting ? <SystemMonitor /> : <SectionContent>{content}</SectionContent>;
return (
<SectionContent>
<List>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}>
<MonitorHeartIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={LL.STATUS_OF(LL.SYSTEM(0))}
secondary={`${systemStatus} (${LL.UPTIME()}: ${formatDurationSec(data.uptime, LL)})`}
/>
</ListItem>
<ListMenuItem
disabled={!me.admin}
icon={MemoryIcon}
bgcolor="#68374d"
label={LL.HARDWARE()}
text={`${formatNumber(data.free_heap)} KB ${LL.FREE_MEMORY()}`}
to="/status/hardwarestatus"
/>
<ListMenuItem
disabled={!me.admin}
icon={DirectionsBusIcon}
bgcolor={busStatusHighlight}
label={LL.DATA_TRAFFIC()}
text={busStatus}
to="/status/activity"
/>
<ListMenuItem
disabled={!me.admin}
icon={
data.network_status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED
? WifiIcon
: RouterIcon
}
bgcolor={networkStatusHighlight}
label={LL.NETWORK(1)}
text={networkStatus}
to="/status/network"
/>
<ListMenuItem
disabled={!me.admin}
icon={DeviceHubIcon}
bgcolor={activeHighlight(data.mqtt_status)}
label="MQTT"
text={data.mqtt_status ? LL.CONNECTED(0) : LL.INACTIVE(0)}
to="/status/mqtt"
/>
<ListMenuItem
disabled={!me.admin}
icon={AccessTimeIcon}
bgcolor={ntpStatusHighlight}
label="NTP"
text={ntpStatus}
to="/status/ntp"
/>
<ListMenuItem
disabled={!me.admin}
icon={SettingsInputAntennaIcon}
bgcolor={activeHighlight(data.ap_status)}
label={LL.ACCESS_POINT(0)}
text={data.ap_status ? LL.ACTIVE() : LL.INACTIVE(0)}
to="/status/ap"
/>
<ListMenuItem
disabled={!me.admin}
icon={LogoDevIcon}
bgcolor="#40828f"
label={LL.LOG_OF(LL.SYSTEM(0))}
text={LL.VIEW_LOG()}
to="/status/log"
/>
</List>
</SectionContent>
);
};
export default SystemStatus;

View File

@@ -1,11 +1,4 @@
import {
memo,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState
} from 'react';
import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp';
@@ -185,8 +178,7 @@ const SystemLog = () => {
};
}, [data]); // Recalculate when data changes (in case layout shifts)
// Memoize message handler to avoid recreating on every render
const handleLogMessage = useCallback((message: { data: string }) => {
const handleLogMessage = (message: { data: string }) => {
const rawData = message.data;
const logentry = JSON.parse(rawData) as LogEntry;
setLogEntries((log) => {
@@ -200,7 +192,7 @@ const SystemLog = () => {
const newLog = [...log, logentry];
return newLog;
});
}, []);
};
useSSE(fetchLogES, {
immediate: true,
@@ -211,7 +203,7 @@ const SystemLog = () => {
toast.error('No connection to Log service');
});
const onDownload = useCallback(() => {
const onDownload = () => {
const result = logEntries
.map((i) => `${i.t} ${levelLabel(i.l)} ${i.i}: [${i.n}] ${i.m}`)
.join('\n');
@@ -225,11 +217,11 @@ const SystemLog = () => {
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}, [logEntries]);
};
const saveSettings = useCallback(async () => {
const saveSettings = async () => {
await saveData();
}, [saveData]);
};
// handle scrolling - optimized to only scroll when needed
const ref = useRef<HTMLDivElement>(null);
@@ -246,7 +238,7 @@ const SystemLog = () => {
}
}, [logEntries.length, autoscroll]);
const sendReadCommand = useCallback(() => {
const sendReadCommand = () => {
if (readValue === '') {
setReadOpen(!readOpen);
return;
@@ -257,7 +249,7 @@ const SystemLog = () => {
setReadOpen(false);
setReadValue('');
}
}, [readValue, readOpen, send]);
};
const content = () => {
if (!data) {

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { useRef, useState } from 'react';
import CancelIcon from '@mui/icons-material/Cancel';
import { Box, Button, Typography } from '@mui/material';
@@ -57,39 +57,31 @@ const SystemMonitor = () => {
void send();
}, 1000); // check every 1 second
const { statusMessage, isUploading, progressValue } = useMemo(() => {
const status = data?.status;
const status = data?.status;
const message =
status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING
? LL.WAIT_FIRMWARE()
: status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART
? LL.APPLICATION_RESTARTING()
: status === SystemStatusCodes.SYSTEM_STATUS_NORMAL
? LL.RESTARTING_PRE()
: status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
? 'Upload Failed'
: LL.RESTARTING_POST();
const statusMessage =
status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING
? LL.WAIT_FIRMWARE()
: status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART
? LL.APPLICATION_RESTARTING()
: status === SystemStatusCodes.SYSTEM_STATUS_NORMAL
? LL.RESTARTING_PRE()
: status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
? 'Upload Failed'
: LL.RESTARTING_POST();
const uploading =
status !== undefined && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING;
const progress =
uploading && status
? Math.round(status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING)
: 0;
const isUploading =
status !== undefined && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING;
const progressValue =
isUploading && status
? Math.round(status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING)
: 0;
return {
statusMessage: message,
isUploading: uploading,
progressValue: progress
};
}, [data?.status, LL]);
const onCancel = useCallback(async () => {
const onCancel = async () => {
setErrorMessage(undefined);
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
document.location.href = '/';
}, [setSystemStatus]);
};
return (
<Box

View File

@@ -1,917 +0,0 @@
import {
memo,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel';
import CloseIcon from '@mui/icons-material/Close';
import CheckIcon from '@mui/icons-material/Done';
import DownloadIcon from '@mui/icons-material/GetApp';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import WarningIcon from '@mui/icons-material/Warning';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
IconButton,
Link,
Table,
TableBody,
TableCell,
TableRow,
Typography
} from '@mui/material';
import * as SystemApi from 'api/system';
import { API, callAction } from 'api/app';
import { getDevVersion, getStableVersion } from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types';
import SystemMonitor from 'app/status/SystemMonitor';
import {
FormLoader,
SectionContent,
SingleUpload,
useLayoutTitle
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { TranslationFunctions } from 'i18n/i18n-types';
import { prettyDateTime } from 'utils/time';
// Constants moved outside component to avoid recreation
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
const STABLE_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
const DEV_RELNOTES_URL =
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
// Types for better type safety
interface PartitionData {
partition: string;
version: string;
install_date?: string;
size: number;
}
interface VersionData {
emsesp_version: string;
arduino_version: string;
esp_platform: string;
flash_chip_size: number;
psram: boolean;
build_flags?: string;
partition: string;
partitions: PartitionData[];
developer_mode: boolean;
}
interface UpgradeCheckData {
emsesp_version: string;
dev_upgradeable: boolean;
stable_upgradeable: boolean;
}
interface VersionInfo {
name: string;
published_at?: string;
}
// Memoized components for better performance
const VersionInfoDialog = memo(
({
showVersionInfo,
latestVersion,
latestDevVersion,
partitionVersion,
partition,
currentPartition,
size,
locale,
LL,
onClose
}: {
showVersionInfo: number;
latestVersion?: VersionInfo;
latestDevVersion?: VersionInfo;
partitionVersion?: VersionInfo | undefined;
partition: string;
currentPartition: string;
size: number;
locale: string;
LL: TranslationFunctions;
onClose: () => void;
}) => {
if (showVersionInfo === 0) return null;
const isStable = showVersionInfo === 1;
const isDev = showVersionInfo === 2;
const isPartition = showVersionInfo === 3;
const version = isStable
? latestVersion
: isDev
? latestDevVersion
: partitionVersion;
const relNotesUrl = isStable
? STABLE_RELNOTES_URL
: isDev
? DEV_RELNOTES_URL
: '';
return (
<Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
<DialogTitle>{LL.FIRMWARE_VERSION_INFO()}</DialogTitle>
<DialogContent dividers>
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
<TableBody>
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
{LL.VERSION()}
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{isPartition
? typeof version === 'string'
? version
: version?.name
: version?.name}
</TableCell>
</TableRow>
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13,
width: 140
}}
>
{isPartition ? LL.TYPE(0) : LL.RELEASE_TYPE()}
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{partition === currentPartition && LL.ACTIVE() + ' '}
{isStable
? LL.STABLE()
: isDev
? LL.DEVELOPMENT()
: 'Partition ' + LL.VERSION()}
</TableCell>
</TableRow>
{isPartition && (
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
Partition
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{partition}
</TableCell>
</TableRow>
)}
{isPartition && (
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
Size
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{size} KB
</TableCell>
</TableRow>
)}
{version?.published_at && (
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
{isPartition ? 'Install Date' : 'Build Date'}
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{prettyDateTime(locale, new Date(version.published_at))}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
{!isPartition && (
<Button
variant="outlined"
component="a"
href={relNotesUrl}
target="_blank"
color="primary"
>
Changelog
</Button>
)}
<Button variant="outlined" onClick={onClose} color="secondary">
{LL.CLOSE()}
</Button>
</DialogActions>
</Dialog>
);
}
);
const InstallDialog = memo(
({
openInstallDialog,
fetchDevVersion,
latestVersion,
latestDevVersion,
upgradeImportantMessageType,
downloadOnly,
platform,
LL,
onClose,
onInstall
}: {
openInstallDialog: boolean;
fetchDevVersion: boolean;
latestVersion?: VersionInfo;
latestDevVersion?: VersionInfo;
upgradeImportantMessageType: number;
downloadOnly: boolean;
platform: string;
LL: TranslationFunctions;
onClose: () => void;
onInstall: (url: string) => void;
}) => {
const binURL = useMemo(() => {
if (!latestVersion || !latestDevVersion) return '';
const version = fetchDevVersion ? latestDevVersion : latestVersion;
const filename = `EMS-ESP-${version.name.replaceAll('.', '_')}-${platform}.bin`;
return fetchDevVersion
? `${DEV_URL}${filename}`
: `${STABLE_URL}v${version.name}/${filename}`;
}, [fetchDevVersion, latestVersion, latestDevVersion, platform]);
return (
<Dialog sx={dialogStyle} open={openInstallDialog} onClose={onClose}>
<DialogTitle>
{`${LL.INSTALL()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`}
</DialogTitle>
<DialogContent dividers>
<Typography sx={{ mb: 2 }}>
{LL.INSTALL_VERSION(
downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(),
fetchDevVersion ? latestDevVersion?.name : latestVersion?.name
)}
</Typography>
{upgradeImportantMessageType === 1 && LL.UPGRADE_IMPORTANT_MESSAGES_1()}
{upgradeImportantMessageType === 2 && LL.UPGRADE_IMPORTANT_MESSAGES_2()}
<Typography sx={{ mt: 2 }}>
<Link
target="_blank"
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
style={{ color: 'lightblue' }}
>
{LL.ONLINE_HELP()}
</Link>
</Typography>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onClose}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
onClick={onClose}
color="primary"
>
<Link underline="none" target="_blank" href={binURL} color="primary">
{LL.DOWNLOAD(0)}
</Link>
</Button>
{!downloadOnly && (
<Button
startIcon={<WarningIcon color="warning" />}
variant="outlined"
onClick={() => onInstall(binURL)}
color="primary"
>
{LL.INSTALL()}
</Button>
)}
</DialogActions>
</Dialog>
);
}
);
const InstallPartitionDialog = memo(
({
openInstallPartitionDialog,
version,
partition,
LL,
onClose,
onInstall
}: {
openInstallPartitionDialog: boolean;
version: string;
partition: string;
LL: TranslationFunctions;
onClose: () => void;
onInstall: (partition: string) => void;
}) => {
return (
<Dialog sx={dialogStyle} open={openInstallPartitionDialog} onClose={onClose}>
<DialogTitle>
{LL.INSTALL()} {LL.STORED_VERSIONS()}
</DialogTitle>
<DialogContent dividers>
<Typography sx={{ mb: 2 }}>
{LL.INSTALL_VERSION(LL.INSTALL(), version)}
</Typography>
</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={onClose}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<WarningIcon color="warning" />}
variant="outlined"
onClick={() => onInstall(partition)}
color="primary"
>
{LL.INSTALL()}
</Button>
</DialogActions>
</Dialog>
);
}
);
// Helper function moved outside component
const getPlatform = (data: VersionData): string => {
return `${data.esp_platform}-${data.flash_chip_size >= 16384 ? '16MB' : '4MB'}${data.psram ? '+' : ''}`;
};
const Version = () => {
const { LL, locale } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
// State management
const [restarting, setRestarting] = useState<boolean>(false);
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
const [partitionVersion, setPartitionVersion] = useState<VersionInfo | undefined>(
undefined
);
const [partition, setPartition] = useState<string>('');
const [openInstallPartitionDialog, setOpenInstallPartitionDialog] =
useState<boolean>(false);
const [usingDevVersion, setUsingDevVersion] = useState<boolean>(false);
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
const [devUpgradeAvailable, setDevUpgradeAvailable] = useState<boolean>(false);
const [stableUpgradeAvailable, setStableUpgradeAvailable] =
useState<boolean>(false);
const [internetLive, setInternetLive] = useState<boolean>(false);
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
const [showVersionInfo, setShowVersionInfo] = useState<number>(0); // 1 = stable, 2 = dev, 3 = partition
const [firmwareSize, setFirmwareSize] = useState<number>(0);
const { send: sendCheckUpgrade } = useRequest(
(versions: string) => callAction({ action: 'checkUpgrade', param: versions }),
{ immediate: false }
).onSuccess((event) => {
const data = event.data as UpgradeCheckData;
setDevUpgradeAvailable(data.dev_upgradeable);
setStableUpgradeAvailable(data.stable_upgradeable);
});
const { send: sendSetPartition } = useRequest(
(partition: string) => callAction({ action: 'setPartition', param: partition }),
{ immediate: false }
).onError((error) => {
toast.error(String(error.error?.message || 'An error occurred'));
});
const {
data,
send: loadData,
error
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
const systemData = event.data as VersionData;
if (systemData.arduino_version.startsWith('Tasmota')) {
setDownloadOnly(true);
}
setUsingDevVersion(systemData.emsesp_version.includes('dev'));
});
const { send: sendUploadURL } = useRequest(
(url: string) => callAction({ action: 'uploadURL', param: url }),
{ immediate: false }
);
const { data: latestVersion } = useRequest(getStableVersion);
const { data: latestDevVersion } = useRequest(getDevVersion);
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const [upgradeImportantMessageType, setUpgradeImportantMessageType] =
useState<number>(0);
const { send: checkUpgradeImportantMessages } = useRequest(
(version: string) =>
callAction({ action: 'upgradeImportantMessages', param: version }),
{
immediate: false
}
)
.onSuccess((event) => {
const upgradeImportantMessageType_n = (
event.data as { upgradeImportantMessageType: number }
).upgradeImportantMessageType;
setUpgradeImportantMessageType(upgradeImportantMessageType_n);
})
.onError((error) => {
toast.error(String(error.error?.message || 'An error occurred'));
});
// Memoized values
const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]);
// Memoize filtered partitions to avoid recomputing on every render
const otherPartitions = useMemo(
() => data?.partitions.filter((p) => p.partition !== data.partition) ?? [],
[data]
);
const setPartitionVersionInfo = useCallback(
(partition: string) => {
setShowVersionInfo(3);
// search for the partition in the data.partitions array
const partitionData = data?.partitions.find((p) => p.partition === partition);
if (partitionData) {
setPartitionVersion({
name: partitionData.version,
published_at: partitionData.install_date ?? ''
});
setPartition(partitionData.partition);
setFirmwareSize(partitionData.size);
}
},
[data]
);
const doRestart = useCallback(async () => {
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
setRestarting(true);
}, [sendAPI]);
const installFirmwareURL = useCallback(
async (url: string) => {
await sendUploadURL(url).catch((error: Error) => {
toast.error(error.message);
});
await doRestart();
},
[sendUploadURL, doRestart]
);
const installPartitionFirmware = useCallback(
async (partition: string) => {
await sendSetPartition(partition).catch((error: Error) => {
toast.error(error.message);
});
setRestarting(true);
},
[sendSetPartition]
);
const showPartitionDialog = useCallback(
(version: string, partition: string, install_date: string) => {
setOpenInstallPartitionDialog(true);
setPartitionVersion({ name: version, published_at: install_date });
setPartition(partition);
},
[]
);
const showFirmwareDialog = useCallback(
(useDevVersion: boolean) => {
setFetchDevVersion(useDevVersion);
void checkUpgradeImportantMessages(
useDevVersion ? latestDevVersion?.name : latestVersion?.name
);
setOpenInstallDialog(true);
},
[latestDevVersion, latestVersion, fetchDevVersion]
);
const closeInstallDialog = useCallback(() => {
setOpenInstallDialog(false);
}, []);
const closeInstallPartitionDialog = useCallback(() => {
setOpenInstallPartitionDialog(false);
}, []);
const handleVersionInfoClose = useCallback(() => {
setShowVersionInfo(0);
setPartitionVersion(undefined);
setPartition('');
}, []);
// check upgrades - only once when both versions are available
const upgradeCheckedRef = useRef(false);
useEffect(() => {
if (latestVersion && latestDevVersion && !upgradeCheckedRef.current) {
upgradeCheckedRef.current = true;
const versions = `${latestDevVersion.name},${latestVersion.name}`;
sendCheckUpgrade(versions)
.catch((error: Error) => {
toast.error(`Failed to check for upgrades: ${error.message}`);
})
.finally(() => {
setInternetLive(true);
});
}
}, [latestVersion, latestDevVersion, sendCheckUpgrade]);
useLayoutTitle('EMS-ESP Firmware');
// Memoized button rendering logic
const showButtons = useCallback(
(showingDev: boolean) => {
const choice = showingDev
? !usingDevVersion
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
: devUpgradeAvailable
? LL.UPDATE_AVAILABLE()
: undefined
: usingDevVersion
? LL.SWITCH_RELEASE_TYPE(LL.STABLE())
: stableUpgradeAvailable
? LL.UPDATE_AVAILABLE()
: undefined;
if (!choice) {
return (
<>
<CheckIcon
color="success"
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
/>
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
</span>
<Button
sx={{ ml: 1 }}
variant="outlined"
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{LL.REINSTALL()}
</Button>
</>
);
}
if (!me.admin) return null;
return (
<Button
sx={{ ml: 1 }}
variant="outlined"
color={choice === LL.UPDATE_AVAILABLE() ? 'success' : 'warning'}
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{choice}
</Button>
);
},
[
usingDevVersion,
devUpgradeAvailable,
stableUpgradeAvailable,
me.admin,
LL,
showFirmwareDialog
]
);
const content = useMemo(() => {
if (!data) {
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
}
return (
<>
<Box sx={{ p: 2, border: '1px solid #565656', borderRadius: 2 }}>
<Typography sx={{ mb: 1 }} variant="h6" color="primary">
{LL.THIS_VERSION()}
</Typography>
<Grid
container
direction="row"
sx={{
justifyContent: 'flex-start',
alignItems: 'baseline'
}}
>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.VERSION()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{data.emsesp_version}
{data.build_flags && (
<Typography variant="caption">
&nbsp; &#40;{data.build_flags}&#41;
</Typography>
)}
<IconButton
onClick={() => setPartitionVersionInfo(data.partition)}
aria-label={LL.FIRMWARE_VERSION_INFO()}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
</Typography>
</Grid>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.PLATFORM()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{platform}
<Typography variant="caption">
&nbsp; &#40;
{data.psram ? (
<CheckIcon
color="success"
sx={{
fontSize: '1.5em',
verticalAlign: 'middle'
}}
/>
) : (
<CloseIcon
color="error"
sx={{
fontSize: '1.5em',
verticalAlign: 'middle'
}}
/>
)}
PSRAM&#41;
</Typography>
</Typography>
</Grid>
</Grid>
{internetLive ? (
<>
<Typography sx={{ mt: 4, mb: 1 }} variant="h6" color="primary">
{LL.AVAILABLE_VERSION()}
</Typography>
<Grid
container
direction="row"
rowSpacing={1}
sx={{
justifyContent: 'flex-start',
alignItems: 'baseline'
}}
>
{otherPartitions.length > 0 && data.developer_mode && (
<>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">
{LL.STORED_VERSIONS()}
</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
{otherPartitions.map((partition) => (
<Typography key={partition.partition} sx={{ mb: 1 }}>
{partition.version}
<IconButton
onClick={() =>
setPartitionVersionInfo(partition.partition)
}
aria-label={LL.FIRMWARE_VERSION_INFO()}
>
<InfoOutlinedIcon
color="primary"
sx={{ fontSize: 18 }}
/>
</IconButton>
<Button
sx={{ ml: 0 }}
variant="outlined"
size="small"
onClick={() =>
showPartitionDialog(
partition.version,
partition.partition,
partition.install_date ?? ''
)
}
>
{LL.INSTALL()}
</Button>
</Typography>
))}
</Grid>
</>
)}
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.STABLE()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{latestVersion?.name}
<IconButton
onClick={() => setShowVersionInfo(1)}
aria-label={LL.FIRMWARE_VERSION_INFO()}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
{showButtons(false)}
</Typography>
</Grid>
<Grid size={{ xs: 4, md: 2 }}>
<Typography color="secondary">{LL.DEVELOPMENT()}</Typography>
</Grid>
<Grid size={{ xs: 8, md: 10 }}>
<Typography>
{latestDevVersion?.name}
<IconButton
onClick={() => setShowVersionInfo(2)}
aria-label={LL.FIRMWARE_VERSION_INFO()}
>
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
</IconButton>
{showButtons(true)}
</Typography>
</Grid>
</Grid>
</>
) : (
<Typography sx={{ mt: 2 }} color="warning">
<WarningIcon color="warning" sx={{ verticalAlign: 'middle', mr: 2 }} />
{LL.INTERNET_CONNECTION_REQUIRED()}
</Typography>
)}
{me.admin && (
<>
<VersionInfoDialog
showVersionInfo={showVersionInfo}
latestVersion={latestVersion}
latestDevVersion={latestDevVersion}
partitionVersion={partitionVersion}
locale={locale}
partition={partition}
currentPartition={data?.partition ?? ''}
size={firmwareSize}
LL={LL}
onClose={handleVersionInfoClose}
/>
<InstallDialog
openInstallDialog={openInstallDialog}
fetchDevVersion={fetchDevVersion}
latestVersion={latestVersion}
latestDevVersion={latestDevVersion}
upgradeImportantMessageType={upgradeImportantMessageType}
downloadOnly={downloadOnly}
platform={platform}
LL={LL}
onClose={closeInstallDialog}
onInstall={installFirmwareURL}
/>
<InstallPartitionDialog
openInstallPartitionDialog={openInstallPartitionDialog}
version={partitionVersion?.name || ''}
partition={partition}
LL={LL}
onClose={closeInstallPartitionDialog}
onInstall={installPartitionFirmware}
/>
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
{LL.UPLOAD()}
</Typography>
<SingleUpload doRestart={doRestart} />
</>
)}
</Box>
</>
);
}, [
data,
error,
loadData,
LL,
platform,
internetLive,
latestVersion,
latestDevVersion,
showVersionInfo,
locale,
openInstallDialog,
fetchDevVersion,
downloadOnly,
me.admin,
showButtons,
handleVersionInfoClose,
closeInstallDialog,
installFirmwareURL,
doRestart,
otherPartitions,
setPartitionVersionInfo,
showPartitionDialog,
partitionVersion,
partition,
firmwareSize,
closeInstallPartitionDialog,
installPartitionFirmware
]);
return restarting ? <SystemMonitor /> : <SectionContent>{content}</SectionContent>;
};
export default memo(Version);

View File

@@ -1,4 +1,4 @@
import { type FC, type PropsWithChildren, memo, useMemo } from 'react';
import { type FC, type PropsWithChildren, memo } from 'react';
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import ErrorIcon from '@mui/icons-material/Error';
@@ -38,18 +38,17 @@ const MessageBox: FC<PropsWithChildren<MessageBoxProps>> = ({
}) => {
const theme = useTheme();
const { Icon, backgroundColor } = useMemo(() => {
const Icon = LEVEL_ICONS[level];
const palettePath = LEVEL_PALETTE_PATHS[level];
const [key, shade] = palettePath.split('.') as [
keyof typeof theme.palette,
string
];
const paletteKey = theme.palette[key] as unknown as Record<string, string>;
const backgroundColor = paletteKey[shade];
return { Icon, backgroundColor };
}, [level, theme]);
const Icon = LEVEL_ICONS[level];
const palettePath = LEVEL_PALETTE_PATHS[level];
const [paletteKeyName, shade] = palettePath.split('.') as [
keyof typeof theme.palette,
string
];
const paletteKey = theme.palette[paletteKeyName] as unknown as Record<
string,
string
>;
const backgroundColor = paletteKey[shade];
return (
<Box

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useContext, useMemo } from 'react';
import { memo, useContext } from 'react';
import type { ChangeEventHandler } from 'react';
import type { CSSProperties } from 'react';
@@ -44,27 +44,14 @@ const LANGUAGE_OPTIONS: LanguageOption[] = [
const LanguageSelector = () => {
const { setLocale, locale, LL } = useContext(I18nContext);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = useCallback(
async ({ target }) => {
const loc = target.value as Locales;
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
setLocale(loc);
},
[setLocale]
);
// Memoize menu items to prevent recreation on every render
const menuItems = useMemo(
() =>
LANGUAGE_OPTIONS.map(({ key, flag, label }) => (
<MenuItem key={key} value={key}>
<img src={flag} style={flagStyle} alt={label} />
&nbsp;{label}
</MenuItem>
)),
[]
);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({
target
}) => {
const loc = target.value as Locales;
localStorage.setItem('lang', loc);
await loadLocaleAsync(loc);
setLocale(loc);
};
return (
<TextField
@@ -76,7 +63,12 @@ const LanguageSelector = () => {
size="small"
select
>
{menuItems}
{LANGUAGE_OPTIONS.map(({ key, flag, label }) => (
<MenuItem key={key} value={key}>
<img src={flag} style={flagStyle} alt={label} />
&nbsp;{label}
</MenuItem>
))}
</TextField>
);
};

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useState } from 'react';
import { memo, useState } from 'react';
import type { FC } from 'react';
import VisibilityIcon from '@mui/icons-material/Visibility';
@@ -13,9 +13,9 @@ type ValidatedPasswordFieldProps = Omit<ValidatedTextFieldProps, 'type'>;
const ValidatedPasswordField: FC<ValidatedPasswordFieldProps> = ({ ...props }) => {
const [showPassword, setShowPassword] = useState<boolean>(false);
const togglePasswordVisibility = useCallback(() => {
const togglePasswordVisibility = () => {
setShowPassword((prev) => !prev);
}, []);
};
return (
<ValidatedTextField

View File

@@ -18,7 +18,6 @@ const LayoutComponent: FC<RequiredChildrenProps> = ({ children }) => {
const [title, setTitle] = useState(PROJECT_NAME);
const { pathname } = useLocation();
// Memoize drawer toggle handler to prevent unnecessary re-renders
const handleDrawerToggle = useCallback(() => {
setMobileOpen((prev) => !prev);
}, []);
@@ -28,7 +27,6 @@ const LayoutComponent: FC<RequiredChildrenProps> = ({ children }) => {
setMobileOpen(false);
}, [pathname]);
// Memoize context value to prevent unnecessary re-renders
const contextValue = useMemo(() => ({ title, setTitle }), [title]);
return (

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useMemo } from 'react';
import { memo } from 'react';
import { Link, useLocation, useNavigate } from 'react-router';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
@@ -39,14 +39,11 @@ const LayoutAppBarComponent = ({ title, onToggleDrawer }: LayoutAppBarProps) =>
const navigate = useNavigate();
const location = useLocation();
const pathnames = useMemo(
() => location.pathname.split('/').filter((x) => x),
[location.pathname]
);
const pathnames = location.pathname.split('/').filter((x) => x);
const handleBackClick = useCallback(() => {
const handleBackClick = () => {
void navigate('/' + pathnames[0]);
}, [navigate, pathnames]);
};
return (
<AppBar position="fixed" sx={appBarStyles}>

View File

@@ -1,4 +1,4 @@
import { memo, useMemo } from 'react';
import { memo } from 'react';
import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
@@ -24,22 +24,18 @@ interface LayoutDrawerProps {
}
const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
// Memoize drawer content to prevent unnecessary re-renders
const drawer = useMemo(
() => (
<>
<Toolbar disableGutters>
<Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
<Typography variant="h6">{PROJECT_NAME}</Typography>
</Box>
<Divider absolute />
</Toolbar>
<Divider />
<LayoutMenu />
</>
),
[]
const drawer = (
<>
<Toolbar disableGutters>
<Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
<Typography variant="h6">{PROJECT_NAME}</Typography>
</Box>
<Divider absolute />
</Toolbar>
<Divider />
<LayoutMenu />
</>
);
return (

View File

@@ -1,4 +1,4 @@
import { memo, useCallback, useContext, useState } from 'react';
import { memo, useContext, useState } from 'react';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import AssessmentIcon from '@mui/icons-material/Assessment';
@@ -18,13 +18,15 @@ import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
const LayoutMenuComponent = () => {
const { me } = useContext(AuthenticatedContext);
const { me, versions } = useContext(AuthenticatedContext);
const { LL } = useI18nContext();
const [menuOpen, setMenuOpen] = useState(true);
const handleMenuToggle = useCallback(() => {
const upgradeAvailable = versions?.current?.upgradeable ?? false;
const handleMenuToggle = () => {
setMenuOpen((prev) => !prev);
}, []);
};
return (
<>
@@ -105,6 +107,7 @@ const LayoutMenuComponent = () => {
label={LL.SETTINGS(0)}
disabled={!me.admin}
to="/settings"
badge={upgradeAvailable}
/>
<LayoutMenuItem icon={LiveHelpIcon} label={LL.HELP()} to={`/help`} />
<Divider />

View File

@@ -1,7 +1,7 @@
import { memo, useMemo } from 'react';
import { memo } from 'react';
import { Link, useLocation } from 'react-router';
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import { Box, ListItemButton, ListItemIcon, ListItemText } from '@mui/material';
import type { SvgIconProps, SxProps, Theme } from '@mui/material';
import { routeMatches } from 'utils';
@@ -11,60 +11,52 @@ interface LayoutMenuItemProps {
label: string;
to: string;
disabled?: boolean;
badge?: boolean;
}
const LayoutMenuItemComponent = ({
icon: Icon,
label,
to,
disabled
disabled,
badge
}: LayoutMenuItemProps) => {
const { pathname } = useLocation();
const selected = useMemo(() => routeMatches(to, pathname), [to, pathname]);
const selected = routeMatches(to, pathname);
// Memoize dynamic styles based on selected state
const buttonStyles: SxProps<Theme> = useMemo(
() => ({
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
borderRadius: '8px',
margin: '2px 8px',
'&:hover': {
backgroundColor: 'rgba(68, 82, 211, 0.39)'
},
'&::before': {
content: '""',
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: selected ? '3px' : '0px',
backgroundColor: '#90caf9',
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
}
}),
[selected]
);
const buttonStyles: SxProps<Theme> = {
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
borderRadius: '8px',
margin: '2px 8px',
'&:hover': {
backgroundColor: 'rgba(68, 82, 211, 0.39)'
},
'&::before': {
content: '""',
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: selected ? '3px' : '0px',
backgroundColor: '#90caf9',
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
}
};
const iconStyles: SxProps<Theme> = useMemo(
() => ({
color: selected ? '#90caf9' : '#9e9e9e',
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
transform: selected ? 'scale(1.1)' : 'scale(1)',
transitionProperty: 'color, transform'
}),
[selected]
);
const iconStyles: SxProps<Theme> = {
color: selected ? '#90caf9' : '#9e9e9e',
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
transform: selected ? 'scale(1.1)' : 'scale(1)',
transitionProperty: 'color, transform'
};
const textStyles: SxProps<Theme> = useMemo(
() => ({
color: selected ? '#90caf9' : '#f5f5f5',
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
transitionProperty: 'color, font-weight'
}),
[selected]
);
const textStyles: SxProps<Theme> = {
color: selected ? '#90caf9' : '#f5f5f5',
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
transitionProperty: 'color, font-weight'
};
return (
<ListItemButton
@@ -78,6 +70,20 @@ const LayoutMenuItemComponent = ({
<Icon />
</ListItemIcon>
<ListItemText sx={textStyles}>{label}</ListItemText>
{badge && (
<Box
aria-label="update available"
sx={{
width: 8,
height: 8,
ml: 1,
borderRadius: '50%',
backgroundColor: '#ffeb3b',
boxShadow: '0 0 6px rgba(255, 235, 59, 0.8)',
flexShrink: 0
}}
/>
)}
</ListItemButton>
);
};

View File

@@ -5,6 +5,7 @@ import { Link } from 'react-router';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import {
Avatar,
Box,
ListItem,
ListItemAvatar,
ListItemButton,
@@ -20,6 +21,7 @@ interface ListMenuItemProps {
text: string;
to?: string;
disabled?: boolean;
badge?: boolean;
}
const iconStyles: CSSProperties = {
@@ -28,15 +30,40 @@ const iconStyles: CSSProperties = {
verticalAlign: 'middle'
};
const Badge = () => (
<Box
component="span"
aria-label="update available"
sx={{
display: 'inline-block',
width: 8,
height: 8,
ml: 1,
verticalAlign: 'middle',
borderRadius: '50%',
backgroundColor: '#ffeb3b',
boxShadow: '0 0 6px rgba(255, 235, 59, 0.8)'
}}
/>
);
const RenderIcon = memo(
({ icon: Icon, bgcolor, label, text }: ListMenuItemProps) => (
({ icon: Icon, bgcolor, label, text, badge }: ListMenuItemProps) => (
<>
<ListItemAvatar>
<Avatar sx={{ bgcolor, color: 'white' }}>
<Icon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={label} secondary={text} />
<ListItemText
primary={
<>
{label}
{badge && <Badge />}
</>
}
secondary={text}
/>
</>
)
);
@@ -47,7 +74,8 @@ const LayoutMenuItem = ({
label,
text,
to,
disabled
disabled,
badge
}: ListMenuItemProps) => (
<>
{to && !disabled ? (
@@ -65,6 +93,7 @@ const LayoutMenuItem = ({
{...(bgcolor && { bgcolor })}
label={label}
text={text}
{...(badge && { badge })}
/>
</ListItemButton>
</ListItem>
@@ -75,6 +104,7 @@ const LayoutMenuItem = ({
{...(bgcolor && { bgcolor })}
label={label}
text={text}
{...(badge && { badge })}
/>
</ListItem>
)}

View File

@@ -1,4 +1,4 @@
import { memo, useCallback } from 'react';
import { memo } from 'react';
import type { Blocker } from 'react-router';
import {
@@ -15,13 +15,13 @@ import { useI18nContext } from 'i18n/i18n-react';
const BlockNavigation = ({ blocker }: { blocker: Blocker }) => {
const { LL } = useI18nContext();
const handleReset = useCallback(() => {
const handleReset = () => {
blocker.reset?.();
}, [blocker]);
};
const handleProceed = useCallback(() => {
const handleProceed = () => {
blocker.proceed?.();
}, [blocker]);
};
return (
<Dialog sx={dialogStyle} open={blocker.state === 'blocked'}>

View File

@@ -1,4 +1,4 @@
import { memo, useCallback } from 'react';
import { memo } from 'react';
import type { FC } from 'react';
import { useNavigate } from 'react-router';
@@ -16,12 +16,9 @@ const RouterTabs: FC<RouterTabsProps> = ({ value, children }) => {
const theme = useTheme();
const smallDown = useMediaQuery(theme.breakpoints.down('sm'));
const handleTabChange = useCallback(
(_event: unknown, path: string) => {
void navigate(path);
},
[navigate]
);
const handleTabChange = (_event: unknown, path: string) => {
void navigate(path);
};
return (
<Tabs

View File

@@ -91,7 +91,9 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
).upgradeImportantMessageType;
setUpgradeImportantMessageType(upgradeImportantMessageType_n);
if (upgradeImportantMessageType_n === 0) {
onFileSelected(file);
if (file) {
onFileSelected(file);
}
}
})
.onError((error) => {
@@ -211,14 +213,15 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
{LL.UPGRADE_IMPORTANT_MESSAGES()}
</DialogTitle>
<DialogContent dividers>
{upgradeImportantMessageType === 1 &&
LL.UPGRADE_IMPORTANT_MESSAGES_1()}
{upgradeImportantMessageType === 2 &&
LL.UPGRADE_IMPORTANT_MESSAGES_2()}
{upgradeImportantMessageType === 1 &&
LL.UPGRADE_IMPORTANT_MESSAGES_1()}
<Typography sx={{ mt: 2 }}>
<Link
target="_blank"
to="https://docs.emsesp.org/FAQ#upgrading-the-firmware"
target="_blank"
rel="noreferrer"
style={{ color: 'lightblue' }}
>
{LL.ONLINE_HELP()}

View File

@@ -3,6 +3,7 @@ import type { FC } from 'react';
import { redirect } from 'react-router';
import { toast } from 'react-toastify';
import { callAction } from 'api/app';
import { ACCESS_TOKEN } from 'api/endpoints';
import * as AuthenticationApi from 'components/routing/authentication';
@@ -10,7 +11,7 @@ import { useRequest } from 'alova/client';
import { LoadingSpinner } from 'components';
import { verifyAuthorization } from 'components/routing/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { Me } from 'types';
import type { Me, VersionsResponse } from 'types';
import type { RequiredChildrenProps } from 'utils';
import { AuthenticationContext } from './context';
@@ -20,17 +21,34 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const [initialized, setInitialized] = useState<boolean>(false);
const [me, setMe] = useState<Me>();
const [versions, setVersions] = useState<VersionsResponse>();
const { send: sendVerifyAuthorization } = useRequest(verifyAuthorization(), {
immediate: false
});
const { send: sendGetVersions } = useRequest(
() => callAction({ action: 'getVersions' }),
{ immediate: false }
)
.onSuccess((event) => {
setVersions(event.data as VersionsResponse);
})
.onError(() => {
setVersions(undefined);
});
const refreshVersions = useCallback(async () => {
await sendGetVersions().catch(() => undefined);
}, []);
const signIn = (accessToken: string) => {
try {
AuthenticationApi.getStorage().setItem(ACCESS_TOKEN, accessToken);
const decodedMe = AuthenticationApi.decodeMeJWT(accessToken);
setMe(decodedMe);
toast.success(LL.LOGGED_IN({ name: decodedMe.username }));
void refreshVersions();
} catch {
setMe(undefined);
throw new Error('Failed to parse JWT');
@@ -40,6 +58,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const signOut = (doRedirect: boolean) => {
AuthenticationApi.clearAccessToken();
setMe(undefined);
setVersions(undefined);
if (doRedirect) {
redirect('/');
}
@@ -49,8 +68,9 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
if (accessToken) {
await sendVerifyAuthorization()
.then(() => {
.then(async () => {
setMe(AuthenticationApi.decodeMeJWT(accessToken));
await refreshVersions();
setInitialized(true);
})
.catch(() => {
@@ -67,15 +87,16 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
void refresh();
}, [refresh]);
// cache object to prevent re-renders
const obj = useMemo(
() => ({
signIn,
signOut,
refresh,
...(me && { me })
refreshVersions,
...(me && { me }),
...(versions && { versions })
}),
[signIn, signOut, me, refresh]
[signIn, signOut, me, refresh, refreshVersions, versions]
);
if (initialized) {

View File

@@ -1,12 +1,14 @@
import { createContext } from 'react';
import type { Me } from 'types';
import type { Me, VersionsResponse } from 'types';
export interface AuthenticationContextValue {
refresh: () => Promise<void>;
signIn: (accessToken: string) => void;
signOut: (redirect: boolean) => void;
me?: Me;
versions?: VersionsResponse;
refreshVersions: () => Promise<void>;
}
const AuthenticationContextDefaultValue = {} as AuthenticationContextValue;

View File

@@ -247,8 +247,7 @@ const cz: Translation = {
TIME_ZONE: 'Časová zóna',
ACCESS_POINT: 'Přístupový bod',
AP_PROVIDE: 'Povolit přístupový bod',
AP_PROVIDE_TEXT_1: 'Vždy',
AP_PROVIDE_TEXT_2: 'Když je WiFi odpojena',
AP_PROVIDE_TEXT_2: 'Když je síťové připojení stratené',
AP_PROVIDE_TEXT_3: 'Nikdy',
AP_PREFERRED_CHANNEL: 'Preferovaný kanál',
AP_HIDE_SSID: 'Skrýt SSID',
@@ -350,18 +349,19 @@ const cz: Translation = {
NO_DATA_1: 'Nebyly nalezeny žádné oblíbené entity. Použijte modul',
NO_DATA_2: 'pro jejich výběr.',
NO_DATA_3: 'Pro zobrazení všech dostupných entit navštivte stránku',
NO_GPIO: 'Nebylo nalezeno žádné volné GPIO',
THIS_VERSION: 'Tato verze',
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ sestavení',
INTERNET_CONNECTION_REQUIRED: 'Pro automatickou kontrolu a instalaci aktualizací je třeba internetové připojení',
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi',
FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru',
NO_DATA: 'Žádná data',
NO_DATA: 'žádné údaje',
USER_PROFILE: 'Uživatelský profil',
STORED_VERSIONS: 'Uložené verze',
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_1: 'Tato aktualizace vyžaduje obnovení továrního nastavení. Ujistěte se, že nejprve stáhnete systémovou zálohu před pokračováním a poté nahrajte tento soubor 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?'

View File

@@ -187,7 +187,7 @@ const de: Translation = {
COMPACT: 'Kompakte Darstellung',
DOWNLOAD_SETTINGS_TEXT: 'Erstellen Sie eine Sicherung Ihrer Konfigurationen und Einstellungen',
DOWNLOAD_SETTINGS_TEXT2: 'Exportiere alle Werte',
DOWNLOAD_SYSTEM_BACKUP: 'System Sicherung',
DOWNLOAD_SYSTEM_BACKUP: 'Systemsicherung',
UPLOAD_TEXT: 'Laden Sie eine neue Firmware-Datei (.bin) oder eine Sicherungsdatei (.json) hoch',
UPLOAD_DROP_TEXT: 'Legen Sie eine Firmware-Datei (.bin) ab oder klicken Sie hier',
ERROR: 'Unerwarteter Fehler, bitte versuchen Sie es erneut.',
@@ -247,8 +247,7 @@ const de: Translation = {
TIME_ZONE: 'Zeitzone',
ACCESS_POINT: 'Zugangspunkt',
AP_PROVIDE: 'Aktiviere Zugangspunkt',
AP_PROVIDE_TEXT_1: 'Immer',
AP_PROVIDE_TEXT_2: 'Wenn WiFi nicht verbunden',
AP_PROVIDE_TEXT_2: 'Wenn Netzwerkverbindung verloren geht',
AP_PROVIDE_TEXT_3: 'Niemals',
AP_PREFERRED_CHANNEL: 'Bevorzugter Kanal',
AP_HIDE_SSID: 'Verstecke SSID',
@@ -350,6 +349,7 @@ const de: Translation = {
NO_DATA_1: 'Keine favorisierten EMS-Entitäten gefunden! Verwenden Sie das Modul',
NO_DATA_2: ', um sie zu markieren.',
NO_DATA_3: 'Um alle verfügbaren Entitäten anzuzeigen, gehen Sie zu',
NO_GPIO: 'Keine freien GPIO gefunden',
THIS_VERSION: 'Diese Version',
PLATFORM: 'Plattform',
RELEASE_TYPE: 'Release Typ',
@@ -361,9 +361,9 @@ const de: Translation = {
STORED_VERSIONS: 'Gespeicherte Versionen',
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_1: 'Für diese Aktualisierung ist ein Werksreset erforderlich. Stellen Sie sicher, dass Sie zuerst eine Systemsicherung herunterladen, bevor Sie fortfahren, und laden Sie diese Datei dann nach der Installation der neuen Version hoch.',
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?'
WARNING_SYSTEM_BACKUP: 'Dies wird eine Sicherung Ihrer vollständigen Systemkonfiguration und Einstellungen erstellen. Alle Passwörter werden in dieser Sicherungsdatei lesbar sein. Seien Sie vorsichtig beim Teilen! Möchten Sie fortfahren?'
};

View File

@@ -247,8 +247,7 @@ const en: Translation = {
TIME_ZONE: 'Time Zone',
ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Enable Access Point',
AP_PROVIDE_TEXT_1: 'Always',
AP_PROVIDE_TEXT_2: 'When WiFi is disconnected',
AP_PROVIDE_TEXT_2: 'When network connection is lost',
AP_PROVIDE_TEXT_3: 'Never',
AP_PREFERRED_CHANNEL: 'Preferred Channel',
AP_HIDE_SSID: 'Hide SSID',
@@ -350,6 +349,7 @@ const en: Translation = {
NO_DATA_1: 'No favorite EMS entities found yet. Use the',
NO_DATA_2: 'module to mark them.',
NO_DATA_3: 'To see all available entities go to',
NO_GPIO: 'No available GPIO found',
THIS_VERSION: 'This Version',
PLATFORM: 'Platform',
RELEASE_TYPE: 'Release Type',
@@ -361,7 +361,7 @@ const en: Translation = {
STORED_VERSIONS: 'Stored Versions',
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_1: 'This upgrade requires a factory reset. Make sure you first download a System Backup before continuing, and then upload this file 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?'

View File

@@ -247,8 +247,7 @@ const fr: Translation = {
TIME_ZONE: 'Fuseau horaire',
ACCESS_POINT: "Point d'accès",
AP_PROVIDE: "Activer le Point d'Accès",
AP_PROVIDE_TEXT_1: 'toujours',
AP_PROVIDE_TEXT_2: 'quand le WiFi est déconnecté',
AP_PROVIDE_TEXT_2: 'quand la connexion réseau est perdue',
AP_PROVIDE_TEXT_3: 'jamais',
AP_PREFERRED_CHANNEL: 'Canal préféré',
AP_HIDE_SSID: 'Cacher le SSID',
@@ -350,6 +349,7 @@ const fr: Translation = {
NO_DATA_1: 'Aucune entité EMS favorite trouvée. Utilisez le',
NO_DATA_2: 'module pour les marquer.',
NO_DATA_3: 'Pour voir toutes les entités disponibles, aller à',
NO_GPIO: "Aucun GPIO disponible n'a été détecté",
THIS_VERSION: 'Cette version',
PLATFORM: 'Plateforme',
RELEASE_TYPE: 'Type de version',
@@ -361,7 +361,7 @@ const fr: Translation = {
STORED_VERSIONS: 'Versions stockées',
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_1: 'Cette mise à jour nécessite une réinitialisation de fabrique. Assurez-vous de télécharger une sauvegarde système 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 ?'

View File

@@ -247,8 +247,7 @@ const it: Translation = {
TIME_ZONE: 'Fuso orario',
ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Abilita Access Point',
AP_PROVIDE_TEXT_1: 'sempre',
AP_PROVIDE_TEXT_2: 'quando WiFi é disconnessa',
AP_PROVIDE_TEXT_2: 'quando la connessione di rete è persa',
AP_PROVIDE_TEXT_3: 'mai',
AP_PREFERRED_CHANNEL: 'Canale preferito',
AP_HIDE_SSID: 'Nascondi SSID',
@@ -350,6 +349,7 @@ const it: Translation = {
NO_DATA_1: 'Nessuna entità EMS preferita trovata. Usa il',
NO_DATA_2: 'modulo per marcarle.',
NO_DATA_3: 'Per vedere tutte le entità disponibili vai a',
NO_GPIO: 'Non è stato trovato alcun GPIO disponibile',
THIS_VERSION: 'Questa versione',
PLATFORM: 'Piattaforma',
RELEASE_TYPE: 'Tipo di rilascio',
@@ -361,7 +361,7 @@ const it: Translation = {
STORED_VERSIONS: 'Versioni memorizzate',
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_1: 'Questa aggiornamento richiede un ripristino di fabbrica. Assicurati di prima scaricare un backup del sistema prima di continuare, e poi caricare questo file 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?'

View File

@@ -247,8 +247,7 @@ const nl: Translation = {
TIME_ZONE: 'Tijdzone',
ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Activeer Access Point',
AP_PROVIDE_TEXT_1: 'altijd',
AP_PROVIDE_TEXT_2: 'als WiFi niet is verbonden',
AP_PROVIDE_TEXT_2: 'als netwerk verbinding verloren gaat',
AP_PROVIDE_TEXT_3: 'nooit',
AP_PREFERRED_CHANNEL: 'Voorkeurskanaal',
AP_HIDE_SSID: 'SSID verbergen',
@@ -350,6 +349,7 @@ const nl: Translation = {
NO_DATA_1: 'Er zijn nog geen favoriete EMS-entiteiten gevonden. Gebruik de',
NO_DATA_2: 'module om ze te markeren.',
NO_DATA_3: 'Om alle beschikbare entiteiten te zien, ga naar',
NO_GPIO: 'Er is geen beschikbare GPIO gevonden',
THIS_VERSION: 'Deze Versie',
PLATFORM: 'Platform',
RELEASE_TYPE: 'Release Typ',
@@ -361,7 +361,7 @@ const nl: Translation = {
STORED_VERSIONS: 'Opgeslagen versies',
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_1: 'Deze upgrade vereist een fabrieksinstelling. Zorg ervoor dat u eerst een Systeem Backup download voordat u doorgaat, en upload deze file 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?'

View File

@@ -247,8 +247,7 @@ const no: Translation = {
TIME_ZONE: 'Tidssone',
ACCESS_POINT: 'Aksesspunkt',
AP_PROVIDE: 'Aktiver Aksesspunkt',
AP_PROVIDE_TEXT_1: 'alltid',
AP_PROVIDE_TEXT_2: 'når WiFi er utilgjengelig',
AP_PROVIDE_TEXT_2: 'når nettverksforbindelsen er utilgjengelig',
AP_PROVIDE_TEXT_3: 'aldri',
AP_PREFERRED_CHANNEL: 'Foretrukket kanal',
AP_HIDE_SSID: 'Skjul SSID',
@@ -350,6 +349,7 @@ const no: Translation = {
NO_DATA_1: 'Ingen favoritte EMS enheter funnet enda. Bruk',
NO_DATA_2: 'modul for å markere dem.',
NO_DATA_3: 'For å se alle tilgjengelige enheter, gå til',
NO_GPIO: 'Det ble ikke funnet noen tilgjengelige GPIO-porter',
THIS_VERSION: 'Denne versjonen',
PLATFORM: 'Plattform',
RELEASE_TYPE: 'Utgivelses type',
@@ -361,7 +361,7 @@ const no: Translation = {
STORED_VERSIONS: 'Lagret versjoner',
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_1: 'Denne oppdateringen krever en fabriksinstilling. Sørg for at du først lastet ned en System Backup før du fortsetter, og last denne filen 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?'

View File

@@ -247,7 +247,6 @@ const pl: BaseTranslation = {
TIME_ZONE: 'Strefa czasowa',
ACCESS_POINT: '{{Punkt|punktu|}} {{dostępowy|dostępowego|}}',
AP_PROVIDE: 'Punkt dostępowy',
AP_PROVIDE_TEXT_1: 'zawsze aktywny',
AP_PROVIDE_TEXT_2: 'aktywny jeśli brak połączenia z siecią',
AP_PROVIDE_TEXT_3: 'nieaktywny',
AP_PREFERRED_CHANNEL: 'Preferowany kanał',
@@ -350,6 +349,7 @@ const pl: BaseTranslation = {
NO_DATA_1: 'Brak ulubionych encji EMS. Użyj',
NO_DATA_2: 'moduł do ich oznaczenia.',
NO_DATA_3: 'Aby zobaczyć wszystkie dostępne encje przejdź do',
NO_GPIO: 'Nie znaleziono dostępnych pinów GPIO',
THIS_VERSION: 'Ta wersja',
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ wydania',
@@ -361,7 +361,7 @@ const pl: BaseTranslation = {
STORED_VERSIONS: 'Zapisane wersje',
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ń po zainstalowaniu nowej wersji.',
UPGRADE_IMPORTANT_MESSAGES_1: 'Ta aktualizacja wymaga resetu fabrycznego. Upewnij się, że najpierw pobierzesz kopię zapasową systemu przed kontynuowaniem, a następnie przesuń tę plik 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ć?'

View File

@@ -247,8 +247,7 @@ const sk: Translation = {
TIME_ZONE: 'Časová zóna',
ACCESS_POINT: 'Prístupový bod',
AP_PROVIDE: 'Povoliť prístupový bod',
AP_PROVIDE_TEXT_1: 'vždy',
AP_PROVIDE_TEXT_2: 'keď je WiFi odpojená',
AP_PROVIDE_TEXT_2: 'keď je sieťové pripojenie stratené',
AP_PROVIDE_TEXT_3: 'nikdy',
AP_PREFERRED_CHANNEL: 'Preferovaný kanál',
AP_HIDE_SSID: 'Skryť SSID',
@@ -350,6 +349,7 @@ const sk: Translation = {
NO_DATA_1: 'Nenašli sa žiadne obľúbené entity EMS. Použite',
NO_DATA_2: 'modul na ich označenie.',
NO_DATA_3: 'Ak chcete zobraziť všetky dostupné entity, prejdite na',
NO_GPIO: 'Nebol nájdený žiadny dostupný GPIO',
THIS_VERSION: 'Táto verzia',
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ vydania',
@@ -361,7 +361,7 @@ const sk: Translation = {
STORED_VERSIONS: 'Uložené verzie',
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_1: 'Táto aktualizácia vyžaduje reštart základných nastavení. Uistite sa, že najprv stiahnete systémovú zálohu pred pokračovaním, a potom nahrajte tento súbor 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ť?'

View File

@@ -247,8 +247,7 @@ const sv: Translation = {
TIME_ZONE: 'Tidszon',
ACCESS_POINT: 'Accesspunkt',
AP_PROVIDE: 'Aktivera accesspunkt',
AP_PROVIDE_TEXT_1: 'alltid',
AP_PROVIDE_TEXT_2: 'när WiFi är nedkopplat',
AP_PROVIDE_TEXT_2: 'när nätverksanslutningen är bortkopplad',
AP_PROVIDE_TEXT_3: 'aldrig',
AP_PREFERRED_CHANNEL: 'Kanal',
AP_HIDE_SSID: 'Göm SSID',
@@ -350,6 +349,7 @@ const sv: Translation = {
NO_DATA_1: 'Inga favorit EMS enheter hittade än. Använd',
NO_DATA_2: 'modul för att markera dem.',
NO_DATA_3: 'För att se alla tillgängliga enheter, gå till',
NO_GPIO: 'Inga tillgängliga GPIO-portar hittades',
THIS_VERSION: 'Denna version',
PLATFORM: 'Plattform',
RELEASE_TYPE: 'Utgivelsestyp',
@@ -361,7 +361,7 @@ const sv: Translation = {
STORED_VERSIONS: 'Lagrad versioner',
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_1: 'Denna uppdatering kräver en fabriksåterställning. Se till att du först laddar ned en System Backup innan du fortsätter, och ladda upp denna fil 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?'

View File

@@ -247,8 +247,7 @@ const tr: Translation = {
TIME_ZONE: 'Saat dilimi',
ACCESS_POINT: 'Erişim Noktası',
AP_PROVIDE: 'Erişim noktasını çalıştır',
AP_PROVIDE_TEXT_1: 'her zaman',
AP_PROVIDE_TEXT_2: 'Kablosuz bağlantı kesildiğinde',
AP_PROVIDE_TEXT_2: 'Ağ bağlantısı kesildiğinde',
AP_PROVIDE_TEXT_3: 'asla',
AP_PREFERRED_CHANNEL: 'Tercih edilen kanal',
AP_HIDE_SSID: 'SSID yi gizle',
@@ -350,6 +349,7 @@ const tr: Translation = {
NO_DATA_1: 'Henüz bir favori EMS varlığı bulunamadı. Kullanın',
NO_DATA_2: 'modülünü kullanın.',
NO_DATA_3: 'Tüm kullanılabilir varlıkları görmek için git',
NO_GPIO: 'Kullanılabilir GPIO bulunamadı',
THIS_VERSION: 'Bu Sürüm',
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Sürüm Tipi',

View File

@@ -1,5 +1,4 @@
export enum APProvisionMode {
AP_MODE_ALWAYS = 0,
AP_MODE_DISCONNECTED = 1,
AP_NEVER = 2
}

View File

@@ -7,3 +7,4 @@ export * from './ntp';
export * from './security';
export * from './signin';
export * from './system';
export * from './versions';

View File

@@ -0,0 +1,23 @@
// Types for the `getVersions` action response coming from the device.
// The device proxies the request to emsesp.org/versions.json. If the device
// is offline the `stable` and `dev` fields are omitted.
export interface VersionInfo {
version: string;
date: string;
}
export interface RemoteVersionInfo extends VersionInfo {
upgradeable: boolean;
}
export interface CurrentVersionInfo extends VersionInfo {
type: 'stable' | 'dev';
upgradeable: boolean;
}
export interface VersionsResponse {
current: CurrentVersionInfo;
stable?: RemoteVersionInfo;
dev?: RemoteVersionInfo;
}

View File

@@ -1,34 +1,27 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
export const usePersistState = <T>(
initial_value: T,
id: string
): [T, (new_state: T) => void] => {
// Set initial value - only computed once on mount
const _initial_value = useMemo(() => {
const [state, setState] = useState<T>(() => {
try {
const local_storage_value_str = localStorage.getItem(`state:${id}`);
// If there is a value stored in localStorage, use that
if (local_storage_value_str) {
return JSON.parse(local_storage_value_str) as T;
const stored = localStorage.getItem(`state:${id}`);
if (stored) {
return JSON.parse(stored) as T;
}
} catch (error) {
// If parsing fails, fall back to initial_value
console.warn(
`Failed to parse localStorage value for key "state:${id}"`,
error
);
}
// Otherwise use initial_value that was passed to the function
return initial_value;
}, [id]); // initial_value intentionally omitted - only read on first mount
const [state, setState] = useState(_initial_value);
});
useEffect(() => {
try {
const state_str = JSON.stringify(state);
localStorage.setItem(`state:${id}`, state_str);
localStorage.setItem(`state:${id}`, JSON.stringify(state));
} catch (error) {
console.warn(
`Failed to save state to localStorage for key "state:${id}"`,

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useState } from 'react';
import { useBlocker } from 'react-router';
import { toast } from 'react-toastify';
@@ -54,61 +54,44 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
}
}, [readData]);
const saveData = useCallback(async () => {
const saveData = async () => {
if (!data) return;
// Reset states before saving
setRestartNeeded(false);
setErrorMessage(undefined);
try {
await writeData(data as D);
// Only update origData on successful save (dirtyFlags cleared by onSuccess handler)
setOrigData(data as D);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message === REBOOT_ERROR_MESSAGE) {
setRestartNeeded(true);
return; // Early return - save succeeded but needs reboot
return;
}
// Restore original data on validation error
if (origData) {
updateData({ data: origData });
}
toast.error(message);
setErrorMessage(message);
setDirtyFlags([]); // Clear flags so user can retry
setDirtyFlags([]);
}
}, [data, writeData, origData, updateData]);
};
return useMemo(
() => ({
loadData,
saveData,
saving: !!saving,
updateDataValue,
data: data as D,
origData: origData as D,
dirtyFlags,
setDirtyFlags,
setOrigData,
blocker,
errorMessage,
restartNeeded
}),
[
loadData,
saveData,
saving,
updateDataValue,
data,
origData,
dirtyFlags,
blocker,
errorMessage,
restartNeeded
]
);
return {
loadData,
saveData,
saving: !!saving,
updateDataValue,
data: data as D,
origData: origData as D,
dirtyFlags,
setDirtyFlags,
setOrigData,
blocker,
errorMessage,
restartNeeded
};
};

View File

@@ -1,6 +1,20 @@
import type { InternalRuleItem, ValidateOption } from 'async-validator';
import type {
InternalRuleItem,
ValidateFieldsError,
ValidateOption
} from 'async-validator';
import type Schema from 'async-validator';
export class ValidationError extends Error {
readonly fieldErrors: ValidateFieldsError;
constructor(fieldErrors: ValidateFieldsError) {
super('Validation failed');
this.name = 'ValidationError';
this.fieldErrors = fieldErrors;
}
}
export const validate = <T extends object>(
validator: Schema,
source: Partial<T>,
@@ -8,7 +22,7 @@ export const validate = <T extends object>(
): Promise<T> =>
new Promise((resolve, reject) => {
void validator.validate(source, options ?? {}, (errors, fieldErrors) => {
errors ? reject(fieldErrors as Error) : resolve(source as T);
errors ? reject(new ValidationError(fieldErrors)) : resolve(source as T);
});
});

View File

@@ -6,16 +6,13 @@ import { Plugin, PluginOption, defineConfig } from 'vite';
import viteImagemin from 'vite-plugin-imagemin';
import zlib from 'zlib';
// @ts-expect-error - mock server doesn't have type declarations
import mockServer from '../mock-api/mockServer.js';
// Constants
const KB_DIVISOR = 1024;
const REPEAT_CHAR = '=';
const REPEAT_COUNT = 50;
const DEFAULT_OUT_DIR = 'dist';
const ES_TARGET = 'es2020';
const CHUNK_SIZE_WARNING_LIMIT = 512;
const CHUNK_SIZE_WARNING_LIMIT = 1024;
const ASSETS_INLINE_LIMIT = 4096;
// Common resolve aliases
@@ -100,6 +97,10 @@ const createPreactPlugin = (devToolsEnabled: boolean) =>
// Patch preact/compat to export stub React 19 APIs (use, useOptimistic) so that
// react-router v7 doesn't trigger IMPORT_IS_UNDEFINED warnings from Rolldown.
// Rolldown tracks the constant strings used in `React[REACT_USE]` /
// `React[USE_OPTIMISTIC]` lookups inside react-router and resolves them
// statically, so simply relying on a runtime guard is not enough — we need
// matching (stub) exports on the aliased preact/compat module.
const preactCompatPatchPlugin = (): Plugin => ({
name: 'preact-compat-react19-patch',
transform(code, id) {
@@ -130,40 +131,9 @@ const createBasePlugins = (
return plugins;
};
// Manual chunk splitting strategy
const createManualChunks = (detailed = false) => {
return (id: string): string | undefined => {
if (id.includes('node_modules')) {
if (id.includes('preact')) return '@preact';
if (detailed) {
if (id.includes('react-router')) return '@react-router';
if (id.includes('@mui/material')) return '@mui-material';
if (id.includes('@mui/icons-material')) return '@mui-icons';
if (id.includes('alova')) return '@alova';
if (id.includes('typesafe-i18n')) return '@i18n';
if (id.includes('react-toastify')) return '@toastify';
if (id.includes('@table-library')) return '@table-library';
if (id.includes('uuid')) return '@uuid';
if (id.includes('axios') || id.includes('fetch')) return '@http';
if (id.includes('lodash') || id.includes('ramda')) return '@utils';
}
return 'vendor';
}
if (detailed) {
// Group circularly dependent modules together to avoid circular chunk warnings
// components, app, and utils are tightly coupled, so combine them
if (
id.includes('components/') ||
id.includes('app/') ||
id.includes('utils/')
) {
return 'app';
}
// Keep api separate as it's typically more independent
if (id.includes('api/')) return 'api';
}
return undefined;
};
const manualChunks = (id: string): string | undefined => {
if (id.includes('node_modules')) return 'vendor';
return undefined;
};
// Common build base configuration
@@ -241,9 +211,11 @@ const imageOptimizationPlugin = {
};
export default defineConfig(
({ command, mode }: { command: string; mode: string }) => {
async ({ command, mode }: { command: string; mode: string }) => {
if (command === 'serve') {
console.log(`Preparing for standalone build with server, mode=${mode}`);
// @ts-expect-error - mock server doesn't have type declarations
const { default: mockServer } = await import('../mock-api/mockServer.js');
return {
plugins: [...createBasePlugins(true, true), mockServer()],
resolve: {
@@ -260,8 +232,7 @@ export default defineConfig(
changeOrigin: true,
secure: false
},
'/rest': 'http://localhost:3080',
'/gh': 'http://localhost:3080'
'/rest': 'http://localhost:3080'
}
},
build: {
@@ -290,7 +261,7 @@ export default defineConfig(
moduleSideEffects: false
},
output: {
manualChunks: createManualChunks(false)
manualChunks
}
}
}
@@ -330,7 +301,7 @@ export default defineConfig(
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
manualChunks: createManualChunks(true),
manualChunks,
sourcemap: false
}
}

View File

@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Marko Živanović
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,7 +0,0 @@
# About
This project is MIT-licensed, C++14 implementation of [semantic versioning](http://semver.org) parser and comparator with support for modifying parsed version strings. Semantic versioning 2.0.0 specification is supported out-of-the-box and the code should be flexible-enough to support future revisions or other similar versioning schemes.
Copyright (c) 2015 Marko Zivanovic
Based on https://github.com/zmarko/semver

Some files were not shown because too many files have changed in this diff Show More