mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-05-11 16:35:52 +00:00
Compare commits
779 Commits
9889b1b5c4
...
core3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db69c43d35 | ||
|
|
97d727a19b | ||
|
|
ff952935a2 | ||
|
|
ae5beccb9d | ||
|
|
a9cdab1ce9 | ||
|
|
015c100a1d | ||
|
|
764c660b14 | ||
|
|
ca0f32b087 | ||
|
|
8ad91de54a | ||
|
|
e7a14183d2 | ||
|
|
5e1b47707f | ||
|
|
56fc303816 | ||
|
|
d7a6a6f803 | ||
|
|
58da157272 | ||
|
|
b5014bf9ac | ||
|
|
14351436a7 | ||
|
|
03d969cb39 | ||
|
|
c55df4eb6e | ||
|
|
cc631ff07f | ||
|
|
b34a1b57d6 | ||
|
|
9cad606a55 | ||
|
|
6587ddcead | ||
|
|
ad05eec952 | ||
|
|
847fa4f46c | ||
|
|
c5897b7ee1 | ||
|
|
99ef4c0c18 | ||
|
|
cc118adec6 | ||
|
|
ca94e37495 | ||
|
|
e2bd721c3e | ||
|
|
033ce24fb7 | ||
|
|
eab7cdd7b5 | ||
|
|
666ba41f67 | ||
|
|
2579450eae | ||
|
|
87a3ca8393 | ||
|
|
9ff4be41f7 | ||
|
|
da3ed6cd3a | ||
|
|
23519a8a90 | ||
|
|
242708358e | ||
|
|
3cc3c74e5a | ||
|
|
cb4cb39396 | ||
|
|
363799c9c6 | ||
|
|
132f83aa79 | ||
|
|
f998714225 | ||
|
|
323fc1bb99 | ||
|
|
3062d3f0e3 | ||
|
|
8f37bb7623 | ||
|
|
a57ed90756 | ||
|
|
eaf8332d16 | ||
|
|
522286ff74 | ||
|
|
747047556e | ||
|
|
df3d75c702 | ||
|
|
e40beeadd4 | ||
|
|
751f10603d | ||
|
|
751c540cb3 | ||
|
|
d7bbc329bb | ||
|
|
41cd49a61c | ||
|
|
fd5a39702b | ||
|
|
4d3408254e | ||
|
|
2cbb5ec5f2 | ||
|
|
3b765b308e | ||
|
|
53ac82520e | ||
|
|
381fcf4080 | ||
|
|
a3f0faf022 | ||
|
|
b3a8737a71 | ||
|
|
6e76bcc9af | ||
|
|
6473c55317 | ||
|
|
1a880f14a0 | ||
|
|
e39af36589 | ||
|
|
c5b262af8a | ||
|
|
43ec5c1925 | ||
|
|
5e260f0239 | ||
|
|
ab67f97b40 | ||
|
|
9ac35e2e14 | ||
|
|
7c6259dddd | ||
|
|
1cff1abc33 | ||
|
|
d834d46586 | ||
|
|
1107e1bdf3 | ||
|
|
3a11327e7e | ||
|
|
74062bab57 | ||
|
|
6802336b6b | ||
|
|
a9db134d3a | ||
|
|
ee7be1d907 | ||
|
|
5ecda88457 | ||
|
|
7056c446fa | ||
|
|
147c09ae64 | ||
|
|
112adf9eb0 | ||
|
|
469d412951 | ||
|
|
6edbac86e2 | ||
|
|
0e08334132 | ||
|
|
3d51acf9e7 | ||
|
|
fd6ea5ed7e | ||
|
|
db2be70d66 | ||
|
|
c36f231990 | ||
|
|
d18e5b1f14 | ||
|
|
20327d817d | ||
|
|
26102121e1 | ||
|
|
8e64c6303e | ||
|
|
051c332426 | ||
|
|
a09258325e | ||
|
|
74c76eb90b | ||
|
|
daffdcf58e | ||
|
|
61dca0cbda | ||
|
|
2bff299193 | ||
|
|
4bc4fa903f | ||
|
|
1329b13db3 | ||
|
|
29380f0303 | ||
|
|
9dd894f0fe | ||
|
|
6b2370b79d | ||
|
|
dbc636c9bf | ||
|
|
30d1ae5642 | ||
|
|
79aceef382 | ||
|
|
a28e52210a | ||
|
|
0c0660c04b | ||
|
|
08eb294213 | ||
|
|
c9fd076394 | ||
|
|
888baed81a | ||
|
|
4de3955db2 | ||
|
|
25f08c7624 | ||
|
|
35550553be | ||
|
|
06ff219385 | ||
|
|
e705a5629f | ||
|
|
1e8013100c | ||
|
|
62c8f55568 | ||
|
|
cb3c9653ce | ||
|
|
0b5a83f6ae | ||
|
|
a079169005 | ||
|
|
845c51d5f9 | ||
|
|
c40d828749 | ||
|
|
d6d3a034ad | ||
|
|
84ad08887a | ||
|
|
ece08d96ee | ||
|
|
ed0a678020 | ||
|
|
854f4d559a | ||
|
|
f186f2a8f2 | ||
|
|
37107d8500 | ||
|
|
6b68cb7c61 | ||
|
|
a1e0288e09 | ||
|
|
e6c173bdf9 | ||
|
|
dde6a8c5db | ||
|
|
e2750b8572 | ||
|
|
acd23925b5 | ||
|
|
b0db054e11 | ||
|
|
d9b6de0652 | ||
|
|
c54da18822 | ||
|
|
51cea8e757 | ||
|
|
bbb086ea41 | ||
|
|
539e6ed080 | ||
|
|
555801dc5c | ||
|
|
1d33a26318 | ||
|
|
86a20fc97a | ||
|
|
d6e00c4534 | ||
|
|
6f81945da6 | ||
|
|
865c309475 | ||
|
|
77b8b21aea | ||
|
|
2f5edffec6 | ||
|
|
71de64502e | ||
|
|
6994d3559a | ||
|
|
a7d484d218 | ||
|
|
a810c41acd | ||
|
|
2fbfdf94ab | ||
|
|
2d7c8f0863 | ||
|
|
c3b734ab47 | ||
|
|
644abf105d | ||
|
|
5a8a451774 | ||
|
|
dae139aa01 | ||
|
|
b13fcd8939 | ||
|
|
26b42b4eea | ||
|
|
c9005e8aa9 | ||
|
|
6658b11adf | ||
|
|
e542f5809f | ||
|
|
ce1dd6233d | ||
|
|
fe488443da | ||
|
|
b264a39780 | ||
|
|
d2302eaa85 | ||
|
|
a813d38108 | ||
|
|
685a49c212 | ||
|
|
994706c9f2 | ||
|
|
2c8eb534af | ||
|
|
5210fab4cb | ||
|
|
49787d27f1 | ||
|
|
dfe7b46461 | ||
|
|
8a72ab42cb | ||
|
|
c4db8e3914 | ||
|
|
8d0225e595 | ||
|
|
f8257de0dd | ||
|
|
3b3ecc9f1d | ||
|
|
966049d0c9 | ||
|
|
907a65a701 | ||
|
|
f97b8e14e7 | ||
|
|
e65f634b21 | ||
|
|
fc71ed2b9d | ||
|
|
84105acf5d | ||
|
|
def5173692 | ||
|
|
6b31fef1af | ||
|
|
c9c059ca65 | ||
|
|
4d3b31e5a1 | ||
|
|
5a8195d430 | ||
|
|
24a7a607f3 | ||
|
|
061f9ffc52 | ||
|
|
9e17936bfc | ||
|
|
18bb2c4f39 | ||
|
|
7c3782a43f | ||
|
|
3ac807bdd5 | ||
|
|
1111458863 | ||
|
|
99c5e2230c | ||
|
|
3317aa845a | ||
|
|
97cd657336 | ||
|
|
3338f919bd | ||
|
|
7dd13bcab7 | ||
|
|
f226cb359f | ||
|
|
abbba0aa42 | ||
|
|
39b5a52b01 | ||
|
|
b6c3fc5bee | ||
|
|
909bea00df | ||
|
|
9522945e06 | ||
|
|
d6a9f2a731 | ||
|
|
0f30c81554 | ||
|
|
e514ba4bb5 | ||
|
|
38e63e3eaa | ||
|
|
0058324edd | ||
|
|
ac143d607a | ||
|
|
e9e3759db3 | ||
|
|
51d90095aa | ||
|
|
fb09e10f19 | ||
|
|
16c0370443 | ||
|
|
67bb38dcf4 | ||
|
|
049231a36e | ||
|
|
349d6b7375 | ||
|
|
b72b368d3c | ||
|
|
7f9fd44a02 | ||
|
|
a400c5974c | ||
|
|
afca995fe5 | ||
|
|
81504fedc5 | ||
|
|
3da3345683 | ||
|
|
c6c2889306 | ||
|
|
b60f0d260a | ||
|
|
cd750e4777 | ||
|
|
4e5d503b35 | ||
|
|
bd09e17e49 | ||
|
|
835eb743bb | ||
|
|
69a129d80e | ||
|
|
434bf483fd | ||
|
|
2b8e170b40 | ||
|
|
dc9b95f3e7 | ||
|
|
1616b0da0a | ||
|
|
91c457b22b | ||
|
|
70c60647c7 | ||
|
|
c0bea66d27 | ||
|
|
ed7cc078ed | ||
|
|
60b7d6d795 | ||
|
|
947f29cca0 | ||
|
|
d2a13ec0da | ||
|
|
cc39ba409e | ||
|
|
09473f17a0 | ||
|
|
ac9db6256e | ||
|
|
096f628d97 | ||
|
|
bbc2de08a5 | ||
|
|
22312812bb | ||
|
|
df808a2bcf | ||
|
|
d04e7c36f3 | ||
|
|
205d826adb | ||
|
|
3584975acb | ||
|
|
30b9ca4e6c | ||
|
|
7c6ff01ebe | ||
|
|
a54edcaf5b | ||
|
|
e446954844 | ||
|
|
4a2d0d6787 | ||
|
|
9725314135 | ||
|
|
4db8e43648 | ||
|
|
e610f0d57f | ||
|
|
8244af2940 | ||
|
|
cc60062678 | ||
|
|
40f371d23b | ||
|
|
817b791e59 | ||
|
|
25a7aac360 | ||
|
|
37115a174d | ||
|
|
1397f81fd0 | ||
|
|
56365cb403 | ||
|
|
dfd245ee7b | ||
|
|
9c81e4b34d | ||
|
|
67676df131 | ||
|
|
a73b129596 | ||
|
|
4600d886b5 | ||
|
|
e3305ab9db | ||
|
|
0fe45a2405 | ||
|
|
db87213242 | ||
|
|
b0157f288e | ||
|
|
5c3c010d5a | ||
|
|
c804cedd7a | ||
|
|
a9f50d9371 | ||
|
|
65a3226404 | ||
|
|
45690f5418 | ||
|
|
aa30ca99bf | ||
|
|
6836b6197f | ||
|
|
c0ca9d1069 | ||
|
|
5e79e1d57f | ||
|
|
8c732f9f1e | ||
|
|
5e94c2f636 | ||
|
|
69d4163b9d | ||
|
|
b1e974a82c | ||
|
|
34a2b20be8 | ||
|
|
f1fc8d9aae | ||
|
|
b04355e3e1 | ||
|
|
cd3ae5cdf2 | ||
|
|
a261ca23af | ||
|
|
cb96904a5c | ||
|
|
4a2d78f8e1 | ||
|
|
f5af4fb52f | ||
|
|
2037bc3a10 | ||
|
|
64d17d7c65 | ||
|
|
92e2633342 | ||
|
|
96a7ea8a02 | ||
|
|
5c4aaa4510 | ||
|
|
c05e1cb77b | ||
|
|
5879ce4090 | ||
|
|
ac3e5c793c | ||
|
|
64e5d29996 | ||
|
|
b320d8ded2 | ||
|
|
4326fb931b | ||
|
|
ced7051ce7 | ||
|
|
0be1b20996 | ||
|
|
6c55460622 | ||
|
|
421da246ed | ||
|
|
d627404dc2 | ||
|
|
148a721e17 | ||
|
|
f317123c26 | ||
|
|
e4df1887b0 | ||
|
|
34142c3e85 | ||
|
|
6e7f8bdf02 | ||
|
|
3dd9fcfb58 | ||
|
|
35e2954b8b | ||
|
|
59aa63db0f | ||
|
|
7a41a190f8 | ||
|
|
6741232450 | ||
|
|
a811670c5a | ||
|
|
72f08a86cf | ||
|
|
27c471f45f | ||
|
|
e303972d26 | ||
|
|
97bb03d703 | ||
|
|
e9f77c1bde | ||
|
|
81cba6c0a8 | ||
|
|
89029df25e | ||
|
|
3463b6818d | ||
|
|
349843e666 | ||
|
|
96ae3bbbba | ||
|
|
b153364b60 | ||
|
|
ccc40937fb | ||
|
|
909edf394d | ||
|
|
ac8ef646e9 | ||
|
|
3ef279ea10 | ||
|
|
769beeda37 | ||
|
|
f83404c216 | ||
|
|
c239658131 | ||
|
|
be82afd778 | ||
|
|
2156c96368 | ||
|
|
f7f078d82a | ||
|
|
95168cf514 | ||
|
|
6dc601c4a2 | ||
|
|
6b87bbb882 | ||
|
|
abdf2c5037 | ||
|
|
7beec1b80f | ||
|
|
3a0e46f064 | ||
|
|
85cc85a923 | ||
|
|
ca0079c0df | ||
|
|
28b662ad43 | ||
|
|
6bac6bbfeb | ||
|
|
958ec1002b | ||
|
|
438852ecaf | ||
|
|
92d82c0a00 | ||
|
|
7d37267f57 | ||
|
|
5641d53cc3 | ||
|
|
8fc6752290 | ||
|
|
cca6f87500 | ||
|
|
758d76051f | ||
|
|
95f7e66cff | ||
|
|
4e194287c9 | ||
|
|
3545830552 | ||
|
|
074f4c32ed | ||
|
|
b3fec5ed7d | ||
|
|
ffb90b8f9a | ||
|
|
584618043d | ||
|
|
d702c485b7 | ||
|
|
d3561da331 | ||
|
|
0e0aaf37df | ||
|
|
5ec068409f | ||
|
|
8796b6d340 | ||
|
|
bfbb18655d | ||
|
|
9088651e53 | ||
|
|
3e8f379502 | ||
|
|
265c2c4231 | ||
|
|
f671d79280 | ||
|
|
97c89d1d13 | ||
|
|
e0a26a38fa | ||
|
|
038f06e59f | ||
|
|
f4d2bae04f | ||
|
|
d443e275ea | ||
|
|
30d2057e01 | ||
|
|
d952b9aaae | ||
|
|
3402215e8d | ||
|
|
f01031dc26 | ||
|
|
01d4d116b9 | ||
|
|
bc7f82eef1 | ||
|
|
efdb355033 | ||
|
|
87c9fd010f | ||
|
|
930f0e615a | ||
|
|
4dbbdb3290 | ||
|
|
8379b3f5aa | ||
|
|
08124fa4db | ||
|
|
39414db732 | ||
|
|
da57a08005 | ||
|
|
b6b8700c3f | ||
|
|
8ee0789dad | ||
|
|
1eabe86015 | ||
|
|
5fe4b315f3 | ||
|
|
03ec96bf96 | ||
|
|
aa05e37fbb | ||
|
|
959a00c19a | ||
|
|
b42060be3a | ||
|
|
33bb433d7e | ||
|
|
28a5d4ef1a | ||
|
|
b78d47cbd0 | ||
|
|
8a7a1383a7 | ||
|
|
3f5163c1e4 | ||
|
|
fad82c8c68 | ||
|
|
6fc3bf30b6 | ||
|
|
43b3e74c08 | ||
|
|
64c9882d8c | ||
|
|
a690510903 | ||
|
|
c732ec301a | ||
|
|
cc1f16596a | ||
|
|
66c74f85a4 | ||
|
|
db667b9437 | ||
|
|
8799015f59 | ||
|
|
d71b3c64e1 | ||
|
|
6d3083fff4 | ||
|
|
56b2c111b8 | ||
|
|
12ce736580 | ||
|
|
debe90eb8d | ||
|
|
8d318143a4 | ||
|
|
59f90f5d1c | ||
|
|
0efbd0528e | ||
|
|
3218620a0e | ||
|
|
a93921c875 | ||
|
|
fb77b455be | ||
|
|
b64c392c58 | ||
|
|
7bc6cf3910 | ||
|
|
e19e76546e | ||
|
|
b9aaaae90a | ||
|
|
86b395d612 | ||
|
|
6204b9acc7 | ||
|
|
0777f420b0 | ||
|
|
4a6a662aa0 | ||
|
|
66837d399f | ||
|
|
34ff5f12ea | ||
|
|
82d160cabb | ||
|
|
2c85d3829b | ||
|
|
cc258dae16 | ||
|
|
577964befe | ||
|
|
f2500013ab | ||
|
|
98fb6941d2 | ||
|
|
7308b8e73d | ||
|
|
e97cfaf9ee | ||
|
|
fd0734d8d8 | ||
|
|
739f32f045 | ||
|
|
b66b49e812 | ||
|
|
d4b81a2909 | ||
|
|
fb57537e88 | ||
|
|
335b1274cf | ||
|
|
f6d1c87eaf | ||
|
|
5e07e9a11b | ||
|
|
dd0ea5df0e | ||
|
|
2b7f592957 | ||
|
|
db7b5df85d | ||
|
|
1c2534ed8f | ||
|
|
90535d7b94 | ||
|
|
0d3a8fc719 | ||
|
|
d624b9eac9 | ||
|
|
068cbf757c | ||
|
|
738d6f0b0f | ||
|
|
d9551bc4c3 | ||
|
|
722659325a | ||
|
|
927e7c80f4 | ||
|
|
39919b4ad8 | ||
|
|
b5defa552e | ||
|
|
e8fbbe5a1c | ||
|
|
949172128f | ||
|
|
35a8db4581 | ||
|
|
3a74abb4db | ||
|
|
bcc7687b1b | ||
|
|
13aa544214 | ||
|
|
696141721a | ||
|
|
4781eea665 | ||
|
|
2fd6bed485 | ||
|
|
db40d1d381 | ||
|
|
33bde8b407 | ||
|
|
bf5990a992 | ||
|
|
cff4bd0a71 | ||
|
|
28ee0834d8 | ||
|
|
9be1cb1d3e | ||
|
|
81d46fede2 | ||
|
|
664a8e9f5f | ||
|
|
def7501c62 | ||
|
|
1cc4dc52d4 | ||
|
|
978c738f27 | ||
|
|
feeb8500ac | ||
|
|
18a55d4622 | ||
|
|
29ea67f438 | ||
|
|
a3df77171b | ||
|
|
aa6f5c50b2 | ||
|
|
53a43ca147 | ||
|
|
da76fe3871 | ||
|
|
84af132e2c | ||
|
|
712a8537c9 | ||
|
|
bb22386f7f | ||
|
|
89dfe11ee3 | ||
|
|
be1e08af9c | ||
|
|
68ebcdded4 | ||
|
|
4afe041880 | ||
|
|
8b690d23da | ||
|
|
62c7fb671b | ||
|
|
5a82064a88 | ||
|
|
4ae4000944 | ||
|
|
616c73f658 | ||
|
|
b698485814 | ||
|
|
b4036bf8cd | ||
|
|
4b457d6cdb | ||
|
|
425b44e334 | ||
|
|
41bf293db3 | ||
|
|
af349edd54 | ||
|
|
5b303bd58a | ||
|
|
b992f90fe2 | ||
|
|
92c34dddba | ||
|
|
1475fc094d | ||
|
|
48f7b48216 | ||
|
|
cd054b293a | ||
|
|
ea6b7c0be0 | ||
|
|
a49a5537d3 | ||
|
|
c407ad04bf | ||
|
|
480e0951b8 | ||
|
|
a06c5bb297 | ||
|
|
77a54792dd | ||
|
|
d0d49397ca | ||
|
|
b51abeabac | ||
|
|
f0e4f17ab8 | ||
|
|
205da33fe5 | ||
|
|
9aa78111be | ||
|
|
c3f93d4aae | ||
|
|
5a5c0d7179 | ||
|
|
ba57942b7d | ||
|
|
c782deb581 | ||
|
|
566edfcd7b | ||
|
|
c3cf38c330 | ||
|
|
4953a41330 | ||
|
|
7ff4ed640d | ||
|
|
898a13fcb5 | ||
|
|
3fdc370466 | ||
|
|
39055ad0d2 | ||
|
|
21e73c973a | ||
|
|
4d03976032 | ||
|
|
eb7587270f | ||
|
|
3ea88a2be0 | ||
|
|
55778ba0b5 | ||
|
|
5d3694abd3 | ||
|
|
21d9ba0182 | ||
|
|
27848feddd | ||
|
|
ce261eeb65 | ||
|
|
8de3ae5468 | ||
|
|
367d27d48f | ||
|
|
1a9b6ab2a5 | ||
|
|
323b8ee67d | ||
|
|
5b95e1d41f | ||
|
|
a272d8e253 | ||
|
|
79285ca12e | ||
|
|
f90f676faf | ||
|
|
3224d8823d | ||
|
|
1a03b98670 | ||
|
|
80f32bfeb4 | ||
|
|
1b4693b981 | ||
|
|
535b760dd7 | ||
|
|
14775f6503 | ||
|
|
a856d249c9 | ||
|
|
484b547df5 | ||
|
|
d73ca2c890 | ||
|
|
6211bd8c69 | ||
|
|
e638a471d1 | ||
|
|
42ee21e883 | ||
|
|
263af58dc0 | ||
|
|
6727c0655a | ||
|
|
cba249938a | ||
|
|
9fbed47617 | ||
|
|
b5dd722888 | ||
|
|
11bef52568 | ||
|
|
d22a369333 | ||
|
|
dfe95296d9 | ||
|
|
8d39893e5e | ||
|
|
b8b8a501e1 | ||
|
|
2a36f378b4 | ||
|
|
6746df37a1 | ||
|
|
364f66b7d4 | ||
|
|
84bbd93216 | ||
|
|
85ef8d7d50 | ||
|
|
2b6606d8ad | ||
|
|
4772a61e7c | ||
|
|
d30375f3c4 | ||
|
|
8c831ac0e9 | ||
|
|
1ede7028a5 | ||
|
|
5b8dd0a693 | ||
|
|
cc041510be | ||
|
|
05baec85b7 | ||
|
|
fb698fd029 | ||
|
|
bbfec136e8 | ||
|
|
36271a2c24 | ||
|
|
bbe1f133dc | ||
|
|
d7b0614556 | ||
|
|
7e9f27a613 | ||
|
|
3bdc97d4d5 | ||
|
|
8a7511a941 | ||
|
|
a9511e6a29 | ||
|
|
ac37ead419 | ||
|
|
39fcda59da | ||
|
|
9c44e104bb | ||
|
|
18f8db7942 | ||
|
|
71281bc82d | ||
|
|
0557def0b6 | ||
|
|
1a3f7fbbee | ||
|
|
7bed8bf84e | ||
|
|
790371a9e2 | ||
|
|
537cf19e97 | ||
|
|
35ad43b7b3 | ||
|
|
3d70e8c1e6 | ||
|
|
7bb30d37ce | ||
|
|
0267f00b48 | ||
|
|
5a7cec91c5 | ||
|
|
642bf63abc | ||
|
|
6f3197f482 | ||
|
|
5c88968879 | ||
|
|
c4a43183b3 | ||
|
|
94b583d7f3 | ||
|
|
37012e55e3 | ||
|
|
ea4d613d12 | ||
|
|
0e6108b5a9 | ||
|
|
fdbaf7509f | ||
|
|
74182031ae | ||
|
|
6c80a34578 | ||
|
|
09f1c13d28 | ||
|
|
5668fe13ae | ||
|
|
78a02b6d85 | ||
|
|
ccab932e8d | ||
|
|
eaea1f383b | ||
|
|
de8309de4a | ||
|
|
bc3269037f | ||
|
|
31131427b8 | ||
|
|
9957bff62b | ||
|
|
f1841347a7 | ||
|
|
1b8b72c443 | ||
|
|
b4affbff6d | ||
|
|
c8b4a38bb6 | ||
|
|
eec373e2ae | ||
|
|
48b261317d | ||
|
|
e6beb01075 | ||
|
|
ecc6e9286a | ||
|
|
179351cb6b | ||
|
|
778fe43012 | ||
|
|
d84d52df4b | ||
|
|
11d4109915 | ||
|
|
0eddbac150 | ||
|
|
99afeb221a | ||
|
|
39d18b78a1 | ||
|
|
4ebe8cc0cc | ||
|
|
6dabfb7fe2 | ||
|
|
611b1d9aca | ||
|
|
2821f8e750 | ||
|
|
1cccd8dc2c | ||
|
|
4d13982594 | ||
|
|
10c63640c0 | ||
|
|
14ad1239db | ||
|
|
2a16cb6e64 | ||
|
|
89fa5947bd | ||
|
|
a81e56e3bf | ||
|
|
90ad2dde54 | ||
|
|
9c243cbe8d | ||
|
|
c1b444541f | ||
|
|
8527b16e9d | ||
|
|
a728420010 | ||
|
|
378d9e8634 | ||
|
|
1973081529 | ||
|
|
45ae6d802c | ||
|
|
c4297e2996 | ||
|
|
310afe3ab8 | ||
|
|
3215f36530 | ||
|
|
55621e12d9 | ||
|
|
458dc516f4 | ||
|
|
f2ae84bd22 | ||
|
|
f093df1cb9 | ||
|
|
05f15f7876 | ||
|
|
12f4a74094 | ||
|
|
15d895fd0a | ||
|
|
1890948924 | ||
|
|
fec246127f | ||
|
|
ecab30d4ac | ||
|
|
f8cc688241 | ||
|
|
f51b3528d5 | ||
|
|
42c94a1017 | ||
|
|
acccb56f07 | ||
|
|
3a2d3ac985 | ||
|
|
0ab18e6e08 | ||
|
|
44db5991e7 | ||
|
|
15a6c50326 | ||
|
|
911aa40ca1 | ||
|
|
2b679daabc | ||
|
|
71b956e613 | ||
|
|
79089a93bc | ||
|
|
da3ac1794e | ||
|
|
bc870b2aa2 | ||
|
|
6bc40ce2e1 | ||
|
|
84544979fa | ||
|
|
2446e4d1fd | ||
|
|
4a15b39945 | ||
|
|
df080bbad9 | ||
|
|
8632af8820 | ||
|
|
27047c0f39 | ||
|
|
95de3e339d | ||
|
|
99f44aece5 | ||
|
|
4e589aecbf | ||
|
|
ab013554bd | ||
|
|
5c966e291b | ||
|
|
068744b681 | ||
|
|
8c794baa7b | ||
|
|
f6a23723f8 | ||
|
|
4b3cc2e3ec | ||
|
|
4ee743d309 | ||
|
|
d3b4bab40a | ||
|
|
c71b54fb5b | ||
|
|
ecbdf01bdf | ||
|
|
c2e8a6c73d | ||
|
|
29037d19fb | ||
|
|
67d242d210 | ||
|
|
91a67def99 | ||
|
|
c8ab89ef37 | ||
|
|
17a9b7eb0a | ||
|
|
cdb592d744 | ||
|
|
af19941a07 | ||
|
|
00dce83096 | ||
|
|
597e60d6f1 | ||
|
|
4e4311cac0 | ||
|
|
c11402195f | ||
|
|
31ec15811f | ||
|
|
739ac7b42e | ||
|
|
ad3049ab0a | ||
|
|
d96c3f3ed7 | ||
|
|
dcfd0d5b11 | ||
|
|
b05712cf83 | ||
|
|
df15485d7c | ||
|
|
bb15fcdc46 | ||
|
|
e34291fbd5 | ||
|
|
55b8c2d04c | ||
|
|
8bd1cd03b4 | ||
|
|
bafb7c35fb | ||
|
|
9425558ff7 | ||
|
|
f20aad3813 | ||
|
|
7a683d3637 | ||
|
|
ac982cbb15 | ||
|
|
7cb647e5f1 | ||
|
|
515b75160c | ||
|
|
a365dc7519 | ||
|
|
764520714b | ||
|
|
43e087ae91 | ||
|
|
26a5f98aae | ||
|
|
67280546af | ||
|
|
1d03056784 | ||
|
|
dd0ab8f962 | ||
|
|
0bc478f9d3 | ||
|
|
882e3bc1cd | ||
|
|
1472e53410 | ||
|
|
5ed4970d62 | ||
|
|
056cf3cbd6 | ||
|
|
2bcd548747 | ||
|
|
9edcf47073 | ||
|
|
084d90e714 |
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -21,8 +21,8 @@ _Make sure your have performed every step and checked the applicable boxes befor
|
|||||||
|
|
||||||
- [ ] Searched the issue in [issues](https://github.com/emsesp/EMS-ESP32/issues)
|
- [ ] Searched the issue in [issues](https://github.com/emsesp/EMS-ESP32/issues)
|
||||||
- [ ] Searched the issue in [discussions](https://github.com/emsesp/EMS-ESP32/discussions)
|
- [ ] Searched the issue in [discussions](https://github.com/emsesp/EMS-ESP32/discussions)
|
||||||
- [ ] Searched the issue in the [docs](https://docs.emsesp.org/Troubleshooting/)
|
- [ ] Searched the issue in the [docs](https://emsesp.org/Troubleshooting/)
|
||||||
- [ ] Searched the issue in the [chat](https://discord.gg/3J3GgnzpyT)
|
- [ ] Searched the issue in the [chat](https://discord.gg/GP9DPSgeJq)
|
||||||
- [ ] Provide the System information in the area below, taken from `http://<IP>/api/system`
|
- [ ] Provide the System information in the area below, taken from `http://<IP>/api/system`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,11 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: EMS-ESP Docs
|
- name: EMS-ESP Docs
|
||||||
url: https://docs.emsesp.org
|
url: https://emsesp.org
|
||||||
about: All the information related to EMS-ESP.
|
about: All the information related to EMS-ESP.
|
||||||
- name: EMS-ESP Discussions and Support
|
- name: EMS-ESP Discussions and Support
|
||||||
url: https://github.com/emsesp/EMS-ESP32/discussions
|
url: https://github.com/emsesp/EMS-ESP32/discussions
|
||||||
about: EMS-ESP usage Questions, Feature Requests and Projects.
|
about: EMS-ESP usage Questions, Feature Requests and Projects.
|
||||||
- name: EMS-ESP Users Chat
|
- name: EMS-ESP Users Chat
|
||||||
url: https://discord.gg/3J3GgnzpyT
|
url: https://discord.gg/GP9DPSgeJq
|
||||||
about: Chat for feedback, questions and troubleshooting.
|
about: Chat for feedback, questions and troubleshooting.
|
||||||
|
|||||||
28
.github/workflows/dev_release.yml
vendored
28
.github/workflows/dev_release.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
2
.github/workflows/pr_check.yml
vendored
2
.github/workflows/pr_check.yml
vendored
@@ -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
|
||||||
|
|||||||
2
.github/workflows/sonar_check.yml
vendored
2
.github/workflows/sonar_check.yml
vendored
@@ -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
|
||||||
|
|||||||
33
.github/workflows/stable_release.yml
vendored
33
.github/workflows/stable_release.yml
vendored
@@ -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
|
||||||
|
|||||||
6
.github/workflows/test_release.yml
vendored
6
.github/workflows/test_release.yml
vendored
@@ -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
40
.github/workflows/update_versions.yml
vendored
Normal 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
4
.gitignore
vendored
@@ -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
101
.vscode/settings.json
vendored
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
116
CHANGELOG.md
116
CHANGELOG.md
@@ -5,6 +5,120 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.8.1] 11 January 2026
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- update time saved in nvs
|
||||||
|
- heatpump entities [#2883](https://github.com/emsesp/EMS-ESP32/issues/2883)
|
||||||
|
- HA input number format (mode) selectable box/slider (slider for max range 100) [#2900](https://github.com/emsesp/EMS-ESP32/discussions/2900)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- fix EMS bus disconnected errors on some systems [#2881](https://github.com/emsesp/EMS-ESP32/issues/2881)
|
||||||
|
- selflowtemp fix [#2876](https://github.com/emsesp/EMS-ESP32/issues/2876)
|
||||||
|
- updated valid GPIOs for ESP32S2, ESP32S3 and ESP32 that caused custom systems to block gpios [#2887](https://github.com/emsesp/EMS-ESP32/issues/2887)
|
||||||
|
- Junkers wwcharge offset [#2860](https://github.com/emsesp/EMS-ESP32/issues/2860)
|
||||||
|
- fixed minflowtemp [#2890](https://github.com/emsesp/EMS-ESP32/issues/2890)
|
||||||
|
- don't add HA uom/classes for bool values [#2885](https://github.com/emsesp/EMS-ESP32/issues/2885)
|
||||||
|
- fixed missing progress bar on web firmware uploads
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- snapshot gpios stored in temporary ram
|
||||||
|
- GPIOs stored along with the name and reported in log if conflicting
|
||||||
|
- free GPIOs depend on board profile [#2901](https://github.com/emsesp/EMS-ESP32/issues/2901)
|
||||||
|
- prefer PSram for mqtt queue [#2889](https://github.com/emsesp/EMS-ESP32/issues/2889)
|
||||||
|
- day schedule defult to all days, no day selected is not allowed
|
||||||
|
- board profile `CUSTOM` can only be selected in developer mode
|
||||||
|
- mqtt sends round values without decimals (`28` instead of `28.0`)
|
||||||
|
|
||||||
|
|
||||||
|
## [3.8.0] 31 December 2025
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- analogsensor types: NTC and RGB-Led
|
||||||
|
- Flag for HMC310 [#2465](https://github.com/emsesp/EMS-ESP32/issues/2465)
|
||||||
|
- boiler auxheatersource [#2489](https://github.com/emsesp/EMS-ESP32/discussions/2489)
|
||||||
|
- thermostat last error for RC100/300 [#2501](https://github.com/emsesp/EMS-ESP32/issues/2501)
|
||||||
|
- boiler 0xC6 telegram [#1963](https://github.com/emsesp/EMS-ESP32/issues/1963)
|
||||||
|
- CS6800i changes [#2448](https://github.com/emsesp/EMS-ESP32/issues/2448), [#2449](https://github.com/emsesp/EMS-ESP32/issues/2449)
|
||||||
|
- charging pump [#2544](https://github.com/emsesp/EMS-ESP32/issues/2544)
|
||||||
|
- hybrid CSH5800iG [#2569](https://github.com/emsesp/EMS-ESP32/issues/2569)
|
||||||
|
- added EMS Device details to Home Assistant MQTT Discovery
|
||||||
|
- disinfection command [#2601](https://github.com/emsesp/EMS-ESP32/issues/2601)
|
||||||
|
- added new board profile for upcoming BBQKees E32V2.2
|
||||||
|
- set differential pressure entity in Mixer device
|
||||||
|
- set set climate action cooling/heating in HA [#2583](https://github.com/emsesp/EMS-ESP32/issues/2583)
|
||||||
|
- Internal sensors of E32V2_2
|
||||||
|
- FW200 display options [#2610](https://github.com/emsesp/EMS-ESP32/discussions/2610)
|
||||||
|
- CR11 mode settings OFF/MANUAL depends on selTemp [#2437](https://github.com/emsesp/EMS-ESP32/issues/2437)
|
||||||
|
- implemented eFuse settings for BBQKees boards to store model type and ESP chipset
|
||||||
|
- analogsensors for pulse output [#2624](https://github.com/emsesp/EMS-ESP32/discussions/2624)
|
||||||
|
- analogsensors frequency input [#2631](https://github.com/emsesp/EMS-ESP32/discussions/2631)
|
||||||
|
- SRC plus thermostats [#2636](https://github.com/emsesp/EMS-ESP32/issues/2636)
|
||||||
|
- Greenstar 2000 [#2645](https://github.com/emsesp/EMS-ESP32/issues/2645)
|
||||||
|
- RC3xx `dhw modetype` [#2659](https://github.com/emsesp/EMS-ESP32/discussions/2659)
|
||||||
|
- new boiler entities VR0,VR1, compressor speed [#2669](https://github.com/emsesp/EMS-ESP32/issues/2669)
|
||||||
|
- solar temperature TS16 [#2690](https://github.com/emsesp/EMS-ESP32/issues/2690)
|
||||||
|
- pumpmode enum for HT3 boilers, add commands for manual defrost, chimneysweeper [#2727](https://github.com/emsesp/EMS-ESP32/issues/2727)
|
||||||
|
- pid settings [#2735](https://github.com/emsesp/EMS-ESP32/issues/2735)
|
||||||
|
- refresh MQTT button added to MQTT Settings page
|
||||||
|
- heating assistance, rounding custum settings [#2763](https://github.com/emsesp/EMS-ESP32/discussions/2763)
|
||||||
|
- added counter 0..2 for short pulses, high frequency [#2758](https://github.com/emsesp/EMS-ESP32/issues/2758)
|
||||||
|
- added LWT (Last Will and Testament) to MQTT entities in Home Assistant
|
||||||
|
- added api/metrics endpoint for prometheus integration by @gr3enk [#2774](https://github.com/emsesp/EMS-ESP32/pull/2774)
|
||||||
|
- added RTL8201 to eth phy list [#2800](https://github.com/emsesp/EMS-ESP32/issues/2800)
|
||||||
|
- added partitions to Web UI Version page, so previous firmware versions can be installed [#2837](https://github.com/emsesp/EMS-ESP32/issues/2837)
|
||||||
|
- button pressures show LED. On a long press (10 seconds) the LED flashes for 5 seconds to indicate a factory reset is about to happen. [#2848](https://github.com/emsesp/EMS-ESP32/issues/2848)
|
||||||
|
- added `txpause` command to pause the TX, by setting Txmode to 0 (disabled) [#2850](https://github.com/emsesp/EMS-ESP32/issues/2850)
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- dhw/switchtime [#2490](https://github.com/emsesp/EMS-ESP32/issues/2490)
|
||||||
|
- switch to secure mqtt [#2492](https://github.com/emsesp/EMS-ESP32/issues/2492)
|
||||||
|
- update link buttons [#2497](https://github.com/emsesp/EMS-ESP32/issues/2497)
|
||||||
|
- refresh scheduler states [#2502](https://github.com/emsesp/EMS-ESP32/discussions/2502)
|
||||||
|
- also rebuild HA config on mqtt connect for scheduler, custom and shower
|
||||||
|
- FB100 controls the hc, not the master [#2510](https://github.com/emsesp/EMS-ESP32/issues/2510)
|
||||||
|
- IPM DHW module, [#2524](https://github.com/emsesp/EMS-ESP32/issues/2524)
|
||||||
|
- charge optimization [#2543](https://github.com/emsesp/EMS-ESP32/issues/2543)
|
||||||
|
- shower active state retained, shows correctly in HA
|
||||||
|
- MQTT Command Topic with slashes [#2571](https://github.com/emsesp/EMS-ESP32/issues/2571)
|
||||||
|
- Add pulsed water meter input to V1.3 gateway with Lilygo S3 [#2550](https://github.com/emsesp/EMS-ESP32/issues/2550)
|
||||||
|
- fix missing long 10-second press of Button to perform a factory reset
|
||||||
|
- fix wwMaxPower on Junkers ZBS14 [#2609](https://github.com/emsesp/EMS-ESP32/issues/2609)
|
||||||
|
- ventilation bypass state from telegram 0x55C [#1197](https://github.com/emsesp/EMS-ESP32/issues/1197)
|
||||||
|
- set selflowtemp for ems+ boilers [#2641](https://github.com/emsesp/EMS-ESP32/discussions/2641)
|
||||||
|
- syslog timestamp [#2704](https://github.com/emsesp/EMS-ESP32/issues/2704)
|
||||||
|
- fixed FS format command [#2720](https://github.com/emsesp/EMS-ESP32/discussions/2720)
|
||||||
|
- dhw priority setting to boiler and mixer, telegrams 0x2CC, 0x2CD, etc.
|
||||||
|
- check for valid GPIOs when board profile is changed [#2841](https://github.com/emsesp/EMS-ESP32/issues/2841)
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- show console log with ISO date/time [#2533](https://github.com/emsesp/EMS-ESP32/discussions/2533)
|
||||||
|
- removed ESP32 CPU temperature
|
||||||
|
- updated core libraries like AsyncTCP, AsyncWebServer and Modbus
|
||||||
|
- remove command `scan deep`
|
||||||
|
- ignore repeated `forceheatingoff` commands [#2641](https://github.com/emsesp/EMS-ESP32/discussions/2641)
|
||||||
|
- optimized web for better performance by adding lazy loading and caching
|
||||||
|
- internal system analog sensors (core_voltage, supply_voltage and gateway_temperature) cannot be accidentally removed
|
||||||
|
- double click button reconnects EMS-ESP to AP
|
||||||
|
- place system message command in side scheduler loop to reduce stack memory usage by 2KB
|
||||||
|
- syslog mark interval set to 1 hour
|
||||||
|
- handle process_telegram in oneloop
|
||||||
|
- improved GPIO validation for Analog Sensors and System GPIOs
|
||||||
|
- entities with no values are greyed out in the Web UI in the Customization page
|
||||||
|
- added System Status to Web Status page
|
||||||
|
- show number on entities and supported languages in log on boot
|
||||||
|
- on tx read fail delay the 3rd. retry 2 sec
|
||||||
|
- move vectors and lists to PSRAM
|
||||||
|
- removed unused last topic/payload echo-check
|
||||||
|
- added Home Assistant device details to MQTT Discovery for all devices
|
||||||
|
- device_class and state_class changes for HA MQTT Discovery [#2825](https://github.com/emsesp/EMS-ESP32/issues/2825)
|
||||||
|
|
||||||
## [3.7.2] 22 March 2025
|
## [3.7.2] 22 March 2025
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
@@ -85,7 +199,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- The automatically generated temperature sensor ID has replaced dashes (`-`) with underscores (`_`) to be compatible with Home Assistant.
|
- The automatically generated temperature sensor ID has replaced dashes (`-`) with underscores (`_`) to be compatible with Home Assistant.
|
||||||
- `api/system/info` has it's JSON key names changed to camelCase syntax.
|
- `api/system/info` has it's JSON key names changed to camelCase syntax.
|
||||||
|
|
||||||
For more details go to [docs.emsesp.org](https://docs.emsesp.org/).
|
For more details go to [emsesp.org](https://emsesp.org/).
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
|
|||||||
@@ -1,77 +1,43 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
For more details go to [docs.emsesp.org](https://docs.emsesp.org/).
|
For more details go to [emsesp.org](https://emsesp.org/).
|
||||||
|
|
||||||
## [3.7.3]
|
## [3.9.0]
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
- analogsensor types: NTC and RGB-Led
|
- comfortpoint for BC400 [#2935](https://github.com/emsesp/EMS-ESP32/issues/2935)
|
||||||
- Flag for HMC310 [#2465](https://github.com/emsesp/EMS-ESP32/issues/2465)
|
- customize device brand [#2784](https://github.com/emsesp/EMS-ESP32/issues/2784)
|
||||||
- boiler auxheatersource [#2489](https://github.com/emsesp/EMS-ESP32/discussions/2489)
|
- set model for ems-esp devices temperature, analog, etc. [#2958](https://github.com/emsesp/EMS-ESP32/discussions/2958)
|
||||||
- thermostat last error for RC100/300 [#2501](https://github.com/emsesp/EMS-ESP32/issues/2501)
|
- prometheus metrics for temperature/analog/scheduler/custom [#2962](https://github.com/emsesp/EMS-ESP32/issues/2962)
|
||||||
- boiler 0xC6 telegram [#1963](https://github.com/emsesp/EMS-ESP32/issues/1963)
|
- boiler pumpkick [#2965](https://github.com/emsesp/EMS-ESP32/discussions/2965)
|
||||||
- CS6800i changes [#2448](https://github.com/emsesp/EMS-ESP32/issues/2448), [#2449](https://github.com/emsesp/EMS-ESP32/issues/2449)
|
- heatpump reset [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933)
|
||||||
- charging pump [#2544](https://github.com/emsesp/EMS-ESP32/issues/2544)
|
- e-mail notification using ReadyMail Client
|
||||||
- hybrid CSH5800iG [#2569](https://github.com/emsesp/EMS-ESP32/issues/2569)
|
- 2.nd freshwater module (dhw4, dhw5) [#2991](https://github.com/emsesp/EMS-ESP32/issues/2991)
|
||||||
- add EMS Device details to Home Assistant MQTT Discovery
|
- full system backup and restore
|
||||||
- disinfection command [#2601](https://github.com/emsesp/EMS-ESP32/issues/2601)
|
- updated version check [#3047](https://github.com/emsesp/EMS-ESP32/issues/3047)
|
||||||
- added new board profile for upcoming BBQKees E32V2.2
|
|
||||||
- set differential pressure entity in Mixer device
|
|
||||||
- set set climate action cooling/heating in HA [#2583](https://github.com/emsesp/EMS-ESP32/issues/2583)
|
|
||||||
- Internal sensors of E32V2_2
|
|
||||||
- FW200 display options [#2610](https://github.com/emsesp/EMS-ESP32/discussions/2610)
|
|
||||||
- CR11 mode settings OFF/MANUAL depends on selTemp [#2437](https://github.com/emsesp/EMS-ESP32/issues/2437)
|
|
||||||
- implemented eFuse settings for BBQKees boards to store model type and ESP chipset
|
|
||||||
- Analogsensors for pulse output [#2624](https://github.com/emsesp/EMS-ESP32/discussions/2624)
|
|
||||||
- Analogsensors frequency input [#2631](https://github.com/emsesp/EMS-ESP32/discussions/2631)
|
|
||||||
- SRC plus thermostats [#2636](https://github.com/emsesp/EMS-ESP32/issues/2636)
|
|
||||||
- Greenstar 2000 [#2645](https://github.com/emsesp/EMS-ESP32/issues/2645)
|
|
||||||
- RC3xx `dhw modetype` [#2659](https://github.com/emsesp/EMS-ESP32/discussions/2659)
|
|
||||||
- new boiler entities VR0,VR1, compressor speed [#2669](https://github.com/emsesp/EMS-ESP32/issues/2669)
|
|
||||||
- solar temperature TS16 [#2690](https://github.com/emsesp/EMS-ESP32/issues/2690)
|
|
||||||
- pumpmode enum for HT3 boilers, add commands for manual defrost, chimneysweeper [#2727](https://github.com/emsesp/EMS-ESP32/issues/2727)
|
|
||||||
- pid settings [#2735](https://github.com/emsesp/EMS-ESP32/issues/2735)
|
|
||||||
- refresh MQTT button added to MQTT Settings page
|
|
||||||
- added LWT (Last Will and Testament) to MQTT entities in Home Assistant
|
|
||||||
- added api/metrics endpoint for prometheus integration by @gr3enk
|
|
||||||
[#2774](https://github.com/emsesp/EMS-ESP32/pull/2774)
|
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
- dhw/switchtime [#2490](https://github.com/emsesp/EMS-ESP32/issues/2490)
|
- SRC climate creation [#2936](https://github.com/emsesp/EMS-ESP32/issues/2936) and [#2960](https://github.com/emsesp/EMS-ESP32/issues/2960)
|
||||||
- switch to secure mqtt [#2492](https://github.com/emsesp/EMS-ESP32/issues/2492)
|
- missing translations [#3015](https://github.com/emsesp/EMS-ESP32/issues/3015)
|
||||||
- update link buttons [#2497](https://github.com/emsesp/EMS-ESP32/issues/2497)
|
- custom entities check fetch length
|
||||||
- refresh scheduler states [#2502](https://github.com/emsesp/EMS-ESP32/discussions/2502)
|
- modbus initialization [#3064](https://github.com/emsesp/EMS-ESP32/issues/3064)
|
||||||
- also rebuild HA config on mqtt connect for scheduler, custom and shower
|
|
||||||
- FB100 controls the hc, not the master [#2510](https://github.com/emsesp/EMS-ESP32/issues/2510)
|
|
||||||
- IPM DHW module, [#2524](https://github.com/emsesp/EMS-ESP32/issues/2524)
|
|
||||||
- charge optimization [#2543](https://github.com/emsesp/EMS-ESP32/issues/2543)
|
|
||||||
- shower active state retained, shows correctly in HA
|
|
||||||
- MQTT Command Topic with slashes [#2571](https://github.com/emsesp/EMS-ESP32/issues/2571)
|
|
||||||
- Add pulsed water meter input to V1.3 gateway with Lilygo S3 [#2550](https://github.com/emsesp/EMS-ESP32/issues/2550)
|
|
||||||
- fix missing long 10-second press of Button to perform a factory reset
|
|
||||||
- fix wwMaxPower on Junkers ZBS14 [#2609](https://github.com/emsesp/EMS-ESP32/issues/2609)
|
|
||||||
- ventilation bypass state from telegram 0x55C [#1197](https://github.com/emsesp/EMS-ESP32/issues/1197)
|
|
||||||
- set selflowtemp for ems+ boilers [#2641](https://github.com/emsesp/EMS-ESP32/discussions/2641)
|
|
||||||
- syslog timestamp [#2704](https://github.com/emsesp/EMS-ESP32/issues/2704)
|
|
||||||
- fixed FS format command [#2720](https://github.com/emsesp/EMS-ESP32/discussions/2720)
|
|
||||||
- dhw priority setting to boiler and mixer, telegrams 0x2CC, 0x2CD, etc.
|
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
- show console log with ISO date/time [#2533](https://github.com/emsesp/EMS-ESP32/discussions/2533)
|
- weblogbuffer up to 1000 messages with PSRAM, mentioned in [#2933](https://github.com/emsesp/EMS-ESP32/issues/2933)
|
||||||
- remove ESP32 CPU temperature
|
- validate custom entity writes, [#2931](https://github.com/emsesp/EMS-ESP32/issues/2931)
|
||||||
- updated core libraries like AsyncTCP, AsyncWebServer and Modbus
|
- remove wrong burnMinPower [#2918](https://github.com/emsesp/EMS-ESP32/issues/2918)
|
||||||
- remove command `scan deep`
|
- store scheduler active state to nvs [#2946](https://github.com/emsesp/EMS-ESP32/discussions/2946)
|
||||||
- ignore repeated `forceheatingoff` commands [#2641](https://github.com/emsesp/EMS-ESP32/discussions/2641)
|
- translated modes `heat` and `eco` for HA-climate mode-str-tpl
|
||||||
- optimized web for better performance by adding lazy loading and caching
|
- support `minflowtemp` and `baseflowtemp` [#2969](https://github.com/emsesp/EMS-ESP32/discussions/2969)
|
||||||
- internal system analog sensors (core_voltage, supply_voltage and gateway_temperature) cannot be accidentally removed
|
- update version if it is 00.00 in first read [#2981](https://github.com/emsesp/EMS-ESP32/issues/2981)
|
||||||
- double click button reconnects EMS-ESP to AP
|
- device class for % values [#2980](https://github.com/emsesp/EMS-ESP32/issues/2980)
|
||||||
- place system message command in side scheduler loop to reduce stack memory usage by 2KB
|
- use tasmota core 2026.03.30
|
||||||
- syslog mark interval set to 1 hour
|
- secure mqtt uses ESP_SSLClient
|
||||||
- handle process_telegram in oneloop
|
- fetch telegrams: set length to fetch [#3017](https://github.com/emsesp/EMS-ESP32/issues/3017)
|
||||||
- improved GPIO validation for Analog Sensors and System GPIOs
|
- move http client from stack to heap
|
||||||
- entities with no values are greyed out in the Web UI in the Customization page
|
- heap optimizations [#3021](https://github.com/emsesp/EMS-ESP32/discussions/3021)
|
||||||
- added System Status to Web Status page
|
- refactored network code into a single class [#3052](https://github.com/emsesp/EMS-ESP32/pull/3052)
|
||||||
- show number on entities and supported languages in log on boot
|
- 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)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Everybody is welcome and invited to contribute to the EMS-ESP Project by:
|
|||||||
|
|
||||||
- providing Pull Requests (Features, Fixes, suggestions)
|
- providing Pull Requests (Features, Fixes, suggestions)
|
||||||
- testing new released features and report issues on your EMS equipment
|
- testing new released features and report issues on your EMS equipment
|
||||||
- contributing to missing [documentation](https://docs.emsesp.org)
|
- contributing to missing [documentation](https://emsesp.org)
|
||||||
|
|
||||||
This document describes rules that are in effect for this repository, meant for handling issues by contributors in the issue tracker and PRs.
|
This document describes rules that are in effect for this repository, meant for handling issues by contributors in the issue tracker and PRs.
|
||||||
|
|
||||||
|
|||||||
21
Makefile
21
Makefile
@@ -47,27 +47,28 @@ 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/* lib_standalone 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
|
||||||
CHECKFLAGS = -q --force --std=gnu++17
|
CHECKFLAGS = -q --force --std=gnu++20
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Languages Standard
|
# Languages Standard
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
C_STANDARD := -std=c17
|
C_STANDARD := -std=c20
|
||||||
CXX_STANDARD := -std=gnu++17
|
CXX_STANDARD := -std=gnu++20
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Defined Symbols
|
# Defined Symbols
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
DEFINES += -DARDUINOJSON_ENABLE -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
DEFINES += -DARDUINOJSON_ENABLE -DARDUINOJSON_ENABLE_ARDUINO_STRING -DARDUINOJSON_USE_DOUBLE=0
|
||||||
DEFINES += -DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_DEBUG -DEMC_RX_BUFFER_SIZE=1500
|
DEFINES += -DEMSESP_STANDALONE -DEMSESP_TEST -DEMSESP_DEBUG -DEMC_RX_BUFFER_SIZE=1500
|
||||||
|
DEFINES += -DNO_TLS_SUPPORT
|
||||||
DEFINES += $(ARGS)
|
DEFINES += $(ARGS)
|
||||||
|
|
||||||
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32S3\"
|
DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32S3\"
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Sources & Files
|
# Sources & Files
|
||||||
@@ -79,6 +80,10 @@ SYMBOLS := $(CURDIR)/$(BUILD)/$(TARGET).out
|
|||||||
CSOURCES := $(shell find $(SOURCES) -name "*.c" 2>/dev/null)
|
CSOURCES := $(shell find $(SOURCES) -name "*.c" 2>/dev/null)
|
||||||
CXXSOURCES := $(shell find $(SOURCES) -name "*.cpp" 2>/dev/null)
|
CXXSOURCES := $(shell find $(SOURCES) -name "*.cpp" 2>/dev/null)
|
||||||
|
|
||||||
|
# Exclude files not needed for standalone build, if they exist
|
||||||
|
CSOURCES := $(filter-out src/core/ModuleLibrary.c,$(CSOURCES))
|
||||||
|
CXXSOURCES := $(filter-out src/core/ModuleLibrary.cpp,$(CXXSOURCES))
|
||||||
|
|
||||||
OBJS := $(patsubst %,$(BUILD)/%.o,$(basename $(CSOURCES)) $(basename $(CXXSOURCES)))
|
OBJS := $(patsubst %,$(BUILD)/%.o,$(basename $(CSOURCES)) $(basename $(CXXSOURCES)))
|
||||||
DEPS := $(patsubst %,$(BUILD)/%.d,$(basename $(CSOURCES)) $(basename $(CXXSOURCES)))
|
DEPS := $(patsubst %,$(BUILD)/%.d,$(basename $(CSOURCES)) $(basename $(CXXSOURCES)))
|
||||||
|
|
||||||
@@ -108,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
|
||||||
@@ -138,6 +143,7 @@ DEPFLAGS += -MF $(BUILD)/$*.d -MT $@
|
|||||||
LINK.o = $(LD) $(LDFLAGS) $(LDLIBS) $^ -o $@
|
LINK.o = $(LD) $(LDFLAGS) $(LDLIBS) $^ -o $@
|
||||||
COMPILE.c = $(CC) $(C_STANDARD) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
|
COMPILE.c = $(CC) $(C_STANDARD) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
|
||||||
COMPILE.cpp = $(CXX) $(CXX_STANDARD) $(CXXFLAGS) $(DEPFLAGS) -c $< -o $@
|
COMPILE.cpp = $(CXX) $(CXX_STANDARD) $(CXXFLAGS) $(DEPFLAGS) -c $< -o $@
|
||||||
|
COMPILE.s = $(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
#----------------------------------------------------------------------
|
||||||
# Special Built-in Target
|
# Special Built-in Target
|
||||||
@@ -181,6 +187,7 @@ $(BUILD)/%.o: %.cpp
|
|||||||
|
|
||||||
$(BUILD)/%.o: %.s
|
$(BUILD)/%.o: %.s
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
|
@$(ECHO) Compiling $@
|
||||||
@$(COMPILE.s)
|
@$(COMPILE.s)
|
||||||
|
|
||||||
cppcheck: $(SOURCES)
|
cppcheck: $(SOURCES)
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -15,10 +15,10 @@
|
|||||||
<a href="https://github.com/emsesp/EMS-ESP32/blob/dev/CONTRIBUTING.md">
|
<a href="https://github.com/emsesp/EMS-ESP32/blob/dev/CONTRIBUTING.md">
|
||||||
<img src="https://img.shields.io/badge/Contribute-ff4785?style=for-the-badge&logo=git&logoColor=white" alt="Contribute" />
|
<img src="https://img.shields.io/badge/Contribute-ff4785?style=for-the-badge&logo=git&logoColor=white" alt="Contribute" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://docs.emsesp.org">
|
<a href="https://emsesp.org">
|
||||||
<img src="https://img.shields.io/badge/Documentation-0077b5?style=for-the-badge&logo=googledocs&logoColor=white" alt="Guides" />
|
<img src="https://img.shields.io/badge/Documentation-0077b5?style=for-the-badge&logo=googledocs&logoColor=white" alt="Guides" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/3J3GgnzpyT">
|
<a href="https://discord.gg/GP9DPSgeJq">
|
||||||
<img src="https://img.shields.io/badge/Discord-7289da?style=for-the-badge&logo=discord&logoColor=white" alt="Discord" />
|
<img src="https://img.shields.io/badge/Discord-7289da?style=for-the-badge&logo=discord&logoColor=white" alt="Discord" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md">
|
<a href="https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md">
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
[](https://sonarcloud.io/summary/new_code?id=emsesp_EMS-ESP32)
|
[](https://sonarcloud.io/summary/new_code?id=emsesp_EMS-ESP32)
|
||||||
[](https://app.codacy.com/gh/emsesp/EMS-ESP32/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
[](https://app.codacy.com/gh/emsesp/EMS-ESP32/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
|
||||||
[](https://github.com/emsesp/EMS-ESP32/releases)
|
[](https://github.com/emsesp/EMS-ESP32/releases)
|
||||||
[](https://discord.gg/3J3GgnzpyT)
|
[](https://discord.gg/GP9DPSgeJq)
|
||||||
|
[](https://deepwiki.com/emsesp/EMS-ESP32)
|
||||||
|
|
||||||
[](https://github.com/emsesp/EMS-ESP32/stargazers)
|
[](https://github.com/emsesp/EMS-ESP32/stargazers)
|
||||||
[](https://github.com/emsesp/EMS-ESP32/network)
|
[](https://github.com/emsesp/EMS-ESP32/network)
|
||||||
@@ -40,7 +41,8 @@
|
|||||||
|
|
||||||
**EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller to communicate with **EMS** (Energy Management System) compatible equipment from manufacturers such as Bosch, Buderus, Nefit, Junkers, Worcester, Sieger, elm.leblanc and iVT.
|
**EMS-ESP** is an open-source firmware for the Espressif ESP32 microcontroller to communicate with **EMS** (Energy Management System) compatible equipment from manufacturers such as Bosch, Buderus, Nefit, Junkers, Worcester, Sieger, elm.leblanc and iVT.
|
||||||
|
|
||||||
It requires a small circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl> or custom built.
|
It requires a small circuit to interface with the EMS bus which can be purchased from <https://bbqkees-electronics.nl>. These gateways are tested thoroughly and certified to work with EMS-ESP.
|
||||||
|
|
||||||
|
|
||||||
## 📦 **Key Features**
|
## 📦 **Key Features**
|
||||||
|
|
||||||
@@ -60,33 +62,31 @@ It requires a small circuit to interface with the EMS bus which can be purchased
|
|||||||
|
|
||||||
## 🚀 **Installing**
|
## 🚀 **Installing**
|
||||||
|
|
||||||
Head over to [download.emsesp.org](https://download.emsesp.org) for instructions on how to install EMS-ESP. There is also further details on which boards are supported in [this section](https://docs.emsesp.org/Installing/) of the documentation.
|
Head over to the [Installation Guide](https://emsesp.org/Installing) section of the documentation for instructions on how to install EMS-ESP.
|
||||||
|
|
||||||
## 📋 **Documentation**
|
## 📋 **Documentation**
|
||||||
|
|
||||||
Visit [emsesp.org](https://docs.emsesp.org) for more details on how to install and configure EMS-ESP. There is also a collection of Frequently Asked Questions and Troubleshooting tips with example customizations from the community.
|
Visit [emsesp.org](https://emsesp.org) for more details on how to setup and configure EMS-ESP. You'll also find more a collection of example configuarations, Frequently Asked Questions and Troubleshooting tips.
|
||||||
|
|
||||||
## 💬 **Getting Support**
|
## 💬 **Getting Support**
|
||||||
|
|
||||||
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
|
To chat with the community reach out on our [Discord Server](https://discord.gg/GP9DPSgeJq).
|
||||||
|
|
||||||
If you find an issue or have a request, see [how to request support](https://docs.emsesp.org/Support/) on how to submit a bug report or feature request.
|
If you find an issue or have a request, see the [Getting Support](https://emsesp.org/Support/) section of the documentation. Note if you are using a non-BBQKees EMS gateway, you may need to contact the manufacturer for support.
|
||||||
|
|
||||||
## 🎥 **Live Demo**
|
## 🎥 **Live Demo**
|
||||||
|
|
||||||
For a live demo go to [demo.emsesp.org](https://demo.emsesp.org). Pick a language from the sign on page and log in with any username or password. Note not all features are operational as it's based on static data.
|
To see a live demo go to [demo.emsesp.org](https://demo.emsesp.org). Pick a language and use any username and password to log in. Note whast you're seeing is static example data so not all features are operational.
|
||||||
|
|
||||||
## 💖 **Contributors**
|
## 💖 **Contributors**
|
||||||
|
|
||||||
EMS-ESP is a project created by [proddy](https://github.com/proddy) and owned and maintained by both [proddy](https://github.com/proddy) and [MichaelDvP](https://github.com/MichaelDvP) with support from [BBQKees Electronics](https://bbqkees-electronics.nl).
|
EMS-ESP is a project originally created by [proddy](https://github.com/proddy) and maintained by the ems-esp community.
|
||||||
|
|
||||||
If you like **EMS-ESP**, please give it a ✨ on GitHub, or even better fork it and contribute. You can also offer a small donation. This is an open-source project maintained by volunteers, and your support is greatly appreciated.
|
If you like **EMS-ESP**, please give it a ✨ on GitHub, or even better fork it and contribute. You can also offer a small donation. This is an open-source project maintained by volunteers, and your support is greatly appreciated.
|
||||||
|
|
||||||
## 📦 **Building**
|
## 📦 **Building**
|
||||||
|
|
||||||
To build the web interface only, run `platformio run -e build_webUI`. This will install the necessary dependencies and build the web interface and also create the embedded code used need to build the firmware. You can run the web interface locally by going to the `interface` directory and running `pnpm standalone`.
|
See the [Building the firmware](https://emsesp.org/Building) guide in the documentation for instructions on how to build EMS-ESP from this source code.
|
||||||
|
|
||||||
To build the firmware, run `platformio run`. This will build the firmware for all ESP32 modules and place the binaries in the `build/firmware` folder. If you want to configure the build for a single platform create a local `pio_local.ni` file in the root directory (see example in `pio_local.ini_example`).
|
|
||||||
|
|
||||||
## 📢 **Libraries used**
|
## 📢 **Libraries used**
|
||||||
|
|
||||||
|
|||||||
5
SECURITY.md
Normal file
5
SECURITY.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report any security vulnerabilities using the [Contact Form](https://emsesp.org/About/#-contact).
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
},
|
},
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": [
|
"extra_flags": [
|
||||||
"-DTASMOTA_SDK",
|
"-DNO_TLS_SUPPORT",
|
||||||
"-DARDUINO_LOLIN_C3_MINI",
|
"-DARDUINO_LOLIN_C3_MINI",
|
||||||
"-DARDUINO_USB_MODE=1",
|
"-DARDUINO_USB_MODE=1",
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": [
|
"extra_flags": [
|
||||||
"-DBOARD_HAS_PSRAM",
|
"-DBOARD_HAS_PSRAM",
|
||||||
"-DTASMOTA_SDK",
|
"-DNO_TLS_SUPPORT",
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
"-DARDUINO_USB_CDC_ON_BOOT=1",
|
||||||
"-DARDUINO_USB_MODE=0"
|
"-DARDUINO_USB_MODE=0"
|
||||||
],
|
],
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
"flash_size": "4MB",
|
"flash_size": "4MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
"maximum_size": 4194304,
|
"maximum_size": 4194304,
|
||||||
"use_1200bps_touch": true,
|
"use_1200bps_touch": false,
|
||||||
"wait_for_upload_port": true,
|
"wait_for_upload_port": false,
|
||||||
"require_upload_port": true,
|
"require_upload_port": true,
|
||||||
"speed": 921600
|
"speed": 921600
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"arduino",
|
"arduino",
|
||||||
"espidf"
|
"espidf"
|
||||||
],
|
],
|
||||||
"name": "Espressif ESP32-S3 32M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
|
"name": "Tasmota ESP32-S3 32M Flash OPI PSRAM, 4608KB Code/OTA, 2MB FS",
|
||||||
"upload": {
|
"upload": {
|
||||||
"flash_size": "32MB",
|
"flash_size": "32MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"build": {
|
"build": {
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": "-DTASMOTA_SDK",
|
"extra_flags": "-DNO_TLS_SUPPORT",
|
||||||
"f_cpu": "240000000L",
|
"f_cpu": "240000000L",
|
||||||
"f_flash": "40000000L",
|
"f_flash": "40000000L",
|
||||||
"flash_mode": "dio",
|
"flash_mode": "dio",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"arduino",
|
"arduino",
|
||||||
"espidf"
|
"espidf"
|
||||||
],
|
],
|
||||||
"name": "Espressif ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
|
"name": "Tasmota ESP32 16M Flash, 4608KB Code/OTA, 2MB FS",
|
||||||
"upload": {
|
"upload": {
|
||||||
"flash_size": "16MB",
|
"flash_size": "16MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"arduino",
|
"arduino",
|
||||||
"espidf"
|
"espidf"
|
||||||
],
|
],
|
||||||
"name": "Espressif ESP32 16M Flash DIO PSRAM, 4608KB Code/OTA, 2MB FS",
|
"name": "Tasmota ESP32 16M Flash DIO PSRAM, 4608KB Code/OTA, 2MB FS",
|
||||||
"upload": {
|
"upload": {
|
||||||
"flash_size": "16MB",
|
"flash_size": "16MB",
|
||||||
"maximum_ram_size": 327680,
|
"maximum_ram_size": 327680,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"build": {
|
"build": {
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": "-DTASMOTA_SDK",
|
"extra_flags": "-DNO_TLS_SUPPORT",
|
||||||
"f_cpu": "240000000L",
|
"f_cpu": "240000000L",
|
||||||
"f_flash": "40000000L",
|
"f_flash": "40000000L",
|
||||||
"flash_mode": "dio",
|
"flash_mode": "dio",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"core": "esp32",
|
"core": "esp32",
|
||||||
"extra_flags": [
|
"extra_flags": [
|
||||||
|
"-DNO_TLS_SUPPORT",
|
||||||
"-DARDUINO_XIAO_ESP32C6",
|
"-DARDUINO_XIAO_ESP32C6",
|
||||||
"-DARDUINO_USB_MODE=1",
|
"-DARDUINO_USB_MODE=1",
|
||||||
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
"-DARDUINO_USB_CDC_ON_BOOT=1"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dictionaries": ["project-words"],
|
"dictionaries": ["project-words"],
|
||||||
|
"caseSensitive": false,
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"compile_commands.json",
|
"compile_commands.json",
|
||||||
@@ -34,6 +35,10 @@
|
|||||||
"sdkconfig.*",
|
"sdkconfig.*",
|
||||||
"managed_components/**",
|
"managed_components/**",
|
||||||
"pnpm-*.yaml",
|
"pnpm-*.yaml",
|
||||||
"vite.config.ts"
|
"vite.config.ts",
|
||||||
|
"lib/esp32-psram/**",
|
||||||
|
"test/test_api/test_api.h",
|
||||||
|
"lib_standalone/**",
|
||||||
|
"**/*.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,85 +1,228 @@
|
|||||||
{
|
{
|
||||||
"type": "settings",
|
"type": "systembackup",
|
||||||
"Network": {
|
"version": "3.8.2",
|
||||||
"ssid": "my_wifi_ssid",
|
"date": "2026-03-29T13:28:15",
|
||||||
"bssid": "",
|
"systembackup": [
|
||||||
"password": "my_wifi_password",
|
{
|
||||||
"hostname": "ems-esp"
|
"type": "settings",
|
||||||
|
"Network": {
|
||||||
|
"ssid": "",
|
||||||
|
"bssid": "",
|
||||||
|
"password": "",
|
||||||
|
"hostname": "ems-esp",
|
||||||
|
"static_ip_config": false,
|
||||||
|
"bandwidth20": false,
|
||||||
|
"nosleep": true,
|
||||||
|
"enableMDNS": true,
|
||||||
|
"enableCORS": false,
|
||||||
|
"CORSOrigin": "*",
|
||||||
|
"tx_power": 0
|
||||||
|
},
|
||||||
|
"AP": {
|
||||||
|
"provision_mode": 2,
|
||||||
|
"ssid": "ems-esp",
|
||||||
|
"password": "ems-esp-neo",
|
||||||
|
"channel": 1,
|
||||||
|
"ssid_hidden": false,
|
||||||
|
"max_clients": 4,
|
||||||
|
"local_ip": "192.168.4.1",
|
||||||
|
"gateway_ip": "192.168.4.1",
|
||||||
|
"subnet_mask": "255.255.255.0"
|
||||||
|
},
|
||||||
|
"MQTT": {
|
||||||
|
"enableTLS": false,
|
||||||
|
"rootCA": "",
|
||||||
|
"enabled": false,
|
||||||
|
"host": "",
|
||||||
|
"port": 1883,
|
||||||
|
"base": "ems-esp",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"client_id": "esp32-b8ffc9ec",
|
||||||
|
"keep_alive": 60,
|
||||||
|
"clean_session": false,
|
||||||
|
"entity_format": 1,
|
||||||
|
"publish_time_boiler": 10,
|
||||||
|
"publish_time_thermostat": 10,
|
||||||
|
"publish_time_solar": 10,
|
||||||
|
"publish_time_mixer": 10,
|
||||||
|
"publish_time_water": 10,
|
||||||
|
"publish_time_other": 60,
|
||||||
|
"publish_time_sensor": 10,
|
||||||
|
"publish_time_heartbeat": 60,
|
||||||
|
"mqtt_qos": 0,
|
||||||
|
"mqtt_retain": false,
|
||||||
|
"ha_enabled": false,
|
||||||
|
"nested_format": 1,
|
||||||
|
"discovery_prefix": "homeassistant",
|
||||||
|
"discovery_type": 0,
|
||||||
|
"ha_number_mode": 0,
|
||||||
|
"publish_single": false,
|
||||||
|
"publish_single2cmd": false,
|
||||||
|
"send_response": false
|
||||||
|
},
|
||||||
|
"NTP": {
|
||||||
|
"enabled": true,
|
||||||
|
"server": "time.google.com",
|
||||||
|
"tz_label": "Europe/Amsterdam",
|
||||||
|
"tz_format": "CET-1CEST,M3.5.0,M10.5.0/3"
|
||||||
|
},
|
||||||
|
"Security": {
|
||||||
|
"jwt_secret": "ems-esp-neo",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin",
|
||||||
|
"admin": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "guest",
|
||||||
|
"password": "guest",
|
||||||
|
"admin": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Settings": {
|
||||||
|
"version": "3.8.2",
|
||||||
|
"board_profile": "E32V2_2",
|
||||||
|
"platform": "ESP32",
|
||||||
|
"locale": "en",
|
||||||
|
"tx_mode": 1,
|
||||||
|
"ems_bus_id": 11,
|
||||||
|
"syslog_enabled": false,
|
||||||
|
"syslog_level": 3,
|
||||||
|
"trace_raw": false,
|
||||||
|
"syslog_mark_interval": 0,
|
||||||
|
"syslog_host": "",
|
||||||
|
"syslog_port": 514,
|
||||||
|
"boiler_heatingoff": false,
|
||||||
|
"remote_timeout": 24,
|
||||||
|
"remote_timeout_en": false,
|
||||||
|
"shower_timer": false,
|
||||||
|
"shower_alert": false,
|
||||||
|
"shower_alert_coldshot": 10,
|
||||||
|
"shower_alert_trigger": 7,
|
||||||
|
"shower_min_duration": 180,
|
||||||
|
"rx_gpio": 4,
|
||||||
|
"tx_gpio": 5,
|
||||||
|
"dallas_gpio": 14,
|
||||||
|
"dallas_parasite": false,
|
||||||
|
"led_gpio": 32,
|
||||||
|
"hide_led": false,
|
||||||
|
"led_type": 1,
|
||||||
|
"low_clock": false,
|
||||||
|
"telnet_enabled": true,
|
||||||
|
"notoken_api": false,
|
||||||
|
"readonly_mode": false,
|
||||||
|
"analog_enabled": true,
|
||||||
|
"pbutton_gpio": 34,
|
||||||
|
"solar_maxflow": 30,
|
||||||
|
"fahrenheit": false,
|
||||||
|
"bool_format": 1,
|
||||||
|
"bool_dashboard": 1,
|
||||||
|
"enum_format": 1,
|
||||||
|
"weblog_level": 6,
|
||||||
|
"weblog_buffer": 50,
|
||||||
|
"weblog_compact": true,
|
||||||
|
"phy_type": 1,
|
||||||
|
"eth_power": 15,
|
||||||
|
"eth_phy_addr": 0,
|
||||||
|
"eth_clock_mode": 1,
|
||||||
|
"modbus_enabled": false,
|
||||||
|
"modbus_port": 502,
|
||||||
|
"modbus_max_clients": 10,
|
||||||
|
"modbus_timeout": 300,
|
||||||
|
"developer_mode": true,
|
||||||
|
"email_enabled": false,
|
||||||
|
"email_ssl": false,
|
||||||
|
"email_starttls": true,
|
||||||
|
"email_server": "smtp.example.net",
|
||||||
|
"email_port": 587,
|
||||||
|
"email_login": "",
|
||||||
|
"email_pass": "",
|
||||||
|
"email_sender": "ems-esp@example.net",
|
||||||
|
"email_recp": "",
|
||||||
|
"email_subject": "ems-esp notification"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"AP": {
|
{
|
||||||
"provision_mode": 2,
|
"type": "schedule",
|
||||||
"ssid": "ems-esp",
|
"Schedule": {
|
||||||
"password": "ems-esp-neo",
|
"schedule": []
|
||||||
"channel": 1,
|
}
|
||||||
"ssid_hidden": false,
|
|
||||||
"max_clients": 4,
|
|
||||||
"local_ip": "192.168.4.1",
|
|
||||||
"gateway_ip": "192.168.4.1",
|
|
||||||
"subnet_mask": "255.255.255.0"
|
|
||||||
},
|
},
|
||||||
"MQTT": {
|
{
|
||||||
"enableTLS": false,
|
"type": "customizations",
|
||||||
"rootCA": "",
|
"Customizations": {
|
||||||
"enabled": false,
|
"ts": [
|
||||||
"host": "127.0.0.1",
|
{
|
||||||
"port": 1883,
|
"id": "28_1767_7B13_2502",
|
||||||
"base": "ems-esp",
|
"name": "gateway_temperature",
|
||||||
"username": "username",
|
"offset": 0,
|
||||||
"password": "password",
|
"is_system": true
|
||||||
"client_id": "ems-esp",
|
}
|
||||||
"entity_format": 1,
|
],
|
||||||
"publish_time_boiler": 10,
|
"as": [
|
||||||
"publish_time_thermostat": 10,
|
{
|
||||||
"publish_time_solar": 10,
|
"gpio": 39,
|
||||||
"publish_time_mixer": 10,
|
"name": "core_voltage",
|
||||||
"publish_time_water": 10,
|
"offset": 0,
|
||||||
"publish_time_other": 60,
|
"factor": 0.003771,
|
||||||
"publish_time_sensor": 10,
|
"uom": 23,
|
||||||
"publish_time_heartbeat": 60,
|
"type": 3,
|
||||||
"mqtt_qos": 0,
|
"is_system": true
|
||||||
"mqtt_retain": false,
|
},
|
||||||
"ha_enabled": false,
|
{
|
||||||
"nested_format": 1,
|
"gpio": 36,
|
||||||
"discovery_prefix": "homeassistant",
|
"name": "supply_voltage",
|
||||||
"discovery_type": 0,
|
"offset": 0,
|
||||||
"publish_single": false,
|
"factor": 0.017,
|
||||||
"publish_single2cmd": false,
|
"uom": 23,
|
||||||
"send_response": false
|
"type": 3,
|
||||||
|
"is_system": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gpio": 2,
|
||||||
|
"name": "led",
|
||||||
|
"offset": 0,
|
||||||
|
"factor": 1,
|
||||||
|
"uom": 0,
|
||||||
|
"type": 6,
|
||||||
|
"is_system": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"masked_entities": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"NTP": {
|
{
|
||||||
"enabled": true,
|
"type": "entities",
|
||||||
"server": "time.google.com",
|
"Entities": {
|
||||||
"tz_label": "Europe/Amsterdam",
|
"entities": []
|
||||||
"tz_format": "CET-1CEST,M3.5.0,M10.5.0/3"
|
}
|
||||||
},
|
},
|
||||||
"Security": {
|
{
|
||||||
"jwt_secret": "ems-esp-neo",
|
"type": "modules",
|
||||||
"users": [
|
"Modules": {
|
||||||
{
|
"modules": []
|
||||||
"username": "admin",
|
}
|
||||||
"password": "admin",
|
|
||||||
"admin": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "guest",
|
|
||||||
"password": "guest",
|
|
||||||
"admin": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"Settings": {
|
{
|
||||||
"board_profile": "S3",
|
"type": "customSupport",
|
||||||
"locale": "en",
|
"Support": {
|
||||||
"tx_mode": 1,
|
"html": [
|
||||||
"ems_bus_id": 11,
|
"This product is installed and managed by:",
|
||||||
"boiler_heatingoff": false,
|
"",
|
||||||
"hide_led": true,
|
"<b>Bosch Installer Example</b>",
|
||||||
"telnet_enabled": true,
|
"",
|
||||||
"notoken_api": false,
|
"Nefit Road 12",
|
||||||
"analog_enabled": true,
|
"1234 AB Amsterdam",
|
||||||
"fahrenheit": false,
|
"Phone: +31 123 456 789",
|
||||||
"bool_format": 1,
|
"email: support@boschinstaller.nl",
|
||||||
"bool_dashboard": 1,
|
"",
|
||||||
"enum_format": 1
|
"For help and questions please <a target='_blank' href='https://emsesp.org'>contact</a> your installer."
|
||||||
|
],
|
||||||
|
"img_url": "https://emsesp.org/media/images/designer.png"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ telegram_type_id,name,is_fetched
|
|||||||
0x19,UBAMonitorSlow,
|
0x19,UBAMonitorSlow,
|
||||||
0x1A,UBASetPoints,
|
0x1A,UBASetPoints,
|
||||||
0x1C,UBAMaintenanceStatus,
|
0x1C,UBAMaintenanceStatus,
|
||||||
0x1E,WM10TempMessage,
|
0x1E,HydrTemp,
|
||||||
0x23,JunkersSetMixer,fetched
|
0x23,JunkersSetMixer,fetched
|
||||||
0x27,UBASettingsWW,fetched
|
0x27,UBASettingsWW,fetched
|
||||||
0x28,WeatherComp,fetched
|
0x28,WeatherComp,fetched
|
||||||
@@ -72,11 +72,12 @@ telegram_type_id,name,is_fetched
|
|||||||
0xE6,UBAParametersPlus,fetched
|
0xE6,UBAParametersPlus,fetched
|
||||||
0xE9,UBAMonitorWWPlus,
|
0xE9,UBAMonitorWWPlus,
|
||||||
0xEA,UBAParameterWWPlus,fetched
|
0xEA,UBAParameterWWPlus,fetched
|
||||||
|
0xEB,PumpKick,fetched
|
||||||
0x0101,ISM1Set,fetched
|
0x0101,ISM1Set,fetched
|
||||||
0x0103,ISM1StatusMessage,fetched
|
0x0103,ISM1StatusMessage,fetched
|
||||||
0x0104,ISM2StatusMessage,
|
0x0104,ISM2StatusMessage,
|
||||||
0x010C,IPMStatusMessage,
|
0x010C,IPMStatusMessage,
|
||||||
0x011E,JunkersDisp,fetched
|
0x011E,IPMTempMessage,
|
||||||
0x012E,HPEnergy1,
|
0x012E,HPEnergy1,
|
||||||
0x013B,HPEnergy2,
|
0x013B,HPEnergy2,
|
||||||
0x0165,JunkersSet,
|
0x0165,JunkersSet,
|
||||||
@@ -111,7 +112,7 @@ telegram_type_id,name,is_fetched
|
|||||||
0x02A0,RC300Curves,
|
0x02A0,RC300Curves,
|
||||||
0x02A1,RC300Curves,
|
0x02A1,RC300Curves,
|
||||||
0x02A2,RC300Curves,
|
0x02A2,RC300Curves,
|
||||||
0x02A5,RC300Monitor,fetched
|
0x02A5,RC300Monitor,
|
||||||
0x02A6,RC300Monitor,
|
0x02A6,RC300Monitor,
|
||||||
0x02A7,RC300Monitor,
|
0x02A7,RC300Monitor,
|
||||||
0x02A8,RC300Monitor,
|
0x02A8,RC300Monitor,
|
||||||
@@ -170,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,
|
||||||
@@ -197,7 +199,7 @@ telegram_type_id,name,is_fetched
|
|||||||
0x04A2,HpInput,fetched
|
0x04A2,HpInput,fetched
|
||||||
0x04A5,HPFan,fetched
|
0x04A5,HPFan,fetched
|
||||||
0x04A7,HPPowerLimit,fetched
|
0x04A7,HPPowerLimit,fetched
|
||||||
0x04AA,HPPower2,fetched
|
0x04AA,HPPower,
|
||||||
0x04AE,HPEnergy,fetched
|
0x04AE,HPEnergy,fetched
|
||||||
0x04AF,HPMeters,fetched
|
0x04AF,HPMeters,fetched
|
||||||
0x055C,VentilationSet,fetched
|
0x055C,VentilationSet,fetched
|
||||||
|
|||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"adapter": "react",
|
"adapter": "react",
|
||||||
"baseLocale": "pl",
|
"baseLocale": "pl",
|
||||||
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json"
|
"$schema": "https://unpkg.com/typesafe-i18n@5.27.1/schema/typesafe-i18n.json"
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import eslint from '@eslint/js';
|
import eslint from '@eslint/js';
|
||||||
import prettierConfig from 'eslint-config-prettier';
|
import prettierConfig from 'eslint-config-prettier';
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
export default tseslint.config(
|
export default defineConfig(
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
...tseslint.configs.recommendedTypeChecked,
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
prettierConfig,
|
prettierConfig,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "EMS-ESP",
|
"name": "EMS-ESP",
|
||||||
"version": "3.7.3",
|
"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",
|
||||||
@@ -12,60 +12,53 @@
|
|||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-hosted": "typesafe-i18n && vite build --mode hosted",
|
"build-hosted": "typesafe-i18n --no-watch && vite build --mode hosted",
|
||||||
"mock-rest": "bun --watch ../mock-api/restServer.ts",
|
"mock-rest": "bun --watch ../mock-api/restServer.ts",
|
||||||
"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\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alova/adapter-xhr": "2.3.0",
|
"@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": "^7.3.6",
|
"@mui/icons-material": "^9.0.1",
|
||||||
"@mui/material": "^7.3.6",
|
"@mui/material": "^9.0.1",
|
||||||
"@preact/compat": "^18.3.1",
|
|
||||||
"@table-library/react-table-library": "4.1.15",
|
"@table-library/react-table-library": "4.1.15",
|
||||||
"alova": "3.4.0",
|
"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.28.0",
|
"preact": "^10.29.1",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.6",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.6.0",
|
||||||
"react-router": "^7.10.1",
|
"react-router": "^7.15.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.1.0",
|
||||||
"typesafe-i18n": "^5.26.2",
|
"typesafe-i18n": "^5.27.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^6.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.28.5",
|
"@eslint/js": "^10.0.1",
|
||||||
"@eslint/js": "^9.39.1",
|
"@preact/preset-vite": "^2.10.5",
|
||||||
"@preact/compat": "^18.3.1",
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||||
"@preact/preset-vite": "^2.10.2",
|
"@types/node": "^25.6.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^6.0.0",
|
"@types/react": "^19.2.14",
|
||||||
"@types/node": "^24.10.1",
|
|
||||||
"@types/react": "^19.2.7",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"axe-core": "^4.11.0",
|
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^10.3.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.8.3",
|
||||||
"rollup-plugin-visualizer": "^6.0.5",
|
"rollup-plugin-visualizer": "^7.0.1",
|
||||||
"terser": "^5.44.1",
|
"terser": "^5.47.0",
|
||||||
"typescript-eslint": "^8.48.1",
|
"typescript-eslint": "^8.59.2",
|
||||||
"vite": "^7.2.6",
|
"vite": "^8.0.11",
|
||||||
"vite-plugin-imagemin": "^0.6.1",
|
"vite-plugin-imagemin": "^0.6.1"
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a"
|
"packageManager": "pnpm@10.33.4"
|
||||||
}
|
}
|
||||||
|
|||||||
2640
interface/pnpm-lock.yaml
generated
2640
interface/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { toast } from 'react-toastify';
|
|||||||
|
|
||||||
import ForwardIcon from '@mui/icons-material/Forward';
|
import ForwardIcon from '@mui/icons-material/Forward';
|
||||||
import { Box, Button, Paper, Typography } from '@mui/material';
|
import { Box, Button, Paper, Typography } from '@mui/material';
|
||||||
|
import type { Theme } from '@mui/material/styles';
|
||||||
|
|
||||||
import * as AuthenticationApi from 'components/routing/authentication';
|
import * as AuthenticationApi from 'components/routing/authentication';
|
||||||
import { useRequest } from 'alova/client';
|
import { useRequest } from 'alova/client';
|
||||||
@@ -17,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);
|
||||||
@@ -36,13 +37,12 @@ const SignIn = memo(() => {
|
|||||||
{
|
{
|
||||||
immediate: false
|
immediate: false
|
||||||
}
|
}
|
||||||
).onSuccess((response) => {
|
).onSuccess((response: { data: { access_token: string } }) => {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
authenticationContext.signIn(response.data.access_token);
|
authenticationContext.signIn(response.data.access_token);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoize callback to prevent recreation on every render
|
|
||||||
const updateLoginRequestValue = useMemo(
|
const updateLoginRequestValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
updateValue((updater) =>
|
updateValue((updater) =>
|
||||||
@@ -64,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')
|
||||||
@@ -73,12 +73,11 @@ 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]);
|
};
|
||||||
|
|
||||||
// Memoize callback to prevent recreation on every render
|
|
||||||
const submitOnEnter = useMemo(() => onEnterCallback(signIn), [signIn]);
|
const submitOnEnter = useMemo(() => onEnterCallback(signIn), [signIn]);
|
||||||
|
|
||||||
// get rid of scrollbar
|
// get rid of scrollbar
|
||||||
@@ -92,13 +91,15 @@ const SignIn = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
sx={(theme: Theme) => ({
|
||||||
height="100vh"
|
display: 'flex',
|
||||||
margin="auto"
|
height: '100vh',
|
||||||
padding={2}
|
margin: 'auto',
|
||||||
justifyContent="center"
|
padding: 2,
|
||||||
flexDirection="column"
|
justifyContent: 'center',
|
||||||
maxWidth={(theme) => theme.breakpoints.values.sm}
|
flexDirection: 'column',
|
||||||
|
maxWidth: theme.breakpoints.values.sm
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<Paper
|
<Paper
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
@@ -111,16 +112,18 @@ const SignIn = memo(() => {
|
|||||||
width: '100%'
|
width: '100%'
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Typography mb={1} variant="h4">
|
<Typography sx={{ mb: 1 }} variant="h4">
|
||||||
{PROJECT_NAME}
|
{PROJECT_NAME}
|
||||||
</Typography>
|
</Typography>
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
<Box
|
<Box
|
||||||
mt={1}
|
sx={{
|
||||||
display="flex"
|
mt: 1,
|
||||||
flexDirection="column"
|
display: 'flex',
|
||||||
gap={1}
|
flexDirection: 'column',
|
||||||
alignItems="center"
|
gap: 1,
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors || {}}
|
fieldErrors={fieldErrors || {}}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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 || ''} />
|
||||||
@@ -310,13 +298,15 @@ const CustomEntities = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Cell>
|
</Cell>
|
||||||
|
<Cell>{ei.ram > 0 ? '' : showHex(ei.device_id as number, 2)}</Cell>
|
||||||
|
<Cell>{ei.ram > 0 ? '' : showHex(ei.type_id as number, 3)}</Cell>
|
||||||
|
<Cell>{ei.ram > 0 ? '' : ei.offset}</Cell>
|
||||||
<Cell>
|
<Cell>
|
||||||
{ei.ram === 1 ? '' : showHex(ei.device_id as number, 2)}
|
{ei.ram === 1
|
||||||
</Cell>
|
? 'RAM'
|
||||||
<Cell>{ei.ram === 1 ? '' : showHex(ei.type_id as number, 3)}</Cell>
|
: ei.ram === 2
|
||||||
<Cell>{ei.ram === 1 ? '' : ei.offset}</Cell>
|
? 'NVS'
|
||||||
<Cell>
|
: DeviceValueTypeNames[ei.value_type]}
|
||||||
{ei.ram === 1 ? 'RAM' : DeviceValueTypeNames[ei.value_type]}
|
|
||||||
</Cell>
|
</Cell>
|
||||||
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
<Cell>{formatValue(ei.value, ei.uom)}</Cell>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -326,24 +316,14 @@ const CustomEntities = () => {
|
|||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
entities,
|
|
||||||
error,
|
|
||||||
fetchEntities,
|
|
||||||
entity_theme,
|
|
||||||
editEntityItem,
|
|
||||||
LL,
|
|
||||||
filteredAndSortedEntities,
|
|
||||||
showHex,
|
|
||||||
formatValue
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
<Box mb={2} color="warning.main">
|
<Typography sx={{ mb: 2 }} color="warning" variant="body1">
|
||||||
<Typography variant="body1">{LL.ENTITIES_HELP_1()}.</Typography>
|
{LL.ENTITIES_HELP_1()}.
|
||||||
</Box>
|
</Typography>
|
||||||
|
|
||||||
{renderEntity()}
|
{renderEntity()}
|
||||||
|
|
||||||
@@ -359,8 +339,8 @@ const CustomEntities = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box mt={2} display="flex" flexWrap="wrap">
|
<Box sx={{ mt: 2, display: 'flex', flexWrap: 'wrap' }}>
|
||||||
<Box flexGrow={1}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
{numChanges > 0 && (
|
{numChanges > 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
@@ -382,7 +362,7 @@ const CustomEntities = () => {
|
|||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -7,7 +7,7 @@ import DoneIcon from '@mui/icons-material/Done';
|
|||||||
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
|
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
|
||||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
import RemoveIcon from '@mui/icons-material/RemoveCircleOutlined';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -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}>
|
||||||
@@ -178,7 +168,7 @@ const CustomEntitiesDialog = ({
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid mt={3}>
|
<Grid sx={{ mt: 3 }}>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -205,16 +195,17 @@ const CustomEntitiesDialog = ({
|
|||||||
>
|
>
|
||||||
<MenuItem value={0}>EMS-{LL.VALUE(1)}</MenuItem>
|
<MenuItem value={0}>EMS-{LL.VALUE(1)}</MenuItem>
|
||||||
<MenuItem value={1}>RAM-{LL.VALUE(1)}</MenuItem>
|
<MenuItem value={1}>RAM-{LL.VALUE(1)}</MenuItem>
|
||||||
|
<MenuItem value={2}>NVS-{LL.VALUE(1)}</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
{editItem.ram === 1 && (
|
{editItem.ram > 0 && (
|
||||||
<>
|
<>
|
||||||
<Grid>
|
<Grid>
|
||||||
<TextField
|
<TextField
|
||||||
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
|
||||||
@@ -237,7 +228,7 @@ const CustomEntitiesDialog = ({
|
|||||||
)}
|
)}
|
||||||
{editItem.ram === 0 && (
|
{editItem.ram === 0 && (
|
||||||
<>
|
<>
|
||||||
<Grid mt={3}>
|
<Grid sx={{ mt: 3 }}>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -259,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: {
|
||||||
@@ -279,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: {
|
||||||
@@ -380,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}
|
||||||
@@ -403,7 +394,7 @@ const CustomEntitiesDialog = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
{!creating && (
|
{!creating && (
|
||||||
<Box flexGrow={1}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<RemoveIcon />}
|
startIcon={<RemoveIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -111,13 +111,14 @@ const Customizations = () => {
|
|||||||
const [selectedDeviceTypeNameURL, setSelectedDeviceTypeNameURL] =
|
const [selectedDeviceTypeNameURL, setSelectedDeviceTypeNameURL] =
|
||||||
useState<string>(''); // needed for API URL
|
useState<string>(''); // needed for API URL
|
||||||
const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
|
const [selectedDeviceName, setSelectedDeviceName] = useState<string>('');
|
||||||
|
const [selectedDeviceBrand, setSelectedDeviceBrand] = useState<string>('');
|
||||||
|
|
||||||
const { send: sendResetCustomizations } = useRequest(resetCustomizations(), {
|
const { send: sendResetCustomizations } = useRequest(resetCustomizations(), {
|
||||||
immediate: false
|
immediate: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const { send: sendDeviceName } = useRequest(
|
const { send: sendDeviceName } = useRequest(
|
||||||
(data: { id: number; name: string }) => writeDeviceName(data),
|
(data: { id: number; name: string; brand: string }) => writeDeviceName(data),
|
||||||
{
|
{
|
||||||
immediate: false
|
immediate: false
|
||||||
}
|
}
|
||||||
@@ -170,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;
|
||||||
}
|
}
|
||||||
@@ -193,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;
|
||||||
@@ -205,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;
|
||||||
@@ -221,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;
|
||||||
}
|
}
|
||||||
@@ -235,9 +234,7 @@ const Customizations = () => {
|
|||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
}),
|
});
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
function hasEntityChanged(de: DeviceEntity) {
|
function hasEntityChanged(de: DeviceEntity) {
|
||||||
return (
|
return (
|
||||||
@@ -267,6 +264,7 @@ const Customizations = () => {
|
|||||||
if (device) {
|
if (device) {
|
||||||
setSelectedDeviceTypeNameURL(device.url || '');
|
setSelectedDeviceTypeNameURL(device.url || '');
|
||||||
setSelectedDeviceName(device.n);
|
setSelectedDeviceName(device.n);
|
||||||
|
setSelectedDeviceBrand(device.b);
|
||||||
}
|
}
|
||||||
setNumChanges(0);
|
setNumChanges(0);
|
||||||
setRestartNeeded(false);
|
setRestartNeeded(false);
|
||||||
@@ -285,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;
|
||||||
@@ -334,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());
|
||||||
@@ -370,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;
|
||||||
}
|
}
|
||||||
@@ -404,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;
|
||||||
}
|
}
|
||||||
@@ -439,10 +425,14 @@ const Customizations = () => {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setOriginalSettings(deviceEntities);
|
setOriginalSettings(deviceEntities);
|
||||||
});
|
});
|
||||||
}, [devices, deviceEntities, selectedDevice, sendCustomizationEntities, LL]);
|
};
|
||||||
|
|
||||||
const renameDevice = useCallback(async () => {
|
const renameDevice = async () => {
|
||||||
await sendDeviceName({ id: selectedDevice, name: selectedDeviceName })
|
await sendDeviceName({
|
||||||
|
id: selectedDevice,
|
||||||
|
name: selectedDeviceName,
|
||||||
|
brand: selectedDeviceBrand
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(LL.UPDATED_OF(LL.NAME(1)));
|
toast.success(LL.UPDATED_OF(LL.NAME(1)));
|
||||||
})
|
})
|
||||||
@@ -453,24 +443,35 @@ const Customizations = () => {
|
|||||||
setRename(false);
|
setRename(false);
|
||||||
await fetchCoreData();
|
await fetchCoreData();
|
||||||
});
|
});
|
||||||
}, [selectedDevice, selectedDeviceName, sendDeviceName, LL, fetchCoreData]);
|
};
|
||||||
|
|
||||||
const renderDeviceList = () => (
|
const renderDeviceList = () => (
|
||||||
<>
|
<>
|
||||||
<Box mb={1} color="warning.main">
|
<Typography sx={{ mb: 1 }} color="warning" variant="body1">
|
||||||
<Typography variant="body1">{LL.CUSTOMIZATIONS_HELP_1()}.</Typography>
|
{LL.CUSTOMIZATIONS_HELP_1()}.
|
||||||
</Box>
|
</Typography>
|
||||||
<Box display="flex" flexWrap="wrap" alignItems="center" gap={2}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 2 }}>
|
||||||
{rename ? (
|
{rename ? (
|
||||||
<TextField
|
<>
|
||||||
name="device"
|
<TextField
|
||||||
label={LL.EMS_DEVICE()}
|
name="device"
|
||||||
fullWidth
|
label={LL.EMS_DEVICE()}
|
||||||
variant="outlined"
|
style={{ minWidth: '48%' }}
|
||||||
value={selectedDeviceName}
|
variant="outlined"
|
||||||
onChange={(e) => setSelectedDeviceName(e.target.value)}
|
value={selectedDeviceName}
|
||||||
margin="normal"
|
onChange={(e) => setSelectedDeviceName(e.target.value)}
|
||||||
/>
|
margin="normal"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
name="brand"
|
||||||
|
label={LL.BRAND()}
|
||||||
|
style={{ minWidth: '48%' }}
|
||||||
|
variant="outlined"
|
||||||
|
value={selectedDeviceBrand}
|
||||||
|
onChange={(e) => setSelectedDeviceBrand(e.target.value)}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
name="device"
|
name="device"
|
||||||
@@ -538,35 +539,27 @@ 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 (
|
||||||
<>
|
<>
|
||||||
<Box color="warning.main">
|
<Typography sx={{ mt: 1, mb: 1 }} color="warning" variant="body2">
|
||||||
<Typography variant="body2" mt={1} mb={1}>
|
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
||||||
<OptionIcon type="favorite" isSet={true} />={LL.CUSTOMIZATIONS_HELP_2()}
|
|
||||||
|
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
||||||
<OptionIcon type="readonly" isSet={true} />={LL.CUSTOMIZATIONS_HELP_3()}
|
|
||||||
|
<OptionIcon type="api_mqtt_exclude" isSet={true} />=
|
||||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />=
|
{LL.CUSTOMIZATIONS_HELP_4()}
|
||||||
{LL.CUSTOMIZATIONS_HELP_4()}
|
<OptionIcon type="web_exclude" isSet={true} />={LL.CUSTOMIZATIONS_HELP_5()}
|
||||||
<OptionIcon type="web_exclude" isSet={true} />=
|
|
||||||
{LL.CUSTOMIZATIONS_HELP_5()}
|
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
||||||
<OptionIcon type="deleted" isSet={true} />={LL.CUSTOMIZATIONS_HELP_6()}
|
</Typography>
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
mb={1}
|
|
||||||
mt={0}
|
|
||||||
spacing={2}
|
spacing={2}
|
||||||
direction="row"
|
direction="row"
|
||||||
justifyContent="flex-start"
|
sx={{ mb: 1, mt: 0, justifyContent: 'flex-start', alignItems: 'center' }}
|
||||||
alignItems="center"
|
|
||||||
>
|
>
|
||||||
<Grid>
|
<Grid>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -755,8 +748,8 @@ const Customizations = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</MessageBox>
|
</MessageBox>
|
||||||
) : (
|
) : (
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||||
<Box flexGrow={1}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
{numChanges !== 0 && (
|
{numChanges !== 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
@@ -788,10 +781,12 @@ const Customizations = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return restarting ? (
|
||||||
|
<SystemMonitor />
|
||||||
|
) : (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{restarting ? <SystemMonitor /> : renderContent()}
|
{renderContent()}
|
||||||
{selectedDeviceEntity && (
|
{selectedDeviceEntity && (
|
||||||
<SettingsCustomizationsDialog
|
<SettingsCustomizationsDialog
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -37,7 +37,7 @@ interface LabelValueProps {
|
|||||||
|
|
||||||
const LabelValue = memo(({ label, value }: LabelValueProps) => (
|
const LabelValue = memo(({ label, value }: LabelValueProps) => (
|
||||||
<Grid container direction="row">
|
<Grid container direction="row">
|
||||||
<Typography variant="body2" color="warning.main">
|
<Typography variant="body2" color="warning">
|
||||||
{label}:
|
{label}:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">{value}</Typography>
|
<Typography variant="body2">{value}</Typography>
|
||||||
@@ -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,36 +95,33 @@ 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 mt={1} mb={2}>
|
<Box sx={{ mt: 1, mb: 2 }}>
|
||||||
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
<EntityMaskToggle onUpdate={updateDeviceEntity} de={editItem} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -172,7 +162,7 @@ const CustomizationsDialog = ({
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Typography variant="body2" color="error" mt={2}>
|
<Typography sx={{ mt: 2 }} variant="body2" color="error">
|
||||||
Error: Check min and max values
|
Error: Check min and max values
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -6,7 +6,7 @@ import { toast } from 'react-toastify';
|
|||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
import HelpOutlineIcon from '@mui/icons-material/HelpOutlined';
|
||||||
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
|
||||||
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||||
import {
|
import {
|
||||||
@@ -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} />
|
{showType(di.n, di.t)}
|
||||||
{showType(di.n, di.t)}
|
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
||||||
<span style={{ color: 'lightblue' }}> ({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) {
|
||||||
@@ -262,12 +239,8 @@ const Dashboard = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!data.connected && (
|
|
||||||
<MessageBox level="error" message={LL.EMS_BUS_WARNING()} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.connected && data.nodes.length > 0 && !hasFavEntities && (
|
{data.connected && data.nodes.length > 0 && !hasFavEntities && (
|
||||||
<MessageBox mb={2} level="warning">
|
<MessageBox sx={{ mb: 2 }} level="warning">
|
||||||
<Typography>
|
<Typography>
|
||||||
{LL.NO_DATA_1()}
|
{LL.NO_DATA_1()}
|
||||||
<Link to="/customizations" style={{ color: 'white' }}>
|
<Link to="/customizations" style={{ color: 'white' }}>
|
||||||
@@ -284,10 +257,12 @@ const Dashboard = memo(() => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
sx={{
|
||||||
justifyContent="flex-end"
|
display: 'flex',
|
||||||
flexWrap="nowrap"
|
justifyContent: 'flex-end',
|
||||||
whiteSpace="nowrap"
|
flexWrap: 'nowrap',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ToggleButtonGroup
|
<ToggleButtonGroup
|
||||||
size="small"
|
size="small"
|
||||||
@@ -310,7 +285,7 @@ const Dashboard = memo(() => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{data.nodes.length > 0 ? (
|
{data.nodes.length > 0 ? (
|
||||||
<Box mt={1} justifyContent="center" flexDirection="column">
|
<Box sx={{ mt: 1, justifyContent: 'center', flexDirection: 'column' }}>
|
||||||
<IconContext.Provider
|
<IconContext.Provider
|
||||||
value={{
|
value={{
|
||||||
color: 'lightblue',
|
color: 'lightblue',
|
||||||
@@ -377,13 +352,8 @@ const Dashboard = memo(() => {
|
|||||||
</IconContext.Provider>
|
</IconContext.Provider>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<Box sx={{ display: 'flex' }}>
|
||||||
display="flex"
|
<Typography sx={{ mt: 1 }} color="warning" variant="body1">
|
||||||
// justifyContent="flex-end"
|
|
||||||
// flexWrap="nowrap"
|
|
||||||
// whiteSpace="nowrap"
|
|
||||||
>
|
|
||||||
<Typography mt={1} color="warning.main" variant="body1">
|
|
||||||
no data
|
no data
|
||||||
</Typography>
|
</Typography>
|
||||||
<Tooltip title={LL.DASHBOARD_1()}>
|
<Tooltip title={LL.DASHBOARD_1()}>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import type { IconType } from 'react-icons';
|
||||||
import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai';
|
import { AiOutlineAlert, AiOutlineControl, AiOutlineGateway } from 'react-icons/ai';
|
||||||
import { CgSmartHomeBoiler } from 'react-icons/cg';
|
import { CgSmartHomeBoiler } from 'react-icons/cg';
|
||||||
import { FaSolarPanel } from 'react-icons/fa';
|
import { FaSolarPanel } from 'react-icons/fa';
|
||||||
@@ -15,14 +16,9 @@ import { PiFan, PiGauge } from 'react-icons/pi';
|
|||||||
import { TiFlowSwitch, TiThermometer } from 'react-icons/ti';
|
import { TiFlowSwitch, TiThermometer } from 'react-icons/ti';
|
||||||
import { VscVmConnect } from 'react-icons/vsc';
|
import { VscVmConnect } from 'react-icons/vsc';
|
||||||
|
|
||||||
import type { SvgIconProps } from '@mui/material';
|
|
||||||
|
|
||||||
import { DeviceType } from './types';
|
import { DeviceType } from './types';
|
||||||
|
|
||||||
const deviceIconLookup: Record<
|
const deviceIconLookup: Record<DeviceType, IconType | null> = {
|
||||||
DeviceType,
|
|
||||||
React.ComponentType<SvgIconProps> | null
|
|
||||||
> = {
|
|
||||||
[DeviceType.TEMPERATURESENSOR]: TiThermometer,
|
[DeviceType.TEMPERATURESENSOR]: TiThermometer,
|
||||||
[DeviceType.ANALOGSENSOR]: PiGauge,
|
[DeviceType.ANALOGSENSOR]: PiGauge,
|
||||||
[DeviceType.BOILER]: CgSmartHomeBoiler,
|
[DeviceType.BOILER]: CgSmartHomeBoiler,
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useMemo,
|
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { IconContext } from 'react-icons';
|
import { IconContext } from 'react-icons';
|
||||||
import { useNavigate } from 'react-router';
|
import { Link, useNavigate } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
||||||
@@ -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(
|
||||||
@@ -534,9 +519,19 @@ const Devices = memo(() => {
|
|||||||
const renderCoreData = () => (
|
const renderCoreData = () => (
|
||||||
<>
|
<>
|
||||||
{!coreData.connected ? (
|
{!coreData.connected ? (
|
||||||
<MessageBox level="error" message={LL.EMS_BUS_WARNING()} />
|
<MessageBox level="error" message={LL.EMS_BUS_WARNING() + '.'}>
|
||||||
|
(
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
to="https://docs.emsesp.org/Troubleshooting#ems-bus-is-not-connecting"
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
>
|
||||||
|
{LL.ONLINE_HELP()}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
</MessageBox>
|
||||||
) : (
|
) : (
|
||||||
<Box justifyContent="center" flexDirection="column">
|
<Box sx={{ justifyContent: 'center', flexDirection: 'column' }}>
|
||||||
<IconContext.Provider
|
<IconContext.Provider
|
||||||
value={{
|
value={{
|
||||||
color: 'lightblue',
|
color: 'lightblue',
|
||||||
@@ -597,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)}
|
||||||
{dv.id.slice(2)}
|
{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
|
||||||
@@ -660,12 +649,12 @@ const Devices = memo(() => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ p: 1 }}>
|
<Box sx={{ p: 1 }}>
|
||||||
<Grid container justifyContent="space-between">
|
<Grid container sx={{ justifyContent: 'space-between' }}>
|
||||||
<Typography noWrap variant="subtitle1" color="warning.main">
|
<Typography noWrap variant="subtitle1" color="warning">
|
||||||
{deviceInfo.n} (
|
{deviceInfo.n} (
|
||||||
{deviceInfo.tn})
|
{deviceInfo.tn})
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid justifyContent="flex-end">
|
<Grid sx={{ justifyContent: 'flex-end' }}>
|
||||||
<ButtonTooltip title={LL.CLOSE()}>
|
<ButtonTooltip title={LL.CLOSE()}>
|
||||||
<IconButton onClick={resetDeviceSelect} aria-label={LL.CLOSE()}>
|
<IconButton onClick={resetDeviceSelect} aria-label={LL.CLOSE()}>
|
||||||
<HighlightOffIcon color="primary" sx={{ fontSize: 18 }} />
|
<HighlightOffIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -128,9 +115,9 @@ const DevicesDialog = ({
|
|||||||
<Dialog sx={dialogStyle} open={open} onClose={onClose}>
|
<Dialog sx={dialogStyle} open={open} onClose={onClose}>
|
||||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
<DialogTitle>{dialogTitle}</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box color="warning.main" mb={2}>
|
<Typography sx={{ mb: 2 }} color="warning" variant="body2">
|
||||||
<Typography variant="body2">{editItem.id.slice(2)}</Typography>
|
{editItem.id.slice(2)}
|
||||||
</Box>
|
</Typography>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid size={12}>
|
<Grid size={12}>
|
||||||
{editItem.l ? (
|
{editItem.l ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Divider,
|
Divider,
|
||||||
|
Grid,
|
||||||
Link,
|
Link,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
@@ -42,8 +43,7 @@ interface CustomSupport {
|
|||||||
html: string | null;
|
html: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constants moved outside component to prevent recreation
|
const DEFAULT_IMAGE_URL = 'https://emsesp.org/media/images/installer.jpeg';
|
||||||
const DEFAULT_IMAGE_URL = 'https://docs.emsesp.org/_media/images/installer.jpeg';
|
|
||||||
|
|
||||||
const SUPPORT_BOX_STYLES: SxProps<Theme> = {
|
const SUPPORT_BOX_STYLES: SxProps<Theme> = {
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
@@ -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,13 +74,7 @@ const HelpComponent = () => {
|
|||||||
});
|
});
|
||||||
const [imgError, setImgError] = useState<boolean>(false);
|
const [imgError, setImgError] = useState<boolean>(false);
|
||||||
|
|
||||||
// Memoize the request method to prevent re-creation on every render
|
useRequest(callAction({ action: 'getCustomSupport' })).onSuccess((event) => {
|
||||||
const getCustomSupportMethod = useMemo(
|
|
||||||
() => 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[] };
|
||||||
@@ -101,57 +97,34 @@ 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://docs.emsesp.org',
|
|
||||||
icon: <MenuBookIcon />,
|
|
||||||
label: () => LL.HELP_INFORMATION_1()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'https://discord.gg/3J3GgnzpyT',
|
|
||||||
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>
|
||||||
{customSupport.html && (
|
{customSupport.html && (
|
||||||
<Stack
|
<Stack
|
||||||
padding={1}
|
|
||||||
mb={2}
|
|
||||||
direction="row"
|
direction="row"
|
||||||
divider={<Divider orientation="vertical" flexItem />}
|
divider={<Divider orientation="vertical" flexItem />}
|
||||||
sx={SUPPORT_BOX_STYLES}
|
sx={{ padding: 1, mb: 2, ...SUPPORT_BOX_STYLES }}
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle1">
|
<Typography variant="subtitle1">
|
||||||
<div dangerouslySetInnerHTML={{ __html: customSupport.html }} />
|
<div dangerouslySetInnerHTML={{ __html: customSupport.html }} />
|
||||||
@@ -160,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}>
|
||||||
@@ -186,23 +159,23 @@ const HelpComponent = () => {
|
|||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box p={2} color="warning.main">
|
<Grid container spacing={2} sx={{ mt: 2, alignItems: 'center' }}>
|
||||||
<Typography mb={1} variant="body1">
|
<Typography sx={{ mb: 1 }} color="warning" variant="body1">
|
||||||
{LL.HELP_INFORMATION_4()}.
|
{LL.HELP_INFORMATION_4()}:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
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>
|
||||||
</Box>
|
</Grid>
|
||||||
|
|
||||||
<Divider sx={{ mt: 4 }} />
|
<Divider sx={{ mt: 4 }} />
|
||||||
|
|
||||||
<Typography color="white" variant="subtitle1" align="center" mt={1}>
|
<Typography color="white" variant="subtitle1" align="center" sx={{ mt: 1 }}>
|
||||||
©
|
©
|
||||||
<Link
|
<Link
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -217,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;
|
||||||
|
|||||||
@@ -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 || ''} />
|
||||||
@@ -186,9 +178,9 @@ const Modules = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box mb={2} color="warning.main">
|
<Typography sx={{ mb: 2 }} color="warning" variant="body1">
|
||||||
<Typography variant="body1">{LL.MODULES_DESCRIPTION()}.</Typography>
|
{LL.MODULES_DESCRIPTION()}.
|
||||||
</Box>
|
</Typography>
|
||||||
<Table
|
<Table
|
||||||
data={{ nodes: modules }}
|
data={{ nodes: modules }}
|
||||||
theme={modules_theme}
|
theme={modules_theme}
|
||||||
@@ -236,8 +228,8 @@ const Modules = () => {
|
|||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<Box mt={1} display="flex" flexWrap="wrap">
|
<Box sx={{ mt: 1, display: 'flex', flexWrap: 'wrap' }}>
|
||||||
<Box flexGrow={1}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
{numChanges !== 0 && (
|
{numChanges !== 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -79,7 +70,7 @@ const ModulesDialog = ({
|
|||||||
label="Enabled"
|
label="Enabled"
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Box mt={2} mb={1}>
|
<Box sx={{ mt: 2, mb: 1 }}>
|
||||||
<TextField
|
<TextField
|
||||||
name="license"
|
name="license"
|
||||||
label="License Key"
|
label="License Key"
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { memo } from 'react';
|
|||||||
|
|
||||||
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
import CommentsDisabledOutlinedIcon from '@mui/icons-material/CommentsDisabledOutlined';
|
||||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
import EditOffOutlinedIcon from '@mui/icons-material/EditOffOutlined';
|
||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
|
import InsertCommentOutlinedIcon from '@mui/icons-material/InsertCommentOutlined';
|
||||||
import StarIcon from '@mui/icons-material/Star';
|
import StarIcon from '@mui/icons-material/Star';
|
||||||
import StarOutlineIcon from '@mui/icons-material/StarOutline';
|
import StarOutlineIcon from '@mui/icons-material/StarOutlined';
|
||||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||||
import type { SvgIconProps } from '@mui/material';
|
import type { SvgIconProps } from '@mui/material';
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ const MIN_ID = -100;
|
|||||||
const MAX_ID = 100;
|
const MAX_ID = 100;
|
||||||
const ICON_SIZE = 16;
|
const ICON_SIZE = 16;
|
||||||
const SCHEDULE_FLAG_THRESHOLD = 127;
|
const SCHEDULE_FLAG_THRESHOLD = 127;
|
||||||
|
const FLAG_ALL_DAYS = 127;
|
||||||
const REFERENCE_YEAR = 2017;
|
const REFERENCE_YEAR = 2017;
|
||||||
const REFERENCE_MONTH = '01';
|
const REFERENCE_MONTH = '01';
|
||||||
const LOG_2 = Math.log(2);
|
const LOG_2 = Math.log(2);
|
||||||
@@ -51,7 +52,7 @@ const WEEK_DAYS = [1, 2, 3, 4, 5, 6, 7] as const;
|
|||||||
const DEFAULT_SCHEDULE_ITEM: Omit<ScheduleItem, 'id' | 'o_id'> = {
|
const DEFAULT_SCHEDULE_ITEM: Omit<ScheduleItem, 'id' | 'o_id'> = {
|
||||||
active: false,
|
active: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
flags: ScheduleFlag.SCHEDULE_DAY,
|
flags: FLAG_ALL_DAYS,
|
||||||
time: '',
|
time: '',
|
||||||
cmd: '',
|
cmd: '',
|
||||||
value: '',
|
value: '',
|
||||||
@@ -131,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 || '') ||
|
||||||
@@ -142,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, {
|
||||||
@@ -168,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
|
||||||
@@ -191,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),
|
||||||
@@ -238,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 (
|
||||||
@@ -277,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 || ''} />
|
||||||
@@ -342,24 +331,14 @@ const Scheduler = () => {
|
|||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
);
|
);
|
||||||
}, [
|
};
|
||||||
schedule,
|
|
||||||
error,
|
|
||||||
fetchSchedule,
|
|
||||||
filteredAndSortedSchedule,
|
|
||||||
schedule_theme,
|
|
||||||
editScheduleItem,
|
|
||||||
LL,
|
|
||||||
dayBox,
|
|
||||||
scheduleType
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
<Box mb={2} color="warning.main">
|
<Typography sx={{ mb: 2 }} color="warning" variant="body1">
|
||||||
<Typography variant="body1">{LL.SCHEDULER_HELP_1()}.</Typography>
|
{LL.SCHEDULER_HELP_1()}.
|
||||||
</Box>
|
</Typography>
|
||||||
{renderSchedule()}
|
{renderSchedule()}
|
||||||
|
|
||||||
{selectedScheduleItem && (
|
{selectedScheduleItem && (
|
||||||
@@ -374,8 +353,8 @@ const Scheduler = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||||
<Box flexGrow={1}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
{numChanges !== 0 && (
|
{numChanges !== 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
@@ -397,7 +376,7 @@ const Scheduler = () => {
|
|||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
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';
|
||||||
import DoneIcon from '@mui/icons-material/Done';
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
import RemoveIcon from '@mui/icons-material/RemoveCircleOutlined';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -26,14 +26,15 @@ 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';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const FLAG_MASK_127 = 127;
|
const FLAG_MASK_127 = 127;
|
||||||
const SCHEDULE_TYPE_THRESHOLD = 128;
|
const SCHEDULE_TYPE_THRESHOLD = 127;
|
||||||
|
const FLAG_ALL_DAYS = 127;
|
||||||
const DEFAULT_TIME = '00:00';
|
const DEFAULT_TIME = '00:00';
|
||||||
const TYPOGRAPHY_FONT_SIZE = 10;
|
const TYPOGRAPHY_FONT_SIZE = 10;
|
||||||
|
|
||||||
@@ -59,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;
|
||||||
@@ -83,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(() => {
|
||||||
@@ -104,135 +107,102 @@ const SchedulerDialog = ({
|
|||||||
// 130 is on condition
|
// 130 is on condition
|
||||||
// 132 is immediate
|
// 132 is immediate
|
||||||
setScheduleType(
|
setScheduleType(
|
||||||
selectedItem.flags < SCHEDULE_TYPE_THRESHOLD
|
selectedItem.flags <= SCHEDULE_TYPE_THRESHOLD
|
||||||
? ScheduleFlag.SCHEDULE_DAY
|
? ScheduleFlag.SCHEDULE_DAY
|
||||||
: selectedItem.flags
|
: selectedItem.flags
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [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 ? 0 : flag;
|
|
||||||
setEditItem((prev) => ({ ...prev, time: '', flags: newFlags }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDOWChange = useCallback(
|
|
||||||
(_event: React.SyntheticEvent<HTMLElement>, flags: string[]) => {
|
|
||||||
const newFlags = 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}>
|
||||||
@@ -336,11 +306,13 @@ const SchedulerDialog = ({
|
|||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
/>
|
/>
|
||||||
{isTimerSchedule && (
|
{isTimerSchedule && (
|
||||||
<Box color="warning.main" ml={2} mt={4}>
|
<Typography
|
||||||
<Typography variant="body2">
|
sx={{ ml: 2, mt: 4 }}
|
||||||
{LL.SCHEDULER_HELP_2()}
|
color="warning"
|
||||||
</Typography>
|
variant="body2"
|
||||||
</Box>
|
>
|
||||||
|
{LL.SCHEDULER_HELP_2()}
|
||||||
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -389,7 +361,7 @@ const SchedulerDialog = ({
|
|||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
{!creating && (
|
{!creating && (
|
||||||
<Box flexGrow={1}>
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<RemoveIcon />}
|
startIcon={<RemoveIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -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 (
|
||||||
@@ -586,11 +542,19 @@ const Sensors = () => {
|
|||||||
creating={creating}
|
creating={creating}
|
||||||
selectedItem={selectedAnalogSensor}
|
selectedItem={selectedAnalogSensor}
|
||||||
analogGPIOList={sensorData.available_gpios}
|
analogGPIOList={sensorData.available_gpios}
|
||||||
|
disabledTypeList={sensorData.exclude_types}
|
||||||
validator={analogSensorItemValidation(sensorData.as, selectedAnalogSensor)}
|
validator={analogSensorItemValidation(sensorData.as, selectedAnalogSensor)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{sensorData?.analog_enabled === true && me.admin && (
|
{sensorData?.analog_enabled === true && me.admin && (
|
||||||
<Box mt={2} display="flex" flexWrap="wrap" justifyContent="flex-end">
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
justifyContent: 'flex-end'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
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';
|
||||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
import RemoveIcon from '@mui/icons-material/RemoveCircleOutlined';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -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';
|
||||||
@@ -35,6 +35,7 @@ interface DashboardSensorsAnalogDialogProps {
|
|||||||
creating: boolean;
|
creating: boolean;
|
||||||
selectedItem: AnalogSensor;
|
selectedItem: AnalogSensor;
|
||||||
analogGPIOList: number[];
|
analogGPIOList: number[];
|
||||||
|
disabledTypeList: number[];
|
||||||
validator: Schema;
|
validator: Schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,75 +46,61 @@ const SensorsAnalogDialog = ({
|
|||||||
creating,
|
creating,
|
||||||
selectedItem,
|
selectedItem,
|
||||||
analogGPIOList,
|
analogGPIOList,
|
||||||
|
disabledTypeList,
|
||||||
validator
|
validator
|
||||||
}: DashboardSensorsAnalogDialogProps) => {
|
}: DashboardSensorsAnalogDialogProps) => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
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.COUNTER && editItem.t <= AnalogType.RATE,
|
editItem.t === AnalogType.RATE ||
|
||||||
[editItem.t]
|
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2);
|
||||||
);
|
const isCounter =
|
||||||
const isFreqType = useMemo(
|
editItem.t === AnalogType.COUNTER ||
|
||||||
() => editItem.t >= AnalogType.FREQ_0 && editItem.t <= AnalogType.FREQ_2,
|
(editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2);
|
||||||
[editItem.t]
|
const isFreqType =
|
||||||
);
|
editItem.t >= AnalogType.FREQ_0 && editItem.t <= AnalogType.FREQ_2;
|
||||||
const isPWM = useMemo(
|
const isPWM =
|
||||||
() =>
|
editItem.t === AnalogType.PWM_0 ||
|
||||||
editItem.t === AnalogType.PWM_0 ||
|
editItem.t === AnalogType.PWM_1 ||
|
||||||
editItem.t === AnalogType.PWM_1 ||
|
editItem.t === AnalogType.PWM_2;
|
||||||
editItem.t === AnalogType.PWM_2,
|
const isDACOutGPIO =
|
||||||
[editItem.t]
|
editItem.t === AnalogType.DIGITAL_OUT &&
|
||||||
);
|
(editItem.g === 25 || editItem.g === 26);
|
||||||
const isDigitalOutGPIO = useMemo(
|
const isDigitalOutGPIO =
|
||||||
() =>
|
editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && editItem.g !== 26;
|
||||||
editItem.t === AnalogType.DIGITAL_OUT &&
|
|
||||||
(editItem.g === 25 || editItem.g === 26),
|
|
||||||
[editItem.t, editItem.g]
|
|
||||||
);
|
|
||||||
const isDigitalOutNonGPIO = 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) => (
|
}))
|
||||||
<MenuItem key={val} value={i + 1}>
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
{val}
|
.map(({ name, value }) => (
|
||||||
</MenuItem>
|
<MenuItem
|
||||||
)),
|
key={name}
|
||||||
[]
|
value={value}
|
||||||
);
|
disabled={disabledTypeList?.includes(value)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
|
||||||
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
|
||||||
@@ -140,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}>
|
||||||
@@ -207,7 +190,10 @@ const SensorsAnalogDialog = ({
|
|||||||
{analogTypeMenuItems}
|
{analogTypeMenuItems}
|
||||||
</ValidatedTextField>
|
</ValidatedTextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
{(isCounterOrRate || isFreqType) && (
|
{(isCounterOrRate ||
|
||||||
|
isFreqType ||
|
||||||
|
editItem.t === AnalogType.ADC ||
|
||||||
|
editItem.t === AnalogType.TIMER) && (
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
name="u"
|
name="u"
|
||||||
@@ -264,7 +250,7 @@ const SensorsAnalogDialog = ({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{editItem.t === AnalogType.COUNTER && (
|
{isCounter && (
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
name="o"
|
name="o"
|
||||||
@@ -293,7 +279,10 @@ const SensorsAnalogDialog = ({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{isCounterOrRate && (
|
{(isCounterOrRate ||
|
||||||
|
isFreqType ||
|
||||||
|
editItem.t === AnalogType.ADC ||
|
||||||
|
editItem.t === AnalogType.TIMER) && (
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
name="f"
|
name="f"
|
||||||
@@ -309,7 +298,7 @@ const SensorsAnalogDialog = ({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{isDigitalOutGPIO && (
|
{isDACOutGPIO && (
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
name="o"
|
name="o"
|
||||||
@@ -325,7 +314,7 @@ const SensorsAnalogDialog = ({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{isDigitalOutNonGPIO && (
|
{isDigitalOutGPIO && (
|
||||||
<>
|
<>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
@@ -456,7 +445,7 @@ const SensorsAnalogDialog = ({
|
|||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
{fieldErrors && Object.keys(fieldErrors).length > 0 && (
|
{fieldErrors && Object.keys(fieldErrors).length > 0 && (
|
||||||
<Box mt={1}>
|
<Box sx={{ mt: 1 }}>
|
||||||
{Object.values(fieldErrors).map((errArr, idx) =>
|
{Object.values(fieldErrors).map((errArr, idx) =>
|
||||||
Array.isArray(errArr)
|
Array.isArray(errArr)
|
||||||
? errArr.map((err, j) => (
|
? errArr.map((err, j) => (
|
||||||
@@ -464,7 +453,7 @@ const SensorsAnalogDialog = ({
|
|||||||
key={`${idx}-${j}`}
|
key={`${idx}-${j}`}
|
||||||
color="error"
|
color="error"
|
||||||
variant="caption"
|
variant="caption"
|
||||||
display="block"
|
sx={{ display: 'block' }}
|
||||||
>
|
>
|
||||||
{err.message}
|
{err.message}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -475,7 +464,7 @@ const SensorsAnalogDialog = ({
|
|||||||
)}
|
)}
|
||||||
{editItem.s && (
|
{editItem.s && (
|
||||||
<Grid>
|
<Grid>
|
||||||
<Typography mt={1} color="warning.main" variant="body2">
|
<Typography sx={{ mt: 1 }} color="warning" variant="body2">
|
||||||
<WarningIcon
|
<WarningIcon
|
||||||
fontSize="small"
|
fontSize="small"
|
||||||
sx={{ mr: 1, verticalAlign: 'middle' }}
|
sx={{ mr: 1, verticalAlign: 'middle' }}
|
||||||
@@ -488,7 +477,7 @@ const SensorsAnalogDialog = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
{!creating && (
|
{!creating && (
|
||||||
<Box flexGrow={1} sx={{ '& button': { mt: 0 } }}>
|
<Box sx={{ flexGrow: 1, '& button': { mt: 0 } }}>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<RemoveIcon />}
|
startIcon={<RemoveIcon />}
|
||||||
disabled={editItem.s}
|
disabled={editItem.s}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
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';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
@@ -22,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';
|
||||||
|
|
||||||
@@ -51,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(() => {
|
||||||
@@ -70,52 +65,29 @@ 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>
|
||||||
<Box color="warning.main" mb={2}>
|
<Typography sx={{ mb: 2 }} color="warning" variant="body2">
|
||||||
<Typography variant="body2">
|
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
|
||||||
{LL.ID_OF(LL.SENSOR(0))}: {editItem.id}
|
</Typography>
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid>
|
<Grid>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
@@ -131,18 +103,29 @@ 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>
|
||||||
{editItem.s && (
|
{editItem.s && (
|
||||||
<Grid>
|
<Grid>
|
||||||
<Typography mt={1} color="warning.main" variant="body2">
|
<Typography sx={{ mt: 1 }} color="warning" variant="body2">
|
||||||
<WarningIcon
|
<WarningIcon
|
||||||
fontSize="small"
|
fontSize="small"
|
||||||
sx={{ mr: 1, verticalAlign: 'middle' }}
|
sx={{ mr: 1, verticalAlign: 'middle' }}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -41,8 +41,12 @@ const UserProfileComponent = () => {
|
|||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
<Box mt={2} mb={2} display="flex" alignItems="center">
|
<Box sx={{ mt: 2, mb: 2, display: 'flex', alignItems: 'center' }}>
|
||||||
<Typography mr={2} variant="body1" align="center">
|
<Typography
|
||||||
|
sx={{ mr: 2, textAlign: 'center' }}
|
||||||
|
color="warning"
|
||||||
|
variant="body1"
|
||||||
|
>
|
||||||
{LL.LANGUAGE()}:
|
{LL.LANGUAGE()}:
|
||||||
</Typography>
|
</Typography>
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +122,7 @@ export interface SensorData {
|
|||||||
as: AnalogSensor[];
|
as: AnalogSensor[];
|
||||||
analog_enabled: boolean;
|
analog_enabled: boolean;
|
||||||
available_gpios: number[];
|
available_gpios: number[];
|
||||||
|
exclude_types: number[];
|
||||||
platform: string;
|
platform: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +206,7 @@ export enum DeviceValueUOM {
|
|||||||
MBAR,
|
MBAR,
|
||||||
LH,
|
LH,
|
||||||
CTKWH,
|
CTKWH,
|
||||||
HZ
|
HERTZ
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeviceValueUOM_s = [
|
export const DeviceValueUOM_s = [
|
||||||
@@ -245,7 +256,10 @@ export enum AnalogType {
|
|||||||
PULSE = 12,
|
PULSE = 12,
|
||||||
FREQ_0 = 13,
|
FREQ_0 = 13,
|
||||||
FREQ_1 = 14,
|
FREQ_1 = 14,
|
||||||
FREQ_2 = 15
|
FREQ_2 = 15,
|
||||||
|
CNT_0 = 16,
|
||||||
|
CNT_1 = 17,
|
||||||
|
CNT_2 = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AnalogTypeNames = [
|
export const AnalogTypeNames = [
|
||||||
@@ -258,12 +272,15 @@ export const AnalogTypeNames = [
|
|||||||
'PWM 0', // 7
|
'PWM 0', // 7
|
||||||
'PWM 1', // 8
|
'PWM 1', // 8
|
||||||
'PWM 2', // 9
|
'PWM 2', // 9
|
||||||
'NTC Temp.', // 10
|
'NTC Temp', // 10
|
||||||
'RGB Led', // 11
|
'RGB Led', // 11
|
||||||
'Pulse', // 12
|
'Pulse', // 12
|
||||||
'Freq 0', // 13
|
'Freq 0', // 13
|
||||||
'Freq 1', // 14
|
'Freq 1', // 14
|
||||||
'Freq 2' // 15
|
'Freq 2', // 15
|
||||||
|
'Counter 0', // 16
|
||||||
|
'Counter 1', // 17
|
||||||
|
'Counter 2' // 18
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const BOARD_PROFILES = {
|
export const BOARD_PROFILES = {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -328,9 +327,9 @@ const ApplicationSettings = () => {
|
|||||||
>
|
>
|
||||||
<MenuItem value={-1}>OFF</MenuItem>
|
<MenuItem value={-1}>OFF</MenuItem>
|
||||||
<MenuItem value={3}>ERR</MenuItem>
|
<MenuItem value={3}>ERR</MenuItem>
|
||||||
|
<MenuItem value={4}>WARN</MenuItem>
|
||||||
<MenuItem value={5}>NOTICE</MenuItem>
|
<MenuItem value={5}>NOTICE</MenuItem>
|
||||||
<MenuItem value={6}>INFO</MenuItem>
|
<MenuItem value={6}>INFO</MenuItem>
|
||||||
<MenuItem value={7}>DEBUG</MenuItem>
|
|
||||||
<MenuItem value={9}>ALL</MenuItem>
|
<MenuItem value={9}>ALL</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -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">
|
||||||
|
({LL.IS_REQUIRED('PSRAM')})
|
||||||
|
</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>
|
||||||
@@ -489,17 +651,25 @@ const ApplicationSettings = () => {
|
|||||||
name="board_profile"
|
name="board_profile"
|
||||||
label={LL.BOARD_PROFILE()}
|
label={LL.BOARD_PROFILE()}
|
||||||
value={data.board_profile}
|
value={data.board_profile}
|
||||||
disabled={processingBoard || hardwareData.model.startsWith('BBQKees')}
|
disabled={processingBoard}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={changeBoardProfile}
|
onChange={changeBoardProfile}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
select
|
select
|
||||||
>
|
>
|
||||||
{boardProfileItems}
|
{hardwareData.model.startsWith('BBQKees') ? (
|
||||||
<Divider />
|
<MenuItem key={hardwareData.board} value={hardwareData.board}>
|
||||||
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
{BOARD_PROFILES[hardwareData.board as BoardProfileKey]}
|
||||||
{LL.CUSTOM()}…
|
</MenuItem>
|
||||||
</MenuItem>
|
) : (
|
||||||
|
boardProfileItems
|
||||||
|
)}
|
||||||
|
{(data.board_profile === 'CUSTOM' || data.developer_mode) && <Divider />}
|
||||||
|
{(data.board_profile === 'CUSTOM' || data.developer_mode) && (
|
||||||
|
<MenuItem key={'CUSTOM'} value={'CUSTOM'}>
|
||||||
|
{LL.CUSTOM()}…
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</TextField>
|
</TextField>
|
||||||
{data.board_profile === 'CUSTOM' && (
|
{data.board_profile === 'CUSTOM' && (
|
||||||
<>
|
<>
|
||||||
@@ -602,6 +772,7 @@ const ApplicationSettings = () => {
|
|||||||
<MenuItem value={0}>{LL.DISABLED(1)}</MenuItem>
|
<MenuItem value={0}>{LL.DISABLED(1)}</MenuItem>
|
||||||
<MenuItem value={1}>LAN8720</MenuItem>
|
<MenuItem value={1}>LAN8720</MenuItem>
|
||||||
<MenuItem value={2}>TLK110</MenuItem>
|
<MenuItem value={2}>TLK110</MenuItem>
|
||||||
|
<MenuItem value={3}>RTL8201</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -762,7 +933,7 @@ const ApplicationSettings = () => {
|
|||||||
label={LL.REMOTE_TIMEOUT_EN()}
|
label={LL.REMOTE_TIMEOUT_EN()}
|
||||||
/>
|
/>
|
||||||
{data.remote_timeout_en && (
|
{data.remote_timeout_en && (
|
||||||
<Box mt={2}>
|
<Box sx={{ mt: 2 }}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors || {}}
|
fieldErrors={fieldErrors || {}}
|
||||||
name="remote_timeout"
|
name="remote_timeout"
|
||||||
@@ -895,10 +1066,12 @@ const ApplicationSettings = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return restarting ? (
|
||||||
|
<SystemMonitor />
|
||||||
|
) : (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{restarting ? <SystemMonitor /> : content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
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 DownloadIcon from '@mui/icons-material/GetApp';
|
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||||
import { Box, Button, Grid, Typography } from '@mui/material';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Grid,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
import * as SystemApi from 'api/system';
|
import * as SystemApi from 'api/system';
|
||||||
import { API, callAction } from 'api/app';
|
import { API, callAction } from 'api/app';
|
||||||
|
|
||||||
|
import { dialogStyle } from '@/CustomTheme';
|
||||||
import { useRequest } from 'alova/client';
|
import { useRequest } from 'alova/client';
|
||||||
import type { APIcall } from 'app/main/types';
|
import type { APIcall } from 'app/main/types';
|
||||||
import SystemMonitor from 'app/status/SystemMonitor';
|
import SystemMonitor from 'app/status/SystemMonitor';
|
||||||
@@ -19,16 +30,11 @@ import {
|
|||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
import { saveFile } from 'utils';
|
import { saveFile } from 'utils';
|
||||||
|
|
||||||
interface DownloadButton {
|
|
||||||
key: string;
|
|
||||||
type: string;
|
|
||||||
label: string | number;
|
|
||||||
isGridButton: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DownloadUpload = () => {
|
const DownloadUpload = () => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
|
const [confirmBackup, setConfirmBackup] = useState<boolean>(false);
|
||||||
|
|
||||||
const [restarting, setRestarting] = useState<boolean>(false);
|
const [restarting, setRestarting] = useState<boolean>(false);
|
||||||
|
|
||||||
const { send: sendExportData } = useRequest(
|
const { send: sendExportData } = useRequest(
|
||||||
@@ -51,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 });
|
||||||
@@ -59,52 +65,18 @@ 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 downloadButtons: DownloadButton[] = useMemo(
|
const handleCloseBackupDialog = () => {
|
||||||
() => [
|
setConfirmBackup(false);
|
||||||
{
|
};
|
||||||
key: 'settings',
|
|
||||||
type: 'settings',
|
|
||||||
label: LL.SETTINGS_OF(LL.APPLICATION()),
|
|
||||||
isGridButton: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'customizations',
|
|
||||||
type: 'customizations',
|
|
||||||
label: LL.CUSTOMIZATIONS(),
|
|
||||||
isGridButton: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'entities',
|
|
||||||
type: 'entities',
|
|
||||||
label: LL.CUSTOM_ENTITIES(0),
|
|
||||||
isGridButton: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'schedule',
|
|
||||||
type: 'schedule',
|
|
||||||
label: LL.SCHEDULE(0),
|
|
||||||
isGridButton: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'allvalues',
|
|
||||||
type: 'allvalues',
|
|
||||||
label: LL.ALLVALUES(),
|
|
||||||
isGridButton: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[LL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDownload = useCallback(
|
const handleDownload = (type: string) => () => {
|
||||||
(type: string) => () => {
|
void sendExportData(type);
|
||||||
void sendExportData(type);
|
setConfirmBackup(false);
|
||||||
},
|
};
|
||||||
[sendExportData]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (restarting) {
|
if (restarting) {
|
||||||
return <SystemMonitor />;
|
return <SystemMonitor />;
|
||||||
@@ -118,58 +90,86 @@ const DownloadUpload = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const gridButtons = downloadButtons.filter((btn) => btn.isGridButton);
|
|
||||||
const standaloneButton = downloadButtons.find((btn) => !btn.isGridButton);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={confirmBackup}
|
||||||
|
onClose={handleCloseBackupDialog}
|
||||||
|
>
|
||||||
|
<DialogTitle>{LL.DOWNLOAD_SYSTEM_BACKUP()}</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<WarningIcon color="warning" sx={{ fontSize: 18 }} />
|
||||||
|
|
||||||
|
{LL.WARNING_SYSTEM_BACKUP()}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
startIcon={<CancelIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleCloseBackupDialog}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<DownloadIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleDownload('systembackup')}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{LL.DOWNLOAD(0)}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pb: 2 }} variant="h6" color="primary">
|
||||||
{LL.DOWNLOAD(0)}
|
{LL.DOWNLOAD(0)}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography mb={1} variant="body1" color="warning">
|
<Grid
|
||||||
{LL.DOWNLOAD_SETTINGS_TEXT()}.
|
container
|
||||||
</Typography>
|
spacing={2}
|
||||||
|
sx={{
|
||||||
<Grid container spacing={2}>
|
alignItems: 'center'
|
||||||
{gridButtons.map((button) => (
|
}}
|
||||||
<Grid key={button.key}>
|
>
|
||||||
<Button
|
<Typography variant="body1" color="warning">
|
||||||
startIcon={<DownloadIcon />}
|
{LL.DOWNLOAD_SETTINGS_TEXT()}:
|
||||||
variant="outlined"
|
</Typography>
|
||||||
color="primary"
|
|
||||||
onClick={handleDownload(button.type)}
|
|
||||||
>
|
|
||||||
{button.label}
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Typography mt={2} mb={1} variant="body1" color="warning">
|
|
||||||
{LL.DOWNLOAD_SETTINGS_TEXT2()}.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{standaloneButton && (
|
|
||||||
<Button
|
<Button
|
||||||
startIcon={<DownloadIcon />}
|
startIcon={<DownloadIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={handleDownload(standaloneButton.type)}
|
onClick={() => setConfirmBackup(true)}
|
||||||
>
|
>
|
||||||
{standaloneButton.label}
|
{LL.DOWNLOAD_SYSTEM_BACKUP()}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container spacing={2} sx={{ mt: 2, alignItems: 'center' }}>
|
||||||
|
<Typography variant="body1" color="warning">
|
||||||
|
{LL.DOWNLOAD_SETTINGS_TEXT2()}:
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
startIcon={<DownloadIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={handleDownload('allvalues')}
|
||||||
|
>
|
||||||
|
{LL.ALLVALUES()}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||||
{LL.UPLOAD()}
|
{LL.UPLOAD()}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box color="warning.main" sx={{ pb: 2 }}>
|
<Typography sx={{ pb: 2 }} color="warning" variant="body1">
|
||||||
<Typography variant="body1">{LL.UPLOAD_TEXT()}.</Typography>
|
{LL.UPLOAD_TEXT()}:
|
||||||
</Box>
|
</Typography>
|
||||||
|
|
||||||
<SingleUpload text={LL.UPLOAD_DRAG()} doRestart={doRestart} />
|
<SingleUpload doRestart={doRestart} />
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 (
|
||||||
@@ -129,7 +117,7 @@ const MqttSettings = () => {
|
|||||||
<SectionContent>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
<>
|
<>
|
||||||
<Box display="flex" gap={2} mb={1}>
|
<Box sx={{ display: 'flex', gap: 2, mb: 1 }}>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -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={{
|
||||||
@@ -345,75 +333,91 @@ const MqttSettings = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid container spacing={2} rowSpacing={0}>
|
{/* <Grid container spacing={2} rowSpacing={0}> */}
|
||||||
<Grid>
|
<Grid>
|
||||||
<BlockFormControlLabel
|
<BlockFormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name="ha_enabled"
|
name="ha_enabled"
|
||||||
checked={data.ha_enabled}
|
checked={data.ha_enabled}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
disabled={data.publish_single}
|
disabled={data.publish_single}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={LL.MQTT_PUBLISH_TEXT_3()}
|
label={LL.MQTT_PUBLISH_TEXT_3()}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
|
||||||
{data.ha_enabled && (
|
|
||||||
<Grid container spacing={2} rowSpacing={0}>
|
|
||||||
<Grid>
|
|
||||||
<TextField
|
|
||||||
name="discovery_type"
|
|
||||||
label={LL.MQTT_PUBLISH_TEXT_5()}
|
|
||||||
value={data.discovery_type}
|
|
||||||
variant="outlined"
|
|
||||||
onChange={updateFormValue}
|
|
||||||
margin="normal"
|
|
||||||
select
|
|
||||||
>
|
|
||||||
<MenuItem value={0}>Home Assistant</MenuItem>
|
|
||||||
<MenuItem value={1}>Domoticz</MenuItem>
|
|
||||||
<MenuItem value={2}>Domoticz (latest)</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
</Grid>
|
|
||||||
<Grid>
|
|
||||||
<TextField
|
|
||||||
name="discovery_prefix"
|
|
||||||
label={LL.MQTT_PUBLISH_TEXT_4()}
|
|
||||||
variant="outlined"
|
|
||||||
value={data.discovery_prefix}
|
|
||||||
onChange={updateFormValue}
|
|
||||||
margin="normal"
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
<Grid>
|
|
||||||
<TextField
|
|
||||||
name="entity_format"
|
|
||||||
label={LL.MQTT_ENTITY_FORMAT()}
|
|
||||||
value={data.entity_format}
|
|
||||||
variant="outlined"
|
|
||||||
onChange={updateFormValue}
|
|
||||||
margin="normal"
|
|
||||||
select
|
|
||||||
>
|
|
||||||
<MenuItem value={0}>{LL.MQTT_ENTITY_FORMAT_0()}</MenuItem>
|
|
||||||
<MenuItem value={3}>
|
|
||||||
{LL.MQTT_ENTITY_FORMAT_1()} (v3.5)
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value={4}>
|
|
||||||
{LL.MQTT_ENTITY_FORMAT_2()} (v3.5)
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value={1}>
|
|
||||||
{LL.MQTT_ENTITY_FORMAT_1()} (latest)
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value={2}>
|
|
||||||
{LL.MQTT_ENTITY_FORMAT_2()} (latest)
|
|
||||||
</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{data.ha_enabled && (
|
||||||
|
<Grid container spacing={2} rowSpacing={0}>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="discovery_type"
|
||||||
|
label={LL.MQTT_PUBLISH_TEXT_5()}
|
||||||
|
value={data.discovery_type}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>Home Assistant</MenuItem>
|
||||||
|
<MenuItem value={1}>Domoticz</MenuItem>
|
||||||
|
<MenuItem value={2}>Domoticz (latest)</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="discovery_prefix"
|
||||||
|
label={LL.MQTT_PUBLISH_TEXT_4()}
|
||||||
|
variant="outlined"
|
||||||
|
value={data.discovery_prefix}
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<TextField
|
||||||
|
name="entity_format"
|
||||||
|
label={LL.MQTT_ENTITY_FORMAT()}
|
||||||
|
value={data.entity_format}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>{LL.MQTT_ENTITY_FORMAT_0()}</MenuItem>
|
||||||
|
<MenuItem value={3}>
|
||||||
|
{LL.MQTT_ENTITY_FORMAT_1()} (v3.5)
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={4}>
|
||||||
|
{LL.MQTT_ENTITY_FORMAT_2()} (v3.5)
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={1}>
|
||||||
|
{LL.MQTT_ENTITY_FORMAT_1()} (latest)
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={2}>
|
||||||
|
{LL.MQTT_ENTITY_FORMAT_2()} (latest)
|
||||||
|
</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
{data.discovery_type === 0 && (
|
||||||
|
<TextField
|
||||||
|
name="ha_number_mode"
|
||||||
|
label={LL.MQTT_INPUT_NUMBER_FORMAT()}
|
||||||
|
value={data.ha_number_mode}
|
||||||
|
variant="outlined"
|
||||||
|
onChange={updateFormValue}
|
||||||
|
sx={{ width: '20ch' }}
|
||||||
|
margin="normal"
|
||||||
|
select
|
||||||
|
>
|
||||||
|
<MenuItem value={0}>Box</MenuItem>
|
||||||
|
<MenuItem value={1}>Slider</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 2 }} variant="h6" color="primary">
|
||||||
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
{LL.MQTT_PUBLISH_INTERVALS()} (0=auto)
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -422,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={{
|
||||||
|
|||||||
@@ -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 || ''} />;
|
||||||
}
|
}
|
||||||
@@ -193,9 +173,9 @@ const NTPSettings = () => {
|
|||||||
{timeZoneItems}
|
{timeZoneItems}
|
||||||
</ValidatedTextField>
|
</ValidatedTextField>
|
||||||
|
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||||
{!data.enabled && !dirtyFlags.length && (
|
{!data.enabled && !dirtyFlags.length && (
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
onClick={openSetTime}
|
onClick={openSetTime}
|
||||||
@@ -236,32 +216,18 @@ 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>
|
||||||
<Box color="warning.main" p={0} pl={0} pr={0} mt={0} mb={2}>
|
<Typography color="warning" variant="body2">
|
||||||
<Typography variant="body2">{LL.SET_TIME_TEXT()}</Typography>
|
{LL.SET_TIME_TEXT()}
|
||||||
</Box>
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
label={LL.LOCAL_TIME(0)}
|
label={LL.LOCAL_TIME(0)}
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
|
|||||||
@@ -1,188 +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
|
|
||||||
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 <SectionContent>{restarting ? <SystemMonitor /> : content}</SectionContent>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { MenuItem } from '@mui/material';
|
import { MenuItem } from '@mui/material';
|
||||||
|
|
||||||
type TimeZones = Record<string, string>;
|
export const TIME_ZONES: Record<string, string> = {
|
||||||
|
|
||||||
export const TIME_ZONES: Readonly<TimeZones> = {
|
|
||||||
'Africa/Abidjan': 'GMT0',
|
'Africa/Abidjan': 'GMT0',
|
||||||
'Africa/Accra': 'GMT0',
|
'Africa/Accra': 'GMT0',
|
||||||
'Africa/Addis_Ababa': 'EAT-3',
|
'Africa/Addis_Ababa': 'EAT-3',
|
||||||
@@ -474,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;
|
||||||
}
|
}
|
||||||
|
|||||||
958
interface/src/app/settings/Version.tsx
Normal file
958
interface/src/app/settings/Version.tsx
Normal 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">
|
||||||
|
({data.build_flags})
|
||||||
|
</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">
|
||||||
|
(
|
||||||
|
{data.psram ? (
|
||||||
|
<CheckIcon
|
||||||
|
color="success"
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.5em',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CloseIcon
|
||||||
|
color="error"
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.5em',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
PSRAM)
|
||||||
|
</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);
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -397,10 +397,12 @@ const NetworkSettings = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return restarting ? (
|
||||||
|
<SystemMonitor />
|
||||||
|
) : (
|
||||||
<SectionContent>
|
<SectionContent>
|
||||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{restarting ? <SystemMonitor /> : content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -54,19 +54,27 @@ const GenerateToken = ({ username, onClose }: GenerateTokenProps) => {
|
|||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
{token ? (
|
{token ? (
|
||||||
<>
|
<>
|
||||||
<MessageBox message={LL.ACCESS_TOKEN_TEXT()} level="info" my={2} />
|
<MessageBox
|
||||||
<Box mt={2} mb={2}>
|
message={LL.ACCESS_TOKEN_TEXT()}
|
||||||
|
level="info"
|
||||||
|
sx={{ mt: 2, mb: 2 }}
|
||||||
|
/>
|
||||||
|
<Box sx={{ mt: 2, mb: 2 }}>
|
||||||
<TextField
|
<TextField
|
||||||
label="Token"
|
label="Token"
|
||||||
multiline
|
multiline
|
||||||
value={token.token}
|
value={token.token}
|
||||||
fullWidth
|
fullWidth
|
||||||
contentEditable={false}
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
readOnly: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Box m={4} textAlign="center">
|
<Box sx={{ m: 4, textAlign: 'center' }}>
|
||||||
<LinearProgress />
|
<LinearProgress />
|
||||||
<Typography variant="h6">{LL.GENERATING_TOKEN()}…</Typography>
|
<Typography variant="h6">{LL.GENERATING_TOKEN()}…</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
@@ -240,12 +225,16 @@ const ManageUsers = () => {
|
|||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
{noAdminConfigured() && (
|
{noAdminConfigured() && (
|
||||||
<MessageBox level="warning" message={LL.USER_WARNING()} my={2} />
|
<MessageBox
|
||||||
|
level="warning"
|
||||||
|
message={LL.USER_WARNING()}
|
||||||
|
sx={{ mt: 2, mb: 2 }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||||
{changed !== 0 && (
|
{changed !== 0 && (
|
||||||
<Box flexGrow={1} sx={{ '& button': { mt: 2 } }}>
|
<Box sx={{ flexGrow: 1, '& button': { mt: 2 } }}>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<CancelIcon />}
|
startIcon={<CancelIcon />}
|
||||||
@@ -270,7 +259,7 @@ const ManageUsers = () => {
|
|||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Box flexWrap="nowrap" whiteSpace="nowrap">
|
<Box sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<PersonAddIcon />}
|
startIcon={<PersonAddIcon />}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 <SectionContent>{restarting ? <SystemMonitor /> : 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;
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -38,8 +31,6 @@ import type { LogEntry, LogSettings } from 'types';
|
|||||||
import { LogLevel } from 'types';
|
import { LogLevel } from 'types';
|
||||||
import { updateValueDirty, useRest } from 'utils';
|
import { updateValueDirty, useRest } from 'utils';
|
||||||
|
|
||||||
const MAX_LOG_ENTRIES = 1000; // Limit log entries to prevent memory issues
|
|
||||||
|
|
||||||
const TextColors: Record<LogLevel, string> = {
|
const TextColors: Record<LogLevel, string> = {
|
||||||
[LogLevel.ERROR]: '#ff0000', // red
|
[LogLevel.ERROR]: '#ff0000', // red
|
||||||
[LogLevel.WARNING]: '#ff0000', // red
|
[LogLevel.WARNING]: '#ff0000', // red
|
||||||
@@ -187,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,13 +190,9 @@ const SystemLog = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newLog = [...log, logentry];
|
const newLog = [...log, logentry];
|
||||||
// Limit log entries to prevent memory issues - only slice when necessary
|
|
||||||
if (newLog.length > MAX_LOG_ENTRIES) {
|
|
||||||
return newLog.slice(-MAX_LOG_ENTRIES);
|
|
||||||
}
|
|
||||||
return newLog;
|
return newLog;
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
useSSE(fetchLogES, {
|
useSSE(fetchLogES, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
@@ -217,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');
|
||||||
@@ -231,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);
|
||||||
@@ -252,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;
|
||||||
@@ -263,7 +249,7 @@ const SystemLog = () => {
|
|||||||
setReadOpen(false);
|
setReadOpen(false);
|
||||||
setReadValue('');
|
setReadValue('');
|
||||||
}
|
}
|
||||||
}, [readValue, readOpen, send]);
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -272,7 +258,7 @@ const SystemLog = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Grid container spacing={2} alignItems="center">
|
<Grid container spacing={2} sx={{ alignItems: 'center' }}>
|
||||||
<Grid>
|
<Grid>
|
||||||
<TextField
|
<TextField
|
||||||
name="level"
|
name="level"
|
||||||
@@ -308,6 +294,8 @@ const SystemLog = () => {
|
|||||||
<MenuItem value={50}>50</MenuItem>
|
<MenuItem value={50}>50</MenuItem>
|
||||||
<MenuItem value={75}>75</MenuItem>
|
<MenuItem value={75}>75</MenuItem>
|
||||||
<MenuItem value={100}>100</MenuItem>
|
<MenuItem value={100}>100</MenuItem>
|
||||||
|
<MenuItem value={500}>500</MenuItem>
|
||||||
|
<MenuItem value={1000}>1000</MenuItem>
|
||||||
</TextField>
|
</TextField>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,41 +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;
|
|
||||||
|
|
||||||
let message = '';
|
const statusMessage =
|
||||||
if (status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING) {
|
status && status >= SystemStatusCodes.SYSTEM_STATUS_UPLOADING
|
||||||
message = LL.WAIT_FIRMWARE();
|
? LL.WAIT_FIRMWARE()
|
||||||
} else if (status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART) {
|
: status === SystemStatusCodes.SYSTEM_STATUS_PENDING_RESTART
|
||||||
message = LL.APPLICATION_RESTARTING();
|
? LL.APPLICATION_RESTARTING()
|
||||||
} else if (status === SystemStatusCodes.SYSTEM_STATUS_NORMAL) {
|
: status === SystemStatusCodes.SYSTEM_STATUS_NORMAL
|
||||||
message = LL.RESTARTING_PRE();
|
? LL.RESTARTING_PRE()
|
||||||
} else if (status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD) {
|
: status === SystemStatusCodes.SYSTEM_STATUS_ERROR_UPLOAD
|
||||||
message = 'Upload Failed';
|
? 'Upload Failed'
|
||||||
} else {
|
: LL.RESTARTING_POST();
|
||||||
message = 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
|
||||||
@@ -103,8 +93,8 @@ const SystemMonitor = () => {
|
|||||||
height: '100vh',
|
height: '100vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center'
|
||||||
backdropFilter: 'blur(8px)'
|
// backdropFilter: 'blur(8px)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@@ -120,17 +110,15 @@ const SystemMonitor = () => {
|
|||||||
p: 3
|
p: 3
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box display="flex" alignItems="center" flexDirection="column">
|
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
|
||||||
<img
|
<img
|
||||||
src="/app/icon.png"
|
src="/app/icon.png"
|
||||||
alt="EMS-ESP"
|
alt="EMS-ESP"
|
||||||
style={{ width: '40px', height: '40px', marginBottom: '16px' }}
|
style={{ width: '40px', height: '40px', marginBottom: '16px' }}
|
||||||
/>
|
/>
|
||||||
<Typography
|
<Typography
|
||||||
color="secondary"
|
sx={{ color: 'secondary', fontWeight: 400, textAlign: 'center' }}
|
||||||
variant="h6"
|
variant="h6"
|
||||||
fontWeight={400}
|
|
||||||
textAlign="center"
|
|
||||||
>
|
>
|
||||||
{statusMessage}
|
{statusMessage}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -150,11 +138,14 @@ const SystemMonitor = () => {
|
|||||||
</MessageBox>
|
</MessageBox>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Typography mt={2} variant="h6" fontWeight={400} textAlign="center">
|
<Typography
|
||||||
|
sx={{ mt: 2, fontWeight: 400, textAlign: 'center' }}
|
||||||
|
variant="h6"
|
||||||
|
>
|
||||||
{LL.PLEASE_WAIT()}…
|
{LL.PLEASE_WAIT()}…
|
||||||
</Typography>
|
</Typography>
|
||||||
{isUploading && (
|
{isUploading && (
|
||||||
<Box width="100%" pl={2} pr={2} py={2}>
|
<Box sx={{ width: '100%', pl: 2, pr: 2, py: 2 }}>
|
||||||
<LinearProgressWithLabel value={progressValue} />
|
<LinearProgressWithLabel value={progressValue} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,629 +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 VersionData {
|
|
||||||
emsesp_version: string;
|
|
||||||
arduino_version: string;
|
|
||||||
esp_platform: string;
|
|
||||||
flash_chip_size: number;
|
|
||||||
psram: boolean;
|
|
||||||
build_flags?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
locale,
|
|
||||||
LL,
|
|
||||||
onClose
|
|
||||||
}: {
|
|
||||||
showVersionInfo: number;
|
|
||||||
latestVersion?: VersionInfo;
|
|
||||||
latestDevVersion?: VersionInfo;
|
|
||||||
locale: string;
|
|
||||||
LL: TranslationFunctions;
|
|
||||||
onClose: () => void;
|
|
||||||
}) => {
|
|
||||||
if (showVersionInfo === 0) return null;
|
|
||||||
|
|
||||||
const isStable = showVersionInfo === 1;
|
|
||||||
const version = isStable ? latestVersion : latestDevVersion;
|
|
||||||
const relNotesUrl = isStable ? STABLE_RELNOTES_URL : 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,
|
|
||||||
width: 90
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{LL.TYPE(0)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
|
||||||
{isStable ? LL.STABLE() : LL.DEVELOPMENT()}
|
|
||||||
</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
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{LL.VERSION()}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
|
|
||||||
{version?.name}
|
|
||||||
</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
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
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>
|
|
||||||
<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,
|
|
||||||
downloadOnly,
|
|
||||||
platform,
|
|
||||||
LL,
|
|
||||||
onClose,
|
|
||||||
onInstall
|
|
||||||
}: {
|
|
||||||
openInstallDialog: boolean;
|
|
||||||
fetchDevVersion: boolean;
|
|
||||||
latestVersion?: VersionInfo;
|
|
||||||
latestDevVersion?: VersionInfo;
|
|
||||||
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.UPDATE()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<Typography mb={2}>
|
|
||||||
{LL.INSTALL_VERSION(
|
|
||||||
downloadOnly ? LL.DOWNLOAD(1) : LL.INSTALL(),
|
|
||||||
fetchDevVersion ? latestDevVersion?.name : latestVersion?.name
|
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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 [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);
|
|
||||||
|
|
||||||
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 {
|
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
// Memoized values
|
|
||||||
const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]);
|
|
||||||
const isDev = useMemo(
|
|
||||||
() => data?.emsesp_version.includes('dev') ?? false,
|
|
||||||
[data?.emsesp_version]
|
|
||||||
);
|
|
||||||
|
|
||||||
const doRestart = useCallback(async () => {
|
|
||||||
setRestarting(true);
|
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
|
||||||
(error: Error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [sendAPI]);
|
|
||||||
|
|
||||||
const installFirmwareURL = useCallback(
|
|
||||||
async (url: string) => {
|
|
||||||
await sendUploadURL(url).catch((error: Error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
});
|
|
||||||
setRestarting(true);
|
|
||||||
},
|
|
||||||
[sendUploadURL]
|
|
||||||
);
|
|
||||||
|
|
||||||
const showFirmwareDialog = useCallback((useDevVersion: boolean) => {
|
|
||||||
setFetchDevVersion(useDevVersion);
|
|
||||||
setOpenInstallDialog(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeInstallDialog = useCallback(() => {
|
|
||||||
setOpenInstallDialog(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleVersionInfoClose = useCallback(() => {
|
|
||||||
setShowVersionInfo(0);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 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: 2 }}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onClick={() => showFirmwareDialog(showingDev)}
|
|
||||||
>
|
|
||||||
{LL.REINSTALL()}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!me.admin) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
sx={{ ml: 2 }}
|
|
||||||
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 p={2} border="1px solid grey" borderRadius={2}>
|
|
||||||
<Typography mb={2} variant="h6" color="primary">
|
|
||||||
{LL.THIS_VERSION()}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
direction="row"
|
|
||||||
rowSpacing={1}
|
|
||||||
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">
|
|
||||||
({data.build_flags})
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</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">
|
|
||||||
(
|
|
||||||
{data.psram ? (
|
|
||||||
<CheckIcon
|
|
||||||
color="success"
|
|
||||||
sx={{
|
|
||||||
fontSize: '1.5em',
|
|
||||||
verticalAlign: 'middle'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CloseIcon
|
|
||||||
color="error"
|
|
||||||
sx={{
|
|
||||||
fontSize: '1.5em',
|
|
||||||
verticalAlign: 'middle'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
PSRAM)
|
|
||||||
</Typography>
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid size={{ xs: 4, md: 2 }}>
|
|
||||||
<Typography color="secondary">{LL.RELEASE_TYPE()}</Typography>
|
|
||||||
</Grid>
|
|
||||||
{isDev ? (
|
|
||||||
<Typography>{LL.DEVELOPMENT()}</Typography>
|
|
||||||
) : (
|
|
||||||
<Typography>{LL.STABLE()}</Typography>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{internetLive ? (
|
|
||||||
<>
|
|
||||||
<Typography mt={2} mb={2} variant="h6" color="primary">
|
|
||||||
{LL.AVAILABLE_VERSION()}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
direction="row"
|
|
||||||
rowSpacing={1}
|
|
||||||
sx={{
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
alignItems: 'baseline'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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 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}
|
|
||||||
locale={locale}
|
|
||||||
LL={LL}
|
|
||||||
onClose={handleVersionInfoClose}
|
|
||||||
/>
|
|
||||||
<InstallDialog
|
|
||||||
openInstallDialog={openInstallDialog}
|
|
||||||
fetchDevVersion={fetchDevVersion}
|
|
||||||
latestVersion={latestVersion}
|
|
||||||
latestDevVersion={latestDevVersion}
|
|
||||||
downloadOnly={downloadOnly}
|
|
||||||
platform={platform}
|
|
||||||
LL={LL}
|
|
||||||
onClose={closeInstallDialog}
|
|
||||||
onInstall={installFirmwareURL}
|
|
||||||
/>
|
|
||||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
|
||||||
{LL.UPLOAD()}
|
|
||||||
</Typography>
|
|
||||||
<SingleUpload text={LL.UPLOAD_DROP_TEXT()} doRestart={doRestart} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
loadData,
|
|
||||||
LL,
|
|
||||||
platform,
|
|
||||||
isDev,
|
|
||||||
internetLive,
|
|
||||||
latestVersion,
|
|
||||||
latestDevVersion,
|
|
||||||
showVersionInfo,
|
|
||||||
locale,
|
|
||||||
openInstallDialog,
|
|
||||||
fetchDevVersion,
|
|
||||||
downloadOnly,
|
|
||||||
me.admin,
|
|
||||||
showButtons,
|
|
||||||
handleVersionInfoClose,
|
|
||||||
closeInstallDialog,
|
|
||||||
installFirmwareURL,
|
|
||||||
doRestart
|
|
||||||
]);
|
|
||||||
|
|
||||||
return <SectionContent>{restarting ? <SystemMonitor /> : content}</SectionContent>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(Version);
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { memo } from 'react';
|
import { type FC, type PropsWithChildren, memo } from 'react';
|
||||||
|
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import type { BoxProps } from '@mui/material';
|
import type { BoxProps } from '@mui/material';
|
||||||
|
|
||||||
const ButtonRow = memo<BoxProps>(({ children, ...rest }) => (
|
const ButtonRow: FC<PropsWithChildren<BoxProps>> = memo(({ children, ...rest }) => (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
'& button, & a, & .MuiCard-root': {
|
'& button, & a, & .MuiCard-root': {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, 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';
|
||||||
@@ -12,6 +12,7 @@ type MessageBoxLevel = 'warning' | 'success' | 'info' | 'error';
|
|||||||
export interface MessageBoxProps extends BoxProps {
|
export interface MessageBoxProps extends BoxProps {
|
||||||
level: MessageBoxLevel;
|
level: MessageBoxLevel;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEVEL_ICONS: Record<MessageBoxLevel, React.ComponentType<SvgIconProps>> = {
|
const LEVEL_ICONS: Record<MessageBoxLevel, React.ComponentType<SvgIconProps>> = {
|
||||||
@@ -28,7 +29,7 @@ const LEVEL_PALETTE_PATHS: Record<MessageBoxLevel, string> = {
|
|||||||
error: 'error.dark'
|
error: 'error.dark'
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageBox: FC<MessageBoxProps> = ({
|
const MessageBox: FC<PropsWithChildren<MessageBoxProps>> = ({
|
||||||
level,
|
level,
|
||||||
message,
|
message,
|
||||||
sx,
|
sx,
|
||||||
@@ -37,27 +38,30 @@ const MessageBox: FC<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
|
||||||
p={2}
|
|
||||||
display="flex"
|
|
||||||
alignItems="center"
|
|
||||||
borderRadius={1}
|
|
||||||
sx={{ backgroundColor, color: 'white', ...sx }}
|
|
||||||
{...rest}
|
{...rest}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: 1,
|
||||||
|
backgroundColor,
|
||||||
|
color: 'white',
|
||||||
|
p: 2,
|
||||||
|
...sx
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon />
|
<Icon />
|
||||||
{(message || children) && (
|
{(message || children) && (
|
||||||
|
|||||||
@@ -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} />
|
|
||||||
{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} />
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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 display="flex" alignItems="center" px={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 (
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
@@ -51,9 +53,7 @@ const LayoutMenuComponent = () => {
|
|||||||
sx={{ my: 0 }}
|
sx={{ my: 0 }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
primary: {
|
primary: {
|
||||||
fontWeight: '600',
|
sx: { fontWeight: 600, mb: '2px', color: 'lightblue' }
|
||||||
mb: '2px',
|
|
||||||
color: 'lightblue'
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -107,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 />
|
||||||
|
|||||||
@@ -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,61 +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 ? '4px' : '0px',
|
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
|
||||||
backgroundColor: '#90caf9',
|
}
|
||||||
borderRadius: '0 2px 2px 0',
|
};
|
||||||
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
|
||||||
@@ -79,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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -32,8 +32,16 @@ const FormLoaderComponent = ({ errorMessage, onRetry }: FormLoaderProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box m={2} py={2} display="flex" alignItems="center" flexDirection="column">
|
<Box
|
||||||
<Box py={2}>
|
sx={{
|
||||||
|
m: 2,
|
||||||
|
py: 2,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
<CircularProgress size={100} />
|
<CircularProgress size={100} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ const circularProgressStyles: SxProps<Theme> = (theme: Theme) => ({
|
|||||||
const LoadingSpinner = ({ height = '100%' }: LoadingSpinnerProps) => {
|
const LoadingSpinner = ({ height = '100%' }: LoadingSpinnerProps) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
sx={{
|
||||||
alignItems="center"
|
display: 'flex',
|
||||||
justifyContent="center"
|
alignItems: 'center',
|
||||||
flexDirection="column"
|
justifyContent: 'center',
|
||||||
padding={2}
|
flexDirection: 'column',
|
||||||
height={height}
|
padding: 2,
|
||||||
|
height
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CircularProgress sx={circularProgressStyles} size={100} />
|
<CircularProgress sx={circularProgressStyles} size={100} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -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'}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Code inspired by Prince Azubuike from https://medium.com/@dprincecoder/creating-a-drag-and-drop-file-upload-component-in-react-a-step-by-step-guide-4d93b6cc21e0
|
// drag/drop code inspired by Prince Azubuike from https://medium.com/@dprincecoder/creating-a-drag-and-drop-file-upload-component-in-react-a-step-by-step-guide-4d93b6cc21e0
|
||||||
import {
|
import {
|
||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
type DragEvent,
|
type DragEvent,
|
||||||
@@ -6,12 +6,28 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||||
import UploadIcon from '@mui/icons-material/Upload';
|
import UploadIcon from '@mui/icons-material/Upload';
|
||||||
import { Box, Button, Typography, styled } from '@mui/material';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Typography,
|
||||||
|
styled
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import { callAction } from 'api/app';
|
||||||
|
|
||||||
|
import { dialogStyle } from '@/CustomTheme';
|
||||||
|
import { useRequest } from 'alova/client';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
|
||||||
const DocumentUploader = styled(Box)<{ active?: boolean }>(({ theme, active }) => ({
|
const DocumentUploader = styled(Box)<{ active?: boolean }>(({ theme, active }) => ({
|
||||||
@@ -58,6 +74,31 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
|
|||||||
const [dragged, setDragged] = useState(false);
|
const [dragged, setDragged] = useState(false);
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
const [showUpgradeDialog, setShowUpgradeDialog] = useState(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);
|
||||||
|
if (upgradeImportantMessageType_n === 0) {
|
||||||
|
if (file) {
|
||||||
|
onFileSelected(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onError((error) => {
|
||||||
|
toast.error(String(error.error?.message || 'An error occurred'));
|
||||||
|
});
|
||||||
|
|
||||||
const checkFileExtension = (file: File) => {
|
const checkFileExtension = (file: File) => {
|
||||||
const validExtensions = ['.json', '.bin', '.md5'];
|
const validExtensions = ['.json', '.bin', '.md5'];
|
||||||
@@ -97,9 +138,8 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
|
|||||||
|
|
||||||
const handleUploadClick = (event: MouseEvent<HTMLButtonElement>) => {
|
const handleUploadClick = (event: MouseEvent<HTMLButtonElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (file) {
|
void checkUpgradeImportantMessages(file?.name || '');
|
||||||
onFileSelected(file);
|
setShowUpgradeDialog(true);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBrowseClick = () => {
|
const handleBrowseClick = () => {
|
||||||
@@ -158,6 +198,56 @@ const DragNdrop = ({ text, onFileSelected }: DragNdropProps) => {
|
|||||||
{LL.UPLOAD()}
|
{LL.UPLOAD()}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
{showUpgradeDialog && upgradeImportantMessageType > 0 && (
|
||||||
|
<Dialog
|
||||||
|
sx={dialogStyle}
|
||||||
|
open={showUpgradeDialog}
|
||||||
|
onClose={() => setShowUpgradeDialog(false)}
|
||||||
|
>
|
||||||
|
<DialogTitle>
|
||||||
|
<WarningIcon
|
||||||
|
color="warning"
|
||||||
|
sx={{ fontSize: 18, verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{LL.UPGRADE_IMPORTANT_MESSAGES()}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
{upgradeImportantMessageType === 2 &&
|
||||||
|
LL.UPGRADE_IMPORTANT_MESSAGES_2()}
|
||||||
|
{upgradeImportantMessageType === 1 &&
|
||||||
|
LL.UPGRADE_IMPORTANT_MESSAGES_1()}
|
||||||
|
<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={() => setShowUpgradeDialog(false)}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
{LL.CANCEL()}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<UploadIcon />}
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => onFileSelected(file)}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{LL.UPLOAD()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DocumentUploader>
|
</DocumentUploader>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user