212 Commits

Author SHA1 Message Date
Proddy
db69c43d35 Merge pull request #3067 from MichaelDvP/core3
core3 fix for #3064
2026-05-07 18:04:04 +02:00
MichaelDvP
97d727a19b update pkg 2026-05-07 12:25:31 +02:00
MichaelDvP
ff952935a2 Merge branch 'dev' into core3 2026-05-07 12:10:53 +02:00
MichaelDvP
ae5beccb9d dev.20, fixes #3064, handling of optional gpios 2026-05-07 12:02:33 +02:00
Proddy
a9cdab1ce9 Merge pull request #3062 from MichaelDvP/core3
sync dev changes to core3
2026-05-06 09:54:18 +02:00
MichaelDvP
015c100a1d Merge branch 'dev' into core3 2026-05-06 09:28:29 +02:00
MichaelDvP
764c660b14 Merge branch 'dev' of https://github.com/emsesp/EMS-ESP32 into dev 2026-05-06 09:23:27 +02:00
MichaelDvP
ca0f32b087 update pkg 2026-05-06 09:06:15 +02:00
Proddy
8ad91de54a Merge pull request #3061 from MichaelDvP/dev 2026-05-06 09:05:10 +02:00
Proddy
e7a14183d2 Merge pull request #3060 from proddy/core3
rename mail -> email
2026-05-05 22:00:42 +02:00
proddy
5e1b47707f rename mail -> email 2026-05-05 21:56:55 +02:00
proddy
56fc303816 package update 2026-05-05 21:55:26 +02:00
proddy
d7a6a6f803 formatting 2026-05-05 21:55:13 +02:00
MichaelDvP
58da157272 chore: update generated files for v3.8.2-dev.20 2026-05-05 16:04:29 +00:00
MichaelDvP
b5014bf9ac dev.20, check and set 0x470 to summer2_typeids[0] ony if received. #2686 2026-05-05 17:51:52 +02:00
MichaelDvP
14351436a7 fixes #3055, revert commit daffdcf 2026-05-05 10:33:56 +02:00
Proddy
03d969cb39 Merge pull request #3059 from proddy/core3
fix warning "esp32-hal-gpio.c:181] __digitalWrite(): IO 2 is not set …
2026-05-05 08:04:43 +02:00
proddy
c55df4eb6e fix standalone build lint warning 2026-05-04 20:57:20 +02:00
proddy
cc631ff07f remove digitalWrite at init 2026-05-04 20:48:12 +02:00
proddy
b34a1b57d6 fix warning "esp32-hal-gpio.c:181] __digitalWrite(): IO 2 is not set as GPIO. Execute digitalMode(2, OUTPUT) first" 2026-05-04 08:38:41 +02:00
Proddy
9cad606a55 Merge pull request #3058 from proddy/core3
small adjustments
2026-05-04 08:23:28 +02:00
proddy
6587ddcead increase timeout to allow slower networks to connect 2026-05-04 08:22:39 +02:00
proddy
ad05eec952 fix standalone and tests 2026-05-04 08:15:27 +02:00
Proddy
847fa4f46c Merge pull request #3056 from MichaelDvP/core3
"Send Testmail" button (not translated) #3050
2026-05-03 23:04:43 +02:00
Proddy
c5897b7ee1 Merge pull request #3057 from proddy/core3
updates to Network code
2026-05-03 23:04:19 +02:00
proddy
99ef4c0c18 updates 2026-05-03 23:03:22 +02:00
proddy
cc118adec6 text changes 2026-05-03 22:55:25 +02:00
proddy
ca94e37495 use a state machine for cycling between Eth-Wifi-AP 2026-05-03 21:59:28 +02:00
proddy
e2bd721c3e remove empty SSID check 2026-05-03 17:16:38 +02:00
proddy
033ce24fb7 udpate 2026-05-03 15:21:26 +02:00
proddy
eab7cdd7b5 updates 2026-05-03 15:19:49 +02:00
proddy
666ba41f67 package update 2026-05-03 14:24:36 +02:00
MichaelDvP
2579450eae "Send Testmail" button (not translated) #3050 2026-05-03 14:24:03 +02:00
MichaelDvP
87a3ca8393 nosleep default on after network selector 2026-05-03 14:22:08 +02:00
proddy
9ff4be41f7 getWifiReconnects -> getNetworkReconnects 2026-05-03 08:43:28 +02:00
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
169 changed files with 6616 additions and 7126 deletions

View File

@@ -28,7 +28,7 @@ jobs:
node-version: 24 node-version: 24
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Enable Corepack - name: Enable Corepack
run: corepack enable pnpm run: corepack enable pnpm
@@ -47,7 +47,7 @@ jobs:
- name: Build webUI - name: Build webUI
run: | run: |
platformio run -e build_webUI platformio run -e build-webUI
- name: Build modbus - name: Build modbus
run: | run: |
@@ -62,13 +62,13 @@ jobs:
platformio run platformio run
- name: Commit the generated files - name: Commit the generated files
uses: stefanzweifel/git-auto-commit-action@v5 uses: stefanzweifel/git-auto-commit-action@v7
with: with:
commit_message: "chore: update generated files for v${{steps.build_info.outputs.VERSION}}" commit_message: "chore: update generated files for v${{steps.build_info.outputs.VERSION}}"
- name: Create GitHub Release - name: Create GitHub Release
id: 'automatic_releases' id: 'automatic_releases'
uses: emsesp/action-automatic-releases@v1.0.0 uses: emsesp/action-automatic-releases@v1.0.1
with: with:
repo_token: '${{ secrets.GITHUB_TOKEN }}' repo_token: '${{ secrets.GITHUB_TOKEN }}'
title: Development Build v${{steps.build_info.outputs.VERSION}} title: Development Build v${{steps.build_info.outputs.VERSION}}
@@ -77,3 +77,23 @@ jobs:
files: | files: |
CHANGELOG_LATEST.md CHANGELOG_LATEST.md
./build/firmware/*.* ./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 contents: read
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: GitHub Releases To Discord - name: GitHub Releases To Discord
uses: SethCohen/github-releases-to-discord@v1.13.1 uses: SethCohen/github-releases-to-discord@v1.13.1

View File

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

View File

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

View File

@@ -26,11 +26,18 @@ jobs:
node-version: 24 node-version: 24
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Enable Corepack - name: Enable Corepack
run: corepack enable pnpm 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 - name: Install PlatformIO
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
@@ -39,7 +46,7 @@ jobs:
- name: Build webUI - name: Build webUI
run: | run: |
platformio run -e build_webUI platformio run -e build-webUI
- name: Build modbus - name: Build modbus
run: | run: |
@@ -54,10 +61,30 @@ jobs:
platformio run platformio run
- name: Create GitHub Release - name: Create GitHub Release
uses: emsesp/action-automatic-releases@v1.0.0 uses: emsesp/action-automatic-releases@v1.0.1
with: with:
repo_token: '${{ secrets.GITHUB_TOKEN }}' repo_token: '${{ secrets.GITHUB_TOKEN }}'
prerelease: false prerelease: false
files: | files: |
CHANGELOG.md CHANGELOG.md
./build/firmware/*.* ./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 node-version: 24
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Enable Corepack - name: Enable Corepack
run: corepack enable pnpm run: corepack enable pnpm
@@ -47,7 +47,7 @@ jobs:
- name: Build webUI - name: Build webUI
run: | run: |
platformio run -e build_webUI platformio run -e build-webUI
- name: Build modbus - name: Build modbus
run: | run: |
@@ -63,7 +63,7 @@ jobs:
- name: Create GitHub Release - name: Create GitHub Release
id: 'automatic_releases' id: 'automatic_releases'
uses: emsesp/action-automatic-releases@v1.0.0 uses: emsesp/action-automatic-releases@v1.0.1
with: with:
repo_token: '${{ secrets.GITHUB_TOKEN }}' repo_token: '${{ secrets.GITHUB_TOKEN }}'
title: Test Build v${{steps.build_info.outputs.VERSION}} 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/c_cpp_properties.json
.vscode/extensions.json .vscode/extensions.json
.vscode/launch.json .vscode/launch.json
.vscode/settings.json
# c++ compiling # c++ compiling
.clang_complete .clang_complete
@@ -63,7 +64,7 @@ words-found-verbose.txt
# sonarlint # sonarlint
compile_commands.json compile_commands.json
# pioarduino + hybrid # other files
managed_components managed_components
dependencies.lock dependencies.lock
CMakeLists.txt CMakeLists.txt
@@ -75,3 +76,4 @@ pnpm-lock.yaml
.cache/ .cache/
interface/.tsbuildinfo interface/.tsbuildinfo
test/test_api/package-lock.json 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/). For more details go to [emsesp.org](https://emsesp.org/).
## [3.8.2] ## [3.9.0]
## Added ## Added
@@ -13,11 +13,16 @@ For more details go to [emsesp.org](https://emsesp.org/).
- boiler pumpkick [#2965](https://github.com/emsesp/EMS-ESP32/discussions/2965) - boiler pumpkick [#2965](https://github.com/emsesp/EMS-ESP32/discussions/2965)
- heatpump reset [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933) - heatpump reset [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933)
- e-mail notification using ReadyMail Client - 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 ## Fixed
- SRC climate creation [#2936](https://github.com/emsesp/EMS-ESP32/issues/2936) and [#2960](https://github.com/emsesp/EMS-ESP32/issues/2960) - 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
- modbus initialization [#3064](https://github.com/emsesp/EMS-ESP32/issues/3064)
## Changed ## Changed
@@ -31,3 +36,8 @@ For more details go to [emsesp.org](https://emsesp.org/).
- device class for % values [#2980](https://github.com/emsesp/EMS-ESP32/issues/2980) - device class for % values [#2980](https://github.com/emsesp/EMS-ESP32/issues/2980)
- use tasmota core 2026.03.30 - use tasmota core 2026.03.30
- secure mqtt uses ESP_SSLClient - 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)
- check and read 0x470 as summer2_typeids[0] only if received [#2686](https://github.com/emsesp/EMS-ESP32/issues/2686), [#3055](https://github.com/emsesp/EMS-ESP32/issues/3055)

View File

@@ -47,8 +47,8 @@ MAKEFLAGS += -j$(JOBS) -l$(shell echo $$(($(JOBS) * 2)))
#---------------------------------------------------------------------- #----------------------------------------------------------------------
TARGET := emsesp TARGET := emsesp
BUILD := build 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 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/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 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 := LIBRARIES :=
CPPCHECK = cppcheck CPPCHECK = cppcheck
@@ -113,7 +113,7 @@ CXX := /usr/bin/g++
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE) CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
CPPFLAGS += -ggdb -g3 -MMD CPPFLAGS += -ggdb -g3 -MMD
CPPFLAGS += -flto=auto 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 += -Wno-unused-parameter -Wno-missing-braces -Wno-vla-cxx-extension
CPPFLAGS += -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics CPPFLAGS += -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics
CPPFLAGS += -Os -DNDEBUG CPPFLAGS += -Os -DNDEBUG

View File

@@ -38,6 +38,7 @@
"vite.config.ts", "vite.config.ts",
"lib/esp32-psram/**", "lib/esp32-psram/**",
"test/test_api/test_api.h", "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 | | 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 | | collectorshutdown | collector shutdown | boolean | | false | DEVICE_DATA | 6 | 1 | 1 |
| cylheated | cyl heated | boolean | | false | DEVICE_DATA | 7 | 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 | | 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 | | 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 | | 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 | | 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 | | collectorshutdown | collector shutdown | boolean | | false | DEVICE_DATA | 6 | 1 | 1 |
| cylheated | cyl heated | boolean | | false | DEVICE_DATA | 7 | 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 | | 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 | | 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 | | energylasthour | energy last hour | uint24 | Wh | false | DEVICE_DATA | 13 | 2 | 1/10 |
@@ -7696,67 +7696,68 @@ uint8
uint8 uint8
| turnoffdiff | pump turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 11 | 1 | 1/10 | | 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 | | 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 | | 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 | | ts8 | (TS8) | int16 | C | false | DEVICE_DATA | 22 | 1 | 1/10 |
| ts16 | (TS16) | 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 | | 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 | | heatassistpower | heat assistance valve power (M1) | uint8 | % | false | DEVICE_DATA | 24 | 1 | 1 |
| solarpump2 | pump 2 (PS4) | boolean | | 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 | 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 | 26 | 1 | 1/10 | | 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 | 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 | 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 | 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 | 30 | 1 | 1 | | cylpumpmod | cylinder pump modulation (PS5) | uint8 | % | false | DEVICE_DATA | 31 | 1 | 1 |
| valvestatus | valve status | boolean | | false | DEVICE_DATA | 31 | 1 | 1 | | valvestatus | valve status | boolean | | false | DEVICE_DATA | 32 | 1 | 1 |
| vs1status | valve status VS1 | 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 | 33 | 1 | 1 | | vs3status | valve status VS3 | boolean | | false | DEVICE_DATA | 34 | 1 | 1 |
| transferpump | transfer pump | 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 | 35 | 1 | 1 | | transferpumpmod | transfer pump modulation | uint8 | % | false | DEVICE_DATA | 36 | 1 | 1 |
uint8 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 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 | | 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 | | energytoday | total energy today | uint24 | Wh | false | DEVICE_DATA | 39 | 2 | 1 |
| energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 40 | 2 | 1/10 | | energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 41 | 2 | 1/10 |
| pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 42 | 2 | 1 | | pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 43 | 2 | 1 |
| m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 44 | 2 | 1 | | m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 45 | 2 | 1 |
| heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 46 | 1 | 1 | | heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 47 | 1 | 1 |
| externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 47 | 1 | 1 | | externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 48 | 1 | 1 |
| thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 48 | 1 | 1 | | thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 49 | 1 | 1 |
| heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 49 | 1 | 1 | | heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 50 | 1 | 1 |
| activated | activated | boolean | | true | DEVICE_DATA | 50 | 1 | 1 | | activated | activated | boolean | | true | DEVICE_DATA | 51 | 1 | 1 |
| solarpumpmode | solar pump mode | enum | | 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 | 52 | 1 | 1 | | solarpumpkick | solar pump kick | boolean | | true | DEVICE_DATA | 53 | 1 | 1 |
| plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 53 | 1 | 1 | | plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 54 | 1 | 1 |
| doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 54 | 1 | 1 | | doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 55 | 1 | 1 |
uint8 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 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 uint8
| turnoffdiff2 | pump 2 turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 57 | 1 | 1/10 | | 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 | 58 | 1 | 1 | | pump2kick | pump kick 2 | boolean | | true | DEVICE_DATA | 59 | 1 | 1 |
uint8 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 uint16
| collector1area | collector 1 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 60 | 1 | 1/10 | | 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 | 61 | 1 | 1 | | collector1type | collector 1 type | enum | | true | DEVICE_DATA | 62 | 1 | 1 |
uint16 uint16
| collector2area | collector 2 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 62 | 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 | 63 | 1 | 1 | | collector2type | collector 2 type | enum | | true | DEVICE_DATA | 64 | 1 | 1 |
| cylpriority | cylinder priority | 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 | 65 | 1 | 1/10 | | heatcntflowtemp | heat counter flow temperature | uint16 | C | false | DEVICE_DATA | 66 | 1 | 1/10 |
| heatcntrettemp | heat counter return 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 | 67 | 1 | 1 | | heatcnt | heat counter impulses | uint8 | | false | DEVICE_DATA | 68 | 1 | 1 |
| swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 68 | 1 | 1/10 | | swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 |
| swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 | | swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 70 | 1 | 1/10 |
int8 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 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 ### SM100, MS100
@@ -7778,67 +7779,68 @@ uint8
uint8 uint8
| turnoffdiff | pump turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 11 | 1 | 1/10 | | 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 | | 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 | | 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 | | ts8 | (TS8) | int16 | C | false | DEVICE_DATA | 22 | 1 | 1/10 |
| ts16 | (TS16) | 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 | | 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 | | heatassistpower | heat assistance valve power (M1) | uint8 | % | false | DEVICE_DATA | 24 | 1 | 1 |
| solarpump2 | pump 2 (PS4) | boolean | | 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 | 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 | 26 | 1 | 1/10 | | 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 | 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 | 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 | 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 | 30 | 1 | 1 | | cylpumpmod | cylinder pump modulation (PS5) | uint8 | % | false | DEVICE_DATA | 31 | 1 | 1 |
| valvestatus | valve status | boolean | | false | DEVICE_DATA | 31 | 1 | 1 | | valvestatus | valve status | boolean | | false | DEVICE_DATA | 32 | 1 | 1 |
| vs1status | valve status VS1 | 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 | 33 | 1 | 1 | | vs3status | valve status VS3 | boolean | | false | DEVICE_DATA | 34 | 1 | 1 |
| transferpump | transfer pump | 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 | 35 | 1 | 1 | | transferpumpmod | transfer pump modulation | uint8 | % | false | DEVICE_DATA | 36 | 1 | 1 |
uint8 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 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 | | 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 | | energytoday | total energy today | uint24 | Wh | false | DEVICE_DATA | 39 | 2 | 1 |
| energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 40 | 2 | 1/10 | | energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 41 | 2 | 1/10 |
| pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 42 | 2 | 1 | | pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 43 | 2 | 1 |
| m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 44 | 2 | 1 | | m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 45 | 2 | 1 |
| heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 46 | 1 | 1 | | heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 47 | 1 | 1 |
| externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 47 | 1 | 1 | | externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 48 | 1 | 1 |
| thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 48 | 1 | 1 | | thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 49 | 1 | 1 |
| heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 49 | 1 | 1 | | heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 50 | 1 | 1 |
| activated | activated | boolean | | true | DEVICE_DATA | 50 | 1 | 1 | | activated | activated | boolean | | true | DEVICE_DATA | 51 | 1 | 1 |
| solarpumpmode | solar pump mode | enum | | 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 | 52 | 1 | 1 | | solarpumpkick | solar pump kick | boolean | | true | DEVICE_DATA | 53 | 1 | 1 |
| plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 53 | 1 | 1 | | plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 54 | 1 | 1 |
| doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 54 | 1 | 1 | | doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 55 | 1 | 1 |
uint8 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 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 uint8
| turnoffdiff2 | pump 2 turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 57 | 1 | 1/10 | | 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 | 58 | 1 | 1 | | pump2kick | pump kick 2 | boolean | | true | DEVICE_DATA | 59 | 1 | 1 |
uint8 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 uint16
| collector1area | collector 1 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 60 | 1 | 1/10 | | 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 | 61 | 1 | 1 | | collector1type | collector 1 type | enum | | true | DEVICE_DATA | 62 | 1 | 1 |
uint16 uint16
| collector2area | collector 2 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 62 | 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 | 63 | 1 | 1 | | collector2type | collector 2 type | enum | | true | DEVICE_DATA | 64 | 1 | 1 |
| cylpriority | cylinder priority | 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 | 65 | 1 | 1/10 | | heatcntflowtemp | heat counter flow temperature | uint16 | C | false | DEVICE_DATA | 66 | 1 | 1/10 |
| heatcntrettemp | heat counter return 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 | 67 | 1 | 1 | | heatcnt | heat counter impulses | uint8 | | false | DEVICE_DATA | 68 | 1 | 1 |
| swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 68 | 1 | 1/10 | | swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 |
| swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 | | swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 70 | 1 | 1/10 |
int8 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 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 ### SM200, MS200
@@ -7860,67 +7862,68 @@ uint8
uint8 uint8
| turnoffdiff | pump turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 11 | 1 | 1/10 | | 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 | | 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 | | 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 | | ts8 | (TS8) | int16 | C | false | DEVICE_DATA | 22 | 1 | 1/10 |
| ts16 | (TS16) | 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 | | 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 | | heatassistpower | heat assistance valve power (M1) | uint8 | % | false | DEVICE_DATA | 24 | 1 | 1 |
| solarpump2 | pump 2 (PS4) | boolean | | 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 | 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 | 26 | 1 | 1/10 | | 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 | 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 | 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 | 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 | 30 | 1 | 1 | | cylpumpmod | cylinder pump modulation (PS5) | uint8 | % | false | DEVICE_DATA | 31 | 1 | 1 |
| valvestatus | valve status | boolean | | false | DEVICE_DATA | 31 | 1 | 1 | | valvestatus | valve status | boolean | | false | DEVICE_DATA | 32 | 1 | 1 |
| vs1status | valve status VS1 | 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 | 33 | 1 | 1 | | vs3status | valve status VS3 | boolean | | false | DEVICE_DATA | 34 | 1 | 1 |
| transferpump | transfer pump | 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 | 35 | 1 | 1 | | transferpumpmod | transfer pump modulation | uint8 | % | false | DEVICE_DATA | 36 | 1 | 1 |
uint8 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 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 | | 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 | | energytoday | total energy today | uint24 | Wh | false | DEVICE_DATA | 39 | 2 | 1 |
| energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 40 | 2 | 1/10 | | energytotal | total energy | uint24 | kWh | false | DEVICE_DATA | 41 | 2 | 1/10 |
| pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 42 | 2 | 1 | | pump2worktime | pump 2 working time | time | minutes | false | DEVICE_DATA | 43 | 2 | 1 |
| m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 44 | 2 | 1 | | m1worktime | differential control working time | time | minutes | false | DEVICE_DATA | 45 | 2 | 1 |
| heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 46 | 1 | 1 | | heattransfersystem | heattransfer system | boolean | | true | DEVICE_DATA | 47 | 1 | 1 |
| externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 47 | 1 | 1 | | externalcyl | external cylinder | boolean | | true | DEVICE_DATA | 48 | 1 | 1 |
| thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 48 | 1 | 1 | | thermaldisinfect | thermal disinfection | boolean | | true | DEVICE_DATA | 49 | 1 | 1 |
| heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 49 | 1 | 1 | | heatmetering | heatmetering | boolean | | true | DEVICE_DATA | 50 | 1 | 1 |
| activated | activated | boolean | | true | DEVICE_DATA | 50 | 1 | 1 | | activated | activated | boolean | | true | DEVICE_DATA | 51 | 1 | 1 |
| solarpumpmode | solar pump mode | enum | | 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 | 52 | 1 | 1 | | solarpumpkick | solar pump kick | boolean | | true | DEVICE_DATA | 53 | 1 | 1 |
| plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 53 | 1 | 1 | | plainwatermode | plain water mode | boolean | | true | DEVICE_DATA | 54 | 1 | 1 |
| doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 54 | 1 | 1 | | doublematchflow | doublematchflow | boolean | | true | DEVICE_DATA | 55 | 1 | 1 |
uint8 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 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 uint8
| turnoffdiff2 | pump 2 turn off difference | uint8 (&gt;=0&lt;=0) | C | true | DEVICE_DATA | 57 | 1 | 1/10 | | 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 | 58 | 1 | 1 | | pump2kick | pump kick 2 | boolean | | true | DEVICE_DATA | 59 | 1 | 1 |
uint8 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 uint16
| collector1area | collector 1 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 60 | 1 | 1/10 | | 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 | 61 | 1 | 1 | | collector1type | collector 1 type | enum | | true | DEVICE_DATA | 62 | 1 | 1 |
uint16 uint16
| collector2area | collector 2 area | uint16 (&gt;=0&lt;=0) | m² | true | DEVICE_DATA | 62 | 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 | 63 | 1 | 1 | | collector2type | collector 2 type | enum | | true | DEVICE_DATA | 64 | 1 | 1 |
| cylpriority | cylinder priority | 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 | 65 | 1 | 1/10 | | heatcntflowtemp | heat counter flow temperature | uint16 | C | false | DEVICE_DATA | 66 | 1 | 1/10 |
| heatcntrettemp | heat counter return 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 | 67 | 1 | 1 | | heatcnt | heat counter impulses | uint8 | | false | DEVICE_DATA | 68 | 1 | 1 |
| swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 68 | 1 | 1/10 | | swapflowtemp | swap flow temperature (TS14) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 |
| swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 69 | 1 | 1/10 | | swaprettemp | swap return temperature (TS15) | uint16 | C | false | DEVICE_DATA | 70 | 1 | 1/10 |
int8 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 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 ## 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,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,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,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,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,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 "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,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,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,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,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,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 "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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,40,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,42,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,44,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,46,1 "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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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 "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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,40,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,42,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,44,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,46,1 "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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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 "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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,40,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,42,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,44,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,46,1 "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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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 "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

@@ -112,10 +112,10 @@ telegram_type_id,name,is_fetched
0x02A0,RC300Curves, 0x02A0,RC300Curves,
0x02A1,RC300Curves, 0x02A1,RC300Curves,
0x02A2,RC300Curves, 0x02A2,RC300Curves,
0x02A5,RC300Monitor,fetched 0x02A5,RC300Monitor,
0x02A6,CRFMonitor, 0x02A6,RC300Monitor,
0x02A7,RC300Monitor, 0x02A7,RC300Monitor,
0x02A8,CRFMonitor, 0x02A8,RC300Monitor,
0x02A9,RC300Monitor, 0x02A9,RC300Monitor,
0x02AA,RC300Monitor, 0x02AA,RC300Monitor,
0x02AB,RC300Monitor, 0x02AB,RC300Monitor,
@@ -171,6 +171,7 @@ telegram_type_id,name,is_fetched
0x0468,HPSet, 0x0468,HPSet,
0x0469,HPSet, 0x0469,HPSet,
0x046A,HPSet, 0x046A,HPSet,
0x0470,RC300Summer2,fetched
0x0471,RC300Summer2, 0x0471,RC300Summer2,
0x0472,RC300Summer2, 0x0472,RC300Summer2,
0x0473,RC300Summer2, 0x0473,RC300Summer2,
1 telegram_type_id name is_fetched
112 0x02A0 RC300Curves
113 0x02A1 RC300Curves
114 0x02A2 RC300Curves
115 0x02A5 RC300Monitor fetched
116 0x02A6 CRFMonitor RC300Monitor
117 0x02A7 RC300Monitor
118 0x02A8 CRFMonitor RC300Monitor
119 0x02A9 RC300Monitor
120 0x02AA RC300Monitor
121 0x02AB RC300Monitor
171 0x0468 HPSet
172 0x0469 HPSet
173 0x046A HPSet
174 0x0470 RC300Summer2 fetched
175 0x0471 RC300Summer2
176 0x0472 RC300Summer2
177 0x0473 RC300Summer2

View File

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

887
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 } other: { count: 0, uncompressed: 0, compressed: 0 }
}; };
const generateWWWClass = // AsyncWebHandler that performs the lookup.
() => `typedef std::function<void(const char * uri, const String & contentType, const uint8_t * content, size_t len, const String & hash)> RouteRegistrationHandler; const generateWWWClass = () => `// Bundle Statistics:
// Bundle Statistics:
// - Total compressed size: ${(totalSize / 1000).toFixed(1)} KB // - 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 // - 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)}% // - 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()} // - Generated on: ${new Date().toISOString()}
class WWWData { struct WWWAsset {
${INDENT}public: ${INDENT}const char * uri;
${INDENT.repeat(2)}static void registerRoutes(RouteRegistrationHandler handler) { ${INDENT}const char * contentType;
${fileInfo.map((f) => `${INDENT.repeat(3)}handler("${f.uri}", "${f.mimeType}", ${f.variable}, ${f.size}, ${f.hash});`).join('\n')} ${INDENT}const uint8_t * content;
${INDENT.repeat(2)}} ${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 = []) => { const getFilesSync = (dir, files = []) => {
@@ -72,6 +78,7 @@ const writeFile = (relativeFilePath, buffer) => {
const zipBuffer = zlib.gzipSync(buffer, { level: 9 }); const zipBuffer = zlib.gzipSync(buffer, { level: 9 });
// const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex'); // const hash = crypto.createHash('sha256').update(zipBuffer).digest('hex');
const hash = etag(zipBuffer); // use smaller md5 instead of sha256 const hash = etag(zipBuffer); // use smaller md5 instead of sha256
const rawHash = hash.replace(/^"|"$/g, '');
zipBuffer.forEach((b) => { zipBuffer.forEach((b) => {
if (!(size % bytesPerLine)) { if (!(size % bytesPerLine)) {
@@ -94,7 +101,8 @@ const writeFile = (relativeFilePath, buffer) => {
mimeType, mimeType,
variable, variable,
size, size,
hash hash,
rawHash
}); });
totalSize += size; 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 { ToastContainer, Zoom } from 'react-toastify';
import AppRouting from 'AppRouting'; import AppRouting from 'AppRouting';
@@ -46,19 +46,17 @@ const App = memo(() => {
const [wasLoaded, setWasLoaded] = useState(false); const [wasLoaded, setWasLoaded] = useState(false);
const [locale, setLocale] = useState<Locales>('en'); 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(() => { 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(); void initializeLocale();
}, [initializeLocale]); }, []);
if (!wasLoaded) return null; 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 { Navigate, Route, Routes } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { import AuthenticatedRouting from 'AuthenticatedRouting';
LoadingSpinner, import SignIn from 'SignIn';
RequireAuthenticated, import { RequireAuthenticated, RequireUnauthenticated } from 'components';
RequireUnauthenticated
} from 'components';
import { Authentication, AuthenticationContext } from 'contexts/authentication'; import { Authentication, AuthenticationContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; 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 { interface SecurityRedirectProps {
readonly message: string; readonly message: string;
readonly signOut?: boolean; readonly signOut?: boolean;
@@ -45,34 +39,32 @@ const AppRouting: FC = memo(() => {
return ( return (
<Authentication> <Authentication>
<Suspense fallback={<LoadingSpinner />}> <Routes>
<Routes> <Route
<Route path="/unauthorized"
path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
/> <Route
<Route path="/fileUpdated"
path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />} />
/> <Route
<Route path="/"
path="/" element={
element={ <RequireUnauthenticated>
<RequireUnauthenticated> <SignIn />
<SignIn /> </RequireUnauthenticated>
</RequireUnauthenticated> }
} />
/> <Route
<Route path="/*"
path="/*" element={
element={ <RequireAuthenticated>
<RequireAuthenticated> <AuthenticatedRouting />
<AuthenticatedRouting /> </RequireAuthenticated>
</RequireAuthenticated> }
} />
/> </Routes>
</Routes>
</Suspense>
</Authentication> </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 { 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'; 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 AuthenticatedRouting = memo(() => {
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);
return ( return (
<Layout> <Layout>
<Suspense fallback={<LoadingSpinner />}> <Routes>
<Routes> <Route path="/dashboard/*" element={<Dashboard />} />
<Route path="/dashboard/*" element={<Dashboard />} /> <Route path="/devices/*" element={<Devices />} />
<Route path="/devices/*" element={<Devices />} /> <Route path="/sensors/*" element={<Sensors />} />
<Route path="/sensors/*" element={<Sensors />} /> <Route path="/help/*" element={<Help />} />
<Route path="/help/*" element={<Help />} /> <Route path="/user/*" element={<UserProfile />} />
<Route path="/user/*" element={<UserProfile />} />
<Route path="/status/*" element={<Status />} /> <Route path="/status/*" element={<Status />} />
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} /> <Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
<Route path="/status/activity" element={<Activity />} /> <Route path="/status/activity" element={<Activity />} />
<Route path="/status/log" element={<SystemLog />} /> <Route path="/status/log" element={<SystemLog />} />
<Route path="/status/mqtt" element={<MqttStatus />} /> <Route path="/status/mqtt" element={<MqttStatus />} />
<Route path="/status/ntp" element={<NTPStatus />} /> <Route path="/status/ntp" element={<NTPStatus />} />
<Route path="/status/ap" element={<APStatus />} /> <Route path="/status/ap" element={<APStatus />} />
<Route path="/status/network" element={<NetworkStatus />} /> <Route path="/status/network" element={<NetworkStatus />} />
<Route path="/status/version" element={<Version />} />
{me.admin && ( {me.admin && (
<> <>
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route <Route path="/settings/version" element={<Version />} />
path="/settings/application" <Route path="/settings/application" element={<ApplicationSettings />} />
element={<ApplicationSettings />} <Route path="/settings/mqtt" element={<MqttSettings />} />
/> <Route path="/settings/ntp" element={<NTPSettings />} />
<Route path="/settings/mqtt" element={<MqttSettings />} /> <Route path="/settings/ap" element={<APSettings />} />
<Route path="/settings/ntp" element={<NTPSettings />} /> <Route path="/settings/modules" element={<Modules />} />
<Route path="/settings/ap" element={<APSettings />} /> <Route path="/settings/downloadUpload" element={<DownloadUpload />} />
<Route path="/settings/modules" element={<Modules />} />
<Route path="/settings/downloadUpload" element={<DownloadUpload />} />
<Route path="/settings/network/*" element={<Network />} /> <Route path="/settings/network/*" element={<Network />} />
<Route path="/settings/security/*" element={<Security />} /> <Route path="/settings/security/*" element={<Security />} />
<Route path="/customizations" element={<Customizations />} /> <Route path="/customizations" element={<Customizations />} />
<Route path="/scheduler" element={<Scheduler />} /> <Route path="/scheduler" element={<Scheduler />} />
<Route path="/customentities" element={<CustomEntities />} /> <Route path="/customentities" element={<CustomEntities />} />
</> </>
)} )}
<Route path="/*" element={<Navigate to="/" />} /> <Route path="/*" element={<Navigate to="/" />} />
</Routes> </Routes>
</Suspense>
</Layout> </Layout>
); );
}); });

View File

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

View File

@@ -57,12 +57,3 @@ export const alovaInstance = createAlova({
onSuccess: handleResponse 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 type { LogSettings, SystemStatus } from 'types';
import { alovaInstance, alovaInstanceGH } from './endpoints'; import { alovaInstance } from './endpoints';
// systemStatus - also used to ping in System Monitor for pinging // systemStatus - also used to ping in System Monitor for pinging
export const readSystemStatus = () => export const readSystemStatus = () =>
@@ -13,29 +13,6 @@ export const updateLogSettings = (data: LogSettings) =>
alovaInstance.Post('/rest/logSettings', data); alovaInstance.Post('/rest/logSettings', data);
export const fetchLogES = () => alovaInstance.Get('/es/log'); 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 const UPLOAD_TIMEOUT = 60000; // 1 minute
export const uploadFile = (file: File) => { 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 { useBlocker } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -57,20 +57,18 @@ const CustomEntities = () => {
initialData: [] initialData: []
}); });
const intervalCallback = useCallback(() => { useInterval(() => {
if (!dialogOpen && !numChanges) { if (!dialogOpen && !numChanges) {
void fetchEntities(); void fetchEntities();
} }
}, [dialogOpen, numChanges, fetchEntities]); });
useInterval(intervalCallback);
const { send: writeEntities } = useRequest( const { send: writeEntities } = useRequest(
(data: Entities) => writeCustomEntities(data), (data: Entities) => writeCustomEntities(data),
{ immediate: false } { immediate: false }
); );
const hasEntityChanged = useCallback((ei: EntityItem) => { const hasEntityChanged = (ei: EntityItem) => {
return ( return (
ei.id !== ei.o_id || ei.id !== ei.o_id ||
ei.ram !== ei.o_ram || ei.ram !== ei.o_ram ||
@@ -86,21 +84,19 @@ const CustomEntities = () => {
ei.deleted !== ei.o_deleted || ei.deleted !== ei.o_deleted ||
(ei.value || '') !== (ei.o_value || '') (ei.value || '') !== (ei.o_value || '')
); );
}, []); };
const entity_theme = useMemo( const entity_theme = useTheme({
() => Table: `
useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 120px; --data-table-library_grid-template-columns: repeat(1, minmax(60px, 1fr)) minmax(80px, auto) 80px 80px 80px 120px;
`, `,
BaseRow: ` BaseRow: `
font-size: 14px; font-size: 14px;
.td { .td {
height: 32px; height: 32px;
} }
`, `,
BaseCell: ` BaseCell: `
&:nth-of-type(1) { &:nth-of-type(1) {
padding: 8px; padding: 8px;
} }
@@ -120,7 +116,7 @@ const CustomEntities = () => {
text-align: center; text-align: center;
} }
`, `,
HeaderRow: ` HeaderRow: `
text-transform: uppercase; text-transform: uppercase;
background-color: black; background-color: black;
color: #90CAF9; color: #90CAF9;
@@ -129,7 +125,7 @@ const CustomEntities = () => {
height: 36px; height: 36px;
} }
`, `,
Row: ` Row: `
background-color: #1e1e1e; background-color: #1e1e1e;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
@@ -140,11 +136,9 @@ const CustomEntities = () => {
background-color: #177ac9; background-color: #177ac9;
} }
` `
}), });
[]
);
const saveEntities = useCallback(async () => { const saveEntities = async () => {
await writeEntities({ await writeEntities({
entities: entities entities: entities
.filter((ei: EntityItem) => !ei.deleted) .filter((ei: EntityItem) => !ei.deleted)
@@ -173,44 +167,41 @@ const CustomEntities = () => {
await fetchEntities(); await fetchEntities();
setNumChanges(0); setNumChanges(0);
}); });
}, [entities, writeEntities, LL, fetchEntities]); };
const editEntityItem = useCallback((ei: EntityItem) => { const editEntityItem = (ei: EntityItem) => {
setCreating(false); setCreating(false);
setSelectedEntityItem(ei); setSelectedEntityItem(ei);
setDialogOpen(true); setDialogOpen(true);
}, []); };
const onDialogClose = useCallback(() => { const onDialogClose = () => {
setDialogOpen(false); setDialogOpen(false);
}, []); };
const onDialogCancel = useCallback(async () => { const onDialogCancel = async () => {
await fetchEntities().then(() => { await fetchEntities().then(() => {
setNumChanges(0); setNumChanges(0);
}); });
}, [fetchEntities]); };
const onDialogSave = useCallback( const onDialogSave = (updatedItem: EntityItem) => {
(updatedItem: EntityItem) => { setDialogOpen(false);
setDialogOpen(false); void updateState(readCustomEntities(), (data: EntityItem[]) => {
void updateState(readCustomEntities(), (data: EntityItem[]) => { const new_data = creating
const new_data = creating ? [
? [ ...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id),
...data.filter((ei) => creating || ei.o_id !== updatedItem.o_id), updatedItem
updatedItem ]
] : data.map((ei) =>
: data.map((ei) => ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei
ei.id === updatedItem.id ? { ...ei, ...updatedItem } : ei );
); setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length);
setNumChanges(new_data.filter((ei) => hasEntityChanged(ei)).length); return new_data;
return new_data; });
}); };
},
[creating, hasEntityChanged]
);
const onDialogDup = useCallback((item: EntityItem) => { const onDialogDup = (item: EntityItem) => {
setCreating(true); setCreating(true);
setSelectedEntityItem({ setSelectedEntityItem({
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID), id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
@@ -228,9 +219,9 @@ const CustomEntities = () => {
value: item.value value: item.value
}); });
setDialogOpen(true); setDialogOpen(true);
}, []); };
const addEntityItem = useCallback(() => { const addEntityItem = () => {
setCreating(true); setCreating(true);
setSelectedEntityItem({ setSelectedEntityItem({
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID), id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
@@ -248,30 +239,27 @@ const CustomEntities = () => {
value: '' value: ''
}); });
setDialogOpen(true); setDialogOpen(true);
}, []); };
const formatValue = useCallback((value: unknown, uom: number) => { const formatValue = (value: unknown, uom: number) => {
return value === undefined return value === undefined
? '' ? ''
: typeof value === 'number' : typeof value === 'number'
? new Intl.NumberFormat().format(value) + ? new Intl.NumberFormat().format(value) +
(uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`) (uom === 0 ? '' : ` ${DeviceValueUOM_s[uom]}`)
: `${value as string}${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')}`; return `0x${value.toString(16).toUpperCase().padStart(digit, '0')}`;
}, []); };
const filteredAndSortedEntities = useMemo( const filteredAndSortedEntities =
() => entities
entities ?.filter((ei: EntityItem) => !ei.deleted)
?.filter((ei: EntityItem) => !ei.deleted) .sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name)) ?? [];
.sort((a: EntityItem, b: EntityItem) => a.name.localeCompare(b.name)) ?? [],
[entities]
);
const renderEntity = useCallback(() => { const renderEntity = () => {
if (!entities) { if (!entities) {
return ( return (
<FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} /> <FormLoader onRetry={fetchEntities} errorMessage={error?.message || ''} />
@@ -328,17 +316,7 @@ const CustomEntities = () => {
)} )}
</Table> </Table>
); );
}, [ };
entities,
error,
fetchEntities,
entity_theme,
editEntityItem,
LL,
filteredAndSortedEntities,
showHex,
formatValue
]);
return ( return (
<SectionContent> <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 AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -28,7 +28,7 @@ import type { ValidateFieldsError } from 'async-validator';
import { BlockFormControlLabel, ValidatedTextField } from 'components'; import { BlockFormControlLabel, ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils'; import { numberValue, updateValue } from 'utils';
import { validate } from 'validators'; import { ValidationError, validate } from 'validators';
import { DeviceValueType, DeviceValueTypeNames, DeviceValueUOM_s } from './types'; import { DeviceValueType, DeviceValueTypeNames, DeviceValueUOM_s } from './types';
import type { EntityItem } from './types'; import type { EntityItem } from './types';
@@ -68,14 +68,10 @@ const CustomEntitiesDialog = ({
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<EntityItem>(selectedItem); const [editItem, setEditItem] = useState<EntityItem>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = useMemo( const updateFormValue = updateValue(
() => setEditItem as unknown as React.Dispatch<
updateValue( React.SetStateAction<Record<string, unknown>>
setEditItem as unknown as React.Dispatch< >
React.SetStateAction<Record<string, unknown>>
>
),
[]
); );
useEffect(() => { useEffect(() => {
@@ -105,16 +101,16 @@ const CustomEntitiesDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = useCallback( const handleClose = (
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => { _event: React.SyntheticEvent,
if (reason !== 'backdropClick') { reason: 'backdropClick' | 'escapeKeyDown'
onClose(); ) => {
} if (reason !== 'backdropClick') {
}, onClose();
[onClose] }
); };
const save = useCallback(async () => { const save = async () => {
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(validator, editItem); await validate(validator, editItem);
@@ -136,29 +132,23 @@ const CustomEntitiesDialog = ({
} }
onSave(processedItem); onSave(processedItem);
} catch (error) { } catch (error) {
setFieldErrors(error as ValidateFieldsError); setFieldErrors((error as ValidationError).fieldErrors);
} }
}, [validator, editItem, onSave]); };
const remove = useCallback(() => { const remove = () => {
const itemWithDeleted = { ...editItem, deleted: true }; onSave({ ...editItem, deleted: true });
onSave(itemWithDeleted); };
}, [editItem, onSave]);
const dup = useCallback(() => { const dup = () => {
onDup(editItem); onDup(editItem);
}, [editItem, onDup]); };
// Memoize UOM menu items to avoid recreating on every render const uomMenuItems = DeviceValueUOM_s.map((val, i) => (
const uomMenuItems = useMemo( <MenuItem key={val} value={i}>
() => {val}
DeviceValueUOM_s.map((val, i) => ( </MenuItem>
<MenuItem key={val} value={i}> ));
{val}
</MenuItem>
)),
[]
);
return ( return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}> <Dialog sx={dialogStyle} open={open} onClose={handleClose}>
@@ -215,7 +205,7 @@ const CustomEntitiesDialog = ({
name="value" name="value"
label={LL.DEFAULT(0) + ' ' + LL.VALUE(0)} label={LL.DEFAULT(0) + ' ' + LL.VALUE(0)}
type="string" type="string"
value={editItem.value as string} value={editItem.value}
variant="outlined" variant="outlined"
onChange={updateFormValue} onChange={updateFormValue}
fullWidth fullWidth
@@ -260,7 +250,7 @@ const CustomEntitiesDialog = ({
margin="normal" margin="normal"
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
type="string" type="string"
value={editItem.device_id as string} value={editItem.device_id}
onChange={updateFormValue} onChange={updateFormValue}
slotProps={{ slotProps={{
input: { input: {
@@ -280,7 +270,7 @@ const CustomEntitiesDialog = ({
margin="normal" margin="normal"
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
type="string" type="string"
value={editItem.type_id as string} value={editItem.type_id}
onChange={updateFormValue} onChange={updateFormValue}
slotProps={{ slotProps={{
input: { input: {
@@ -381,7 +371,7 @@ const CustomEntitiesDialog = ({
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors || {}}
name="factor" name="factor"
label={LL.BITMASK()} label={LL.BITMASK()}
value={editItem.factor as string} value={editItem.factor}
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
variant="outlined" variant="outlined"
onChange={updateFormValue} 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 { useBlocker, useLocation } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -171,19 +171,17 @@ const Customizations = () => {
); );
}; };
const entities_theme = useMemo( const entities_theme = useTheme({
() => Table: `
useTheme({
Table: `
--data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto); --data-table-library_grid-template-columns: 156px repeat(1, minmax(80px, 1fr)) 45px minmax(45px, auto) minmax(120px, auto);
`, `,
BaseRow: ` BaseRow: `
font-size: 14px; font-size: 14px;
.td { .td {
height: 32px; height: 32px;
} }
`, `,
BaseCell: ` BaseCell: `
&:nth-of-type(3) { &:nth-of-type(3) {
text-align: right; text-align: right;
} }
@@ -194,7 +192,7 @@ const Customizations = () => {
text-align: right; text-align: right;
} }
`, `,
HeaderRow: ` HeaderRow: `
text-transform: uppercase; text-transform: uppercase;
background-color: black; background-color: black;
color: #90CAF9; color: #90CAF9;
@@ -206,7 +204,7 @@ const Customizations = () => {
text-align: center; text-align: center;
} }
`, `,
Row: ` Row: `
background-color: #1e1e1e; background-color: #1e1e1e;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
@@ -222,7 +220,7 @@ const Customizations = () => {
background-color: #177ac9; background-color: #177ac9;
} }
`, `,
Cell: ` Cell: `
&:nth-of-type(2) { &:nth-of-type(2) {
padding: 8px; padding: 8px;
} }
@@ -236,9 +234,7 @@ const Customizations = () => {
padding-right: 8px; padding-right: 8px;
} }
` `
}), });
[]
);
function hasEntityChanged(de: DeviceEntity) { function hasEntityChanged(de: DeviceEntity) {
return ( return (
@@ -287,26 +283,23 @@ const Customizations = () => {
return value as string; return value as string;
} }
const isCommand = useCallback((de: DeviceEntity) => { const isCommand = (de: DeviceEntity) => {
return de.n && de.n[0] === '!'; return de.n && de.n[0] === '!';
}, []); };
const formatName = useCallback( const formatName = (de: DeviceEntity, withShortname: boolean) => {
(de: DeviceEntity, withShortname: boolean) => { let name: string;
let name: string; if (isCommand(de)) {
if (isCommand(de)) { name = de.t
name = de.t ? `${LL.COMMAND(1)}: ${de.t} ${de.n?.slice(1)}`
? `${LL.COMMAND(1)}: ${de.t} ${de.n?.slice(1)}` : `${LL.COMMAND(1)}: ${de.n?.slice(1)}`;
: `${LL.COMMAND(1)}: ${de.n?.slice(1)}`; } else if (de.cn && de.cn !== '') {
} else if (de.cn && de.cn !== '') { name = de.t ? `${de.t} ${de.cn}` : de.cn;
name = de.t ? `${de.t} ${de.cn}` : de.cn; } else {
} else { name = de.t ? `${de.t} ${de.n}` : de.n || '';
name = de.t ? `${de.t} ${de.n}` : de.n || ''; }
} return withShortname ? `${name} ${de.id}` : name;
return withShortname ? `${name} ${de.id}` : name; };
},
[LL]
);
const getMaskNumber = (newMask: string[]) => { const getMaskNumber = (newMask: string[]) => {
let new_mask = 0; let new_mask = 0;
@@ -336,33 +329,27 @@ const Customizations = () => {
return new_masks; return new_masks;
}; };
const filter_entity = useCallback( const filter_entity = (de: DeviceEntity) =>
(de: DeviceEntity) => (de.m & selectedFilters || !selectedFilters) &&
(de.m & selectedFilters || !selectedFilters) && formatName(de, true).toLowerCase().includes(search.toLowerCase());
formatName(de, true).toLowerCase().includes(search.toLowerCase()),
[selectedFilters, search, formatName]
);
const maskDisabled = useCallback( const maskDisabled = (set: boolean) => {
(set: boolean) => { setDeviceEntities((prev) =>
setDeviceEntities((prev) => prev.map((de) => {
prev.map((de) => { if (filter_entity(de)) {
if (filter_entity(de)) { const excludeMask =
const excludeMask = DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE;
DeviceEntityMask.DV_API_MQTT_EXCLUDE | DeviceEntityMask.DV_WEB_EXCLUDE; return {
return { ...de,
...de, m: set ? de.m | excludeMask : de.m & ~excludeMask
m: set ? de.m | excludeMask : de.m & ~excludeMask };
}; }
} return de;
return de; })
}) );
); };
},
[filter_entity]
);
const resetCustomization = useCallback(async () => { const resetCustomization = async () => {
try { try {
await sendResetCustomizations(); await sendResetCustomizations();
toast.info(LL.CUSTOMIZATIONS_RESTART()); toast.info(LL.CUSTOMIZATIONS_RESTART());
@@ -372,30 +359,27 @@ const Customizations = () => {
setConfirmReset(false); setConfirmReset(false);
setRestarting(true); setRestarting(true);
} }
}, [sendResetCustomizations, LL]); };
const onDialogClose = () => { const onDialogClose = () => {
setDialogOpen(false); setDialogOpen(false);
}; };
const updateDeviceEntity = useCallback((updatedItem: DeviceEntity) => { const updateDeviceEntity = (updatedItem: DeviceEntity) => {
setDeviceEntities( setDeviceEntities(
(prev) => (prev) =>
prev?.map((de) => prev?.map((de) =>
de.id === updatedItem.id ? { ...de, ...updatedItem } : de de.id === updatedItem.id ? { ...de, ...updatedItem } : de
) ?? [] ) ?? []
); );
}, []); };
const onDialogSave = useCallback( const onDialogSave = (updatedItem: DeviceEntity) => {
(updatedItem: DeviceEntity) => { setDialogOpen(false);
setDialogOpen(false); updateDeviceEntity(updatedItem);
updateDeviceEntity(updatedItem); };
},
[updateDeviceEntity]
);
const editDeviceEntity = useCallback((de: DeviceEntity) => { const editDeviceEntity = (de: DeviceEntity) => {
if (de.n === undefined || (de.n && de.n[0] === '!')) { if (de.n === undefined || (de.n && de.n[0] === '!')) {
return; return;
} }
@@ -406,9 +390,9 @@ const Customizations = () => {
setSelectedDeviceEntity(de); setSelectedDeviceEntity(de);
setDialogOpen(true); setDialogOpen(true);
}, []); };
const saveCustomization = useCallback(async () => { const saveCustomization = async () => {
if (!devices || !deviceEntities || selectedDevice === -1) { if (!devices || !deviceEntities || selectedDevice === -1) {
return; return;
} }
@@ -441,9 +425,9 @@ const Customizations = () => {
.finally(() => { .finally(() => {
setOriginalSettings(deviceEntities); setOriginalSettings(deviceEntities);
}); });
}, [devices, deviceEntities, selectedDevice, sendCustomizationEntities, LL]); };
const renameDevice = useCallback(async () => { const renameDevice = async () => {
await sendDeviceName({ await sendDeviceName({
id: selectedDevice, id: selectedDevice,
name: selectedDeviceName, name: selectedDeviceName,
@@ -459,14 +443,7 @@ const Customizations = () => {
setRename(false); setRename(false);
await fetchCoreData(); await fetchCoreData();
}); });
}, [ };
selectedDevice,
selectedDeviceName,
selectedDeviceBrand,
sendDeviceName,
LL,
fetchCoreData
]);
const renderDeviceList = () => ( const renderDeviceList = () => (
<> <>
@@ -562,10 +539,7 @@ const Customizations = () => {
</> </>
); );
const filteredEntities = useMemo( const filteredEntities = deviceEntities.filter((de) => filter_entity(de));
() => deviceEntities.filter((de) => filter_entity(de)),
[deviceEntities, filter_entity]
);
const renderDeviceData = () => { const renderDeviceData = () => {
return ( 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 CancelIcon from '@mui/icons-material/Cancel';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
@@ -57,23 +57,16 @@ const CustomizationsDialog = ({
const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem); const [editItem, setEditItem] = useState<DeviceEntity>(selectedItem);
const [error, setError] = useState<boolean>(false); const [error, setError] = useState<boolean>(false);
const updateFormValue = useMemo( const updateFormValue = updateValue(
() => setEditItem as unknown as React.Dispatch<
updateValue( React.SetStateAction<Record<string, unknown>>
setEditItem as unknown as React.Dispatch< >
React.SetStateAction<Record<string, unknown>>
>
),
[]
); );
const isWriteableNumber = useMemo( const isWriteableNumber =
() => typeof editItem.v === 'number' &&
typeof editItem.v === 'number' && editItem.w &&
editItem.w && !(editItem.m & DeviceEntityMask.DV_READONLY);
!(editItem.m & DeviceEntityMask.DV_READONLY),
[editItem.v, editItem.w, editItem.m]
);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@@ -82,16 +75,16 @@ const CustomizationsDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = useCallback( const handleClose = (
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => { _event: React.SyntheticEvent,
if (reason !== 'backdropClick') { reason: 'backdropClick' | 'escapeKeyDown'
onClose(); ) => {
} if (reason !== 'backdropClick') {
}, onClose();
[onClose] }
); };
const save = useCallback(() => { const save = () => {
if ( if (
isWriteableNumber && isWriteableNumber &&
editItem.mi && editItem.mi &&
@@ -102,34 +95,31 @@ const CustomizationsDialog = ({
} else { } else {
onSave(editItem); onSave(editItem);
} }
}, [isWriteableNumber, editItem, onSave]); };
const updateDeviceEntity = useCallback((updatedItem: DeviceEntity) => { const updateDeviceEntity = (updatedItem: DeviceEntity) => {
setEditItem((prev) => ({ ...prev, m: updatedItem.m })); 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 ( return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}> <Dialog sx={dialogStyle} open={open} onClose={handleClose}>
<DialogTitle>{dialogTitle}</DialogTitle> <DialogTitle>{`${LL.EDIT()} ${LL.ENTITY()}`}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<LabelValue label={LL.ID_OF(LL.ENTITY())} value={editItem.id} /> <LabelValue label={LL.ID_OF(LL.ENTITY())} value={editItem.id} />
<LabelValue <LabelValue
label={`${LL.DEFAULT(1)} ${LL.ENTITY_NAME(1)}`} label={`${LL.DEFAULT(1)} ${LL.ENTITY_NAME(1)}`}
value={editItem.n} 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 }}> <Box sx={{ mt: 1, mb: 2 }}>
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} /> <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 { IconContext } from 'react-icons/lib';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -77,40 +77,35 @@ const Dashboard = memo(() => {
} }
); );
const deviceValueDialogSave = useCallback( const deviceValueDialogSave = async (devicevalue: DeviceValue) => {
async (devicevalue: DeviceValue) => { if (!selectedDashboardItem) {
if (!selectedDashboardItem) { return;
return; }
} const id = selectedDashboardItem.parentNode.id; // this is the parent ID
const id = selectedDashboardItem.parentNode.id; // this is the parent ID await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v })
await sendDeviceValue({ id, c: devicevalue.c ?? '', v: devicevalue.v }) .then(() => {
.then(() => { toast.success(LL.WRITE_CMD_SENT());
toast.success(LL.WRITE_CMD_SENT()); })
}) .catch((error: Error) => {
.catch((error: Error) => { toast.error(error.message);
toast.error(error.message); })
}) .finally(() => {
.finally(() => { setDeviceValueDialogOpen(false);
setDeviceValueDialogOpen(false); setSelectedDashboardItem(undefined);
setSelectedDashboardItem(undefined); });
}); };
},
[selectedDashboardItem, sendDeviceValue, LL]
);
const dashboard_theme = useMemo( const dashboard_theme = useTheme({
() => Table: `
useTheme({
Table: `
--data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px; --data-table-library_grid-template-columns: minmax(80px, auto) 120px 32px;
`, `,
BaseRow: ` BaseRow: `
font-size: 14px; font-size: 14px;
.td { .td {
height: 28px; height: 28px;
} }
`, `,
Row: ` Row: `
cursor: pointer; cursor: pointer;
background-color: #1e1e1e; background-color: #1e1e1e;
&:nth-of-type(odd) .td { &:nth-of-type(odd) .td {
@@ -120,7 +115,7 @@ const Dashboard = memo(() => {
background-color: #177ac9; background-color: #177ac9;
}, },
`, `,
BaseCell: ` BaseCell: `
&:nth-of-type(2) { &:nth-of-type(2) {
text-align: right; text-align: right;
} }
@@ -128,9 +123,7 @@ const Dashboard = memo(() => {
text-align: right; text-align: right;
} }
` `
}), });
[]
);
const tree = useTree( const tree = useTree(
{ nodes: [...data.nodes] }, { nodes: [...data.nodes] },
@@ -164,79 +157,64 @@ const Dashboard = memo(() => {
} }
}); });
const nodeIds = useMemo(
() => data.nodes.map((item: DashboardItem) => item.id),
[data.nodes]
);
useEffect(() => { useEffect(() => {
const nodeIds = data.nodes.map((item: DashboardItem) => item.id);
showAll showAll
? tree.fns.onAddAll(nodeIds) // expand tree ? tree.fns.onAddAll(nodeIds) // expand tree
: tree.fns.onRemoveAll(); // collapse tree : tree.fns.onRemoveAll(); // collapse tree
}, [parentNodes]); }, [parentNodes]);
const showType = useCallback( const showType = (n?: string, t?: number) => {
(n?: string, t?: number) => { // if we have a name show it
// if we have a name show it if (n) {
if (n) { return 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 return '';
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]
);
const showName = useCallback( const showName = (di: DashboardItem) => {
(di: DashboardItem) => { if (di.id < 100) {
if (di.id < 100) { // if its a device (parent node) and has entities
// if its a device (parent node) and has entities if (di.nodes?.length) {
if (di.nodes?.length) { return (
return ( <span style={{ fontSize: '15px' }}>
<span style={{ fontSize: '15px' }}> <DeviceIcon type_id={di.t ?? 0} />
<DeviceIcon type_id={di.t ?? 0} /> &nbsp;&nbsp;{showType(di.n, di.t)}
&nbsp;&nbsp;{showType(di.n, di.t)} <span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span>
<span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span> </span>
</span> );
);
}
} }
if (di.dv) { }
return <span>{di.dv.id.slice(2)}</span>; if (di.dv) {
} return <span>{di.dv.id.slice(2)}</span>;
return null; }
}, return null;
[showType] };
);
const hasMask = useCallback( const hasMask = (id: string, mask: number) =>
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask, (parseInt(id.slice(0, 2), 16) & mask) === mask;
[]
);
const editDashboardValue = useCallback( const editDashboardValue = (di: DashboardItem) => {
(di: DashboardItem) => { if (me.admin && di.dv?.c) {
if (me.admin && di.dv?.c) { setSelectedDashboardItem(di);
setSelectedDashboardItem(di); setDeviceValueDialogOpen(true);
setDeviceValueDialogOpen(true); }
} };
},
[me.admin]
);
const handleShowAll = ( const handleShowAll = (
_event: React.MouseEvent<HTMLElement>, _event: React.MouseEvent<HTMLElement>,
@@ -248,10 +226,9 @@ const Dashboard = memo(() => {
} }
}; };
const hasFavEntities = useMemo( const hasFavEntities = data.nodes.filter(
() => data.nodes.filter((item: DashboardItem) => item.id <= 90).length, (item: DashboardItem) => item.id <= 90
[data.nodes] ).length;
);
const renderContent = () => { const renderContent = () => {
if (!data) { if (!data) {

View File

@@ -4,7 +4,6 @@ import {
useContext, useContext,
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useMemo,
useState useState
} from 'react'; } from 'react';
import { IconContext } from 'react-icons'; import { IconContext } from 'react-icons';
@@ -133,21 +132,19 @@ const Devices = memo(() => {
}; };
}, []); }, []);
const leftOffset = useCallback(() => { const leftOffset = () => {
const devicesWindow = document.getElementById('devices-window'); const devicesWindow = document.getElementById('devices-window');
if (!devicesWindow) return 0; if (!devicesWindow) return 0;
const { left, right } = devicesWindow.getBoundingClientRect(); const { left, right } = devicesWindow.getBoundingClientRect();
if (!left || !right) return 0; if (!left || !right) return 0;
return left + (right - left < 400 ? 0 : 200); return left + (right - left < 400 ? 0 : 200);
}, []); };
const common_theme = useMemo( const common_theme = useTheme({
() => BaseRow: `
useTheme({
BaseRow: `
font-size: 14px; font-size: 14px;
`, `,
HeaderRow: ` HeaderRow: `
text-transform: uppercase; text-transform: uppercase;
background-color: black; background-color: black;
color: #90CAF9; color: #90CAF9;
@@ -155,7 +152,7 @@ const Devices = memo(() => {
border-bottom: 1px solid #565656; border-bottom: 1px solid #565656;
} }
`, `,
Row: ` Row: `
cursor: pointer; cursor: pointer;
background-color: #1E1E1E; background-color: #1E1E1E;
.td { .td {
@@ -165,88 +162,78 @@ const Devices = memo(() => {
background-color: #177ac9; background-color: #177ac9;
} }
` `
}), });
[]
);
const device_theme = useMemo( const device_theme = useTheme([
() => common_theme,
useTheme([ {
common_theme, BaseRow: `
{ font-size: 15px;
BaseRow: ` .td {
font-size: 15px; height: 28px;
.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;
} }
`, `,
BaseRow: ` Table: `
.td { --data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
height: 32px; `,
} HeaderRow: `
`, .th {
BaseCell: ` padding: 8px;
&:nth-of-type(1) { `,
border-left: 1px solid #177ac9; Row: `
}, &:nth-of-type(odd) .td {
&: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; background-color: #303030;
}, },
&:hover .td { &:hover .td {
background-color: #177ac9; 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;
} }
` `,
} BaseRow: `
]), .td {
[common_theme] 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) => { const getSortIcon = (state: State, sortKey: unknown) => {
if (state.sortKey === sortKey && state.reverse) { if (state.sortKey === sortKey && state.reverse) {
@@ -345,10 +332,8 @@ const Devices = memo(() => {
return sc; return sc;
}; };
const hasMask = useCallback( const hasMask = (id: string, mask: number) =>
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask, (parseInt(id.slice(0, 2), 16) & mask) === mask;
[]
);
const handleDownloadCsv = () => { const handleDownloadCsv = () => {
const deviceIndex = coreData.devices.findIndex( const deviceIndex = coreData.devices.findIndex(
@@ -607,41 +592,35 @@ const Devices = memo(() => {
return; return;
} }
const showDeviceValue = useCallback((dv: DeviceValue) => { const showDeviceValue = (dv: DeviceValue) => {
setSelectedDeviceValue(dv); setSelectedDeviceValue(dv);
setDeviceValueDialogOpen(true); setDeviceValueDialogOpen(true);
}, []); };
const renderNameCell = useCallback( const renderNameCell = (dv: DeviceValue) => (
(dv: DeviceValue) => ( <>
<> {dv.id.slice(2)}&nbsp;
{dv.id.slice(2)}&nbsp; {hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && ( <StarIcon color="primary" sx={{ fontSize: 12 }} />
<StarIcon color="primary" sx={{ fontSize: 12 }} /> )}
)} {hasMask(dv.id, DeviceEntityMask.DV_READONLY) && (
{hasMask(dv.id, DeviceEntityMask.DV_READONLY) && ( <EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
<EditOffOutlinedIcon color="primary" sx={{ fontSize: 12 }} /> )}
)} {hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && (
{hasMask(dv.id, DeviceEntityMask.DV_API_MQTT_EXCLUDE) && ( <CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} /> )}
)} </>
</>
),
[hasMask]
); );
const shown_data = useMemo(() => { const shown_data = onlyFav
if (onlyFav) { ? deviceData.nodes.filter(
return deviceData.nodes.filter(
(dv: DeviceValue) => (dv: DeviceValue) =>
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
dv.id.slice(2).toLowerCase().includes(search.toLowerCase()) 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( const deviceIndex = coreData.devices.findIndex(
(d: Device) => d.id === device_select.state.id (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 CancelIcon from '@mui/icons-material/Cancel';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
@@ -24,7 +24,7 @@ import type { ValidateFieldsError } from 'async-validator';
import { ValidatedTextField } from 'components'; import { ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils'; import { numberValue, updateValue } from 'utils';
import { validate } from 'validators'; import { ValidationError, validate } from 'validators';
import { DeviceValueUOM, DeviceValueUOM_s } from './types'; import { DeviceValueUOM, DeviceValueUOM_s } from './types';
import type { DeviceValue } from './types'; import type { DeviceValue } from './types';
@@ -52,7 +52,7 @@ const DevicesDialog = ({
const [editItem, setEditItem] = useState<DeviceValue>(selectedItem); const [editItem, setEditItem] = useState<DeviceValue>(selectedItem);
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const updateFormValue = useMemo(() => updateValue(setEditItem), [setEditItem]); const updateFormValue = updateValue(setEditItem);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@@ -61,36 +61,33 @@ const DevicesDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const save = useCallback(async () => { const save = async () => {
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(validator, editItem); await validate(validator, editItem);
onSave(editItem); onSave(editItem);
} catch (error) { } catch (error) {
setFieldErrors(error as ValidateFieldsError); setFieldErrors((error as ValidationError).fieldErrors);
} }
}, [validator, editItem, onSave]); };
const setUom = useCallback( const setUom = (uom?: DeviceValueUOM) => {
(uom?: DeviceValueUOM) => { if (uom === undefined) {
if (uom === undefined) { return;
return; }
} switch (uom) {
switch (uom) { case DeviceValueUOM.HOURS:
case DeviceValueUOM.HOURS: return LL.HOURS();
return LL.HOURS(); case DeviceValueUOM.MINUTES:
case DeviceValueUOM.MINUTES: return LL.MINUTES();
return LL.MINUTES(); case DeviceValueUOM.SECONDS:
case DeviceValueUOM.SECONDS: return LL.SECONDS();
return LL.SECONDS(); default:
default: return DeviceValueUOM_s[uom];
return DeviceValueUOM_s[uom]; }
} };
},
[LL]
);
const showHelperText = useCallback((dv: DeviceValue) => { const showHelperText = (dv: DeviceValue) => {
if (dv.h) return dv.h; if (dv.h) return dv.h;
if (dv.l) return dv.l.join(' | '); if (dv.l) return dv.l.join(' | ');
if (dv.m !== undefined && dv.x !== undefined) { if (dv.m !== undefined && dv.x !== undefined) {
@@ -101,26 +98,16 @@ const DevicesDialog = ({
); );
} }
return undefined; return undefined;
}, []); };
const isCommand = useMemo( const isCommand = selectedItem.v === '' && selectedItem.c;
() => selectedItem.v === '' && selectedItem.c, const dialogTitle = isCommand
[selectedItem.v, selectedItem.c] ? LL.RUN_COMMAND()
); : writeable
? LL.CHANGE_VALUE()
const dialogTitle = useMemo(() => { : LL.VALUE(0);
if (isCommand) return LL.RUN_COMMAND(); const buttonLabel = isCommand ? LL.EXECUTE() : LL.UPDATE();
return writeable ? LL.CHANGE_VALUE() : LL.VALUE(0); const helperText = showHelperText(editItem);
}, [isCommand, writeable, LL]);
const buttonLabel = useMemo(() => {
return isCommand ? LL.EXECUTE() : LL.UPDATE();
}, [isCommand, LL]);
const helperText = useMemo(
() => showHelperText(editItem),
[editItem, showHelperText]
);
const valueLabel = LL.VALUE(0); const valueLabel = LL.VALUE(0);

View File

@@ -1,5 +1,3 @@
import { useCallback, useMemo } from 'react';
import { ToggleButton, ToggleButtonGroup } from '@mui/material'; import { ToggleButton, ToggleButtonGroup } from '@mui/material';
import OptionIcon from './OptionIcon'; import OptionIcon from './OptionIcon';
@@ -11,7 +9,6 @@ interface EntityMaskToggleProps {
de: DeviceEntity; de: DeviceEntity;
} }
// Available mask values
const MASK_VALUES = [ const MASK_VALUES = [
DeviceEntityMask.DV_WEB_EXCLUDE, // 1 DeviceEntityMask.DV_WEB_EXCLUDE, // 1
DeviceEntityMask.DV_API_MQTT_EXCLUDE, // 2 DeviceEntityMask.DV_API_MQTT_EXCLUDE, // 2
@@ -20,123 +17,95 @@ const MASK_VALUES = [
DeviceEntityMask.DV_DELETED // 128 DeviceEntityMask.DV_DELETED // 128
]; ];
/** const getMaskNumber = (newMask: string[]): number =>
* Converts an array of mask strings to a bitmask number newMask.reduce((mask, entry) => mask | Number(entry), 0);
*/
const getMaskNumber = (newMask: string[]): number => {
return newMask.reduce((mask, entry) => mask | Number(entry), 0);
};
/** const getMaskString = (mask: number): string[] =>
* Converts a bitmask number to an array of mask strings MASK_VALUES.filter((value) => (mask & value) === value).map((value) =>
*/
const getMaskString = (mask: number): string[] => {
return MASK_VALUES.filter((value) => (mask & value) === value).map((value) =>
String(value) String(value)
); );
};
/**
* Checks if a specific mask bit is set
*/
const hasMask = (mask: number, flag: number): boolean => (mask & flag) === flag; const hasMask = (mask: number, flag: number): boolean => (mask & flag) === flag;
const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => { const EntityMaskToggle = ({ onUpdate, de }: EntityMaskToggleProps) => {
const handleChange = useCallback( const handleChange = (_event: unknown, mask: string[]) => {
(_event: unknown, mask: string[]) => { const newMask = getMaskNumber(mask);
// Convert selected masks to a number const updatedDe = { ...de };
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 entity has no name and is set to readonly, also exclude from web if (updatedDe.n === '' && hasMask(newMask, DeviceEntityMask.DV_READONLY)) {
if (updatedDe.n === '' && hasMask(newMask, DeviceEntityMask.DV_READONLY)) { updatedDe.m = newMask | DeviceEntityMask.DV_WEB_EXCLUDE;
updatedDe.m = newMask | DeviceEntityMask.DV_WEB_EXCLUDE; } else {
} else { updatedDe.m = newMask;
updatedDe.m = newMask; }
}
// If excluded from web, cannot be favorite // If excluded from web, cannot be favorite
if (hasMask(updatedDe.m, DeviceEntityMask.DV_WEB_EXCLUDE)) { if (hasMask(updatedDe.m, DeviceEntityMask.DV_WEB_EXCLUDE)) {
updatedDe.m = updatedDe.m & ~DeviceEntityMask.DV_FAVORITE; updatedDe.m = updatedDe.m & ~DeviceEntityMask.DV_FAVORITE;
} }
onUpdate(updatedDe); 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]
);
return ( return (
<ToggleButtonGroup <ToggleButtonGroup
size="small" size="small"
color="secondary" color="secondary"
value={maskStringValue} value={getMaskString(de.m)}
onChange={handleChange} onChange={handleChange}
> >
<ToggleButton value="8" disabled={isFavoriteDisabled}> <ToggleButton
<OptionIcon type="favorite" isSet={isFavoriteSet} /> 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>
<ToggleButton value="4" disabled={isReadonlyDisabled}> <ToggleButton
<OptionIcon type="readonly" isSet={isReadonlySet} /> 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>
<ToggleButton value="2" disabled={isApiMqttExcludeDisabled}> <ToggleButton
<OptionIcon type="api_mqtt_exclude" isSet={isApiMqttExcludeSet} /> 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>
<ToggleButton value="1" disabled={isWebExcludeDisabled}> <ToggleButton
<OptionIcon type="web_exclude" isSet={isWebExcludeSet} /> 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>
<ToggleButton value="128"> <ToggleButton value="128">
<OptionIcon type="deleted" isSet={isDeletedSet} /> <OptionIcon
type="deleted"
isSet={hasMask(de.m, DeviceEntityMask.DV_DELETED)}
/>
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </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 type { ReactElement } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -60,6 +60,8 @@ const AVATAR_STYLES: SxProps<Theme> = {
bgcolor: '#72caf9' bgcolor: '#72caf9'
}; };
const SYSTEM_INFO_API: APIcall = { device: 'system', cmd: 'info', id: 0 };
const HelpComponent = () => { const HelpComponent = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle(LL.HELP()); useLayoutTitle(LL.HELP());
@@ -72,12 +74,7 @@ const HelpComponent = () => {
}); });
const [imgError, setImgError] = useState<boolean>(false); const [imgError, setImgError] = useState<boolean>(false);
const getCustomSupportMethod = useMemo( useRequest(callAction({ action: 'getCustomSupport' })).onSuccess((event) => {
() => callAction({ action: 'getCustomSupport' }),
[]
);
useRequest(getCustomSupportMethod).onSuccess((event) => {
if (event?.data && Object.keys(event.data).length !== 0) { if (event?.data && Object.keys(event.data).length !== 0) {
const { Support } = event.data as { const { Support } = event.data as {
Support: { img_url?: string; html?: string[] }; Support: { img_url?: string; html?: string[] };
@@ -100,47 +97,26 @@ const HelpComponent = () => {
toast.error(String(error.error?.message || 'An error occurred')); toast.error(String(error.error?.message || 'An error occurred'));
}); });
// Optimize API call memoization const helpLinks: HelpLink[] = [
const apiCall = useMemo(() => ({ device: 'system', cmd: 'info', id: 0 }), []); {
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(() => { const imageSrc =
void sendAPI(apiCall); imgError || !customSupport.img_url ? DEFAULT_IMAGE_URL : customSupport.img_url;
}, [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]
);
return ( return (
<SectionContent> <SectionContent>
@@ -157,13 +133,13 @@ const HelpComponent = () => {
component="img" component="img"
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
sx={IMAGE_STYLES} sx={IMAGE_STYLES}
onError={handleImageError} onError={() => setImgError(true)}
src={imageSrc} src={imageSrc}
/> />
</Stack> </Stack>
)} )}
{isAdmin && ( {me?.admin && (
<List> <List>
{helpLinks.map(({ href, icon, label }) => ( {helpLinks.map(({ href, icon, label }) => (
<ListItem key={href}> <ListItem key={href}>
@@ -191,7 +167,7 @@ const HelpComponent = () => {
startIcon={<DownloadIcon />} startIcon={<DownloadIcon />}
variant="outlined" variant="outlined"
color="primary" color="primary"
onClick={handleDownloadSystemInfo} onClick={() => void sendAPI(SYSTEM_INFO_API)}
> >
{LL.SUPPORT_INFORMATION(0)} {LL.SUPPORT_INFORMATION(0)}
</Button> </Button>
@@ -214,7 +190,6 @@ const HelpComponent = () => {
); );
}; };
// Memoize the component to prevent unnecessary re-renders
const Help = memo(HelpComponent); const Help = memo(HelpComponent);
export default Help; 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 { useBlocker } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -69,58 +69,53 @@ const Modules = () => {
} }
); );
const modules_theme = useTheme( const modules_theme = useTheme({
useMemo( Table: `
() => ({ --data-table-library_grid-template-columns: 48px 180px 120px 100px repeat(1, minmax(160px, 1fr)) 180px;
Table: ` `,
--data-table-library_grid-template-columns: 48px 180px 120px 100px repeat(1, minmax(160px, 1fr)) 180px; BaseRow: `
`, font-size: 14px;
BaseRow: ` .td {
font-size: 14px; height: 32px;
.td { }
height: 32px; `,
} BaseCell: `
`, &:nth-of-type(1) {
BaseCell: ` text-align: center;
&:nth-of-type(1) { }
text-align: center; `,
} HeaderRow: `
`, text-transform: uppercase;
HeaderRow: ` background-color: black;
text-transform: uppercase; color: #90CAF9;
background-color: black; .th {
color: #90CAF9; border-bottom: 1px solid #565656;
.th { height: 36px;
border-bottom: 1px solid #565656; }
height: 36px; `,
} Row: `
`, background-color: #1e1e1e;
Row: ` position: relative;
background-color: #1e1e1e; cursor: pointer;
position: relative; .td {
cursor: pointer; border-top: 1px solid #565656;
.td { border-bottom: 1px solid #565656;
border-top: 1px solid #565656; }
border-bottom: 1px solid #565656; &:hover .td {
} border-top: 1px solid #177ac9;
&:hover .td { border-bottom: 1px solid #177ac9;
border-top: 1px solid #177ac9; }
border-bottom: 1px solid #177ac9; &:nth-of-type(odd) .td {
} background-color: #303030;
&:nth-of-type(odd) .td { }
background-color: #303030; `
} });
`
}),
[]
)
);
const onDialogClose = useCallback(() => { const onDialogClose = () => {
setDialogOpen(false); setDialogOpen(false);
}, []); };
const updateModuleItem = useCallback((updatedItem: ModuleItem) => { const updateModuleItem = (updatedItem: ModuleItem) => {
void updateState(readModules(), (data: ModuleItem[]) => { void updateState(readModules(), (data: ModuleItem[]) => {
const new_data = data.map((mi) => const new_data = data.map((mi) =>
mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi mi.id === updatedItem.id ? { ...mi, ...updatedItem } : mi
@@ -128,28 +123,25 @@ const Modules = () => {
setNumChanges(new_data.filter(hasModulesChanged).length); setNumChanges(new_data.filter(hasModulesChanged).length);
return new_data; return new_data;
}); });
}, []); };
const onDialogSave = useCallback( const onDialogSave = (updatedItem: ModuleItem) => {
(updatedItem: ModuleItem) => { setDialogOpen(false);
setDialogOpen(false); updateModuleItem(updatedItem);
updateModuleItem(updatedItem); };
},
[updateModuleItem]
);
const editModuleItem = useCallback((mi: ModuleItem) => { const editModuleItem = (mi: ModuleItem) => {
setSelectedModuleItem(mi); setSelectedModuleItem(mi);
setDialogOpen(true); setDialogOpen(true);
}, []); };
const onCancel = useCallback(async () => { const onCancel = async () => {
await fetchModules().then(() => { await fetchModules().then(() => {
setNumChanges(0); setNumChanges(0);
}); });
}, [fetchModules]); };
const saveModules = useCallback(async () => { const saveModules = async () => {
try { try {
await Promise.all( await Promise.all(
modules.map((condensed_mi: ModuleItem) => modules.map((condensed_mi: ModuleItem) =>
@@ -167,9 +159,9 @@ const Modules = () => {
await fetchModules(); await fetchModules();
setNumChanges(0); setNumChanges(0);
} }
}, [modules, updateModules, LL, fetchModules]); };
const content = useMemo(() => { const renderContent = () => {
if (!modules) { if (!modules) {
return ( return (
<FormLoader onRetry={fetchModules} errorMessage={error?.message || ''} /> <FormLoader onRetry={fetchModules} errorMessage={error?.message || ''} />
@@ -262,22 +254,12 @@ const Modules = () => {
</Box> </Box>
</> </>
); );
}, [ };
modules,
fetchModules,
error,
modules_theme,
editModuleItem,
LL,
numChanges,
onCancel,
saveModules
]);
return ( return (
<SectionContent> <SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{content} {renderContent()}
{selectedModuleItem && ( {selectedModuleItem && (
<ModulesDialog <ModulesDialog
open={dialogOpen} 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 CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
@@ -37,14 +37,10 @@ const ModulesDialog = ({
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [editItem, setEditItem] = useState<ModuleItem>(selectedItem); const [editItem, setEditItem] = useState<ModuleItem>(selectedItem);
const updateFormValue = useMemo( const updateFormValue = updateValue(
() => setEditItem as unknown as React.Dispatch<
updateValue( React.SetStateAction<Record<string, unknown>>
setEditItem as unknown as React.Dispatch< >
React.SetStateAction<Record<string, unknown>>
>
),
[]
); );
// Sync form state when dialog opens or selected item changes // Sync form state when dialog opens or selected item changes
@@ -54,18 +50,13 @@ const ModulesDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleSave = useCallback(() => { const handleSave = () => {
onSave(editItem); onSave(editItem);
}, [editItem, onSave]); };
const dialogTitle = useMemo(
() => `${LL.EDIT()} ${editItem.key}`,
[LL, editItem.key]
);
return ( return (
<Dialog sx={dialogStyle} fullWidth maxWidth="xs" open={open} onClose={onClose}> <Dialog sx={dialogStyle} fullWidth maxWidth="xs" open={open} onClose={onClose}>
<DialogTitle>{dialogTitle}</DialogTitle> <DialogTitle>{`${LL.EDIT()} ${editItem.key}`}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Grid container> <Grid container>
<BlockFormControlLabel <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 { useBlocker } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -132,7 +132,7 @@ const Scheduler = () => {
} }
); );
const hasScheduleChanged = useCallback((si: ScheduleItem) => { const hasScheduleChanged = (si: ScheduleItem) => {
return ( return (
si.id !== si.o_id || si.id !== si.o_id ||
(si.name || '') !== (si.o_name || '') || (si.name || '') !== (si.o_name || '') ||
@@ -143,15 +143,13 @@ const Scheduler = () => {
si.cmd !== si.o_cmd || si.cmd !== si.o_cmd ||
si.value !== si.o_value si.value !== si.o_value
); );
}, []); };
const intervalCallback = useCallback(() => { useInterval(() => {
if (numChanges === 0) { if (numChanges === 0) {
void fetchSchedule(); void fetchSchedule();
} }
}, [numChanges, fetchSchedule]); }, INTERVAL_DELAY);
useInterval(intervalCallback, INTERVAL_DELAY);
useEffect(() => { useEffect(() => {
const formatter = new Intl.DateTimeFormat(locale, { const formatter = new Intl.DateTimeFormat(locale, {
@@ -169,7 +167,7 @@ const Scheduler = () => {
const schedule_theme = useTheme(scheduleTheme); const schedule_theme = useTheme(scheduleTheme);
const saveSchedule = useCallback(async () => { const saveSchedule = async () => {
try { try {
await updateSchedule({ await updateSchedule({
schedule: schedule schedule: schedule
@@ -192,46 +190,43 @@ const Scheduler = () => {
await fetchSchedule(); await fetchSchedule();
setNumChanges(0); setNumChanges(0);
} }
}, [LL, schedule, updateSchedule, fetchSchedule]); };
const editScheduleItem = useCallback((si: ScheduleItem) => { const editScheduleItem = (si: ScheduleItem) => {
setCreating(false); setCreating(false);
setSelectedScheduleItem(si); setSelectedScheduleItem(si);
setDialogOpen(true); setDialogOpen(true);
if (si.o_name === undefined) { if (si.o_name === undefined) {
si.o_name = si.name; si.o_name = si.name;
} }
}, []); };
const onDialogClose = useCallback(() => { const onDialogClose = () => {
setDialogOpen(false); setDialogOpen(false);
}, []); };
const onDialogCancel = useCallback(async () => { const onDialogCancel = async () => {
await fetchSchedule().then(() => { await fetchSchedule().then(() => {
setNumChanges(0); setNumChanges(0);
}); });
}, [fetchSchedule]); };
const onDialogSave = useCallback( const onDialogSave = (updatedItem: ScheduleItem) => {
(updatedItem: ScheduleItem) => { setDialogOpen(false);
setDialogOpen(false); void updateState(readSchedule(), (data: ScheduleItem[]) => {
void updateState(readSchedule(), (data: ScheduleItem[]) => { const new_data = creating
const new_data = creating ? [...data, updatedItem]
? [...data, updatedItem] : data.map((si) =>
: data.map((si) => si.id === updatedItem.id ? { ...si, ...updatedItem } : 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; return new_data;
}); });
}, };
[creating, hasScheduleChanged]
);
const addScheduleItem = useCallback(() => { const addScheduleItem = () => {
setCreating(true); setCreating(true);
const newItem: ScheduleItem = { const newItem: ScheduleItem = {
id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID), id: Math.floor(Math.random() * (MAX_ID - MIN_ID) + MIN_ID),
@@ -239,36 +234,29 @@ const Scheduler = () => {
}; };
setSelectedScheduleItem(newItem); setSelectedScheduleItem(newItem);
setDialogOpen(true); setDialogOpen(true);
}, []); };
const filteredAndSortedSchedule = useMemo( const filteredAndSortedSchedule = schedule
() => .filter((si: ScheduleItem) => !si.deleted)
schedule .sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags);
.filter((si: ScheduleItem) => !si.deleted)
.sort((a: ScheduleItem, b: ScheduleItem) => a.flags - b.flags),
[schedule]
);
const dayBox = useCallback( const dayBox = (si: ScheduleItem, flag: number) => {
(si: ScheduleItem, flag: number) => { const dayIndex = Math.log(flag) / LOG_2;
const dayIndex = Math.log(flag) / LOG_2; const isActive = (si.flags & flag) === flag;
const isActive = (si.flags & flag) === flag;
return ( return (
<> <>
<Box> <Box>
<Typography sx={{ fontSize: 11 }} color={isActive ? 'primary' : 'grey'}> <Typography sx={{ fontSize: 11 }} color={isActive ? 'primary' : 'grey'}>
{dow[dayIndex]} {dow[dayIndex]}
</Typography> </Typography>
</Box> </Box>
<Divider orientation="vertical" flexItem /> <Divider orientation="vertical" flexItem />
</> </>
); );
}, };
[dow]
);
const scheduleType = useCallback((si: ScheduleItem) => { const scheduleType = (si: ScheduleItem) => {
const label = scheduleTypeLabels[si.flags]; const label = scheduleTypeLabels[si.flags];
return ( return (
@@ -278,9 +266,9 @@ const Scheduler = () => {
</Typography> </Typography>
</Box> </Box>
); );
}, []); };
const renderSchedule = useCallback(() => { const renderSchedule = () => {
if (!schedule) { if (!schedule) {
return ( return (
<FormLoader onRetry={fetchSchedule} errorMessage={error?.message || ''} /> <FormLoader onRetry={fetchSchedule} errorMessage={error?.message || ''} />
@@ -343,17 +331,7 @@ const Scheduler = () => {
)} )}
</Table> </Table>
); );
}, [ };
schedule,
error,
fetchSchedule,
filteredAndSortedSchedule,
schedule_theme,
editScheduleItem,
LL,
dayBox,
scheduleType
]);
return ( return (
<SectionContent> <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 AddIcon from '@mui/icons-material/Add';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -26,7 +26,7 @@ import type { ValidateFieldsError } from 'async-validator';
import { BlockFormControlLabel, ValidatedTextField } from 'components'; import { BlockFormControlLabel, ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { updateValue } from 'utils'; import { updateValue } from 'utils';
import { validate } from 'validators'; import { ValidationError, validate } from 'validators';
import { ScheduleFlag } from './types'; import { ScheduleFlag } from './types';
import type { ScheduleItem } from './types'; import type { ScheduleItem } from './types';
@@ -60,6 +60,12 @@ const FLAG_VALUES = [
ScheduleFlag.SCHEDULE_SAT ScheduleFlag.SCHEDULE_SAT
] as const; ] 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 { interface SchedulerDialogProps {
open: boolean; open: boolean;
creating: boolean; creating: boolean;
@@ -84,14 +90,10 @@ const SchedulerDialog = ({
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const [scheduleType, setScheduleType] = useState<ScheduleFlag>(); const [scheduleType, setScheduleType] = useState<ScheduleFlag>();
const updateFormValue = useMemo( const updateFormValue = updateValue(
() => setEditItem as unknown as React.Dispatch<
updateValue( React.SetStateAction<Record<string, unknown>>
setEditItem as unknown as React.Dispatch< >
React.SetStateAction<Record<string, unknown>>
>
),
[]
); );
useEffect(() => { useEffect(() => {
@@ -112,129 +114,95 @@ const SchedulerDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
// Helper function to handle save operations const handleSave = async (itemToSave: ScheduleItem) => {
const handleSave = useCallback( try {
async (itemToSave: ScheduleItem) => { setFieldErrors(undefined);
try { await validate(validator, itemToSave);
setFieldErrors(undefined); onSave(itemToSave);
await validate(validator, itemToSave); } catch (error) {
onSave(itemToSave); setFieldErrors((error as ValidationError).fieldErrors);
} 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;
} }
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_TIMER) return LL.TIMER(1);
if (scheduleType === ScheduleFlag.SCHEDULE_CONDITION) return LL.CONDITION(); if (scheduleType === ScheduleFlag.SCHEDULE_CONDITION) return LL.CONDITION();
if (scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE) return LL.ONCHANGE(); if (scheduleType === ScheduleFlag.SCHEDULE_ONCHANGE) return LL.ONCHANGE();
if (scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE) return LL.IMMEDIATE(); if (scheduleType === ScheduleFlag.SCHEDULE_IMMEDIATE) return LL.IMMEDIATE();
return LL.TIME(1); return LL.TIME(1);
}, [scheduleType, LL]); })();
return ( return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}> <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 { toast } from 'react-toastify';
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined'; import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
@@ -158,18 +158,16 @@ const Sensors = () => {
} }
); );
const intervalCallback = useCallback(() => { useInterval(() => {
if (!temperatureDialogOpen && !analogDialogOpen) { if (!temperatureDialogOpen && !analogDialogOpen) {
void fetchSensorData(); void fetchSensorData();
} }
}, [temperatureDialogOpen, analogDialogOpen, fetchSensorData]); });
useInterval(intervalCallback);
const temperature_theme = useTheme([common_theme, temperature_theme_config]); const temperature_theme = useTheme([common_theme, temperature_theme_config]);
const analog_theme = useTheme([common_theme, analog_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) { if (state.sortKey === sortKey && state.reverse) {
return <KeyboardArrowDownOutlinedIcon />; return <KeyboardArrowDownOutlinedIcon />;
} }
@@ -177,7 +175,7 @@ const Sensors = () => {
return <KeyboardArrowUpOutlinedIcon />; return <KeyboardArrowUpOutlinedIcon />;
} }
return <UnfoldMoreOutlinedIcon />; return <UnfoldMoreOutlinedIcon />;
}, []); };
const analog_sort = useSort( const analog_sort = useSort(
{ nodes: sensorData.as }, { nodes: sensorData.as },
@@ -234,121 +232,106 @@ const Sensors = () => {
useLayoutTitle(LL.SENSORS()); useLayoutTitle(LL.SENSORS());
const formatDurationMin = useCallback( const formatDurationMin = (duration_min: number) => {
(duration_min: number) => { const totalMs = duration_min * MS_PER_MINUTE;
const totalMs = duration_min * MS_PER_MINUTE; const days = Math.trunc(totalMs / MS_PER_DAY);
const days = Math.trunc(totalMs / MS_PER_DAY); const hours = Math.trunc(totalMs / MS_PER_HOUR) % 24;
const hours = Math.trunc(totalMs / MS_PER_HOUR) % 24; const minutes = Math.trunc(totalMs / MS_PER_MINUTE) % 60;
const minutes = Math.trunc(totalMs / MS_PER_MINUTE) % 60;
const parts: string[] = []; const parts: string[] = [];
if (days > 0) { if (days > 0) {
parts.push(LL.NUM_DAYS({ num: days })); parts.push(LL.NUM_DAYS({ num: days }));
} }
if (hours > 0) { if (hours > 0) {
parts.push(LL.NUM_HOURS({ num: hours })); parts.push(LL.NUM_HOURS({ num: hours }));
} }
if (minutes > 0) { if (minutes > 0) {
parts.push(LL.NUM_MINUTES({ num: minutes })); parts.push(LL.NUM_MINUTES({ num: minutes }));
} }
return parts.join(' '); return parts.join(' ');
}, };
[LL]
);
const formatValue = useCallback( const formatValue = (value: unknown, uom: DeviceValueUOM) => {
(value: unknown, uom: DeviceValueUOM) => { if (value === undefined) {
if (value === undefined) { return '';
return ''; }
} if (typeof value !== 'number') {
if (typeof value !== 'number') { return value as string;
return value as string; }
} switch (uom) {
switch (uom) { case DeviceValueUOM.HOURS:
case DeviceValueUOM.HOURS: return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 });
return value ? formatDurationMin(value * 60) : LL.NUM_HOURS({ num: 0 }); case DeviceValueUOM.MINUTES:
case DeviceValueUOM.MINUTES: return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 });
return value ? formatDurationMin(value) : LL.NUM_MINUTES({ num: 0 }); case DeviceValueUOM.SECONDS:
case DeviceValueUOM.SECONDS: return LL.NUM_SECONDS({ num: value });
return LL.NUM_SECONDS({ num: value }); case DeviceValueUOM.NONE:
case DeviceValueUOM.NONE: return new Intl.NumberFormat().format(value);
return new Intl.NumberFormat().format(value); case DeviceValueUOM.DEGREES:
case DeviceValueUOM.DEGREES: case DeviceValueUOM.DEGREES_R:
case DeviceValueUOM.DEGREES_R: case DeviceValueUOM.FAHRENHEIT:
case DeviceValueUOM.FAHRENHEIT: return (
return ( new Intl.NumberFormat(undefined, {
new Intl.NumberFormat(undefined, { minimumFractionDigits: 1
minimumFractionDigits: 1 }).format(value) +
}).format(value) + ' ' +
' ' + DeviceValueUOM_s[uom]
DeviceValueUOM_s[uom] );
); default:
default: return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom];
return new Intl.NumberFormat().format(value) + ' ' + DeviceValueUOM_s[uom]; }
} };
},
[formatDurationMin, LL]
);
const updateTemperatureSensor = useCallback( const updateTemperatureSensor = (ts: TemperatureSensor) => {
(ts: TemperatureSensor) => { if (me.admin) {
if (me.admin) { ts.o_n = ts.n;
ts.o_n = ts.n; setSelectedTemperatureSensor(ts);
setSelectedTemperatureSensor(ts); setTemperatureDialogOpen(true);
setTemperatureDialogOpen(true); }
} };
},
[me.admin]
);
const onTemperatureDialogClose = useCallback(() => { const onTemperatureDialogClose = () => {
setTemperatureDialogOpen(false); setTemperatureDialogOpen(false);
void fetchSensorData(); void fetchSensorData();
}, [fetchSensorData]); };
const onTemperatureDialogSave = useCallback( const onTemperatureDialogSave = async (ts: TemperatureSensor) => {
async (ts: TemperatureSensor) => { await sendTemperatureSensor({
await sendTemperatureSensor({ id: ts.id,
id: ts.id, name: ts.n,
name: ts.n, offset: ts.o,
offset: ts.o, is_system: ts.s
is_system: ts.s })
.then(() => {
toast.success(LL.UPDATED_OF(LL.SENSOR(1)));
}) })
.then(() => { .catch(() => {
toast.success(LL.UPDATED_OF(LL.SENSOR(1))); toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1));
}) })
.catch(() => { .finally(() => {
toast.error(LL.UPDATE_OF(LL.SENSOR(2)) + ' ' + LL.FAILED(1)); setTemperatureDialogOpen(false);
}) setSelectedTemperatureSensor(undefined);
.finally(() => { void fetchSensorData();
setTemperatureDialogOpen(false); });
setSelectedTemperatureSensor(undefined); };
void fetchSensorData();
});
},
[sendTemperatureSensor, LL, fetchSensorData]
);
const updateAnalogSensor = useCallback( const updateAnalogSensor = (as: AnalogSensor) => {
(as: AnalogSensor) => { if (me.admin) {
if (me.admin) { setCreating(false);
setCreating(false); as.o_n = as.n;
as.o_n = as.n; setSelectedAnalogSensor(as);
setSelectedAnalogSensor(as); setAnalogDialogOpen(true);
setAnalogDialogOpen(true); }
} };
},
[me.admin]
);
const onAnalogDialogClose = useCallback(() => { const onAnalogDialogClose = () => {
setAnalogDialogOpen(false); setAnalogDialogOpen(false);
void fetchSensorData(); void fetchSensorData();
}, [fetchSensorData]); };
const addAnalogSensor = useCallback(() => { const addAnalogSensor = () => {
if (firstAvailableGPIO.current === undefined) { if (firstAvailableGPIO.current === undefined) {
toast.error('No available GPIO found'); toast.error(LL.NO_GPIO());
return; return;
} }
setCreating(true); setCreating(true);
@@ -366,194 +349,167 @@ const Sensors = () => {
o_n: '' o_n: ''
}); });
setAnalogDialogOpen(true); setAnalogDialogOpen(true);
}, []); };
const onAnalogDialogSave = useCallback( const onAnalogDialogSave = async (as: AnalogSensor) => {
async (as: AnalogSensor) => { await sendAnalogSensor({
await sendAnalogSensor({ id: as.id,
id: as.id, gpio: as.g,
gpio: as.g, name: as.n,
name: as.n, offset: as.o,
offset: as.o, factor: as.f,
factor: as.f, uom: as.u,
uom: as.u, type: as.t,
type: as.t, deleted: as.d,
deleted: as.d, is_system: as.s
is_system: as.s })
.then(() => {
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2)));
}) })
.then(() => { .catch(() => {
toast.success(LL.UPDATED_OF(LL.ANALOG_SENSOR(2))); toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1));
}) })
.catch(() => { .finally(() => {
toast.error(LL.UPDATE_OF(LL.ANALOG_SENSOR(5)) + ' ' + LL.FAILED(1)); setAnalogDialogOpen(false);
}) setSelectedAnalogSensor(undefined);
.finally(() => { void fetchSensorData();
setAnalogDialogOpen(false); });
setSelectedAnalogSensor(undefined); };
void fetchSensorData();
}); const RenderAnalogSensors = (
}, <Table
[sendAnalogSensor, LL, fetchSensorData] 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( const RenderTemperatureSensors = (
() => ( <Table
<Table data={{ nodes: sensorData.ts }}
data={{ nodes: sensorData.as }} theme={temperature_theme}
theme={analog_theme} sort={temperature_sort}
sort={analog_sort} layout={{ custom: true }}
layout={{ custom: true }} >
> {(tableList: TemperatureSensor[]) => (
{(tableList: AnalogSensor[]) => ( <>
<> <Header>
<Header> <HeaderRow>
<HeaderRow> <HeaderCell resize>
<HeaderCell stiff> <Button
<Button fullWidth
fullWidth style={HEADER_BUTTON_STYLE}
style={HEADER_BUTTON_STYLE} endIcon={getSortIcon(temperature_sort.state, 'NAME')}
endIcon={getSortIcon(analog_sort.state, 'GPIO')} onClick={() =>
onClick={() => analog_sort.fns.onToggleSort({ sortKey: 'GPIO' })} temperature_sort.fns.onToggleSort({ sortKey: 'NAME' })
> }
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> {LL.NAME(0)}
<Cell>{as.n}</Cell> </Button>
<Cell stiff>{AnalogTypeNames[as.t - 1]} </Cell> </HeaderCell>
{(as.t === AnalogType.DIGITAL_OUT && <HeaderCell stiff>
as.g !== GPIO_25 && <Button
as.g !== GPIO_26) || fullWidth
as.t === AnalogType.DIGITAL_IN || style={HEADER_BUTTON_STYLE_END}
as.t === AnalogType.PULSE ? ( endIcon={getSortIcon(temperature_sort.state, 'VALUE')}
<Cell stiff>{as.v ? LL.ON() : LL.OFF()}</Cell> onClick={() =>
) : ( temperature_sort.fns.onToggleSort({ sortKey: 'VALUE' })
<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)}
> >
<Cell>{ts.n}</Cell> {LL.VALUE(0)}
<Cell>{formatValue(ts.t, ts.u)}</Cell> </Button>
</Row> </HeaderCell>
))} </HeaderRow>
</Body> </Header>
</> <Body>
)} {tableList.map((ts: TemperatureSensor) => (
</Table> <Row
), style={{ color: ts.s ? 'grey' : 'inherit' }}
[ key={ts.id}
temperature_sort, item={ts}
temperature_theme, onClick={() => updateTemperatureSensor(ts)}
getSortIcon, >
sensorData.ts, <Cell>{ts.n}</Cell>
LL, <Cell>{formatValue(ts.t, ts.u)}</Cell>
updateTemperatureSensor, </Row>
formatValue ))}
] </Body>
</>
)}
</Table>
); );
return ( 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 CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
@@ -23,7 +23,7 @@ import type { ValidateFieldsError } from 'async-validator';
import { ValidatedTextField } from 'components'; import { ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils'; import { numberValue, updateValue } from 'utils';
import { validate } from 'validators'; import { ValidationError, validate } from 'validators';
import { AnalogType, AnalogTypeNames, DeviceValueUOM_s } from './types'; import { AnalogType, AnalogTypeNames, DeviceValueUOM_s } from './types';
import type { AnalogSensor } from './types'; import type { AnalogSensor } from './types';
@@ -53,84 +53,54 @@ const SensorsAnalogDialog = ({
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const [editItem, setEditItem] = useState<AnalogSensor>(selectedItem); const [editItem, setEditItem] = useState<AnalogSensor>(selectedItem);
const updateFormValue = useMemo( const updateFormValue = updateValue((updater) =>
() => setEditItem(
updateValue((updater) => (prev) =>
setEditItem( updater(
(prev) => prev as unknown as Record<string, unknown>
updater( ) as unknown as AnalogSensor
prev as unknown as Record<string, unknown> )
) as unknown as AnalogSensor
)
),
[setEditItem]
); );
// Memoize helper functions to check sensor type conditions const isCounterOrRate =
const isCounterOrRate = useMemo( editItem.t === AnalogType.COUNTER ||
() => editItem.t === AnalogType.RATE ||
editItem.t === AnalogType.COUNTER || (editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2);
editItem.t === AnalogType.RATE || const isCounter =
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2), editItem.t === AnalogType.COUNTER ||
[editItem.t] (editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2);
); const isFreqType =
const isCounter = useMemo( editItem.t >= AnalogType.FREQ_0 && editItem.t <= AnalogType.FREQ_2;
() => const isPWM =
editItem.t === AnalogType.COUNTER || editItem.t === AnalogType.PWM_0 ||
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2), editItem.t === AnalogType.PWM_1 ||
[editItem.t] editItem.t === AnalogType.PWM_2;
); const isDACOutGPIO =
const isFreqType = useMemo( editItem.t === AnalogType.DIGITAL_OUT &&
() => editItem.t >= AnalogType.FREQ_0 && editItem.t <= AnalogType.FREQ_2, (editItem.g === 25 || editItem.g === 26);
[editItem.t] const isDigitalOutGPIO =
); editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26;
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]
);
// Memoize menu items to avoid recreation on each render const analogTypeMenuItems = AnalogTypeNames.map((val, i) => ({
const analogTypeMenuItems = useMemo( name: val,
() => value: i + 1
AnalogTypeNames.map((val, i) => ({ name: val, value: i + 1 })) }))
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.map(({ name, value }) => ( .map(({ name, value }) => (
<MenuItem <MenuItem
key={name} key={name}
value={value} value={value}
disabled={disabledTypeList?.includes(value)} disabled={disabledTypeList?.includes(value)}
> >
{name} {name}
</MenuItem> </MenuItem>
)), ));
[disabledTypeList]
);
const uomMenuItems = useMemo( const uomMenuItems = DeviceValueUOM_s.map((val, i) => (
() => <MenuItem key={val} value={i}>
DeviceValueUOM_s.map((val, i) => ( {val}
<MenuItem key={val} value={i}> </MenuItem>
{val} ));
</MenuItem>
)),
[]
);
const analogGPIOMenuItems = () => const analogGPIOMenuItems = () =>
// add selectedItem.g to the list // add selectedItem.g to the list
@@ -157,34 +127,30 @@ const SensorsAnalogDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = useCallback( const handleClose = (
(_event: React.SyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown') => { _event: React.SyntheticEvent,
if (reason !== 'backdropClick') { reason: 'backdropClick' | 'escapeKeyDown'
onClose(); ) => {
} if (reason !== 'backdropClick') {
}, onClose();
[onClose] }
); };
const save = useCallback(async () => { const save = async () => {
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(validator, editItem); await validate(validator, editItem);
onSave(editItem); onSave(editItem);
} catch (error) { } catch (error) {
setFieldErrors(error as ValidateFieldsError); setFieldErrors((error as ValidationError).fieldErrors);
} }
}, [validator, editItem, onSave]); };
const remove = useCallback(() => { const remove = () => {
onSave({ ...editItem, d: true }); onSave({ ...editItem, d: true });
}, [editItem, onSave]); };
const dialogTitle = useMemo( const dialogTitle = `${creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()} ${LL.ANALOG_SENSOR(0)}`;
() =>
`${creating ? LL.ADD(1) + ' ' + LL.NEW(0) : LL.EDIT()} ${LL.ANALOG_SENSOR(0)}`,
[creating, LL]
);
return ( return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}> <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 CancelIcon from '@mui/icons-material/Cancel';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
@@ -21,7 +21,7 @@ import type { ValidateFieldsError } from 'async-validator';
import { ValidatedTextField } from 'components'; import { ValidatedTextField } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValue } from 'utils'; import { numberValue, updateValue } from 'utils';
import { validate } from 'validators'; import { ValidationError, validate } from 'validators';
import type { TemperatureSensor } from './types'; import type { TemperatureSensor } from './types';
@@ -50,16 +50,12 @@ const SensorsTemperatureDialog = ({
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem); const [editItem, setEditItem] = useState<TemperatureSensor>(selectedItem);
const updateFormValue = useMemo( const updateFormValue = updateValue(
() => setEditItem as unknown as (
updateValue( updater: (
setEditItem as unknown as ( prevState: Readonly<Record<string, unknown>>
updater: ( ) => Record<string, unknown>
prevState: Readonly<Record<string, unknown>> ) => void
) => Record<string, unknown>
) => void
),
[setEditItem]
); );
useEffect(() => { useEffect(() => {
@@ -69,46 +65,25 @@ const SensorsTemperatureDialog = ({
} }
}, [open, selectedItem]); }, [open, selectedItem]);
const handleClose = useCallback( const handleClose = (_event: React.SyntheticEvent, reason?: string) => {
(_event: React.SyntheticEvent, reason?: string) => { if (reason !== 'backdropClick') {
if (reason !== 'backdropClick') { onClose();
onClose(); }
} };
},
[onClose]
);
const save = useCallback(async () => { const save = async () => {
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(validator, editItem); await validate(validator, editItem);
onSave(editItem); onSave(editItem);
} catch (error) { } 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 ( return (
<Dialog sx={dialogStyle} open={open} onClose={handleClose}> <Dialog sx={dialogStyle} open={open} onClose={handleClose}>
<DialogTitle>{dialogTitle}</DialogTitle> <DialogTitle>{`${LL.EDIT()} ${LL.TEMP_SENSOR()}`}</DialogTitle>
<DialogContent dividers> <DialogContent dividers>
<Typography sx={{ mb: 2 }} color="warning" variant="body2"> <Typography sx={{ mb: 2 }} color="warning" variant="body2">
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id} {LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
@@ -128,12 +103,23 @@ const SensorsTemperatureDialog = ({
<TextField <TextField
name="o" name="o"
label={LL.OFFSET()} label={LL.OFFSET()}
value={offsetValue} value={numberValue(editItem.o)}
sx={{ width: '11ch' }} sx={{ width: '11ch' }}
type="number" type="number"
variant="outlined" variant="outlined"
onChange={updateFormValue} 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>
</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 PersonIcon from '@mui/icons-material/Person';
import { import {
@@ -23,9 +23,9 @@ const UserProfileComponent = () => {
useLayoutTitle(LL.USER_PROFILE()); useLayoutTitle(LL.USER_PROFILE());
const handleSignOut = useCallback(() => { const handleSignOut = () => {
signOut(true); signOut(true);
}, [signOut]); };
return ( return (
<SectionContent> <SectionContent>

View File

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

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -28,12 +28,13 @@ import {
FormLoader, FormLoader,
MessageBox, MessageBox,
SectionContent, SectionContent,
ValidatedPasswordField,
ValidatedTextField, ValidatedTextField,
useLayoutTitle useLayoutTitle
} from 'components'; } from 'components';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import { numberValue, updateValueDirty, useRest } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { ValidationError, validate } from 'validators';
import { API, getBoardProfile, readSettings, writeSettings } from '../../api/app'; import { API, getBoardProfile, readSettings, writeSettings } from '../../api/app';
import { BOARD_PROFILES } from '../main/types'; import { BOARD_PROFILES } from '../main/types';
@@ -106,82 +107,80 @@ const ApplicationSettings = () => {
}); });
}); });
// Memoized input props to prevent recreation on every render const SecondsInputProps = {
const SecondsInputProps = useMemo( endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
() => ({ };
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
}),
[LL]
);
const MinutesInputProps = useMemo( const MinutesInputProps = {
() => ({ endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment>
endAdornment: <InputAdornment position="end">{LL.MINUTES()}</InputAdornment> };
}),
[LL]
);
const HoursInputProps = useMemo( const HoursInputProps = {
() => ({ endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment>
endAdornment: <InputAdornment position="end">{LL.HOURS()}</InputAdornment> };
}),
[LL]
);
const doRestart = useCallback(async () => { const doRestart = async () => {
setRestarting(true); setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch( await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => { (error: Error) => {
toast.error(error.message); toast.error(error.message);
} }
); );
}, [sendAPI]); };
const updateBoardProfile = useCallback( const updateBoardProfile = async (board_profile: string) => {
async (board_profile: string) => { await readBoardProfile(board_profile).catch((error: Error) => {
await readBoardProfile(board_profile).catch((error: Error) => { toast.error(error.message);
toast.error(error.message); });
}); };
},
[readBoardProfile]
);
useLayoutTitle(LL.APPLICATION()); useLayoutTitle(LL.APPLICATION());
const validateAndSubmit = useCallback(async () => { const validateAndSubmit = async () => {
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(createSettingsValidator(data), data); await validate(createSettingsValidator(data), data);
} catch (error) { } catch (error) {
setFieldErrors(error as ValidateFieldsError); setFieldErrors((error as ValidationError).fieldErrors);
} finally { } finally {
await saveData(); await saveData();
} }
}, [data, saveData]); };
const changeBoardProfile = useCallback( const changeBoardProfile = (event: React.ChangeEvent<HTMLInputElement>) => {
(event: React.ChangeEvent<HTMLInputElement>) => { const boardProfile = event.target.value;
const boardProfile = event.target.value; updateFormValue(event);
updateFormValue(event); if (boardProfile === 'CUSTOM') {
if (boardProfile === 'CUSTOM') { updateDataValue({
updateDataValue({ ...data,
...data, board_profile: boardProfile
board_profile: boardProfile });
}); } else {
} else { void updateBoardProfile(boardProfile);
void updateBoardProfile(boardProfile); }
} };
},
[data, updateBoardProfile, updateFormValue, updateDataValue]
);
const restart = useCallback(async () => { const restart = async () => {
await validateAndSubmit(); await validateAndSubmit();
await doRestart(); await doRestart();
}, [validateAndSubmit, doRestart]); };
// Memoize board profile select items to prevent recreation const sendemail = async () => {
const boardProfileItems = useMemo(() => boardProfileSelectItems(), []); await sendAPI({
device: 'system',
cmd: 'sendemail',
data: 'Email notification test successful!',
id: 0
})
.then(() => {
toast.success(LL.TEST_EMAIL_SUCCESSFUL());
})
.catch((error: Error) => {
toast.error(error.message);
});
};
const boardProfileItems = boardProfileSelectItems();
const content = () => { const content = () => {
if (!data || !hardwareData) { if (!data || !hardwareData) {
@@ -351,6 +350,169 @@ const ApplicationSettings = () => {
</Grid> </Grid>
</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"
sx={{ 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}
sx={{ 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>
<Button
sx={{ mt: 3 }}
variant="outlined"
color="primary"
disabled={dirtyFlags.length !== 0}
onClick={sendemail}
>
Send test email
</Button>
</Grid>
</Grid>
</>
)}
<Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary"> <Typography sx={{ pb: 1, pt: 2 }} variant="h6" color="primary">
{LL.SENSORS()} {LL.SENSORS()}
</Typography> </Typography>

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -57,7 +57,7 @@ const DownloadUpload = () => {
const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus); const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus);
const doRestart = useCallback(async () => { const doRestart = async () => {
setRestarting(true); setRestarting(true);
try { try {
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }); await sendAPI({ device: 'system', cmd: 'restart', id: 0 });
@@ -65,16 +65,33 @@ const DownloadUpload = () => {
toast.error((error as Error).message); toast.error((error as Error).message);
setRestarting(false); setRestarting(false);
} }
}, [sendAPI]); };
useLayoutTitle(LL.DOWNLOAD_UPLOAD()); useLayoutTitle(LL.DOWNLOAD_UPLOAD());
const handleCloseBackupDialog = useCallback(() => { const handleCloseBackupDialog = () => {
setConfirmBackup(false); 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 <Dialog
sx={dialogStyle} sx={dialogStyle}
open={confirmBackup} open={confirmBackup}
@@ -98,39 +115,13 @@ const DownloadUpload = () => {
<Button <Button
startIcon={<DownloadIcon />} startIcon={<DownloadIcon />}
variant="outlined" variant="outlined"
onClick={() => handleDownload('systembackup')()} onClick={handleDownload('systembackup')}
color="primary" color="primary"
> >
{LL.DOWNLOAD(0)} {LL.DOWNLOAD(0)}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </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"> <Typography sx={{ pb: 2 }} variant="h6" color="primary">
{LL.DOWNLOAD(0)} {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 { toast } from 'react-toastify';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -31,7 +31,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { MqttSettingsType } from 'types'; import type { MqttSettingsType } from 'types';
import { numberValue, updateValueDirty, useRest } from 'utils'; import { numberValue, updateValueDirty, useRest } from 'utils';
import { createMqttSettingsValidator, validate } from 'validators'; import { ValidationError, createMqttSettingsValidator, validate } from 'validators';
import { callAction } from '../../api/app'; import { callAction } from '../../api/app';
@@ -57,7 +57,7 @@ const MqttSettings = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>(); const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const sendResetMQTT = useCallback(() => { const sendResetMQTT = () => {
void callAction({ action: 'resetMQTT' }) void callAction({ action: 'resetMQTT' })
.then(() => { .then(() => {
toast.success('MQTT ' + LL.REFRESH() + ' successful'); toast.success('MQTT ' + LL.REFRESH() + ' successful');
@@ -65,56 +65,44 @@ const MqttSettings = () => {
.catch((error) => { .catch((error) => {
toast.error(String(error.error?.message || 'An error occurred')); toast.error(String(error.error?.message || 'An error occurred'));
}); });
}, []); };
const updateFormValue = useMemo( const updateFormValue = updateValueDirty(
() => origData as unknown as Record<string, unknown>,
updateValueDirty( dirtyFlags,
origData as unknown as Record<string, unknown>, setDirtyFlags,
dirtyFlags, updateDataValue as (value: unknown) => void
setDirtyFlags,
updateDataValue as (value: unknown) => void
),
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
); );
const SecondsInputProps = useMemo( const SecondsInputProps = {
() => ({ endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment>
endAdornment: <InputAdornment position="end">{LL.SECONDS()}</InputAdornment> };
}),
[LL]
);
const emptyFieldErrors = useMemo(() => ({}), []); const validateAndSubmit = async () => {
const validateAndSubmit = useCallback(async () => {
if (!data) return; if (!data) return;
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(createMqttSettingsValidator(data), data); await validate(createMqttSettingsValidator(data), data);
await saveData(); await saveData();
} catch (error) { } catch (error) {
setFieldErrors(error as ValidateFieldsError); setFieldErrors((error as ValidationError).fieldErrors);
} }
}, [data, saveData]); };
const publishIntervalFields = useMemo( const publishIntervalFields = [
() => [ { name: 'publish_time_heartbeat', label: 'Heartbeat', validated: true },
{ name: 'publish_time_heartbeat', label: 'Heartbeat', validated: true }, { name: 'publish_time_boiler', label: LL.MQTT_INT_BOILER(), validated: false },
{ name: 'publish_time_boiler', label: LL.MQTT_INT_BOILER(), validated: false }, {
{ name: 'publish_time_thermostat',
name: 'publish_time_thermostat', label: LL.MQTT_INT_THERMOSTATS(),
label: LL.MQTT_INT_THERMOSTATS(), validated: false
validated: false },
}, { name: 'publish_time_solar', label: LL.MQTT_INT_SOLAR(), 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_mixer', label: LL.MQTT_INT_MIXER(), validated: false }, { name: 'publish_time_water', label: LL.MQTT_INT_WATER(), 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_sensor', label: LL.SENSORS(), validated: false }, { name: 'publish_time_other', label: LL.DEFAULT(0), validated: false }
{ name: 'publish_time_other', label: LL.DEFAULT(0), validated: false } ];
],
[LL]
);
if (!data) { if (!data) {
return ( return (
@@ -154,7 +142,7 @@ const MqttSettings = () => {
<Grid container spacing={2} rowSpacing={0}> <Grid container spacing={2} rowSpacing={0}>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors ?? emptyFieldErrors} fieldErrors={fieldErrors ?? {}}
name="host" name="host"
label={LL.ADDRESS_OF(LL.BROKER())} label={LL.ADDRESS_OF(LL.BROKER())}
multiline multiline
@@ -166,7 +154,7 @@ const MqttSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors ?? emptyFieldErrors} fieldErrors={fieldErrors ?? {}}
name="port" name="port"
label="Port" label="Port"
variant="outlined" variant="outlined"
@@ -178,7 +166,7 @@ const MqttSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors ?? emptyFieldErrors} fieldErrors={fieldErrors ?? {}}
name="base" name="base"
label={LL.BASE_TOPIC()} label={LL.BASE_TOPIC()}
variant="outlined" variant="outlined"
@@ -219,7 +207,7 @@ const MqttSettings = () => {
</Grid> </Grid>
<Grid> <Grid>
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors ?? emptyFieldErrors} fieldErrors={fieldErrors ?? {}}
name="keep_alive" name="keep_alive"
label="Keep Alive" label="Keep Alive"
slotProps={{ slotProps={{
@@ -438,7 +426,7 @@ const MqttSettings = () => {
<Grid key={field.name}> <Grid key={field.name}>
{field.validated ? ( {field.validated ? (
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors ?? emptyFieldErrors} fieldErrors={fieldErrors ?? {}}
name={field.name} name={field.name}
label={field.label} label={field.label}
slotProps={{ slotProps={{

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime'; import AccessTimeIcon from '@mui/icons-material/AccessTime';
@@ -36,7 +36,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { NTPSettingsType, Time } from 'types'; import type { NTPSettingsType, Time } from 'types';
import { formatLocalDateTime, updateValueDirty, useRest } from 'utils'; import { formatLocalDateTime, updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { ValidationError, validate } from 'validators';
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp'; import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
import { TIME_ZONES, selectedTimeZone, useTimeZoneSelectItems } from './TZ'; import { TIME_ZONES, selectedTimeZone, useTimeZoneSelectItems } from './TZ';
@@ -61,14 +61,11 @@ const NTPSettings = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
useLayoutTitle('NTP'); useLayoutTitle('NTP');
// Memoized timezone select items for better performance
const timeZoneItems = useTimeZoneSelectItems(); const timeZoneItems = useTimeZoneSelectItems();
// Memoized selected timezone value const selectedTzValue = data
const selectedTzValue = useMemo( ? selectedTimeZone(data.tz_label, data.tz_format)
() => (data ? selectedTimeZone(data.tz_label, data.tz_format) : undefined), : undefined;
[data?.tz_label, data?.tz_format]
);
const [localTime, setLocalTime] = useState<string>(''); const [localTime, setLocalTime] = useState<string>('');
const [settingTime, setSettingTime] = useState<boolean>(false); const [settingTime, setSettingTime] = useState<boolean>(false);
@@ -82,32 +79,22 @@ const NTPSettings = () => {
} }
); );
// Memoize updateFormValue to prevent recreation on every render const updateFormValue = updateValueDirty(
const updateFormValue = useMemo( origData as unknown as Record<string, unknown>,
() => dirtyFlags,
updateValueDirty( setDirtyFlags,
origData as unknown as Record<string, unknown>, updateDataValue as (value: unknown) => void
dirtyFlags,
setDirtyFlags,
updateDataValue as (value: unknown) => void
),
[origData, dirtyFlags, setDirtyFlags, updateDataValue]
); );
// Memoize updateLocalTime handler const updateLocalTime = (event: React.ChangeEvent<HTMLInputElement>) =>
const updateLocalTime = useCallback( setLocalTime(event.target.value);
(event: React.ChangeEvent<HTMLInputElement>) => setLocalTime(event.target.value),
[]
);
// Memoize openSetTime handler const openSetTime = () => {
const openSetTime = useCallback(() => {
setLocalTime(formatLocalDateTime(new Date())); setLocalTime(formatLocalDateTime(new Date()));
setSettingTime(true); setSettingTime(true);
}, []); };
// Memoize configureTime handler const configureTime = async () => {
const configureTime = useCallback(async () => {
setProcessing(true); setProcessing(true);
try { try {
@@ -120,38 +107,31 @@ const NTPSettings = () => {
} finally { } finally {
setProcessing(false); setProcessing(false);
} }
}, [localTime, updateTime, LL, loadData]); };
// Memoize close dialog handler const handleCloseSetTime = () => setSettingTime(false);
const handleCloseSetTime = useCallback(() => setSettingTime(false), []);
// Memoize validate and submit handler const validateAndSubmit = async () => {
const validateAndSubmit = useCallback(async () => {
if (!data) return; if (!data) return;
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(NTP_SETTINGS_VALIDATOR, data); await validate(NTP_SETTINGS_VALIDATOR, data);
await saveData(); await saveData();
} catch (error) { } catch (error) {
setFieldErrors(error as ValidateFieldsError); setFieldErrors((error as ValidationError).fieldErrors);
} }
}, [data, saveData]); };
// Memoize timezone change handler const changeTimeZone = (event: React.ChangeEvent<HTMLInputElement>) => {
const changeTimeZone = useCallback( void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
(event: React.ChangeEvent<HTMLInputElement>) => { ...settings,
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({ tz_label: event.target.value,
...settings, tz_format: TIME_ZONES[event.target.value]
tz_label: event.target.value, }));
tz_format: TIME_ZONES[event.target.value] updateFormValue(event);
})); };
updateFormValue(event);
},
[updateFormValue]
);
// Memoize render content to prevent unnecessary re-renders const renderContent = () => {
const renderContent = useMemo(() => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />; 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 ( return (
<SectionContent> <SectionContent>
{blocker ? <BlockNavigation blocker={blocker} /> : null} {blocker ? <BlockNavigation blocker={blocker} /> : null}
{renderContent} {renderContent()}
<Dialog sx={dialogStyle} open={settingTime} onClose={handleCloseSetTime}> <Dialog sx={dialogStyle} open={settingTime} onClose={handleCloseSetTime}>
<DialogTitle>{LL.SET_TIME(1)}</DialogTitle> <DialogTitle>{LL.SET_TIME(1)}</DialogTitle>
<DialogContent dividers> <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 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 DeviceHubIcon from '@mui/icons-material/DeviceHub';
import ImportExportIcon from '@mui/icons-material/ImportExport'; import ImportExportIcon from '@mui/icons-material/ImportExport';
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet'; import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import TuneIcon from '@mui/icons-material/Tune'; import TuneIcon from '@mui/icons-material/Tune';
import ViewModuleIcon from '@mui/icons-material/ViewModule'; import ViewModuleIcon from '@mui/icons-material/ViewModule';
import { import { List } from '@mui/material';
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
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 { SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem'; import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import SystemMonitor from '../status/SystemMonitor';
const Settings = () => { const Settings = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const { versions } = useContext(AuthenticatedContext);
useLayoutTitle(LL.SETTINGS(0)); useLayoutTitle(LL.SETTINGS(0));
const [confirmFactoryReset, setConfirmFactoryReset] = useState(false); const upgradeAvailable = versions?.current?.upgradeable ?? false;
const [restarting, setRestarting] = useState<boolean>(); const firmwareText = versions?.current?.version
? `v${versions.current.version}${upgradeAvailable ? ` (${LL.UPDATE_AVAILABLE()})` : ''}`
: '';
const { send: sendAPI } = useRequest((data: APIcall) => API(data), { return (
immediate: false <SectionContent>
}); <List>
<ListMenuItem
icon={BuildIcon}
bgcolor="#72caf9"
label="EMS-ESP Firmware"
text={firmwareText}
to="/settings/version"
badge={upgradeAvailable}
/>
const doFormat = useCallback(async () => { <ListMenuItem
await sendAPI({ device: 'system', cmd: 'format', id: 0 }).then(() => { icon={TuneIcon}
setRestarting(true); bgcolor="#134ba2"
setConfirmFactoryReset(false); label={LL.APPLICATION()}
}); text={LL.APPLICATION_SETTINGS_1()}
}, [sendAPI]); to="application"
/>
const handleFactoryResetClose = useCallback(() => { <ListMenuItem
setConfirmFactoryReset(false); icon={SettingsEthernetIcon}
}, []); bgcolor="#40828f"
label={LL.NETWORK(0)}
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))}
to="network"
/>
const handleFactoryResetClick = useCallback(() => { <ListMenuItem
setConfirmFactoryReset(true); icon={SettingsInputAntennaIcon}
}, []); bgcolor="#5f9a5f"
label={LL.ACCESS_POINT(0)}
text={LL.CONFIGURE(LL.ACCESS_POINT(1))}
to="ap"
/>
const content = useMemo(() => { <ListMenuItem
return ( icon={AccessTimeIcon}
<> bgcolor="#c5572c"
<List> label="NTP"
<ListMenuItem text={LL.CONFIGURE(LL.LOCAL_TIME(1))}
icon={TuneIcon} to="ntp"
bgcolor="#134ba2" />
label={LL.APPLICATION()}
text={LL.APPLICATION_SETTINGS_1()}
to="application"
/>
<ListMenuItem <ListMenuItem
icon={SettingsEthernetIcon} icon={DeviceHubIcon}
bgcolor="#40828f" bgcolor="#68374d"
label={LL.NETWORK(0)} label="MQTT"
text={LL.CONFIGURE(LL.SETTINGS_OF(LL.NETWORK(1)))} text={LL.CONFIGURE('MQTT')}
to="network" to="mqtt"
/> />
<ListMenuItem <ListMenuItem
icon={SettingsInputAntennaIcon} icon={LockIcon}
bgcolor="#5f9a5f" label={LL.SECURITY(0)}
label={LL.ACCESS_POINT(0)} text={LL.SECURITY_1()}
text={LL.CONFIGURE(LL.ACCESS_POINT(1))} to="security"
to="ap" />
/>
<ListMenuItem <ListMenuItem
icon={AccessTimeIcon} icon={ViewModuleIcon}
bgcolor="#c5572c" bgcolor="#efc34b"
label="NTP" label={LL.MODULES()}
text={LL.CONFIGURE(LL.LOCAL_TIME(1))} text={LL.MODULES_1()}
to="ntp" to="modules"
/> />
<ListMenuItem <ListMenuItem
icon={DeviceHubIcon} icon={ImportExportIcon}
bgcolor="#68374d" bgcolor="#5d89f7"
label="MQTT" label={LL.DOWNLOAD_UPLOAD()}
text={LL.CONFIGURE('MQTT')} text={LL.DOWNLOAD_UPLOAD_1()}
to="mqtt" to="downloadUpload"
/> />
</List>
<ListMenuItem </SectionContent>
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>;
}; };
export default Settings; export default Settings;

View File

@@ -1,5 +1,3 @@
import { useMemo } from 'react';
import { MenuItem } from '@mui/material'; import { MenuItem } from '@mui/material';
export const TIME_ZONES: Record<string, string> = { 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; 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) => ( const precomputedTimeZoneItems = TIME_ZONE_LABELS.map((label) => (
<MenuItem key={label} value={label}> <MenuItem key={label} value={label}>
{label} {label}
</MenuItem> </MenuItem>
)); ));
export function useTimeZoneSelectItems() {
return precomputedTimeZoneItems;
}
export function timeZoneSelectItems() { export function timeZoneSelectItems() {
return precomputedTimeZoneItems; 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 { import {
Navigate, Navigate,
Route, Route,
@@ -40,26 +40,20 @@ const Network = () => {
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>(); const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork>();
const selectNetwork = useCallback( const selectNetwork = (network: WiFiNetwork) => {
(network: WiFiNetwork) => { setSelectedNetwork(network);
setSelectedNetwork(network); void navigate('/settings/network/settings');
void navigate('/settings/network/settings'); };
},
[navigate]
);
const deselectNetwork = useCallback(() => { const deselectNetwork = () => {
setSelectedNetwork(undefined); setSelectedNetwork(undefined);
}, []); };
const contextValue = useMemo( const contextValue = {
() => ({ ...(selectedNetwork && { selectedNetwork }),
...(selectedNetwork && { selectedNetwork }), selectNetwork,
selectNetwork, deselectNetwork
deselectNetwork };
}),
[selectedNetwork, selectNetwork, deselectNetwork]
);
return ( return (
<WiFiConnectionContext.Provider value={contextValue}> <WiFiConnectionContext.Provider value={contextValue}>

View File

@@ -40,7 +40,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { NetworkSettingsType } from 'types'; import type { NetworkSettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils'; import { updateValueDirty, useRest } from 'utils';
import { validate } from 'validators'; import { ValidationError, validate } from 'validators';
import { createNetworkSettingsValidator } from 'validators/network'; import { createNetworkSettingsValidator } from 'validators/network';
import SystemMonitor from '../../status/SystemMonitor'; import SystemMonitor from '../../status/SystemMonitor';
@@ -89,7 +89,7 @@ const NetworkSettings = () => {
static_ip_config: false, static_ip_config: false,
bandwidth20: false, bandwidth20: false,
tx_power: 0, tx_power: 0,
nosleep: false, nosleep: true,
enableMDNS: true, enableMDNS: true,
enableCORS: false, enableCORS: false,
CORSOrigin: '*' CORSOrigin: '*'
@@ -116,24 +116,24 @@ const NetworkSettings = () => {
await validate(createNetworkSettingsValidator(data), data); await validate(createNetworkSettingsValidator(data), data);
await saveData(); await saveData();
} catch (error) { } catch (error) {
setFieldErrors(error as ValidateFieldsError); setFieldErrors((error as ValidationError).fieldErrors);
} }
deselectNetwork(); deselectNetwork();
}, [data, saveData, deselectNetwork]); }, [data, saveData, deselectNetwork]);
const setCancel = useCallback(async () => { const setCancel = async () => {
deselectNetwork(); deselectNetwork();
await loadData(); await loadData();
}, [deselectNetwork, loadData]); };
const doRestart = useCallback(async () => { const doRestart = async () => {
setRestarting(true); setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch( await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => { (error: Error) => {
toast.error(error.message); toast.error(error.message);
} }
); );
}, [sendAPI]); };
const content = () => { const content = () => {
if (!data) { if (!data) {
@@ -173,7 +173,7 @@ const NetworkSettings = () => {
<ValidatedTextField <ValidatedTextField
fieldErrors={fieldErrors || {}} fieldErrors={fieldErrors || {}}
name="ssid" name="ssid"
label={'SSID (' + LL.NETWORK_BLANK_SSID() + ')'} label="SSID"
fullWidth fullWidth
variant="outlined" variant="outlined"
value={data.ssid} value={data.ssid}

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 PermScanWifiIcon from '@mui/icons-material/PermScanWifi';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
@@ -48,12 +48,12 @@ const WiFiNetworkScanner = () => {
} }
}); });
const renderNetworkScanner = useCallback(() => { const renderNetworkScanner = () => {
if (!networkList) { if (!networkList) {
return <FormLoader errorMessage={errorMessage || ''} />; return <FormLoader errorMessage={errorMessage || ''} />;
} }
return <WiFiNetworkSelector networkList={networkList} />; return <WiFiNetworkSelector networkList={networkList} />;
}, [networkList, errorMessage]); };
return ( return (
<SectionContent> <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 LockIcon from '@mui/icons-material/Lock';
import LockOpenIcon from '@mui/icons-material/LockOpen'; import LockOpenIcon from '@mui/icons-material/LockOpen';
@@ -63,34 +63,31 @@ const WiFiNetworkSelector = ({ networkList }: { networkList: WiFiNetworkList })
const wifiConnectionContext = useContext(WiFiConnectionContext); const wifiConnectionContext = useContext(WiFiConnectionContext);
const renderNetwork = useCallback( const renderNetwork = (network: WiFiNetwork) => (
(network: WiFiNetwork) => ( <ListItem
<ListItem key={network.bssid}
key={network.bssid} onClick={() => wifiConnectionContext.selectNetwork(network)}
onClick={() => wifiConnectionContext.selectNetwork(network)} >
> <ListItemAvatar>
<ListItemAvatar> <Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar>
<Avatar>{isNetworkOpen(network) ? <LockOpenIcon /> : <LockIcon />}</Avatar> </ListItemAvatar>
</ListItemAvatar> <ListItemText
<ListItemText primary={network.ssid}
primary={network.ssid} secondary={
secondary={ 'Security: ' +
'Security: ' + networkSecurityMode(network) +
networkSecurityMode(network) + ', Ch: ' +
', Ch: ' + network.channel +
network.channel + ', bssid: ' +
', bssid: ' + network.bssid
network.bssid }
} />
/> <ListItemIcon>
<ListItemIcon> <Badge badgeContent={network.rssi + 'dBm'}>
<Badge badgeContent={network.rssi + 'dBm'}> <WifiIcon sx={{ color: networkQualityHighlight(network, theme) }} />
<WifiIcon sx={{ color: networkQualityHighlight(network, theme) }} /> </Badge>
</Badge> </ListItemIcon>
</ListItemIcon> </ListItem>
</ListItem>
),
[wifiConnectionContext, theme]
); );
if (networkList.networks.length === 0) { 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 { useBlocker } from 'react-router';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -55,16 +55,14 @@ const ManageUsers = () => {
const blocker = useBlocker(changed !== 0); const blocker = useBlocker(changed !== 0);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const table_theme = useMemo( const table_theme = useTheme({
() => Table: `
useTheme({
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) minmax(120px, max-content) 120px; --data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) minmax(120px, max-content) 120px;
`, `,
BaseRow: ` BaseRow: `
font-size: 14px; font-size: 14px;
`, `,
HeaderRow: ` HeaderRow: `
text-transform: uppercase; text-transform: uppercase;
background-color: black; background-color: black;
color: #90CAF9; color: #90CAF9;
@@ -74,7 +72,7 @@ const ManageUsers = () => {
border-bottom: 1px solid #565656; border-bottom: 1px solid #565656;
} }
`, `,
Row: ` Row: `
.td { .td {
padding: 8px; padding: 8px;
border-top: 1px solid #565656; border-top: 1px solid #565656;
@@ -87,7 +85,7 @@ const ManageUsers = () => {
background-color: #1e1e1e; background-color: #1e1e1e;
} }
`, `,
BaseCell: ` BaseCell: `
&:nth-of-type(2) { &:nth-of-type(2) {
text-align: center; text-align: center;
} }
@@ -95,44 +93,36 @@ const ManageUsers = () => {
text-align: right; text-align: right;
} }
` `
}), });
[]
);
const noAdminConfigured = useCallback( const noAdminConfigured = () => !data?.users.find((u) => u.admin);
() => !data?.users.find((u) => u.admin),
[data]
);
const removeUser = useCallback( const removeUser = (toRemove: UserType) => {
(toRemove: UserType) => { if (!data) return;
if (!data) return; const users = data.users.filter((u) => u.username !== toRemove.username);
const users = data.users.filter((u) => u.username !== toRemove.username); updateDataValue({ ...data, users });
updateDataValue({ ...data, users }); setChanged(changed + 1);
setChanged(changed + 1); };
},
[data, updateDataValue, changed]
);
const createUser = useCallback(() => { const createUser = () => {
setCreating(true); setCreating(true);
setUser({ setUser({
username: '', username: '',
password: '', password: '',
admin: true admin: true
}); });
}, []); };
const editUser = useCallback((toEdit: UserType) => { const editUser = (toEdit: UserType) => {
setCreating(false); setCreating(false);
setUser({ ...toEdit }); setUser({ ...toEdit });
}, []); };
const cancelEditingUser = useCallback(() => { const cancelEditingUser = () => {
setUser(undefined); setUser(undefined);
}, []); };
const doneEditingUser = useCallback(() => { const doneEditingUser = () => {
if (user && data) { if (user && data) {
const users = [ const users = [
...data.users.filter( ...data.users.filter(
@@ -144,26 +134,26 @@ const ManageUsers = () => {
setUser(undefined); setUser(undefined);
setChanged(changed + 1); setChanged(changed + 1);
} }
}, [user, data, updateDataValue, changed]); };
const closeGenerateToken = useCallback(() => { const closeGenerateToken = useCallback(() => {
setGeneratingToken(undefined); setGeneratingToken(undefined);
}, []); }, []);
const generateTokenForUser = useCallback((username: string) => { const generateTokenForUser = (username: string) => {
setGeneratingToken(username); setGeneratingToken(username);
}, []); };
const onSubmit = useCallback(async () => { const onSubmit = async () => {
await saveData(); await saveData();
await authenticatedContext.refresh(); await authenticatedContext.refresh();
setChanged(0); setChanged(0);
}, [saveData, authenticatedContext]); };
const onCancelSubmit = useCallback(async () => { const onCancelSubmit = async () => {
await loadData(); await loadData();
setChanged(0); setChanged(0);
}, [loadData]); };
const content = () => { const content = () => {
if (!data) { if (!data) {
@@ -177,15 +167,10 @@ const ManageUsers = () => {
admin: boolean; admin: boolean;
} }
// add id to the type, needed for the table const user_table = data.users.map((u) => ({
const user_table = useMemo( ...u,
() => id: u.username
data.users.map((u) => ({ })) as UserType2[];
...u,
id: u.username
})) as UserType2[],
[data.users]
);
return ( 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 { Navigate, Route, Routes, matchRoutes, useLocation } from 'react-router';
import { Tab } from '@mui/material'; import { Tab } from '@mui/material';
@@ -15,19 +15,15 @@ const Security = () => {
const location = useLocation(); const location = useLocation();
const matchedRoutes = useMemo( const matchedRoutes = matchRoutes(
() => [
matchRoutes( {
[ path: '/settings/security/settings',
{ element: <ManageUsers />
path: '/settings/security/settings', },
element: <ManageUsers /> { path: '/settings/security/users', element: <SecuritySettings /> }
}, ],
{ path: '/settings/security/users', element: <SecuritySettings /> } location
],
location
),
[location]
); );
const routerTab = matchedRoutes?.[0]?.route.path || false; 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 { useI18nContext } from 'i18n/i18n-react';
import type { SecuritySettingsType } from 'types'; import type { SecuritySettingsType } from 'types';
import { updateValueDirty, useRest } from 'utils'; import { updateValueDirty, useRest } from 'utils';
import { SECURITY_SETTINGS_VALIDATOR, validate } from 'validators'; import { SECURITY_SETTINGS_VALIDATOR, ValidationError, validate } from 'validators';
const SecuritySettings = () => { const SecuritySettings = () => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
@@ -58,7 +58,7 @@ const SecuritySettings = () => {
await saveData(); await saveData();
await authenticatedContext.refresh(); await authenticatedContext.refresh();
} catch (error) { } catch (error) {
setFieldErrors(error as ValidateFieldsError); setFieldErrors((error as ValidationError).fieldErrors);
} }
}, [data, saveData, authenticatedContext]); }, [data, saveData, authenticatedContext]);
@@ -79,7 +79,7 @@ const SecuritySettings = () => {
onChange={updateFormValue} onChange={updateFormValue}
margin="normal" 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 && ( {dirtyFlags && dirtyFlags.length !== 0 && (
<ButtonRow> <ButtonRow>
<Button <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 type { FC } from 'react';
import CancelIcon from '@mui/icons-material/Cancel'; import CancelIcon from '@mui/icons-material/Cancel';
@@ -24,7 +24,7 @@ import {
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
import type { UserType } from 'types'; import type { UserType } from 'types';
import { updateValue } from 'utils'; import { updateValue } from 'utils';
import { validate } from 'validators'; import { ValidationError, validate } from 'validators';
interface UserFormProps { interface UserFormProps {
creating: boolean; creating: boolean;
@@ -62,17 +62,17 @@ const User: FC<UserFormProps> = ({
} }
}, [open]); }, [open]);
const validateAndDone = useCallback(async () => { const validateAndDone = async () => {
if (user) { if (user) {
try { try {
setFieldErrors(undefined); setFieldErrors(undefined);
await validate(validator, user); await validate(validator, user);
onDoneEditing(); onDoneEditing();
} catch (error) { } catch (error) {
setFieldErrors(error as ValidateFieldsError); setFieldErrors((error as ValidationError).fieldErrors);
} }
} }
}, [user, validator, onDoneEditing]); };
return ( return (
<Dialog <Dialog

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
import { useMemo } from 'react';
import DeviceHubIcon from '@mui/icons-material/DeviceHub'; import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DnsIcon from '@mui/icons-material/Dns'; import DnsIcon from '@mui/icons-material/Dns';
import GiteIcon from '@mui/icons-material/Gite'; import GiteIcon from '@mui/icons-material/Gite';
@@ -124,16 +122,20 @@ const NetworkStatus = () => {
const theme = useTheme(); const theme = useTheme();
const content = useMemo(() => { if (!data) {
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);
return ( 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> <List>
<ListItem> <ListItem>
<ListItemAvatar> <ListItemAvatar>
@@ -227,10 +229,8 @@ const NetworkStatus = () => {
</> </>
)} )}
</List> </List>
); </SectionContent>
}, [data, error, loadData, LL, theme]); );
return <SectionContent>{content}</SectionContent>;
}; };
export default NetworkStatus; export default NetworkStatus;

View File

@@ -1,25 +1,16 @@
import { useCallback, useContext, useMemo, useState } from 'react'; import { useContext } from 'react';
import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime'; 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 DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus'; import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
import LogoDevIcon from '@mui/icons-material/LogoDev'; import LogoDevIcon from '@mui/icons-material/LogoDev';
import MemoryIcon from '@mui/icons-material/Memory'; import MemoryIcon from '@mui/icons-material/Memory';
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart'; import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import RouterIcon from '@mui/icons-material/Router'; import RouterIcon from '@mui/icons-material/Router';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna'; import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import WifiIcon from '@mui/icons-material/Wifi'; import WifiIcon from '@mui/icons-material/Wifi';
import { import {
Avatar, Avatar,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
List, List,
ListItem, ListItem,
ListItemAvatar, ListItemAvatar,
@@ -27,12 +18,10 @@ import {
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import { API } from 'api/app';
import { readSystemStatus } from 'api/system'; import { readSystemStatus } from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client'; 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 { FormLoader, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem'; import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication'; import { AuthenticatedContext } from 'contexts/authentication';
@@ -41,9 +30,6 @@ import { NTPSyncStatus, NetworkConnectionStatus, SystemStatusCodes } from 'types
import { useInterval } from 'utils'; import { useInterval } from 'utils';
import { formatDateTime } from 'utils/time'; 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 formatNumber = (num: number) => new Intl.NumberFormat().format(num);
const formatDurationSec = ( const formatDurationSec = (
@@ -72,24 +58,7 @@ const SystemStatus = () => {
const { me } = useContext(AuthenticatedContext); const { me } = useContext(AuthenticatedContext);
const [confirmRestart, setConfirmRestart] = useState<boolean>(false); const { data, send: loadData, error } = useRequest(readSystemStatus);
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();
}
}
});
useInterval(() => { useInterval(() => {
void loadData(); void loadData();
@@ -97,10 +66,8 @@ const SystemStatus = () => {
const theme = useTheme(); const theme = useTheme();
// Memoize derived status values to avoid recalculation on every render const busStatus = (() => {
const busStatus = useMemo(() => {
if (!data) return 'EMS state unknown'; if (!data) return 'EMS state unknown';
switch (data.bus_status) { switch (data.bus_status) {
case busConnectionStatus.BUS_STATUS_CONNECTED: case busConnectionStatus.BUS_STATUS_CONNECTED:
return `EMS ${LL.CONNECTED(0)} (${formatDurationSec(data.bus_uptime, LL)})`; return `EMS ${LL.CONNECTED(0)} (${formatDurationSec(data.bus_uptime, LL)})`;
@@ -111,12 +78,10 @@ const SystemStatus = () => {
default: default:
return 'EMS state unknown'; return 'EMS state unknown';
} }
}, [data?.bus_status, data?.bus_uptime, LL]); })();
// Memoize derived status values to avoid recalculation on every render const systemStatus = (() => {
const systemStatus = useMemo(() => {
if (!data) return '??'; if (!data) return '??';
switch (data.status) { switch (data.status) {
case SystemStatusCodes.SYSTEM_STATUS_PENDING_UPLOAD: case SystemStatusCodes.SYSTEM_STATUS_PENDING_UPLOAD:
case SystemStatusCodes.SYSTEM_STATUS_UPLOADING: case SystemStatusCodes.SYSTEM_STATUS_UPLOADING:
@@ -129,14 +94,12 @@ const SystemStatus = () => {
case SystemStatusCodes.SYSTEM_STATUS_INVALID_GPIO: case SystemStatusCodes.SYSTEM_STATUS_INVALID_GPIO:
return LL.GPIO_OF(LL.FAILED(0)); return LL.GPIO_OF(LL.FAILED(0));
default: default:
// SystemStatusCodes.SYSTEM_STATUS_NORMAL
return 'OK'; return 'OK';
} }
}, [data?.status, LL]); })();
const busStatusHighlight = useMemo(() => { const busStatusHighlight = (() => {
if (!data) return theme.palette.warning.main; if (!data) return theme.palette.warning.main;
switch (data.bus_status) { switch (data.bus_status) {
case busConnectionStatus.BUS_STATUS_TX_ERRORS: case busConnectionStatus.BUS_STATUS_TX_ERRORS:
return theme.palette.warning.main; return theme.palette.warning.main;
@@ -147,11 +110,10 @@ const SystemStatus = () => {
default: default:
return theme.palette.warning.main; return theme.palette.warning.main;
} }
}, [data?.bus_status, theme.palette]); })();
const ntpStatus = useMemo(() => { const ntpStatus = (() => {
if (!data) return LL.UNKNOWN(); if (!data) return LL.UNKNOWN();
switch (data.ntp_status) { switch (data.ntp_status) {
case NTPSyncStatus.NTP_DISABLED: case NTPSyncStatus.NTP_DISABLED:
return LL.NOT_ENABLED(); return LL.NOT_ENABLED();
@@ -164,11 +126,10 @@ const SystemStatus = () => {
default: default:
return LL.UNKNOWN(); return LL.UNKNOWN();
} }
}, [data?.ntp_status, data?.ntp_time, LL]); })();
const ntpStatusHighlight = useMemo(() => { const ntpStatusHighlight = (() => {
if (!data) return theme.palette.error.main; if (!data) return theme.palette.error.main;
switch (data.ntp_status) { switch (data.ntp_status) {
case NTPSyncStatus.NTP_DISABLED: case NTPSyncStatus.NTP_DISABLED:
return theme.palette.info.main; return theme.palette.info.main;
@@ -179,11 +140,10 @@ const SystemStatus = () => {
default: default:
return theme.palette.error.main; return theme.palette.error.main;
} }
}, [data?.ntp_status, theme.palette]); })();
const networkStatusHighlight = useMemo(() => { const networkStatusHighlight = (() => {
if (!data) return theme.palette.warning.main; if (!data) return theme.palette.warning.main;
switch (data.network_status) { switch (data.network_status) {
case NetworkConnectionStatus.WIFI_STATUS_IDLE: case NetworkConnectionStatus.WIFI_STATUS_IDLE:
case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED: case NetworkConnectionStatus.WIFI_STATUS_DISCONNECTED:
@@ -198,11 +158,10 @@ const SystemStatus = () => {
default: default:
return theme.palette.warning.main; return theme.palette.warning.main;
} }
}, [data?.network_status, theme.palette]); })();
const networkStatus = useMemo(() => { const networkStatus = (() => {
if (!data) return LL.UNKNOWN(); if (!data) return LL.UNKNOWN();
switch (data.network_status) { switch (data.network_status) {
case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD: case NetworkConnectionStatus.WIFI_STATUS_NO_SHIELD:
return LL.INACTIVE(1); return LL.INACTIVE(1);
@@ -223,227 +182,103 @@ const SystemStatus = () => {
default: default:
return LL.UNKNOWN(); return LL.UNKNOWN();
} }
}, [data?.network_status, data?.wifi_rssi, LL]); })();
const activeHighlight = useCallback( const activeHighlight = (value: boolean) =>
(value: boolean) => value ? theme.palette.success.main : theme.palette.info.main;
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 || ''} />;
}
if (!data || !LL) {
return ( return (
<> <SectionContent>
<List> <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />
<ListMenuItem </SectionContent>
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}
</>
); );
}, [ }
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; export default SystemStatus;

View File

@@ -1,11 +1,4 @@
import { import { memo, useEffect, useLayoutEffect, useRef, useState } from 'react';
memo,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState
} from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import DownloadIcon from '@mui/icons-material/GetApp'; import DownloadIcon from '@mui/icons-material/GetApp';
@@ -185,8 +178,7 @@ const SystemLog = () => {
}; };
}, [data]); // Recalculate when data changes (in case layout shifts) }, [data]); // Recalculate when data changes (in case layout shifts)
// Memoize message handler to avoid recreating on every render const handleLogMessage = (message: { data: string }) => {
const handleLogMessage = useCallback((message: { data: string }) => {
const rawData = message.data; const rawData = message.data;
const logentry = JSON.parse(rawData) as LogEntry; const logentry = JSON.parse(rawData) as LogEntry;
setLogEntries((log) => { setLogEntries((log) => {
@@ -200,7 +192,7 @@ const SystemLog = () => {
const newLog = [...log, logentry]; const newLog = [...log, logentry];
return newLog; return newLog;
}); });
}, []); };
useSSE(fetchLogES, { useSSE(fetchLogES, {
immediate: true, immediate: true,
@@ -211,7 +203,7 @@ const SystemLog = () => {
toast.error('No connection to Log service'); toast.error('No connection to Log service');
}); });
const onDownload = useCallback(() => { const onDownload = () => {
const result = logEntries const result = logEntries
.map((i) => `${i.t} ${levelLabel(i.l)} ${i.i}: [${i.n}] ${i.m}`) .map((i) => `${i.t} ${levelLabel(i.l)} ${i.i}: [${i.n}] ${i.m}`)
.join('\n'); .join('\n');
@@ -225,11 +217,11 @@ const SystemLog = () => {
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
}, [logEntries]); };
const saveSettings = useCallback(async () => { const saveSettings = async () => {
await saveData(); await saveData();
}, [saveData]); };
// handle scrolling - optimized to only scroll when needed // handle scrolling - optimized to only scroll when needed
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@@ -246,7 +238,7 @@ const SystemLog = () => {
} }
}, [logEntries.length, autoscroll]); }, [logEntries.length, autoscroll]);
const sendReadCommand = useCallback(() => { const sendReadCommand = () => {
if (readValue === '') { if (readValue === '') {
setReadOpen(!readOpen); setReadOpen(!readOpen);
return; return;
@@ -257,7 +249,7 @@ const SystemLog = () => {
setReadOpen(false); setReadOpen(false);
setReadValue(''); setReadValue('');
} }
}, [readValue, readOpen, send]); };
const content = () => { const content = () => {
if (!data) { 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 CancelIcon from '@mui/icons-material/Cancel';
import { Box, Button, Typography } from '@mui/material'; import { Box, Button, Typography } from '@mui/material';
@@ -57,39 +57,31 @@ const SystemMonitor = () => {
void send(); void send();
}, 1000); // check every 1 second }, 1000); // check every 1 second
const { statusMessage, isUploading, progressValue } = useMemo(() => { const status = data?.status;
const status = data?.status;
const message = const statusMessage =
status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING
? LL.WAIT_FIRMWARE() ? LL.WAIT_FIRMWARE()
: status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART : status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART
? LL.APPLICATION_RESTARTING() ? LL.APPLICATION_RESTARTING()
: status === SystemStatusCodes.SYSTEM_STATUS_NORMAL : status === SystemStatusCodes.SYSTEM_STATUS_NORMAL
? LL.RESTARTING_PRE() ? LL.RESTARTING_PRE()
: status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD : status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
? 'Upload Failed' ? 'Upload Failed'
: LL.RESTARTING_POST(); : LL.RESTARTING_POST();
const uploading = const isUploading =
status !== undefined && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING; status !== undefined && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING;
const progress = const progressValue =
uploading && status isUploading && status
? Math.round(status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING) ? Math.round(status - SystemStatusCodes.SYSTEM_STATUS_UPLOADING)
: 0; : 0;
return { const onCancel = async () => {
statusMessage: message,
isUploading: uploading,
progressValue: progress
};
}, [data?.status, LL]);
const onCancel = useCallback(async () => {
setErrorMessage(undefined); setErrorMessage(undefined);
await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL)); await setSystemStatus(String(SystemStatusCodes.SYSTEM_STATUS_NORMAL));
document.location.href = '/'; document.location.href = '/';
}, [setSystemStatus]); };
return ( return (
<Box <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 CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
@@ -38,18 +38,17 @@ const MessageBox: FC<PropsWithChildren<MessageBoxProps>> = ({
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { Icon, backgroundColor } = useMemo(() => { const Icon = LEVEL_ICONS[level];
const Icon = LEVEL_ICONS[level]; const palettePath = LEVEL_PALETTE_PATHS[level];
const palettePath = LEVEL_PALETTE_PATHS[level]; const [paletteKeyName, shade] = palettePath.split('.') as [
const [key, shade] = palettePath.split('.') as [ keyof typeof theme.palette,
keyof typeof theme.palette, string
string ];
]; const paletteKey = theme.palette[paletteKeyName] as unknown as Record<
const paletteKey = theme.palette[key] as unknown as Record<string, string>; string,
const backgroundColor = paletteKey[shade]; string
>;
return { Icon, backgroundColor }; const backgroundColor = paletteKey[shade];
}, [level, theme]);
return ( return (
<Box <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 { ChangeEventHandler } from 'react';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
@@ -44,27 +44,14 @@ const LANGUAGE_OPTIONS: LanguageOption[] = [
const LanguageSelector = () => { const LanguageSelector = () => {
const { setLocale, locale, LL } = useContext(I18nContext); const { setLocale, locale, LL } = useContext(I18nContext);
const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = useCallback( const onLocaleSelected: ChangeEventHandler<HTMLInputElement> = async ({
async ({ target }) => { target
const loc = target.value as Locales; }) => {
localStorage.setItem('lang', loc); const loc = target.value as Locales;
await loadLocaleAsync(loc); localStorage.setItem('lang', loc);
setLocale(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>
)),
[]
);
return ( return (
<TextField <TextField
@@ -76,7 +63,12 @@ const LanguageSelector = () => {
size="small" size="small"
select select
> >
{menuItems} {LANGUAGE_OPTIONS.map(({ key, flag, label }) => (
<MenuItem key={key} value={key}>
<img src={flag} style={flagStyle} alt={label} />
&nbsp;{label}
</MenuItem>
))}
</TextField> </TextField>
); );
}; };

View File

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

View File

@@ -18,7 +18,6 @@ const LayoutComponent: FC<RequiredChildrenProps> = ({ children }) => {
const [title, setTitle] = useState(PROJECT_NAME); const [title, setTitle] = useState(PROJECT_NAME);
const { pathname } = useLocation(); const { pathname } = useLocation();
// Memoize drawer toggle handler to prevent unnecessary re-renders
const handleDrawerToggle = useCallback(() => { const handleDrawerToggle = useCallback(() => {
setMobileOpen((prev) => !prev); setMobileOpen((prev) => !prev);
}, []); }, []);
@@ -28,7 +27,6 @@ const LayoutComponent: FC<RequiredChildrenProps> = ({ children }) => {
setMobileOpen(false); setMobileOpen(false);
}, [pathname]); }, [pathname]);
// Memoize context value to prevent unnecessary re-renders
const contextValue = useMemo(() => ({ title, setTitle }), [title]); const contextValue = useMemo(() => ({ title, setTitle }), [title]);
return ( 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 { Link, useLocation, useNavigate } from 'react-router';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
@@ -39,14 +39,11 @@ const LayoutAppBarComponent = ({ title, onToggleDrawer }: LayoutAppBarProps) =>
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const pathnames = useMemo( const pathnames = location.pathname.split('/').filter((x) => x);
() => location.pathname.split('/').filter((x) => x),
[location.pathname]
);
const handleBackClick = useCallback(() => { const handleBackClick = () => {
void navigate('/' + pathnames[0]); void navigate('/' + pathnames[0]);
}, [navigate, pathnames]); };
return ( return (
<AppBar position="fixed" sx={appBarStyles}> <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'; import { Box, Divider, Drawer, Toolbar, Typography, styled } from '@mui/material';
@@ -24,22 +24,18 @@ interface LayoutDrawerProps {
} }
const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => { const LayoutDrawerComponent = ({ mobileOpen, onClose }: LayoutDrawerProps) => {
// Memoize drawer content to prevent unnecessary re-renders const drawer = (
const drawer = useMemo( <>
() => ( <Toolbar disableGutters>
<> <Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}>
<Toolbar disableGutters> <LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} />
<Box sx={{ display: 'flex', alignItems: 'center', p: 2 }}> <Typography variant="h6">{PROJECT_NAME}</Typography>
<LayoutDrawerLogo src="/app/icon.png" alt={PROJECT_NAME} /> </Box>
<Typography variant="h6">{PROJECT_NAME}</Typography> <Divider absolute />
</Box> </Toolbar>
<Divider absolute /> <Divider />
</Toolbar> <LayoutMenu />
<Divider /> </>
<LayoutMenu />
</>
),
[]
); );
return ( 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 AccountCircleIcon from '@mui/icons-material/AccountCircle';
import AssessmentIcon from '@mui/icons-material/Assessment'; import AssessmentIcon from '@mui/icons-material/Assessment';
@@ -18,13 +18,15 @@ import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react'; import { useI18nContext } from 'i18n/i18n-react';
const LayoutMenuComponent = () => { const LayoutMenuComponent = () => {
const { me } = useContext(AuthenticatedContext); const { me, versions } = useContext(AuthenticatedContext);
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const [menuOpen, setMenuOpen] = useState(true); const [menuOpen, setMenuOpen] = useState(true);
const handleMenuToggle = useCallback(() => { const upgradeAvailable = versions?.current?.upgradeable ?? false;
const handleMenuToggle = () => {
setMenuOpen((prev) => !prev); setMenuOpen((prev) => !prev);
}, []); };
return ( return (
<> <>
@@ -105,6 +107,7 @@ const LayoutMenuComponent = () => {
label={LL.SETTINGS(0)} label={LL.SETTINGS(0)}
disabled={!me.admin} disabled={!me.admin}
to="/settings" to="/settings"
badge={upgradeAvailable}
/> />
<LayoutMenuItem icon={LiveHelpIcon} label={LL.HELP()} to={`/help`} /> <LayoutMenuItem icon={LiveHelpIcon} label={LL.HELP()} to={`/help`} />
<Divider /> <Divider />

View File

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

View File

@@ -5,6 +5,7 @@ import { Link } from 'react-router';
import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import { import {
Avatar, Avatar,
Box,
ListItem, ListItem,
ListItemAvatar, ListItemAvatar,
ListItemButton, ListItemButton,
@@ -20,6 +21,7 @@ interface ListMenuItemProps {
text: string; text: string;
to?: string; to?: string;
disabled?: boolean; disabled?: boolean;
badge?: boolean;
} }
const iconStyles: CSSProperties = { const iconStyles: CSSProperties = {
@@ -28,15 +30,40 @@ const iconStyles: CSSProperties = {
verticalAlign: 'middle' 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( const RenderIcon = memo(
({ icon: Icon, bgcolor, label, text }: ListMenuItemProps) => ( ({ icon: Icon, bgcolor, label, text, badge }: ListMenuItemProps) => (
<> <>
<ListItemAvatar> <ListItemAvatar>
<Avatar sx={{ bgcolor, color: 'white' }}> <Avatar sx={{ bgcolor, color: 'white' }}>
<Icon /> <Icon />
</Avatar> </Avatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={label} secondary={text} /> <ListItemText
primary={
<>
{label}
{badge && <Badge />}
</>
}
secondary={text}
/>
</> </>
) )
); );
@@ -47,7 +74,8 @@ const LayoutMenuItem = ({
label, label,
text, text,
to, to,
disabled disabled,
badge
}: ListMenuItemProps) => ( }: ListMenuItemProps) => (
<> <>
{to && !disabled ? ( {to && !disabled ? (
@@ -65,6 +93,7 @@ const LayoutMenuItem = ({
{...(bgcolor && { bgcolor })} {...(bgcolor && { bgcolor })}
label={label} label={label}
text={text} text={text}
{...(badge && { badge })}
/> />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
@@ -75,6 +104,7 @@ const LayoutMenuItem = ({
{...(bgcolor && { bgcolor })} {...(bgcolor && { bgcolor })}
label={label} label={label}
text={text} text={text}
{...(badge && { badge })}
/> />
</ListItem> </ListItem>
)} )}

View File

@@ -1,4 +1,4 @@
import { memo, useCallback } from 'react'; import { memo } from 'react';
import type { Blocker } from 'react-router'; import type { Blocker } from 'react-router';
import { import {
@@ -15,13 +15,13 @@ import { useI18nContext } from 'i18n/i18n-react';
const BlockNavigation = ({ blocker }: { blocker: Blocker }) => { const BlockNavigation = ({ blocker }: { blocker: Blocker }) => {
const { LL } = useI18nContext(); const { LL } = useI18nContext();
const handleReset = useCallback(() => { const handleReset = () => {
blocker.reset?.(); blocker.reset?.();
}, [blocker]); };
const handleProceed = useCallback(() => { const handleProceed = () => {
blocker.proceed?.(); blocker.proceed?.();
}, [blocker]); };
return ( return (
<Dialog sx={dialogStyle} open={blocker.state === 'blocked'}> <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 type { FC } from 'react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
@@ -16,12 +16,9 @@ const RouterTabs: FC<RouterTabsProps> = ({ value, children }) => {
const theme = useTheme(); const theme = useTheme();
const smallDown = useMediaQuery(theme.breakpoints.down('sm')); const smallDown = useMediaQuery(theme.breakpoints.down('sm'));
const handleTabChange = useCallback( const handleTabChange = (_event: unknown, path: string) => {
(_event: unknown, path: string) => { void navigate(path);
void navigate(path); };
},
[navigate]
);
return ( return (
<Tabs <Tabs

View File

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

View File

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

View File

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

View File

@@ -247,8 +247,7 @@ const cz: Translation = {
TIME_ZONE: 'Časová zóna', TIME_ZONE: 'Časová zóna',
ACCESS_POINT: 'Přístupový bod', ACCESS_POINT: 'Přístupový bod',
AP_PROVIDE: 'Povolit přístupový bod', AP_PROVIDE: 'Povolit přístupový bod',
AP_PROVIDE_TEXT_1: 'Vždy', AP_PROVIDE_TEXT_2: 'Když je síťové připojení stratené',
AP_PROVIDE_TEXT_2: 'Když je WiFi odpojena',
AP_PROVIDE_TEXT_3: 'Nikdy', AP_PROVIDE_TEXT_3: 'Nikdy',
AP_PREFERRED_CHANNEL: 'Preferovaný kanál', AP_PREFERRED_CHANNEL: 'Preferovaný kanál',
AP_HIDE_SSID: 'Skrýt SSID', AP_HIDE_SSID: 'Skrýt SSID',
@@ -262,7 +261,6 @@ const cz: Translation = {
SCAN_AGAIN: 'Skenovat znovu', SCAN_AGAIN: 'Skenovat znovu',
NETWORK_SCANNER: 'Síťový skener', NETWORK_SCANNER: 'Síťový skener',
NETWORK_NO_WIFI: 'Nenalezeny žádné WiFi sítě', NETWORK_NO_WIFI: 'Nenalezeny žádné WiFi sítě',
NETWORK_BLANK_SSID: 'ponechte prázdné pro deaktivaci WiFi a povolení ETH',
NETWORK_BLANK_BSSID: 'ponechte prázdné pokud použijete jen SSID', NETWORK_BLANK_BSSID: 'ponechte prázdné pokud použijete jen SSID',
TX_POWER: 'Vysílací výkon', TX_POWER: 'Vysílací výkon',
HOSTNAME: 'Název hostitele', HOSTNAME: 'Název hostitele',
@@ -350,21 +348,22 @@ const cz: Translation = {
NO_DATA_1: 'Nebyly nalezeny žádné oblíbené entity. Použijte modul', NO_DATA_1: 'Nebyly nalezeny žádné oblíbené entity. Použijte modul',
NO_DATA_2: 'pro jejich výběr.', NO_DATA_2: 'pro jejich výběr.',
NO_DATA_3: 'Pro zobrazení všech dostupných entit navštivte stránku', 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', THIS_VERSION: 'Tato verze',
PLATFORM: 'Platforma', PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ sestavení', RELEASE_TYPE: 'Typ sestavení',
INTERNET_CONNECTION_REQUIRED: 'Pro automatickou kontrolu a instalaci aktualizací je třeba internetové připojení', INTERNET_CONNECTION_REQUIRED: 'Pro automatickou kontrolu a instalaci aktualizací je třeba internetové připojení',
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi', SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi',
FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru', FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru',
NO_DATA: 'Žádná data', NO_DATA: 'žádné údaje',
USER_PROFILE: 'Uživatelský profil', USER_PROFILE: 'Uživatelský profil',
STORED_VERSIONS: 'Uložené verze', STORED_VERSIONS: 'Uložené verze',
ONLINE_HELP: 'online nápověda', ONLINE_HELP: 'online nápověda',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovat důležité zprávy', UPGRADE_IMPORTANT_MESSAGES: '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.', 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?' 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?',
TEST_EMAIL_SUCCESSFUL: 'Test email byl úspěšně odeslán'
}; };
export default cz; export default cz;

View File

@@ -187,7 +187,7 @@ const de: Translation = {
COMPACT: 'Kompakte Darstellung', COMPACT: 'Kompakte Darstellung',
DOWNLOAD_SETTINGS_TEXT: 'Erstellen Sie eine Sicherung Ihrer Konfigurationen und Einstellungen', DOWNLOAD_SETTINGS_TEXT: 'Erstellen Sie eine Sicherung Ihrer Konfigurationen und Einstellungen',
DOWNLOAD_SETTINGS_TEXT2: 'Exportiere alle Werte', 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_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', UPLOAD_DROP_TEXT: 'Legen Sie eine Firmware-Datei (.bin) ab oder klicken Sie hier',
ERROR: 'Unerwarteter Fehler, bitte versuchen Sie es erneut.', ERROR: 'Unerwarteter Fehler, bitte versuchen Sie es erneut.',
@@ -247,8 +247,7 @@ const de: Translation = {
TIME_ZONE: 'Zeitzone', TIME_ZONE: 'Zeitzone',
ACCESS_POINT: 'Zugangspunkt', ACCESS_POINT: 'Zugangspunkt',
AP_PROVIDE: 'Aktiviere Zugangspunkt', AP_PROVIDE: 'Aktiviere Zugangspunkt',
AP_PROVIDE_TEXT_1: 'Immer', AP_PROVIDE_TEXT_2: 'Wenn Netzwerkverbindung verloren geht',
AP_PROVIDE_TEXT_2: 'Wenn WiFi nicht verbunden',
AP_PROVIDE_TEXT_3: 'Niemals', AP_PROVIDE_TEXT_3: 'Niemals',
AP_PREFERRED_CHANNEL: 'Bevorzugter Kanal', AP_PREFERRED_CHANNEL: 'Bevorzugter Kanal',
AP_HIDE_SSID: 'Verstecke SSID', AP_HIDE_SSID: 'Verstecke SSID',
@@ -262,7 +261,6 @@ const de: Translation = {
SCAN_AGAIN: 'Erneute Suche', SCAN_AGAIN: 'Erneute Suche',
NETWORK_SCANNER: 'Netzwerksuche', NETWORK_SCANNER: 'Netzwerksuche',
NETWORK_NO_WIFI: 'Keine WiFi-Netzwerke gefunden', NETWORK_NO_WIFI: 'Keine WiFi-Netzwerke gefunden',
NETWORK_BLANK_SSID: 'Freilassen, um WiFi zu deaktivieren und ETH zu aktivieren.',
NETWORK_BLANK_BSSID: 'Freilassen, um nur SSID für die Verbindung zu nutzen.', NETWORK_BLANK_BSSID: 'Freilassen, um nur SSID für die Verbindung zu nutzen.',
TX_POWER: 'Tx Leistung', TX_POWER: 'Tx Leistung',
HOSTNAME: 'Hostname', HOSTNAME: 'Hostname',
@@ -350,6 +348,7 @@ const de: Translation = {
NO_DATA_1: 'Keine favorisierten EMS-Entitäten gefunden! Verwenden Sie das Modul', NO_DATA_1: 'Keine favorisierten EMS-Entitäten gefunden! Verwenden Sie das Modul',
NO_DATA_2: ', um sie zu markieren.', NO_DATA_2: ', um sie zu markieren.',
NO_DATA_3: 'Um alle verfügbaren Entitäten anzuzeigen, gehen Sie zu', NO_DATA_3: 'Um alle verfügbaren Entitäten anzuzeigen, gehen Sie zu',
NO_GPIO: 'Keine freien GPIO gefunden',
THIS_VERSION: 'Diese Version', THIS_VERSION: 'Diese Version',
PLATFORM: 'Plattform', PLATFORM: 'Plattform',
RELEASE_TYPE: 'Release Typ', RELEASE_TYPE: 'Release Typ',
@@ -361,10 +360,10 @@ const de: Translation = {
STORED_VERSIONS: 'Gespeicherte Versionen', STORED_VERSIONS: 'Gespeicherte Versionen',
ONLINE_HELP: 'Online-Hilfe', ONLINE_HELP: 'Online-Hilfe',
UPGRADE_IMPORTANT_MESSAGES: 'Wichtige Nachrichten aktualisieren', 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.', 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?',
TEST_EMAIL_SUCCESSFUL: 'Test email erfolgreich gesendet'
}; };
export default de; export default de;

View File

@@ -247,8 +247,7 @@ const en: Translation = {
TIME_ZONE: 'Time Zone', TIME_ZONE: 'Time Zone',
ACCESS_POINT: 'Access Point', ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Enable Access Point', AP_PROVIDE: 'Enable Access Point',
AP_PROVIDE_TEXT_1: 'Always', AP_PROVIDE_TEXT_2: 'When network connection is lost',
AP_PROVIDE_TEXT_2: 'When WiFi is disconnected',
AP_PROVIDE_TEXT_3: 'Never', AP_PROVIDE_TEXT_3: 'Never',
AP_PREFERRED_CHANNEL: 'Preferred Channel', AP_PREFERRED_CHANNEL: 'Preferred Channel',
AP_HIDE_SSID: 'Hide SSID', AP_HIDE_SSID: 'Hide SSID',
@@ -262,7 +261,6 @@ const en: Translation = {
SCAN_AGAIN: 'Scan again', SCAN_AGAIN: 'Scan again',
NETWORK_SCANNER: 'Network Scanner', NETWORK_SCANNER: 'Network Scanner',
NETWORK_NO_WIFI: 'No WiFi networks found', NETWORK_NO_WIFI: 'No WiFi networks found',
NETWORK_BLANK_SSID: 'leave blank to disable WiFi and enable ETH',
NETWORK_BLANK_BSSID: 'leave blank to use only SSID', NETWORK_BLANK_BSSID: 'leave blank to use only SSID',
TX_POWER: 'Tx Power', TX_POWER: 'Tx Power',
HOSTNAME: 'Hostname', HOSTNAME: 'Hostname',
@@ -350,6 +348,7 @@ const en: Translation = {
NO_DATA_1: 'No favorite EMS entities found yet. Use the', NO_DATA_1: 'No favorite EMS entities found yet. Use the',
NO_DATA_2: 'module to mark them.', NO_DATA_2: 'module to mark them.',
NO_DATA_3: 'To see all available entities go to', NO_DATA_3: 'To see all available entities go to',
NO_GPIO: 'No available GPIO found',
THIS_VERSION: 'This Version', THIS_VERSION: 'This Version',
PLATFORM: 'Platform', PLATFORM: 'Platform',
RELEASE_TYPE: 'Release Type', RELEASE_TYPE: 'Release Type',
@@ -361,10 +360,10 @@ const en: Translation = {
STORED_VERSIONS: 'Stored Versions', STORED_VERSIONS: 'Stored Versions',
ONLINE_HELP: 'online help', ONLINE_HELP: 'online help',
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Important Messages', 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.', 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?' 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?',
TEST_EMAIL_SUCCESSFUL: 'Test email sent successfully'
}; };
export default en; export default en;

View File

@@ -247,8 +247,7 @@ const fr: Translation = {
TIME_ZONE: 'Fuseau horaire', TIME_ZONE: 'Fuseau horaire',
ACCESS_POINT: "Point d'accès", ACCESS_POINT: "Point d'accès",
AP_PROVIDE: "Activer le Point d'Accès", AP_PROVIDE: "Activer le Point d'Accès",
AP_PROVIDE_TEXT_1: 'toujours', AP_PROVIDE_TEXT_2: 'quand la connexion réseau est perdue',
AP_PROVIDE_TEXT_2: 'quand le WiFi est déconnecté',
AP_PROVIDE_TEXT_3: 'jamais', AP_PROVIDE_TEXT_3: 'jamais',
AP_PREFERRED_CHANNEL: 'Canal préféré', AP_PREFERRED_CHANNEL: 'Canal préféré',
AP_HIDE_SSID: 'Cacher le SSID', AP_HIDE_SSID: 'Cacher le SSID',
@@ -262,7 +261,6 @@ const fr: Translation = {
SCAN_AGAIN: 'Rescanner', SCAN_AGAIN: 'Rescanner',
NETWORK_SCANNER: 'Scan réseau', NETWORK_SCANNER: 'Scan réseau',
NETWORK_NO_WIFI: 'Pas de réseau WiFi trouvé', NETWORK_NO_WIFI: 'Pas de réseau WiFi trouvé',
NETWORK_BLANK_SSID: 'laisser vide pour désactiver le WiFi',
NETWORK_BLANK_BSSID: 'laisser vide pour utiliser uniquement le SSID', NETWORK_BLANK_BSSID: 'laisser vide pour utiliser uniquement le SSID',
TX_POWER: 'Puissance Tx', TX_POWER: 'Puissance Tx',
HOSTNAME: "Nom d'hôte", HOSTNAME: "Nom d'hôte",
@@ -350,6 +348,7 @@ const fr: Translation = {
NO_DATA_1: 'Aucune entité EMS favorite trouvée. Utilisez le', NO_DATA_1: 'Aucune entité EMS favorite trouvée. Utilisez le',
NO_DATA_2: 'module pour les marquer.', NO_DATA_2: 'module pour les marquer.',
NO_DATA_3: 'Pour voir toutes les entités disponibles, aller à', 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', THIS_VERSION: 'Cette version',
PLATFORM: 'Plateforme', PLATFORM: 'Plateforme',
RELEASE_TYPE: 'Type de version', RELEASE_TYPE: 'Type de version',
@@ -361,10 +360,10 @@ const fr: Translation = {
STORED_VERSIONS: 'Versions stockées', STORED_VERSIONS: 'Versions stockées',
ONLINE_HELP: 'aide en ligne', ONLINE_HELP: 'aide en ligne',
UPGRADE_IMPORTANT_MESSAGES: 'Mettre à jour les messages importants', UPGRADE_IMPORTANT_MESSAGES: '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.', 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 ?' 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 ?',
TEST_EMAIL_SUCCESSFUL: 'Test email envoyé avec succès'
}; };
export default fr; export default fr;

View File

@@ -247,8 +247,7 @@ const it: Translation = {
TIME_ZONE: 'Fuso orario', TIME_ZONE: 'Fuso orario',
ACCESS_POINT: 'Access Point', ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Abilita Access Point', AP_PROVIDE: 'Abilita Access Point',
AP_PROVIDE_TEXT_1: 'sempre', AP_PROVIDE_TEXT_2: 'quando la connessione di rete è persa',
AP_PROVIDE_TEXT_2: 'quando WiFi é disconnessa',
AP_PROVIDE_TEXT_3: 'mai', AP_PROVIDE_TEXT_3: 'mai',
AP_PREFERRED_CHANNEL: 'Canale preferito', AP_PREFERRED_CHANNEL: 'Canale preferito',
AP_HIDE_SSID: 'Nascondi SSID', AP_HIDE_SSID: 'Nascondi SSID',
@@ -262,7 +261,6 @@ const it: Translation = {
SCAN_AGAIN: 'Scansiona ancora', SCAN_AGAIN: 'Scansiona ancora',
NETWORK_SCANNER: 'Scansione Rete', NETWORK_SCANNER: 'Scansione Rete',
NETWORK_NO_WIFI: 'Nessuana rete WiFi trovata', NETWORK_NO_WIFI: 'Nessuana rete WiFi trovata',
NETWORK_BLANK_SSID: 'lasciare vuoto per disattivare WiFi',
NETWORK_BLANK_BSSID: 'lasciare vuoto per usare solo SSID', NETWORK_BLANK_BSSID: 'lasciare vuoto per usare solo SSID',
TX_POWER: 'Potenza Tx', TX_POWER: 'Potenza Tx',
HOSTNAME: 'Nome ospite', HOSTNAME: 'Nome ospite',
@@ -350,6 +348,7 @@ const it: Translation = {
NO_DATA_1: 'Nessuna entità EMS preferita trovata. Usa il', NO_DATA_1: 'Nessuna entità EMS preferita trovata. Usa il',
NO_DATA_2: 'modulo per marcarle.', NO_DATA_2: 'modulo per marcarle.',
NO_DATA_3: 'Per vedere tutte le entità disponibili vai a', NO_DATA_3: 'Per vedere tutte le entità disponibili vai a',
NO_GPIO: 'Non è stato trovato alcun GPIO disponibile',
THIS_VERSION: 'Questa versione', THIS_VERSION: 'Questa versione',
PLATFORM: 'Piattaforma', PLATFORM: 'Piattaforma',
RELEASE_TYPE: 'Tipo di rilascio', RELEASE_TYPE: 'Tipo di rilascio',
@@ -361,10 +360,10 @@ const it: Translation = {
STORED_VERSIONS: 'Versioni memorizzate', STORED_VERSIONS: 'Versioni memorizzate',
ONLINE_HELP: 'aiuto online', ONLINE_HELP: 'aiuto online',
UPGRADE_IMPORTANT_MESSAGES: 'Aggiorna Messaggi Importanti', 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.', 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?' 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?',
TEST_EMAIL_SUCCESSFUL: 'Test email inviata con successo'
}; };
export default it; export default it;

View File

@@ -247,8 +247,7 @@ const nl: Translation = {
TIME_ZONE: 'Tijdzone', TIME_ZONE: 'Tijdzone',
ACCESS_POINT: 'Access Point', ACCESS_POINT: 'Access Point',
AP_PROVIDE: 'Activeer Access Point', AP_PROVIDE: 'Activeer Access Point',
AP_PROVIDE_TEXT_1: 'altijd', AP_PROVIDE_TEXT_2: 'als netwerk verbinding verloren gaat',
AP_PROVIDE_TEXT_2: 'als WiFi niet is verbonden',
AP_PROVIDE_TEXT_3: 'nooit', AP_PROVIDE_TEXT_3: 'nooit',
AP_PREFERRED_CHANNEL: 'Voorkeurskanaal', AP_PREFERRED_CHANNEL: 'Voorkeurskanaal',
AP_HIDE_SSID: 'SSID verbergen', AP_HIDE_SSID: 'SSID verbergen',
@@ -262,7 +261,6 @@ const nl: Translation = {
SCAN_AGAIN: 'Opnieuw scannen', SCAN_AGAIN: 'Opnieuw scannen',
NETWORK_SCANNER: 'Netwerk Scannen', NETWORK_SCANNER: 'Netwerk Scannen',
NETWORK_NO_WIFI: 'Geen WiFi netwerken gevonden', NETWORK_NO_WIFI: 'Geen WiFi netwerken gevonden',
NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen',
NETWORK_BLANK_BSSID: 'laat leeg om alleen SSID te bebruiken', NETWORK_BLANK_BSSID: 'laat leeg om alleen SSID te bebruiken',
TX_POWER: 'Tx Vermogen', TX_POWER: 'Tx Vermogen',
HOSTNAME: 'Hostnaam', HOSTNAME: 'Hostnaam',
@@ -350,6 +348,7 @@ const nl: Translation = {
NO_DATA_1: 'Er zijn nog geen favoriete EMS-entiteiten gevonden. Gebruik de', NO_DATA_1: 'Er zijn nog geen favoriete EMS-entiteiten gevonden. Gebruik de',
NO_DATA_2: 'module om ze te markeren.', NO_DATA_2: 'module om ze te markeren.',
NO_DATA_3: 'Om alle beschikbare entiteiten te zien, ga naar', NO_DATA_3: 'Om alle beschikbare entiteiten te zien, ga naar',
NO_GPIO: 'Er is geen beschikbare GPIO gevonden',
THIS_VERSION: 'Deze Versie', THIS_VERSION: 'Deze Versie',
PLATFORM: 'Platform', PLATFORM: 'Platform',
RELEASE_TYPE: 'Release Typ', RELEASE_TYPE: 'Release Typ',
@@ -361,10 +360,10 @@ const nl: Translation = {
STORED_VERSIONS: 'Opgeslagen versies', STORED_VERSIONS: 'Opgeslagen versies',
ONLINE_HELP: 'online help', ONLINE_HELP: 'online help',
UPGRADE_IMPORTANT_MESSAGES: 'Upgrade Belangrijke Berichten', 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.', 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?' 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?',
TEST_EMAIL_SUCCESSFUL: 'Test email verzonden succesvol'
}; };
export default nl; export default nl;

View File

@@ -247,8 +247,7 @@ const no: Translation = {
TIME_ZONE: 'Tidssone', TIME_ZONE: 'Tidssone',
ACCESS_POINT: 'Aksesspunkt', ACCESS_POINT: 'Aksesspunkt',
AP_PROVIDE: 'Aktiver Aksesspunkt', AP_PROVIDE: 'Aktiver Aksesspunkt',
AP_PROVIDE_TEXT_1: 'alltid', AP_PROVIDE_TEXT_2: 'når nettverksforbindelsen er utilgjengelig',
AP_PROVIDE_TEXT_2: 'når WiFi er utilgjengelig',
AP_PROVIDE_TEXT_3: 'aldri', AP_PROVIDE_TEXT_3: 'aldri',
AP_PREFERRED_CHANNEL: 'Foretrukket kanal', AP_PREFERRED_CHANNEL: 'Foretrukket kanal',
AP_HIDE_SSID: 'Skjul SSID', AP_HIDE_SSID: 'Skjul SSID',
@@ -262,7 +261,6 @@ const no: Translation = {
SCAN_AGAIN: 'Søk igjen', SCAN_AGAIN: 'Søk igjen',
NETWORK_SCANNER: 'Nettverk Scanner', NETWORK_SCANNER: 'Nettverk Scanner',
NETWORK_NO_WIFI: 'Ingen trådløse nett funnet', NETWORK_NO_WIFI: 'Ingen trådløse nett funnet',
NETWORK_BLANK_SSID: 'la feltet være blankt for å deaktivisere trådløst nettverk',
NETWORK_BLANK_BSSID: 'la feltet være blankt for å bruke kun SSID', NETWORK_BLANK_BSSID: 'la feltet være blankt for å bruke kun SSID',
TX_POWER: 'Tx Effekt', TX_POWER: 'Tx Effekt',
HOSTNAME: 'Hostname', HOSTNAME: 'Hostname',
@@ -350,6 +348,7 @@ const no: Translation = {
NO_DATA_1: 'Ingen favoritte EMS enheter funnet enda. Bruk', NO_DATA_1: 'Ingen favoritte EMS enheter funnet enda. Bruk',
NO_DATA_2: 'modul for å markere dem.', NO_DATA_2: 'modul for å markere dem.',
NO_DATA_3: 'For å se alle tilgjengelige enheter, gå til', NO_DATA_3: 'For å se alle tilgjengelige enheter, gå til',
NO_GPIO: 'Det ble ikke funnet noen tilgjengelige GPIO-porter',
THIS_VERSION: 'Denne versjonen', THIS_VERSION: 'Denne versjonen',
PLATFORM: 'Plattform', PLATFORM: 'Plattform',
RELEASE_TYPE: 'Utgivelses type', RELEASE_TYPE: 'Utgivelses type',
@@ -361,10 +360,10 @@ const no: Translation = {
STORED_VERSIONS: 'Lagret versjoner', STORED_VERSIONS: 'Lagret versjoner',
ONLINE_HELP: 'online hjelp', ONLINE_HELP: 'online hjelp',
UPGRADE_IMPORTANT_MESSAGES: 'Oppdater viktige meldinger', 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.', 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?' 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?',
TEST_EMAIL_SUCCESSFUL: 'Test email sendt suksessfullt'
}; };
export default no; export default no;

View File

@@ -247,7 +247,6 @@ const pl: BaseTranslation = {
TIME_ZONE: 'Strefa czasowa', TIME_ZONE: 'Strefa czasowa',
ACCESS_POINT: '{{Punkt|punktu|}} {{dostępowy|dostępowego|}}', ACCESS_POINT: '{{Punkt|punktu|}} {{dostępowy|dostępowego|}}',
AP_PROVIDE: 'Punkt dostępowy', 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_2: 'aktywny jeśli brak połączenia z siecią',
AP_PROVIDE_TEXT_3: 'nieaktywny', AP_PROVIDE_TEXT_3: 'nieaktywny',
AP_PREFERRED_CHANNEL: 'Preferowany kanał', AP_PREFERRED_CHANNEL: 'Preferowany kanał',
@@ -262,7 +261,6 @@ const pl: BaseTranslation = {
SCAN_AGAIN: 'Skanuj ponownie', SCAN_AGAIN: 'Skanuj ponownie',
NETWORK_SCANNER: 'Skaner sieci WiFi', NETWORK_SCANNER: 'Skaner sieci WiFi',
NETWORK_NO_WIFI: 'Brak sieci WiFi w zasięgu', NETWORK_NO_WIFI: 'Brak sieci WiFi w zasięgu',
NETWORK_BLANK_SSID: 'pozostaw puste aby wyłączyć WiFi i włączyć ETH',
NETWORK_BLANK_BSSID: 'pozostaw puste aby używać tylko SSID', NETWORK_BLANK_BSSID: 'pozostaw puste aby używać tylko SSID',
TX_POWER: 'Moc nadawania', TX_POWER: 'Moc nadawania',
HOSTNAME: 'Nazwa w sieci', HOSTNAME: 'Nazwa w sieci',
@@ -350,6 +348,7 @@ const pl: BaseTranslation = {
NO_DATA_1: 'Brak ulubionych encji EMS. Użyj', NO_DATA_1: 'Brak ulubionych encji EMS. Użyj',
NO_DATA_2: 'moduł do ich oznaczenia.', NO_DATA_2: 'moduł do ich oznaczenia.',
NO_DATA_3: 'Aby zobaczyć wszystkie dostępne encje przejdź do', NO_DATA_3: 'Aby zobaczyć wszystkie dostępne encje przejdź do',
NO_GPIO: 'Nie znaleziono dostępnych pinów GPIO',
THIS_VERSION: 'Ta wersja', THIS_VERSION: 'Ta wersja',
PLATFORM: 'Platforma', PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ wydania', RELEASE_TYPE: 'Typ wydania',
@@ -361,10 +360,10 @@ const pl: BaseTranslation = {
STORED_VERSIONS: 'Zapisane wersje', STORED_VERSIONS: 'Zapisane wersje',
ONLINE_HELP: 'pomoc online', ONLINE_HELP: 'pomoc online',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizuj ważne wiadomości', 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.', 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ć?' 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ć?',
TEST_EMAIL_SUCCESSFUL: 'Test email wysłany pomyślnie'
}; };
export default pl; export default pl;

View File

@@ -247,8 +247,7 @@ const sk: Translation = {
TIME_ZONE: 'Časová zóna', TIME_ZONE: 'Časová zóna',
ACCESS_POINT: 'Prístupový bod', ACCESS_POINT: 'Prístupový bod',
AP_PROVIDE: 'Povoliť prístupový bod', AP_PROVIDE: 'Povoliť prístupový bod',
AP_PROVIDE_TEXT_1: 'vždy', AP_PROVIDE_TEXT_2: 'keď je sieťové pripojenie stratené',
AP_PROVIDE_TEXT_2: 'keď je WiFi odpojená',
AP_PROVIDE_TEXT_3: 'nikdy', AP_PROVIDE_TEXT_3: 'nikdy',
AP_PREFERRED_CHANNEL: 'Preferovaný kanál', AP_PREFERRED_CHANNEL: 'Preferovaný kanál',
AP_HIDE_SSID: 'Skryť SSID', AP_HIDE_SSID: 'Skryť SSID',
@@ -262,7 +261,6 @@ const sk: Translation = {
SCAN_AGAIN: 'Skenovať znova', SCAN_AGAIN: 'Skenovať znova',
NETWORK_SCANNER: 'Sieťový skener', NETWORK_SCANNER: 'Sieťový skener',
NETWORK_NO_WIFI: 'WiFi siete nenájdené', NETWORK_NO_WIFI: 'WiFi siete nenájdené',
NETWORK_BLANK_SSID: 'nechajte prázdne, ak chcete zakázať WiFi a povoliť ETH',
NETWORK_BLANK_BSSID: 'ponechajte prázdne, ak chcete používať iba SSID', NETWORK_BLANK_BSSID: 'ponechajte prázdne, ak chcete používať iba SSID',
TX_POWER: 'Tx výkon', TX_POWER: 'Tx výkon',
HOSTNAME: 'Hostname', HOSTNAME: 'Hostname',
@@ -350,6 +348,7 @@ const sk: Translation = {
NO_DATA_1: 'Nenašli sa žiadne obľúbené entity EMS. Použite', NO_DATA_1: 'Nenašli sa žiadne obľúbené entity EMS. Použite',
NO_DATA_2: 'modul na ich označenie.', NO_DATA_2: 'modul na ich označenie.',
NO_DATA_3: 'Ak chcete zobraziť všetky dostupné entity, prejdite na', 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', THIS_VERSION: 'Táto verzia',
PLATFORM: 'Platforma', PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ vydania', RELEASE_TYPE: 'Typ vydania',
@@ -361,10 +360,10 @@ const sk: Translation = {
STORED_VERSIONS: 'Uložené verzie', STORED_VERSIONS: 'Uložené verzie',
ONLINE_HELP: 'online pomoc', ONLINE_HELP: 'online pomoc',
UPGRADE_IMPORTANT_MESSAGES: 'Aktualizovať dôležité správy', UPGRADE_IMPORTANT_MESSAGES: '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.', 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ť?' 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ť?',
TEST_EMAIL_SUCCESSFUL: 'Test email bol úspešne odoslaný'
}; };
export default sk; export default sk;

View File

@@ -247,8 +247,7 @@ const sv: Translation = {
TIME_ZONE: 'Tidszon', TIME_ZONE: 'Tidszon',
ACCESS_POINT: 'Accesspunkt', ACCESS_POINT: 'Accesspunkt',
AP_PROVIDE: 'Aktivera accesspunkt', AP_PROVIDE: 'Aktivera accesspunkt',
AP_PROVIDE_TEXT_1: 'alltid', AP_PROVIDE_TEXT_2: 'när nätverksanslutningen är bortkopplad',
AP_PROVIDE_TEXT_2: 'när WiFi är nedkopplat',
AP_PROVIDE_TEXT_3: 'aldrig', AP_PROVIDE_TEXT_3: 'aldrig',
AP_PREFERRED_CHANNEL: 'Kanal', AP_PREFERRED_CHANNEL: 'Kanal',
AP_HIDE_SSID: 'Göm SSID', AP_HIDE_SSID: 'Göm SSID',
@@ -262,7 +261,6 @@ const sv: Translation = {
SCAN_AGAIN: 'Sök igen', SCAN_AGAIN: 'Sök igen',
NETWORK_SCANNER: 'Hittade nätverk', NETWORK_SCANNER: 'Hittade nätverk',
NETWORK_NO_WIFI: 'Inga WiFi-nätverk hittades', NETWORK_NO_WIFI: 'Inga WiFi-nätverk hittades',
NETWORK_BLANK_SSID: 'lämna blankt för att inaktivera WiFi',
NETWORK_BLANK_BSSID: 'lämna blankt för att bara använda SSID', NETWORK_BLANK_BSSID: 'lämna blankt för att bara använda SSID',
TX_POWER: 'Tx effekt', TX_POWER: 'Tx effekt',
HOSTNAME: 'Värdnamn', HOSTNAME: 'Värdnamn',
@@ -350,6 +348,7 @@ const sv: Translation = {
NO_DATA_1: 'Inga favorit EMS enheter hittade än. Använd', NO_DATA_1: 'Inga favorit EMS enheter hittade än. Använd',
NO_DATA_2: 'modul för att markera dem.', NO_DATA_2: 'modul för att markera dem.',
NO_DATA_3: 'För att se alla tillgängliga enheter, gå till', 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', THIS_VERSION: 'Denna version',
PLATFORM: 'Plattform', PLATFORM: 'Plattform',
RELEASE_TYPE: 'Utgivelsestyp', RELEASE_TYPE: 'Utgivelsestyp',
@@ -361,10 +360,10 @@ const sv: Translation = {
STORED_VERSIONS: 'Lagrad versioner', STORED_VERSIONS: 'Lagrad versioner',
ONLINE_HELP: 'online hjälp', ONLINE_HELP: 'online hjälp',
UPGRADE_IMPORTANT_MESSAGES: 'Uppdatera viktiga meddelanden', 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.', 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?' 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?',
TEST_EMAIL_SUCCESSFUL: 'Test email skickad lyckades'
}; };
export default sv; export default sv;

View File

@@ -247,8 +247,7 @@ const tr: Translation = {
TIME_ZONE: 'Saat dilimi', TIME_ZONE: 'Saat dilimi',
ACCESS_POINT: 'Erişim Noktası', ACCESS_POINT: 'Erişim Noktası',
AP_PROVIDE: 'Erişim noktasını çalıştır', AP_PROVIDE: 'Erişim noktasını çalıştır',
AP_PROVIDE_TEXT_1: 'her zaman', AP_PROVIDE_TEXT_2: 'Ağ bağlantısı kesildiğinde',
AP_PROVIDE_TEXT_2: 'Kablosuz bağlantı kesildiğinde',
AP_PROVIDE_TEXT_3: 'asla', AP_PROVIDE_TEXT_3: 'asla',
AP_PREFERRED_CHANNEL: 'Tercih edilen kanal', AP_PREFERRED_CHANNEL: 'Tercih edilen kanal',
AP_HIDE_SSID: 'SSID yi gizle', AP_HIDE_SSID: 'SSID yi gizle',
@@ -262,7 +261,6 @@ const tr: Translation = {
SCAN_AGAIN: 'Tekrar tara', SCAN_AGAIN: 'Tekrar tara',
NETWORK_SCANNER: 'Ağ Tarayıcısı', NETWORK_SCANNER: 'Ağ Tarayıcısı',
NETWORK_NO_WIFI: 'Hiçbir Kablosuz Ağ bulunamadı', NETWORK_NO_WIFI: 'Hiçbir Kablosuz Ağ bulunamadı',
NETWORK_BLANK_SSID: 'Kablosuz ağı devre dışı bırakmak için boş bırakın',
NETWORK_BLANK_BSSID: 'sadece SSID kullanmak için boş bırakın', NETWORK_BLANK_BSSID: 'sadece SSID kullanmak için boş bırakın',
TX_POWER: 'Aktarım gücü', TX_POWER: 'Aktarım gücü',
HOSTNAME: 'Ana Makine Adı', HOSTNAME: 'Ana Makine Adı',
@@ -350,6 +348,7 @@ const tr: Translation = {
NO_DATA_1: 'Henüz bir favori EMS varlığı bulunamadı. Kullanın', NO_DATA_1: 'Henüz bir favori EMS varlığı bulunamadı. Kullanın',
NO_DATA_2: 'modülünü 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_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', THIS_VERSION: 'Bu Sürüm',
PLATFORM: 'Platforma', PLATFORM: 'Platforma',
RELEASE_TYPE: 'Sürüm Tipi', RELEASE_TYPE: 'Sürüm Tipi',
@@ -363,8 +362,8 @@ const tr: Translation = {
UPGRADE_IMPORTANT_MESSAGES: 'Önemli Mesajları Güncelle', UPGRADE_IMPORTANT_MESSAGES: 'Önemli Mesajları Güncelle',
UPGRADE_IMPORTANT_MESSAGES_1: 'Bu güncelleme továrnı ayarlarını gerektirir. Yapılandırmanızı ve ayarlarınızı önce yedekleyin ve ardından yeni sürüm yüklendikten sonra yükleyin.', UPGRADE_IMPORTANT_MESSAGES_1: 'Bu güncelleme továrnı ayarlarını gerektirir. Yapılandırmanızı ve ayarlarınızı önce yedekleyin ve ardından yeni sürüm yüklendikten sonra yükleyin.',
UPGRADE_IMPORTANT_MESSAGES_2: 'Yeni bir büyük sürüme yükselteceksiniz. Değişiklikleri ChangeLogı okuduğunuzdan emin olun.', UPGRADE_IMPORTANT_MESSAGES_2: 'Yeni bir büyük sürüme yükselteceksiniz. Değişiklikleri ChangeLogı okuduğunuzdan emin olun.',
WARNING_SYSTEM_BACKUP: 'Bu, sistem yapılandırmanızı ve ayarlarınızın bir yedeklemesi oluşturacaktır. Tüm şifreler yedekleme dosyasında okunabilir olacaktır. Paylaşırken dikkatli olun! Devam etmek istediğinize emin misiniz?' WARNING_SYSTEM_BACKUP: 'Bu, sistem yapılandırmanızı ve ayarlarınızın bir yedeklemesi oluşturacaktır. Tüm şifreler yedekleme dosyasında okunabilir olacaktır. Paylaşırken dikkatli olun! Devam etmek istediğinize emin misiniz?',
TEST_EMAIL_SUCCESSFUL: 'Test email başarıyla gönderildi'
}; };
export default tr; export default tr;

View File

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

View File

@@ -7,3 +7,4 @@ export * from './ntp';
export * from './security'; export * from './security';
export * from './signin'; export * from './signin';
export * from './system'; 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>( export const usePersistState = <T>(
initial_value: T, initial_value: T,
id: string id: string
): [T, (new_state: T) => void] => { ): [T, (new_state: T) => void] => {
// Set initial value - only computed once on mount const [state, setState] = useState<T>(() => {
const _initial_value = useMemo(() => {
try { try {
const local_storage_value_str = localStorage.getItem(`state:${id}`); const stored = localStorage.getItem(`state:${id}`);
// If there is a value stored in localStorage, use that if (stored) {
if (local_storage_value_str) { return JSON.parse(stored) as T;
return JSON.parse(local_storage_value_str) as T;
} }
} catch (error) { } catch (error) {
// If parsing fails, fall back to initial_value
console.warn( console.warn(
`Failed to parse localStorage value for key "state:${id}"`, `Failed to parse localStorage value for key "state:${id}"`,
error error
); );
} }
// Otherwise use initial_value that was passed to the function
return initial_value; return initial_value;
}, [id]); // initial_value intentionally omitted - only read on first mount });
const [state, setState] = useState(_initial_value);
useEffect(() => { useEffect(() => {
try { try {
const state_str = JSON.stringify(state); localStorage.setItem(`state:${id}`, JSON.stringify(state));
localStorage.setItem(`state:${id}`, state_str);
} catch (error) { } catch (error) {
console.warn( console.warn(
`Failed to save state to localStorage for key "state:${id}"`, `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 { useBlocker } from 'react-router';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -54,61 +54,44 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
} }
}, [readData]); }, [readData]);
const saveData = useCallback(async () => { const saveData = async () => {
if (!data) return; if (!data) return;
// Reset states before saving
setRestartNeeded(false); setRestartNeeded(false);
setErrorMessage(undefined); setErrorMessage(undefined);
try { try {
await writeData(data as D); await writeData(data as D);
// Only update origData on successful save (dirtyFlags cleared by onSuccess handler)
setOrigData(data as D); setOrigData(data as D);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
if (message === REBOOT_ERROR_MESSAGE) { if (message === REBOOT_ERROR_MESSAGE) {
setRestartNeeded(true); setRestartNeeded(true);
return; // Early return - save succeeded but needs reboot return;
} }
// Restore original data on validation error
if (origData) { if (origData) {
updateData({ data: origData }); updateData({ data: origData });
} }
toast.error(message); toast.error(message);
setErrorMessage(message); setErrorMessage(message);
setDirtyFlags([]); // Clear flags so user can retry setDirtyFlags([]);
} }
}, [data, writeData, origData, updateData]); };
return useMemo( return {
() => ({ loadData,
loadData, saveData,
saveData, saving: !!saving,
saving: !!saving, updateDataValue,
updateDataValue, data: data as D,
data: data as D, origData: origData as D,
origData: origData as D, dirtyFlags,
dirtyFlags, setDirtyFlags,
setDirtyFlags, setOrigData,
setOrigData, blocker,
blocker, errorMessage,
errorMessage, restartNeeded
restartNeeded };
}),
[
loadData,
saveData,
saving,
updateDataValue,
data,
origData,
dirtyFlags,
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'; 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>( export const validate = <T extends object>(
validator: Schema, validator: Schema,
source: Partial<T>, source: Partial<T>,
@@ -8,7 +22,7 @@ export const validate = <T extends object>(
): Promise<T> => ): Promise<T> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
void validator.validate(source, options ?? {}, (errors, fieldErrors) => { 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 viteImagemin from 'vite-plugin-imagemin';
import zlib from 'zlib'; import zlib from 'zlib';
// @ts-expect-error - mock server doesn't have type declarations
import mockServer from '../mock-api/mockServer.js';
// Constants // Constants
const KB_DIVISOR = 1024; const KB_DIVISOR = 1024;
const REPEAT_CHAR = '='; const REPEAT_CHAR = '=';
const REPEAT_COUNT = 50; const REPEAT_COUNT = 50;
const DEFAULT_OUT_DIR = 'dist'; const DEFAULT_OUT_DIR = 'dist';
const ES_TARGET = 'es2020'; const ES_TARGET = 'es2020';
const CHUNK_SIZE_WARNING_LIMIT = 512; const CHUNK_SIZE_WARNING_LIMIT = 1024;
const ASSETS_INLINE_LIMIT = 4096; const ASSETS_INLINE_LIMIT = 4096;
// Common resolve aliases // Common resolve aliases
@@ -100,6 +97,10 @@ const createPreactPlugin = (devToolsEnabled: boolean) =>
// Patch preact/compat to export stub React 19 APIs (use, useOptimistic) so that // 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. // 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 => ({ const preactCompatPatchPlugin = (): Plugin => ({
name: 'preact-compat-react19-patch', name: 'preact-compat-react19-patch',
transform(code, id) { transform(code, id) {
@@ -130,40 +131,9 @@ const createBasePlugins = (
return plugins; return plugins;
}; };
// Manual chunk splitting strategy const manualChunks = (id: string): string | undefined => {
const createManualChunks = (detailed = false) => { if (id.includes('node_modules')) return 'vendor';
return (id: string): string | undefined => { return 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;
};
}; };
// Common build base configuration // Common build base configuration
@@ -241,9 +211,11 @@ const imageOptimizationPlugin = {
}; };
export default defineConfig( export default defineConfig(
({ command, mode }: { command: string; mode: string }) => { async ({ command, mode }: { command: string; mode: string }) => {
if (command === 'serve') { if (command === 'serve') {
console.log(`Preparing for standalone build with server, mode=${mode}`); 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 { return {
plugins: [...createBasePlugins(true, true), mockServer()], plugins: [...createBasePlugins(true, true), mockServer()],
resolve: { resolve: {
@@ -260,8 +232,7 @@ export default defineConfig(
changeOrigin: true, changeOrigin: true,
secure: false secure: false
}, },
'/rest': 'http://localhost:3080', '/rest': 'http://localhost:3080'
'/gh': 'http://localhost:3080'
} }
}, },
build: { build: {
@@ -290,7 +261,7 @@ export default defineConfig(
moduleSideEffects: false moduleSideEffects: false
}, },
output: { output: {
manualChunks: createManualChunks(false) manualChunks
} }
} }
} }
@@ -330,7 +301,7 @@ export default defineConfig(
chunkFileNames: 'assets/[name]-[hash].js', chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js', entryFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]', assetFileNames: 'assets/[name]-[hash].[ext]',
manualChunks: createManualChunks(true), manualChunks,
sourcemap: false 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